段错误调试分析:从系统日志到代码修复
·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) - 可以使用
addr2line和objdump进行分析
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读取值到%rcxmov 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;
}
}
调用链:
printStats()遍历m_stats(std::unordered_map)- 对每个
PerformanceStats调用getMedian()和getPercentile() - 这些方法访问
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];
}
问题分析:
边界检查不一致:
- 使用
m_count进行边界检查 - 但实际访问的是
m_samples向量 - 理论上
m_count == m_samples.size(),但如果出现不一致,会导致越界
- 使用
潜在的越界场景:
- 如果
m_count > m_samples.size(),访问m_samples[m_count/2]会越界 - 如果
index >= m_samples.size(),访问m_samples[index]会越界 - 数组越界会损坏堆内存,影响
m_stats哈希表的内部结构
- 如果
间接崩溃机制:
数组越界访问 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_count 和 m_samples.size() 应该始终一致。
但实际上:
- 如果
push_back()抛出异常(内存不足),m_count已增加但m_samples未增加 - 如果存在内存损坏,
m_count和m_samples.size()可能不一致 - 防御性编程应该使用实际向量大小进行边界检查
六、修复方案 #
6.1 修复原则 #
- 使用实际向量大小:使用
m_samples.size()而不是m_count进行边界检查 - 添加参数验证:在
getPercentile()中验证参数范围 - 防御性编程:添加额外的边界检查,防止越界访问
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 修复要点 #
- 一致性:边界检查和使用实际访问的容器大小一致
- 安全性:添加参数验证和索引边界保护
- 健壮性:即使出现异常情况,也不会导致崩溃
七、调试技巧总结 #
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 关键洞察 #
- 间接崩溃:真正的 bug(数组越界)和崩溃位置(哈希表遍历)可能不同
- 内存损坏:数组越界可能不会立即崩溃,而是损坏内存,导致后续操作失败
- 防御性编程:即使理论上不会出错,也应该使用实际容器大小进行边界检查
八、经验教训 #
8.1 代码质量 #
- 边界检查一致性:边界检查应该使用实际访问的容器大小
- 参数验证:函数应该验证输入参数的有效性
- 异常安全:考虑异常情况下的数据一致性
8.2 调试方法 #
- 系统日志是宝贵的:即使没有 coredump,系统日志也能提供关键信息
- 工具链组合使用:
addr2line、objdump、nm等工具组合使用可以精确定位问题 - 代码审查很重要:在定位到崩溃位置后,代码审查可以发现根本原因
8.3 预防措施 #
- 使用静态分析工具:可以检测潜在的数组越界问题
- 使用 AddressSanitizer:可以在运行时检测内存错误
- 代码审查:重点关注边界检查和数组访问
九、结论 #
通过系统日志分析、地址解析、汇编代码分析和代码审查,成功定位并修复了 PerformanceMonitor 中的数组越界问题。这次调试展示了在没有完整 coredump 文件的情况下,如何仅凭系统日志和调试工具进行问题定位。
关键发现:
- 崩溃发生在
std::unordered_map的遍历中 - 根本原因是
getMedian()和getPercentile()中的数组越界 - 数组越界导致内存损坏,进而影响哈希表结构
修复效果:
- 使用
m_samples.size()而不是m_count进行边界检查 - 添加参数验证和索引边界保护
- 提高了代码的健壮性和安全性
参考文献 #
- Linux man pages:
dmesg(1),journalctl(1),addr2line(1),objdump(1),nm(1) - GDB Documentation: Debugging with GDB
- C++ Standard Library:
std::unordered_map,std::vector - System Programming: Memory Management and Debugging Techniques