Skip to main content

段错误调试分析:从系统日志到代码修复

·5 mins

摘要 #

本文详细记录了一次段错误(SEGV)的完整调试过程。通过系统日志分析、地址解析、汇编代码分析和代码审查,成功定位并修复了 PerformanceMonitor 中的数组越界问题。本文展示了如何在没有完整 coredump 文件的情况下,仅凭系统日志和调试工具进行问题定位。


一、问题现象 #

1.1 崩溃信息 #

coredumpctl 获取的崩溃信息:

PID: 178581 (panda_strategy-)
Signal: 11 (SEGV)
Timestamp: Mon 2026-01-19 21:25:12 CST
Executable: /home/jason/panda/panda-strategy/Strategy/out/build/wsl-profile/panda_strategy-1.0.0
Message: Coredump entry has no core attached

关键问题:coredump 文件未保存,无法使用 GDB 直接分析。

1.2 崩溃时间点分析 #

从日志文件 main.log 中可以看到:

[21:25:12.816398][178634][info][strategy] Funding rates updated, total entries: 93, updated entries: 5
[21:25:12.816474][178634][info][strategy] Updated tradability parameters: 274 margin entries
[21:25:12.816493][178634][info][strategy] Reference updated for 3 symbols
[21:25:12.816502][178634][info][strategy] Fee rate table updated, total entries: 36, updated entries: 36
[21:25:12.818139][178634][info][strategy] 
=== Performance Statistics ===

观察:崩溃发生在性能统计输出过程中,最后一条日志是空行,紧接着开始输出性能统计表格。


二、系统日志分析 #

2.1 使用 dmesg 查看内核日志 #

dmesg | grep -A 20 -B 5 "panda_strategy\|SEGV\|segfault\|178581"

关键发现

[269897.434395] adapter-poller[178646]: segfault at 7f0264018 ip 00000000004d02d0 sp 00007f029cff8380 error 4 in panda_strategy-1.0.0[4b5000+23d000]
[276922.350810] traps: adapter-poller[178646] general protection fault ip:6a6418 sp:7f026d7f90d0 error:0 in panda_strategy-1.0.0[4b5000+23d000]

分析

  • 崩溃地址:ip:6a6418(指令指针)
  • 错误类型:general protection fault(不是简单的 segfault)
  • 进程:adapter-poller[178646](注意 PID 是 178646,不是主进程 178581)

2.2 使用 journalctl 查看系统日志 #

journalctl --since "2026-01-19 21:25:00" --until "2026-01-19 21:26:00" | grep "178581\|panda_strategy\|SEGV"

关键信息

Jan 19 21:25:12 kernel: traps: adapter-poller[178646] general protection fault ip:6a6418
Jan 19 21:25:12 systemd-coredump[182096]: Resource limits disable core dumping for process 178581

结论:系统资源限制导致 coredump 未保存,但内核日志中保留了崩溃地址。


三、地址解析与符号定位 #

3.1 检查可执行文件信息 #

file panda_strategy-1.0.0

输出

ELF 64-bit LSB executable, x86-64, version 1 (GNU/Linux), 
dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, 
for GNU/Linux 3.2.0, with debug_info, not stripped

