【微知】Linux内核态和用户态如何打印函数的调用栈?( dump_stack)

Linux内核与用户态调用栈打印方法

背景

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 内核中可用于分析调用栈的调试工具函数 在 Linux 内核中,除了 `dump_stack()` 之外,还有多种调试工具函数可用于分析调用栈,帮助开发者定位问题、分析执行路径以及理解系统行为。 #### `oops` `panic` 日志 当内核遇到严重错误时,会触发 `oops` 或 `panic`,并输出详细的错误信息,包括调用栈。例如,`oops` 会打印出当前执行路径的调用栈信息,帮助开发者分析问题。这些信息通常包含寄存器状态、堆栈内容以及函数调用链等关键数据[^3]。 #### `kprobe` `uprobe` `kprobe` 是一种动态调试机制,允许在内核函数中插入探针,从而在运行时捕获函数调用栈。`uprobe` 则是用户空间的类似机制。通过 `kprobe`,开发者可以在特定函数入口或任意地址插入探针,记录调用栈并分析函数调用关系。例如,以下代码展示了如何在内核中注册一个 `kprobe`: ```c #include <linux/kprobes.h> static struct kprobe kp = { .symbol_name = "target_function", }; static int handler_pre(struct kprobe *p, struct pt_regs *regs) { dump_stack(); return 0; } static int __init kprobe_init(void) { kp.pre_handler = handler_pre; register_kprobe(&kp); return 0; } static void __exit kprobe_exit(void) { unregister_kprobe(&kp); } module_init(kprobe_init); module_exit(kprobe_exit); ``` #### `perf` 工具 `perf` 是一个强大的性能分析工具,支持采样跟踪调用栈。它可以通过 `perf record` `perf report` 等命令记录并分析调用栈,适用于性能瓶颈分析。例如,以下命令可以记录调用栈并生成报告: ```bash perf record -g -a sleep 10 perf report --call-graph ``` 需要注意的是,由于 `perf` 是基于采样的机制,有时收集的样本可能不足,导致分析结果不准确。因此,建议延长采样时间以获得更全面的数据[^5]。 #### `ftrace` 跟踪工具 `ftrace` 是 Linux 内核内置的跟踪工具,支持多种跟踪模式,包括函数调用栈分析。通过配置 `/sys/kernel/debug/tracing/options/stacktrace`,可以启用调用栈记录功能。例如: ```bash echo 1 > /sys/kernel/debug/tracing/options/stacktrace echo function > /sys/kernel/debug/tracing/current_tracer cat /sys/kernel/debug/tracing/trace ``` #### `sysrq` 触发调用栈打印 通过 `sysrq` 键盘组合,可以手动触发调用栈打印。例如,在系统运行时按下 `Alt + SysRq + t`,可以在控制台输出所有线程的调用栈信息。该功能对于分析死锁或长时间阻塞问题非常有用。 #### `kallsyms` 解析函数名 `kallsyms` 是内核符号表工具,可以将地址转换为函数名。结合 `dump_stack()` 的输出,可以使用 `kallsyms_lookup` 函数解析地址对应的函数名。例如: ```c unsigned long address = ...; char buffer[128]; sprint_symbol(buffer, address); printk(KERN_INFO "Function: %s\n", buffer); ``` #### `crash` 工具 `crash` 是一个用于分析内核崩溃转储文件的工具,支持查看调用栈、寄存器状态、内存映像等信息。它可以结合 `kdump` 生成的转储文件进行深入分析,尤其适用于无法直接调试的生产环境问题。 ###
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值