背景
Linux内核态和用户态如何打印函数的调用栈。
方法
内核态
直接调用 dump_stack(); 函数
用户态
定义一个函数,然后使用的地方调用。利用backtrace相关功能
#include <execinfo.h> // 提供 backtrace()、backtrace_symbols()
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
// 模拟用户态 dump_stack,打印调用栈
void user_dump_stack(void) {
void *callstack[100]; // 存储调用栈地址,100层足够覆盖大多数场景
int stack_depth; // 实际获取到的调用栈深度
// 1. 获取调用栈地址:填充 callstack 数组,返回栈深度
// 参数1:存储地址的数组;参数2:数组最大容量
stack_depth = backtrace(callstack, sizeof(callstack)/sizeof(callstack[0]));
if (stack_depth <= 0) {
printf("Failed to get callstack\n");
return;
}
// 2. 将地址转换为可读符号(函数名、地址等)
// 返回值:动态分配的字符串数组,需手动 free
char **symbols = backtrace_symbols(callstack, stack_depth);
if (symbols == NULL) {
printf("Failed to resolve symbols\n");
return;
}
// 3. 打印调用栈(从0到stack_depth-1,0是当前函数 user_dump_stack)
printf("User态调用栈(共 %d 层):\n", stack_depth);
for (int i = 0; i < stack_depth; i++) {
printf("[%2d] %s\n", i, symbols[i]);
}
// 4. 释放动态分配的符号数组
free(symbols);
}
调用位置直接call函数:

效果:

函数未解析出来,添加-g管理,依然无效

使用addr2line来解析方式
基础款
#include <execinfo.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
#include <string.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <elf.h>
// 获取共享库的加载基址(用于计算相对地址)
unsigned long get_module_base(const char *module_name) {
FILE *fp = fopen("/proc/self/maps", "r");
if (!fp) {
perror("fopen /proc/self/maps failed");
return 0;
}
char line[1024];
unsigned long base = 0;
while (fgets(line, sizeof(line), fp)) {
// 格式示例:7f3813e8d000-7f3813ebf000 r-xp 00000000 08:01 123456 /lib64/libmlx5.so.1
if (strstr(line, module_name)) {
// 提取起始地址(十六进制)
sscanf(line, "%lx-", &base);
break;
}
}
fclose(fp);
return base;
}
// 解析地址对应的函数名和行号(修正地址计算)
void resolve_address(void *addr) {
unsigned long abs_addr = (unsigned long)addr;
char module_name[256] = {0};
unsigned long base_addr = 0;
// 1. 确定地址所属的模块(可执行文件或共享库)
FILE *fp = fopen("/proc/self/maps", "r");
if (fp) {
char line[1024];
while (fgets(line, sizeof(line), fp)) {
unsigned long start, end;
if (sscanf(line, "%lx-%lx", &start, &end) == 2) {
if (abs_addr >= start && abs_addr < end) {
// 提取模块路径
char *path = strchr(line, '/');
if (path) {
// 去掉行尾的换行符
path[strcspn(path, "\n")] = '\0';
strncpy(module_name, path, sizeof(module_name)-1);
base_addr = start; // 模块加载基址
}
break;
}
}
}
fclose(fp);
}
// 2. 计算相对地址(绝对地址 - 基地址)
unsigned long rel_addr = abs_addr - base_addr;
// 3. 构建addr2line命令(使用模块路径和相对地址)
char cmd[2048];
if (module_name[0] == '\0') {
// 未找到模块,使用当前程序路径
readlink("/proc/self/exe", module_name, sizeof(module_name)-1);
rel_addr = abs_addr; // 可执行文件的地址无需减基址
}
snprintf(cmd, sizeof(cmd),
"addr2line -e %s -f -C 0x%lx", // -e指定模块,-f显示函数名
module_name, rel_addr);
// 4. 执行命令并输出结果
FILE *addr_fp = popen(cmd, "r");
if (!addr_fp) {
printf(" 解析失败:无法执行addr2line\n");
return;
}
char buffer[1024];
printf(" 模块: %s (基址: 0x%lx, 相对地址: 0x%lx)\n",
module_name, base_addr, rel_addr);
printf(" 解析结果:");
while (fgets(buffer, sizeof(buffer), addr_fp) != NULL) {
printf("%s", buffer);
}
pclose(addr_fp);
}
// 调用栈打印函数
void user_dump_stack(void) {
void *callstack[100];
int stack_depth = backtrace(callstack, sizeof(callstack)/sizeof(callstack[0]));
if (stack_depth <= 0) {
printf("获取调用栈失败\n");
return;
}
printf("User态调用栈(共 %d 层):\n", stack_depth);
for (int i = 0; i < stack_depth; i++) {
printf("[%2d] 绝对地址: 0x%lx\n", i, (unsigned long)callstack[i]);
resolve_address(callstack[i]);
}
}
效果:

紧凑风格的:
#include <execinfo.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <fcntl.h>
#include <sys/wait.h>
// 从路径中提取文件名(去掉目录部分)
static const char* get_filename(const char* path) {
const char* last_slash = strrchr(path, '/');
return last_slash ? last_slash + 1 : path;
}
// 解析地址并单行输出调用栈信息
static void print_stack_frame(void* addr, int frame_num) {
unsigned long abs_addr = (unsigned long)addr;
char module_path[256] = {0};
unsigned long base_addr = 0, rel_addr = 0;
char func_name[256] = "??";
char line_info[128] = "??:0";
// 1. 从/proc/self/maps获取模块信息和基地址
FILE* maps = fopen("/proc/self/maps", "r");
if (maps) {
char line[1024];
while (fgets(line, sizeof(line), maps)) {
unsigned long start, end;
if (sscanf(line, "%lx-%lx", &start, &end) == 2 &&
abs_addr >= start && abs_addr < end) {
// 提取模块路径和基地址
char* path = strchr(line, '/');
if (path) {
path[strcspn(path, " \n")] = '\0';
strncpy(module_path, path, sizeof(module_path)-1);
}
base_addr = start;
rel_addr = abs_addr - base_addr;
break;
}
}
fclose(maps);
}
// 2. 调用addr2line获取函数名和行号
if (module_path[0] == '\0') {
readlink("/proc/self/exe", module_path, sizeof(module_path)-1);
rel_addr = abs_addr; // 可执行文件使用绝对地址
}
char cmd[1024];
snprintf(cmd, sizeof(cmd),
"addr2line -e %s -f -C 0x%lx 2>/dev/null",
module_path, rel_addr);
FILE* fp = popen(cmd, "r");
if (fp) {
// 读取函数名
if (fgets(func_name, sizeof(func_name), fp)) {
func_name[strcspn(func_name, "\n")] = '\0';
// 读取行号信息
if (fgets(line_info, sizeof(line_info), fp)) {
line_info[strcspn(line_info, "\n")] = '\0';
}
}
pclose(fp);
}
// 3. 单行输出调用栈信息(格式:[帧号] 模块名+偏移 (函数名:行号) 绝对地址)
printf("[%2d] %s+0x%lx (%s:%s) @0x%lx\n",
frame_num,
get_filename(module_path), // 只显示模块文件名
rel_addr,
func_name,
line_info,
abs_addr);
}
// 紧凑风格的调用栈打印(类似dump_stack)
void user_dump_stack(void) {
void* callstack[100];
int depth = backtrace(callstack, sizeof(callstack)/sizeof(callstack[0]));
printf("User stack trace: (total %d frames)\n", depth);
for (int i = 0; i < depth; i++) {
print_stack_frame(callstack[i], i);
}
}
效果:

紧凑+去掉文件dirname的(推荐)
#include <execinfo.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
// 从路径中提取文件名(去掉目录部分)
static const char* get_basename(const char* path) {
const char* last_slash = strrchr(path, '/');
return last_slash ? last_slash + 1 : path;
}
// 解析地址并生成单行调用栈信息
static void print_stack_frame(void* addr, int frame_num) {
unsigned long abs_addr = (unsigned long)addr;
char module_path[256] = {0};
char source_file[256] = "??";
char func_name[256] = "??";
char line_num[32] = "0";
unsigned long base_addr = 0, rel_addr = 0;
// 1. 获取模块路径和基地址
FILE* maps = fopen("/proc/self/maps", "r");
if (maps) {
char line[1024];
while (fgets(line, sizeof(line), maps)) {
unsigned long start, end;
if (sscanf(line, "%lx-%lx", &start, &end) == 2 &&
abs_addr >= start && abs_addr < end) {
char* path = strchr(line, '/');
if (path) {
path[strcspn(path, " \n")] = '\0';
strncpy(module_path, path, sizeof(module_path)-1);
}
base_addr = start;
rel_addr = abs_addr - base_addr;
break;
}
}
fclose(maps);
}
// 2. 若未找到模块,使用当前程序路径
if (module_path[0] == '\0') {
readlink("/proc/self/exe", module_path, sizeof(module_path)-1);
rel_addr = abs_addr;
}
// 3. 调用addr2line解析函数名和源文件信息
char cmd[1024];
snprintf(cmd, sizeof(cmd),
"addr2line -e %s -f -C 0x%lx 2>/dev/null",
module_path, rel_addr);
FILE* fp = popen(cmd, "r");
if (fp) {
// 读取函数名
if (fgets(func_name, sizeof(func_name), fp)) {
func_name[strcspn(func_name, "\n")] = '\0';
}
// 读取源文件路径并提取文件名
if (fgets(source_file, sizeof(source_file), fp)) {
source_file[strcspn(source_file, "\n")] = '\0';
// 分离文件名和行号(格式如 "file.c:123")
char* colon = strchr(source_file, ':');
if (colon) {
*colon = '\0'; // 截断为文件名
strncpy(line_num, colon + 1, sizeof(line_num)-1);
}
// 只保留文件名(去掉路径)
const char* basename = get_basename(source_file);
strncpy(source_file, basename, sizeof(source_file)-1);
}
pclose(fp);
}
// 4. 单行输出(帧号 模块+偏移 (函数名@文件名:行号) 绝对地址)
printf("[%2d] %s+0x%lx (%s@%s:%s) @0x%lx\n",
frame_num,
get_basename(module_path), // 模块文件名
rel_addr,
func_name, // 函数名
source_file, // 源文件名(无路径)
line_num, // 行号
abs_addr); // 绝对地址
}
// 优化后的调用栈打印函数
void user_dump_stack(void) {
void* callstack[100];
int depth = backtrace(callstack, sizeof(callstack)/sizeof(callstack[0]));
printf("User stack trace: (%d frames)\n", depth);
for (int i = 0; i < depth; i++) {
print_stack_frame(callstack[i], i);
}
}
效果:

综述
dump调用栈对于分析问题效果很好,内核态用户态可以常用。
Linux内核与用户态调用栈打印方法
2154

被折叠的 条评论
为什么被折叠?