关键信息

  • 包含调试信息(with debug_info
  • 符号表未剥离(not stripped
  • 可以使用 addr2lineobjdump 进行分析

3.2 使用 addr2line 解析崩溃地址 #

addr2line -e panda_strategy-1.0.0 -f -C 0x6a6418

输出

std::_Hashtable<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >, 
std::pair<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const, 
pandadt::PerformanceStats>, ...>::_M_find_before_node(unsigned long, 
std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const&, 
unsigned long) const [clone .isra.0]
PerformanceMonitor.cpp:?

分析

  • 崩溃发生在 std::unordered_map 的内部实现 _M_find_before_node
  • 文件:PerformanceMonitor.cpp
  • 这是 std::unordered_map 在查找节点时调用的内部方法

3.3 使用 nm 查找相关符号 #

nm panda_strategy-1.0.0 | grep "PerformanceMonitor\|printStats"

输出

00000000006a69d0 T _ZNK7pandadt18PerformanceMonitor10printStatsERSo
00000000006a7990 T _ZN7pandadt18PerformanceMonitor14recordDurationERKNSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEEEd

分析

  • printStats 函数地址:0x6a69d0
  • 崩溃地址:0x6a6418
  • 崩溃发生在 printStats 函数内部(地址在函数范围内)

四、汇编代码分析 #

4.1 反汇编崩溃地址附近的代码 #

objdump -d panda_strategy-1.0.0 | grep -A 20 "6a6418:"

输出

  6a6418:	48 8b 4e 60          	mov    0x60(%rsi),%rcx
  6a641c:	31 d2                	xor    %edx,%edx
  6a641e:	48 89 c8             	mov    %rcx,%rax
  6a6421:	49 f7 f4             	div    %r12
  6a6424:	49 89 dd             	mov    %rbx,%r13
  6a6427:	49 39 d6             	cmp    %rdx,%r14
  6a642a:	75 44                	jne    6a6470
  6a642c:	48 89 f3             	mov    %rsi,%rbx
  6a642f:	49 39 cf             	cmp    %rcx,%r15
  6a6432:	75 dc                	jne    6a6410
  6a6434:	49 8b 51 08          	mov    0x8(%r9),%rdx
  6a6438:	48 3b 53 10          	cmp    0x10(%rbx),%rdx
  6a643c:	75 d2                	jne    6a6410
  6a643e:	48 85 d2             	test   %rdx,%rdx
  6a6441:	74 18                	je     6a645b
  6a6443:	49 8b 39             	mov    (%r9),%rdi
  6a6446:	48 8b 73 08          	mov    0x8(%rbx),%rsi
  6a644a:	4c 89 4d c8          	mov    %r9,-0x38(%rbp)
  6a644e:	e8 2d 03 e1 ff       	callq  4b6780 <memcmp@plt>

分析

  • mov 0x60(%rsi),%rcx:从 %rsi+0x60 读取值到 %rcx
  • mov 0x8(%rbx),%rsi:从 %rbx+0x8 读取值到 %rsi
  • 这些操作在访问哈希表的内部结构(bucket 指针、节点指针等)
  • 如果这些指针无效,会导致 general protection fault

4.2 使用 GDB 分析函数范围 #

gdb -batch -ex "info symbol 0x6a6418" panda_strategy-1.0.0

输出

std::_Hashtable<...>::_M_find_before_node(...) const [clone .isra.0] + 56 
in section .text

确认:崩溃确实发生在 std::unordered_map 的内部实现中。


五、代码审查与问题定位 #

5.1 查看 PerformanceMonitor::printStats 实现 #

void PerformanceMonitor::printStats(std::ostream& os) const {
    // ... 输出表头 ...
    
    for (const auto& [name, stats] : m_stats) {  // ← 遍历 unordered_map
        auto sortedStats = stats;
        sortedStats.sortSamples();
        os << std::setw(30) << std::left << name
            << std::setw(10) << sortedStats.m_count
            << std::setw(12) << sortedStats.getAverageUs()
            << std::setw(12) << sortedStats.m_minUs
            << std::setw(12) << sortedStats.getMedian()      // ← 调用 getMedian
            << std::setw(12) << sortedStats.getPercentile(0.60)  // ← 调用 getPercentile
            << std::setw(12) << sortedStats.getPercentile(0.70)
            // ... 更多 getPercentile 调用 ...
            << std::endl;
    }
}

调用链

  1. printStats() 遍历 m_statsstd::unordered_map
  2. 对每个 PerformanceStats 调用 getMedian()getPercentile()
  3. 这些方法访问 m_samples 向量

5.2 发现问题:边界检查不一致 #

原始代码

double getMedian() const {
    if (m_count == 0) return 0.0;  // ❌ 使用 m_count 检查
    if (m_count % 2 == 0) {
        return (m_samples[m_count / 2 - 1] + m_samples[m_count / 2]) / 2.0;
        // ❌ 但访问 m_samples[m_count]
    }
    return m_samples[m_count / 2];  // ❌ 使用 m_count 作为索引
}

double getPercentile(double p) const {
    if (m_count == 0) return 0.0;  // ❌ 使用 m_count 检查
    double pos = p * (m_count - 1);  // ❌ 使用 m_count 计算
    size_t index = static_cast<size_t>(pos);
    // ❌ 但访问 m_samples[index],没有检查 index < m_samples.size()
    return m_samples[index];
}

问题分析

  1. 边界检查不一致

    • 使用 m_count 进行边界检查
    • 但实际访问的是 m_samples 向量
    • 理论上 m_count == m_samples.size(),但如果出现不一致,会导致越界
  2. 潜在的越界场景

    • 如果 m_count > m_samples.size(),访问 m_samples[m_count/2] 会越界
    • 如果 index >= m_samples.size(),访问 m_samples[index] 会越界
    • 数组越界会损坏堆内存,影响 m_stats 哈希表的内部结构
  3. 间接崩溃机制

    数组越界访问 m_samples[index]
    ↓
    损坏堆内存(可能影响 m_stats 哈希表的 bucket 指针)
    ↓
    后续遍历 m_stats 时访问无效指针
    ↓
    general protection fault
    

5.3 验证假设:检查 addSample 实现 #

void addSample(double durationUs) {
    m_minUs = std::min(m_minUs, durationUs);
    m_maxUs = std::max(m_maxUs, durationUs);
    m_totalUs += durationUs;
    m_count++;                    // ← 增加计数
    m_samples.push_back(durationUs);  // ← 添加样本
}

理论上m_countm_samples.size() 应该始终一致。

但实际上

  • 如果 push_back() 抛出异常(内存不足),m_count 已增加但 m_samples 未增加
  • 如果存在内存损坏,m_countm_samples.size() 可能不一致
  • 防御性编程应该使用实际向量大小进行边界检查

六、修复方案 #

6.1 修复原则 #

  1. 使用实际向量大小:使用 m_samples.size() 而不是 m_count 进行边界检查
  2. 添加参数验证:在 getPercentile() 中验证参数范围
  3. 防御性编程:添加额外的边界检查,防止越界访问

6.2 修复后的代码 #

double getMedian() const {
    size_t size = m_samples.size();  // ✅ 使用实际向量大小
    if (size == 0) return 0.0;
    if (size % 2 == 0) {
        return (m_samples[size / 2 - 1] + m_samples[size / 2]) / 2.0;
    }
    return m_samples[size / 2];
}

double getPercentile(double p) const {
    size_t size = m_samples.size();  // ✅ 使用实际向量大小
    if (size == 0) return 0.0;
    
    // ✅ 参数范围检查
    if (p < 0.0) p = 0.0;
    if (p > 1.0) p = 1.0;
    
    double pos = p * (size - 1);
    size_t index = static_cast<size_t>(pos);
    
    // ✅ 确保索引在有效范围内
    if (index >= size) index = size - 1;
    
    double fraction = pos - index;
    
    if (index + 1 < size) {
        return m_samples[index] + fraction * (m_samples[index + 1] - m_samples[index]);
    }
    return m_samples[index];
}

6.3 修复要点 #

  1. 一致性:边界检查和使用实际访问的容器大小一致
  2. 安全性:添加参数验证和索引边界保护
  3. 健壮性:即使出现异常情况,也不会导致崩溃

七、调试技巧总结 #

7.1 工具链 #

工具用途关键命令
dmesg查看内核日志dmesg | grep segfault
journalctl查看系统日志journalctl --since "..."
addr2line地址到源码映射addr2line -e binary address
objdump反汇编objdump -d binary
nm符号表nm binary | grep symbol
gdb调试器gdb -batch -ex "info symbol addr"

7.2 调试流程 #

1. 收集崩溃信息
   ↓
2. 查看系统日志(dmesg/journalctl)
   ↓
3. 提取崩溃地址(IP 寄存器值)
   ↓
4. 解析地址到函数(addr2line)
   ↓
5. 分析汇编代码(objdump)
   ↓
6. 代码审查(找到调用链)
   ↓
7. 定位根本原因(边界检查问题)
   ↓
8. 修复并验证

7.3 关键洞察 #

  1. 间接崩溃:真正的 bug(数组越界)和崩溃位置(哈希表遍历)可能不同
  2. 内存损坏:数组越界可能不会立即崩溃,而是损坏内存,导致后续操作失败
  3. 防御性编程:即使理论上不会出错,也应该使用实际容器大小进行边界检查

八、经验教训 #

8.1 代码质量 #

  1. 边界检查一致性:边界检查应该使用实际访问的容器大小
  2. 参数验证:函数应该验证输入参数的有效性
  3. 异常安全:考虑异常情况下的数据一致性

8.2 调试方法 #

  1. 系统日志是宝贵的:即使没有 coredump,系统日志也能提供关键信息
  2. 工具链组合使用addr2lineobjdumpnm 等工具组合使用可以精确定位问题
  3. 代码审查很重要:在定位到崩溃位置后,代码审查可以发现根本原因

8.3 预防措施 #

  1. 使用静态分析工具:可以检测潜在的数组越界问题
  2. 使用 AddressSanitizer:可以在运行时检测内存错误
  3. 代码审查:重点关注边界检查和数组访问

九、结论 #

通过系统日志分析、地址解析、汇编代码分析和代码审查,成功定位并修复了 PerformanceMonitor 中的数组越界问题。这次调试展示了在没有完整 coredump 文件的情况下,如何仅凭系统日志和调试工具进行问题定位。

关键发现

  • 崩溃发生在 std::unordered_map 的遍历中
  • 根本原因是 getMedian()getPercentile() 中的数组越界
  • 数组越界导致内存损坏,进而影响哈希表结构

修复效果

  • 使用 m_samples.size() 而不是 m_count 进行边界检查
  • 添加参数验证和索引边界保护
  • 提高了代码的健壮性和安全性

参考文献 #

  1. Linux man pages: dmesg(1), journalctl(1), addr2line(1), objdump(1), nm(1)
  2. GDB Documentation: Debugging with GDB
  3. C++ Standard Library: std::unordered_map, std::vector
  4. System Programming: Memory Management and Debugging Techniques