在 Linux C++ 环境中,通过异常的 PC(Program Counter)指针获取源代码文件名和行号,需要结合调试信息和地址到源码的映射。以下是实现这一目标的详细步骤和示例代码:
核心原理
- 编译时保留调试信息:需使用
-g
编译选项生成调试符号。 - 解析地址到源码位置:通过
dladdr
获取基础符号信息,或使用libdwarf
/addr2line
解析调试信息。 - 处理 ASLR(地址随机化):计算加载基址偏移。
方法一:使用 dladdr
+ addr2line
1. 编译代码时添加调试信息
bash
复制
g++ -g -rdynamic -o my_program my_program.cpp
2. 示例代码
cpp
复制
#include <iostream>
#include <dlfcn.h>
#include <execinfo.h>
#include <cxxabi.h>
void print_source_info(void* pc) {
Dl_info info;
if (dladdr(pc, &info) && info.dli_sname) {
int status;
char* demangled = abi::__cxa_demangle(info.dli_sname, nullptr, nullptr, &status);
std::cout << "Function: " << (demangled ? demangled : info.dli_sname) << "\n";
// 使用 addr2line 命令行工具解析行号
char cmd[256];
snprintf(cmd, sizeof(cmd), "addr2line -e %s -f -C -p -i %p",
program_invocation_name, pc);
FILE* fp = popen(cmd, "r");
if (fp) {
char line[1024];
while (fgets(line, sizeof(line), fp)) {
std::cout << line;
}
pclose(fp);
}
free(demangled);
} else {
std::cout << "No debug info available\n";
}
}
void signal_handler(int sig) {
void* array[10];
size_t size = backtrace(array, 10);
backtrace_symbols_fd(array, size, STDERR_FILENO);
// 获取异常发生时的 PC 地址(通常是数组第一个元素)
print_source_info(array[0]);
exit(1);
}
int main() {
signal(SIGSEGV, signal_handler);
// 触发测试代码
int* ptr = nullptr;
*ptr = 42; // 触发段错误
return 0;
}
3. 运行结果示例
markdown
复制
Segmentation fault (core dumped)
0x000055a5b3e4a567 in main () at my_program.cpp:25
25 *ptr = 42; // 触发段错误
方法二:使用 libdwarf
直接解析调试信息
需要安装 libdwarf-dev
库:
bash
复制
sudo apt-get install libdwarf-dev
示例代码片段
cpp
复制
#include <dwarf.h>
#include <libdwarf.h>
void parse_dwarf(void* pc) {
Dwarf_Addr pc_addr = (Dwarf_Addr)pc;
Dwarf_Die die;
Dwarf_Error error = nullptr;
Dwarf_Unsigned lineno;
// 打开当前可执行文件
int fd = open(program_invocation_name, O_RDONLY);
Dwarf* dbg = dwarf_begin(fd, DWARF_C_READ);
if (!dbg) return;
// 遍历编译单元
Dwarf_Unsigned cu_header_length;
Dwarf_Half version_stamp;
Dwarf_Off abbrev_offset;
Dwarf_Unsigned next_cu_header_offset;
while (dwarf_nextcu(dbg, 0, &cu_header_length, &version_stamp,
&abbrev_offset, &next_cu_header_offset, &error) == DW_DLV_OK) {
if (dwarf_get_die(dbg, 0, &die, &error) != DW_DLV_OK) continue;
// 获取该 CU 的低/高地址范围
Dwarf_Addr low_pc, high_pc;
if (dwarf_getattr(die, DW_AT_low_pc, &low_pc, &error) != DW_DLV_OK) continue;
if (dwarf_getattr(die, DW_AT_high_pc, &high_pc, &error) != DW_DLV_OK) continue;
if (pc_addr >= low_pc && pc_addr <= high_pc) {
// 在此编译单元中查找行号
Dwarf_Line* line;
Dwarf_Unsigned lineno;
if (dwarf_getsrc_die(dbg, pc_addr, &line, &lineno) == DW_DLV_OK) {
const char* file = dwarf_linesrc(line, nullptr, nullptr);
std::cout << "File: " << file << ", Line: " << lineno << "\n";
}
break;
}
}
dwarf_end(dbg);
close(fd);
}
关键注意事项
- 调试符号保留:必须使用
-g
编译,且不要剥离符号(strip --strip-debug
会删除调试信息)。 - ASLR 影响:如果系统启用 ASLR,需计算加载基址偏移:
cpp
复制
ElfW(Addr) load_bias = elf_exec_load_bias(exec_map); void* adjusted_pc = (void*)((uintptr_t)pc - load_bias);
- 多线程安全:
dladdr
和libdwarf
需确保线程安全。 - 性能开销:直接解析调试信息较慢,建议仅在异常处理时使用。
推荐工具库
- Google Breakpad:跨平台的崩溃报告库,自动解析堆栈和源码位置。
- libunwind:高效的堆栈回溯库,支持源码级信息解析。
- Boost.Stacktrace(需 C++17):
cpp
复制
#include <boost/stacktrace.hpp> void handler() { std::cerr << boost::stacktrace::stacktrace() << "\n"; }
通过上述方法,可以在异常发生时精确映射到源代码的文件名和行号,适用于调试生产环境崩溃或复杂异常分析。