如何在C/C++代码中实现类似GDB的带参数栈回溯打印
实现带参数的C/C++调用栈回溯
我正在C/C++程序中实现调用栈打印功能,已找到依赖libdw、execinfo库的实现示例,该示例可输出栈帧地址、函数名及对应代码位置,但无法打印函数参数。现需要实现类似GDB的bt命令效果,输出包含函数参数的完整栈回溯。
现有示例代码
#include <cassert> #include <iostream> #include <memory> #include <sstream> #include <string> #include <iomanip> #include <cxxabi.h> // __cxa_demangle #include <elfutils/libdwfl.h> // Dwfl* #include <execinfo.h> // backtrace #include <unistd.h> // getpid using namespace std; // 解析C++ mangled函数名 std::string demangle(const char* name) { int status = -4; std::unique_ptr<char, void(*)(void*)> res { abi::__cxa_demangle(name, NULL, NULL, &status), std::free }; return (status==0) ? res.get() : name ; } std::string debug_info(Dwfl* dwfl, void* ip) { uintptr_t ip2 = reinterpret_cast<uintptr_t>(ip); Dwfl_Module* module = dwfl_addrmodule(dwfl, ip2); char const* name = dwfl_module_addrname(module, ip2); std::string function_name = name ? demangle(name) : "<unknown>"; int line_num = -1; char const* file_name; if (Dwfl_Line* dwfl_line = dwfl_module_getsrc(module, ip2)) { Dwarf_Addr addr; file_name = dwfl_lineinfo(dwfl_line, &addr, &line_num, nullptr, nullptr, nullptr); } std::stringstream ss; ss << std::setw(16)<<std::setfill('0') <<ip << ' ' << function_name; if (file_name) ss << " at " << file_name << ':' << line_num; ss << std::endl; return ss.str(); } std::string gen_framestack_backtrace() { // 初始化Dwfl Dwfl* dwfl = nullptr; { Dwfl_Callbacks callbacks = {}; char* debuginfo_path = nullptr; callbacks.find_elf = dwfl_linux_proc_find_elf; callbacks.find_debuginfo = dwfl_standard_find_debuginfo; callbacks.debuginfo_path = &debuginfo_path; dwfl = dwfl_begin(&callbacks); assert(dwfl); int r; r = dwfl_linux_proc_report(dwfl, getpid()); assert(!r); r = dwfl_report_end(dwfl, nullptr, nullptr); assert(!r); static_cast<void>(r); } // 遍历栈帧 std::stringstream ss; { void* stack[512]; int stack_size = ::backtrace(stack, sizeof stack / sizeof *stack); for (int i = 0; i < stack_size; ++i) { ss << i << ": "; ss << debug_info(dwfl, stack[i]); } } dwfl_end(dwfl); return ss.str(); } void my_func_2() { std::cout << gen_framestack_backtrace() << std::endl; std::cout.flush(); } void my_func_1(double f) { (void)f; my_func_2(); } void my_func_1(int i) { (void)i; my_func_2(); } int main(int argc, char **argv) { long long unsigned int n; if (argc > 1) { n = strtoul(argv[1], NULL, 0); } else { n = 1; } for (long long unsigned int i = 0; i < n; ++i) { my_func_1(1); // line 122 my_func_1(2.0); // line 123 } }
编译运行命令
$ sudo apt install libdw-dev libunwind-dev $ g++ -fno-pie -ggdb3 -O0 -no-pie -o a.out -std=c++11 -Wall -Wextra -pedantic-errors test.cpp -ldw -lunwind -ggdb $ ./a.out
现有运行结果
0: 000000000x401ab1 stacktrace[abi:cxx11]() at /home/wxq/test/test7.cpp:71 1: 000000000x401c11 my_func_2() at /home/wxq/test/test7.cpp:106 2: 000000000x401ca2 my_func_1(int) at /home/wxq/test/test7.cpp:117 3: 000000000x401d01 main at /home/wxq/test/test7.cpp:128 4: 000x7f3e4ee4dbf6 __libc_start_main at ../csu/libc-start.c:310 5: 000000000x401479 _start at ../csu/libc-start.c:-1 0: 000000000x401ab1 stacktrace[abi:cxx11]() at /home/wxq/test/test7.cpp:71 1: 000000000x401c11 my_func_2() at /home/wxq/test/test7.cpp:106 2: 000000000x401c8f my_func_1(double) at /home/wxq/test/test7.cpp:112 3: 000000000x401d16 main at /home/wxq/test/test7.cpp:129 4: 000x7f3e4ee4dbf6 __libc_start_main at ../csu/libc-start.c:310 5: 000000000x401479 _start at ../csu/libc-start.c:-1
期望输出效果(类似GDB的bt命令)
#0 createObj1 (handle=0x5555559291c0, shimHandle=0x55555595a850) at /home/wxq/setup.cpp:983 #1 0x00007ffff60a3763 in initialize (this=0x55555595a850, config=...)at /home/wxq/test.cpp:197 #2 0x00007ffff60a24f2 in create_extended (setup=0x5555559291c0) at /home/wxq/test.cpp:509 #3 0x0000555555555538 in main (argc=5, argv=0x7fffffffe0e8) at /home/wxq/core_model.cpp:145
解决方案:添加函数参数打印功能
要实现参数打印,需要解析DWARF调试信息获取函数参数的定义,再结合栈帧布局读取参数值。以下是基于现有代码的修改方案:
核心思路
- 获取函数DWARF调试信息:通过
dwfl_module_getdwarf获取模块的Dwarf对象,再根据指令地址定位到对应的函数调试条目(DIE)。 - 解析参数元信息:遍历函数DIE的子条目,提取参数名称、类型以及参数在栈/寄存器中的位置。
- 读取栈中参数值:根据目标架构的调用约定(如x86-64 System V ABI),从当前栈帧中读取参数的实际值。
修改后的核心代码
扩展debug_info函数,添加参数解析逻辑:
#include <elfutils/dwarf.h> // 解析函数参数并返回参数字符串 std::string get_function_args(Dwfl* dwfl, void* ip, uintptr_t frame_ptr) { uintptr_t ip2 = reinterpret_cast<uintptr_t>(ip); Dwfl_Module* module = dwfl_addrmodule(dwfl, ip2); if (!module) return ""; Dwarf_Die die_mem; Dwarf_Die* func_die = dwfl_module_addrdie(module, ip2, &die_mem); if (!func_die) return ""; std::stringstream args_ss; args_ss << "("; bool first_arg = true; // 遍历函数的参数DIE Dwarf_Die child; if (dwarf_child(func_die, &child) == 0) { do { if (dwarf_tag(&child) == DW_TAG_formal_parameter) { const char* arg_name = dwarf_name(&child); if (!arg_name) arg_name = "<unknown>"; // 这里简化处理:假设参数在栈上,基于x86-64 System V ABI // 实际需要根据DWARF的location信息判断参数位置(栈/寄存器) uintptr_t arg_value = 0; // 示例:从栈帧指针偏移处读取参数值(仅作演示,需根据实际调用约定调整) if (frame_ptr != 0) { // 跳过返回地址,第一个栈上参数在rbp+16位置(x86-64) static int arg_idx = 0; arg_value = *(uintptr_t*)(frame_ptr + 16 + arg_idx * sizeof(uintptr_t)); arg_idx++; } if (!first_arg) args_ss << ", "; args_ss << arg_name << "=0x" << std::hex << arg_value; first_arg = false; } } while (dwarf_siblingof(&child, &child) == 0); } args_ss << ")"; return args_ss.str(); } // 修改后的debug_info函数 std::string debug_info(Dwfl* dwfl, void* ip, uintptr_t frame_ptr) { uintptr_t ip2 = reinterpret_cast<uintptr_t>(ip); Dwfl_Module* module = dwfl_addrmodule(dwfl, ip2); char const* name = dwfl_module_addrname(module, ip2); std::string function_name = name ? demangle(name) : "<unknown>"; // 添加参数打印 std::string args_str = get_function_args(dwfl, ip, frame_ptr); int line_num = -1; char const* file_name; if (Dwfl_Line* dwfl_line = dwfl_module_getsrc(module, ip2)) { Dwarf_Addr addr; file_name = dwfl_lineinfo(dwfl_line, &addr, &line_num, nullptr, nullptr, nullptr); } std::stringstream ss; ss << std::setw(16)<<std::setfill('0') <<ip << ' ' << function_name << args_str; if (file_name) ss << " at " << file_name << ':' << line_num; ss << std::endl; return ss.str(); }
注意事项
- 调用约定适配:不同架构(x86、x86-64、ARM)的调用约定不同,参数可能存放在寄存器或栈上,需要根据DWARF的
DW_AT_location信息精准定位参数位置。 - 调试信息完整性:必须用
-ggdb3编译,保留完整DWARF调试信息;开启优化(如-O1及以上)可能导致参数被优化或栈帧布局变化,无法正确读取参数。 - 栈帧指针获取:需要结合libunwind获取栈帧指针(RBP/x86-64),替换原代码中
backtrace仅获取指令地址的逻辑,才能准确读取栈上参数。
内容的提问来源于stack exchange,提问作者xiaoqiang-wang
相关产品推荐
相关产品推荐

