C++异常行号处理(腾讯元宝)

发布于:2025-05-01 ⋅ 阅读:(18) ⋅ 点赞:(0)

在 Linux C++ 环境中,通过异常的 PC(Program Counter)指针获取源代码文件名和行号,需要结合调试信息和地址到源码的映射。以下是实现这一目标的详细步骤和示例代码:


核心原理

  1. 编译时保留调试信息:需使用 -g 编译选项生成调试符号。
  2. 解析地址到源码位置:通过 dladdr 获取基础符号信息,或使用 libdwarf/addr2line 解析调试信息。
  3. 处理 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);
}

关键注意事项

  1. 调试符号保留:必须使用 -g 编译,且不要剥离符号(strip --strip-debug 会删除调试信息)。
  2. ASLR 影响:如果系统启用 ASLR,需计算加载基址偏移:
     

    cpp

    复制

    ElfW(Addr) load_bias = elf_exec_load_bias(exec_map);
    void* adjusted_pc = (void*)((uintptr_t)pc - load_bias);
  3. 多线程安全dladdrlibdwarf 需确保线程安全。
  4. 性能开销:直接解析调试信息较慢,建议仅在异常处理时使用。

推荐工具库

  • Google Breakpad:跨平台的崩溃报告库,自动解析堆栈和源码位置。
  • libunwind:高效的堆栈回溯库,支持源码级信息解析。
  • Boost.Stacktrace​(需 C++17):
     

    cpp

    复制

    #include <boost/stacktrace.hpp>
    void handler() {
        std::cerr << boost::stacktrace::stacktrace() << "\n";
    }

通过上述方法,可以在异常发生时精确映射到源代码的文件名和行号,适用于调试生产环境崩溃或复杂异常分析。


网站公告

今日签到

点亮在社区的每一天
去签到