Linux中内核堆栈跟踪函数dump_stack的实现

文章目录

参考博客

内核模块符号查找函数get_ksymbol: https://blog.csdn.net/weixin_51019352/article/details/152290491

内核数据结构链表list: https://blog.csdn.net/weixin_51019352/article/details/152038955

container_of宏的实现: https://blog.csdn.net/weixin_51019352/article/details/151870691

内核符号查找核心函数kallsyms_lookup: https://blog.csdn.net/weixin_51019352/article/details/152315179

一、内核符号打印函数__print_symbol

c 复制代码
/* 源码文件:kernel/kallsyms.c */
void __print_symbol(const char *fmt, unsigned long address)
{
        char *modname;
        const char *name;
        unsigned long offset, size;
        char namebuf[KSYM_NAME_LEN+1];
        char buffer[sizeof("%s+%#lx/%#lx [%s]") + KSYM_NAME_LEN +
                    2*(BITS_PER_LONG*3/10) + MODULE_NAME_LEN + 1];

        name = kallsyms_lookup(address, &size, &offset, &modname, namebuf);

        if (!name)
                sprintf(buffer, "0x%lx", address);
        else {
                if (modname)
                        sprintf(buffer, "%s+%#lx/%#lx [%s]", name, offset,
                                size, modname);
                else
                        sprintf(buffer, "%s+%#lx/%#lx", name, offset, size);
        }
        printk(fmt, buffer);
}

这个函数是内核中用于将地址转换为可读符号格式并打印的核心函数,广泛应用于调试信息和错误报告

1. 函数原型和参数

c 复制代码
void __print_symbol(const char *fmt, unsigned long address)

参数

  • fmt:格式字符串,用于控制最终输出的格式
  • address:要解析和打印的内存地址

2. 缓冲区定义

c 复制代码
char namebuf[KSYM_NAME_LEN+1];
char buffer[sizeof("%s+%#lx/%#lx [%s]") + KSYM_NAME_LEN +
            2*(BITS_PER_LONG*3/10) + MODULE_NAME_LEN + 1];

namebuf缓冲区

  • 大小:KSYM_NAME_LEN + 1
  • 用途:存储符号名称
  • 额外+1确保字符串终止符空间

buffer缓冲区

这是一个精确计算的缓冲区,避免内存浪费和溢出:

组成部分

  1. 基础格式字符串sizeof("%s+%#lx/%#lx [%s]")
  2. 符号名称KSYM_NAME_LEN(通常128)
  3. 两个地址数值2*(BITS_PER_LONG*3/10)
    • BITS_PER_LONG:长整型的位数(32或64)
    • *3/10:计算一个 long 类型十六进制数的最大字符长度 (包括 0x 前缀)
    • 64位系统:2*(64*3/10) = 2*19 = 38
  4. 模块名称MODULE_NAME_LEN(通常64)
  5. 结束符+1

3. 符号查找

c 复制代码
name = kallsyms_lookup(address, &size, &offset, &modname, namebuf);

查找结果

  • name:符号名称(找到时为字符串,未找到为NULL)
  • size:符号大小
  • offset:地址在符号内的偏移量
  • modname:模块名称(内核核心代码为NULL)

4. 格式化输出

情况1:未找到符号

c 复制代码
if (!name)
    sprintf(buffer, "0x%lx", address);

直接输出地址的十六进制表示

情况2:内核核心符号

c 复制代码
else {
    if (modname)
        // 模块符号
    else
        sprintf(buffer, "%s+%#lx/%#lx", name, offset, size);
}

输出示例do_sys_open+0x45/0x100

情况3:模块符号

c 复制代码
if (modname)
    sprintf(buffer, "%s+%#lx/%#lx [%s]", name, offset, size, modname);

输出示例nvidia_ioctl+0x50/0x80 [nvidia]

5. 格式说明

符号格式符号名称+偏移量/大小 [模块名]

实际应用示例

text 复制代码
// 内核核心函数
do_sys_open+0x45/0x100
↑          ↑   ↑
函数名     偏移 函数大小

// 模块函数  
nvidia_ioctl+0x50/0x80 [nvidia]
↑             ↑   ↑     ↑
函数名        偏移 大小  模块名

6. 最终输出

c 复制代码
printk(fmt, buffer);

使用传入的格式字符串来包装结果:

二、模块文本地址查找函数__module_text_address

c 复制代码
/* 源码文件:kernel/module.c */
static inline int within(unsigned long addr, void *start, unsigned long size)
{
	return ((void *)addr >= start && (void *)addr < start + size);
}

struct module *__module_text_address(unsigned long addr)
{
        struct module *mod;

        list_for_each_entry(mod, &modules, list)
                if (within(addr, mod->module_init, mod->init_text_size)
                    || within(addr, mod->module_core, mod->core_text_size))
                        return mod;
        return NULL;
}

这个函数用于确定一个地址是否属于某个内核模块的代码段,并返回对应的模块结构

1. 函数原型和目的

c 复制代码
struct module *__module_text_address(unsigned long addr)

功能:查找给定地址所属的内核模块

参数

  • addr:要查询的内存地址

返回值

  • 成功:指向struct module的指针
  • 失败:NULL(地址不属于任何模块)

2. 辅助函数:within

c 复制代码
static inline int within(unsigned long addr, void *start, unsigned long size)
{
	return ((void *)addr >= start && (void *)addr < start + size);
}

作用:检查地址是否在指定的内存区域内

区间 :左闭右开 [start, start + size)

  • 包含起始地址 start
  • 不包含结束地址 start + size

3. 模块链表遍历

c 复制代码
list_for_each_entry(mod, &modules, list)

Linux内核链表机制

c 复制代码
struct module {
    struct list_head list;    // 链表节点
    // ... 其他模块信息
};

static LIST_HEAD(modules);    // 全局模块链表头

宏展开效果

c 复制代码
// 相当于:
for (mod = list_entry(&modules->next, struct module, list); 
     &mod->list != &modules; 
     mod = list_entry(mod->list.next, list))

4. 地址范围检查

c 复制代码
if (within(addr, mod->module_init, mod->init_text_size)
    || within(addr, mod->module_core, mod->core_text_size))
    return mod;

初始化代码段

c 复制代码
within(addr, mod->module_init, mod->init_text_size)
  • mod->module_init:模块初始化代码段的起始地址
  • mod->init_text_size:初始化代码段的大小
  • 包含标记为__init的函数

核心代码段

c 复制代码
within(addr, mod->module_core, mod->core_text_size)
  • mod->module_core:模块核心代码段的起始地址
  • mod->core_text_size:核心代码段的大小
  • 包含模块的主要运行时代码

三、内核文本地址判断函数__kernel_text_address

c 复制代码
/* 源码文件:kernel/extable.c */
static int core_kernel_text(unsigned long addr)
{
        if (addr >= (unsigned long)_stext &&
            addr <= (unsigned long)_etext)
                return 1;

        if (addr >= (unsigned long)_sinittext &&
            addr <= (unsigned long)_einittext)
                return 1;
        return 0;
}

int __kernel_text_address(unsigned long addr)
{
	if (core_kernel_text(addr))
		return 1;
	return __module_text_address(addr) != NULL;
}

这两个函数组成了一个完整的内核地址类型判断系统,用于确定一个地址是否指向有效的内核或模块代码

  • core_kernel_text
    • _stext:内核主代码段起始地址
    • _etext:内核主代码段结束地址
    • 区间 :闭区间 [_stext, _etext]
    • 包含内核的主要运行时代码
    • _sinittext:初始化代码段起始地址
    • _einittext:初始化代码段结束地址
    • 区间 :闭区间 [_sinittext, _einittext]
    • 包含标记为__init的初始化函数
  • __kernel_text_address
    • 第一级 :检查是否为核心内核代码
      • 快速检查,纯地址比较
      • 如果匹配立即返回
    • 第二级 :检查是否为模块代码
      • 较慢的模块链表遍历
      • 只有核心检查失败时才执行

四、内核栈回溯函数print_context_stack

c 复制代码
/* 源码文件:include/linux/kallsyms.h */
#define print_symbol(fmt, addr)                 \
do {                                            \
        __check_printsym_format(fmt, "");       \
        __print_symbol(fmt, addr);              \
} while(0)

/* 源码文件:arch/i386/kernel/traps.c */
static inline int valid_stack_ptr(struct thread_info *tinfo, void *p)
{
	return	p > (void *)tinfo &&
		p < (void *)tinfo + THREAD_SIZE - 3;
}

static inline unsigned long print_context_stack(struct thread_info *tinfo,
                                unsigned long *stack, unsigned long ebp)
{
        unsigned long addr;

#ifdef  CONFIG_FRAME_POINTER
        while (valid_stack_ptr(tinfo, (void *)ebp)) {
                addr = *(unsigned long *)(ebp + 4);
                printk(" [<%08lx>] ", addr);
                print_symbol("%s", addr);
                printk("\n");
                ebp = *(unsigned long *)ebp;
        }
#else
        while (valid_stack_ptr(tinfo, stack)) {
                addr = *stack++;
                if (__kernel_text_address(addr)) {
                        printk(" [<%08lx>]", addr);
                        print_symbol(" %s", addr);
                        printk("\n");
                }
        }
#endif
        return ebp;
}

这段代码实现了Linux内核的栈回溯功能,用于在系统崩溃或调试时显示函数调用链

c 复制代码
#define print_symbol(fmt, addr)                 \
do {                                            \
        __check_printsym_format(fmt, "");       \
        __print_symbol(fmt, addr);              \
} while(0)

宏设计分析

do { ... } while(0) 技巧

  • 创建一个独立的代码块
  • 允许在if/else语句中安全使用

__check_printsym_format(fmt, "")

  • 编译时格式字符串检查
  • 确保fmt包含%s占位符
  • 防止格式字符串错误

2. valid_stack_ptr 函数

c 复制代码
static inline int valid_stack_ptr(struct thread_info *tinfo, void *p)
{
    return p > (void *)tinfo &&
           p < (void *)tinfo + THREAD_SIZE - 3;
}

检查下界p > (void *)tinfo

  • 确保指针在thread_info结构之后
  • thread_info位于栈底

检查上界p < (void *)tinfo + THREAD_SIZE - 3

  • 确保指针在栈空间内
  • -3:为安全余量,防止访问栈外

这个函数有两种实现,取决于是否启用帧指针

配置1:启用帧指针 (CONFIG_FRAME_POINTER)

c 复制代码
#ifdef CONFIG_FRAME_POINTER
while (valid_stack_ptr(tinfo, (void *)ebp)) {
    addr = *(unsigned long *)(ebp + 4);    // 获取返回地址
    printk(" [<%08lx>] ", addr);
    print_symbol("%s", addr);
    printk("\n");
    ebp = *(unsigned long *)ebp;           // 移动到上一个栈帧
}

基于帧指针的栈回溯

栈帧结构

text 复制代码
高地址 +------------------+
      |    参数n          |
      +------------------+
      |    ...           |
      +------------------+
      |    参数1          |
      +------------------+
      |    返回地址        | ← ebp + 4
      +------------------+
      |    保存的ebp       | ← 当前ebp
      +------------------+
      |    局部变量        |
低地址 +------------------+

回溯过程

  1. 从当前ebp获取返回地址 (ebp + 4)
  2. 打印符号信息
  3. 移动到上一个栈帧 (*ebp)
  4. 重复直到栈边界

配置2:禁用帧指针

c 复制代码
#else
while (valid_stack_ptr(tinfo, stack)) {
    addr = *stack++;
    if (__kernel_text_address(addr)) {
        printk(" [<%08lx>]", addr);
        print_symbol(" %s", addr);
        printk("\n");
    }
}
#endif

基于栈扫描的回溯

  • 线性扫描栈空间
  • 检查每个值是否为有效代码地址
  • 只打印有效的内核文本地址

可能误报的情况

复制代码
// 栈中的巧合值正好落在代码地址范围内
int data = 0x81234567;  // 巧合等于某个函数地址
// 会被错误识别为返回地址

可能漏报的情况

复制代码
// 返回地址被优化或破坏
// 尾调用优化可能不保存返回地址
// 内联函数没有独立的返回地址

4. 两种方法的对比

特性 帧指针方法 栈扫描方法
准确性 高(精确调用链) 中(可能有误报)
性能 快(O(n)帧数) 慢(O(栈大小))
内存开销 每个栈帧保存ebp 无额外开销
调试信息 完整调用链 可能缺失中间帧

五、内核堆栈跟踪函数show_trace

c 复制代码
/* 源码文件:arch/i386/kernel/traps.c */
void show_trace(struct task_struct *task, unsigned long * stack)
{
        unsigned long ebp;

        if (!task)
                task = current;

        if (task == current) {
                /* Grab ebp right from our regs */
                asm ("movl %%ebp, %0" : "=r" (ebp) : );
        } else {
                /* ebp is the last reg pushed by switch_to */
                ebp = *(unsigned long *) task->thread.esp;
        }

        while (1) {
                struct thread_info *context;
                context = (struct thread_info *)
                        ((unsigned long)stack & (~(THREAD_SIZE - 1)));
                ebp = print_context_stack(context, stack, ebp);
                stack = (unsigned long*)context->previous_esp;
                if (!stack)
                        break;
                printk(" =======================\n");
        }
}

void dump_stack(void)
{
	unsigned long stack;
	show_trace(current, &stack);
}

这段代码实现了Linux内核的完整堆栈跟踪功能,支持当前任务和任意任务的堆栈分析

1. show_trace 函数分析

c 复制代码
void show_trace(struct task_struct *task, unsigned long *stack)

参数

  • task:要分析的任务,NULL表示当前任务
  • stack:起始堆栈指针

2. 任务处理逻辑

c 复制代码
if (!task)
    task = current;

if (task == current) {v
    /* Grab ebp right from our regs */
    asm ("movl %%ebp, %0" : "=r" (ebp) : );
} else {
    /* ebp is the last reg pushed by switch_to */
    ebp = *(unsigned long *) task->thread.esp;
}

当前任务处理

c 复制代码
asm ("movl %%ebp, %0" : "=r" (ebp) : );
  • 使用内联汇编直接读取当前任务的ebp寄存器
  • 获取最准确的当前栈帧指针

其他任务处理

c 复制代码
ebp = *(unsigned long *) task->thread.esp;
  • 从任务的线程结构中获取保存的栈指针
  • thread.esp保存了任务被切换出去时的栈指针

3. 多上下文堆栈遍历

c 复制代码
while (1) {
    struct thread_info *context;
    context = (struct thread_info *)
            ((unsigned long)stack & (~(THREAD_SIZE - 1)));
    ebp = print_context_stack(context, stack, ebp);
    stack = (unsigned long*)context->previous_esp;
    if (!stack)
        break;
    printk(" =======================\n");
}

计算thread_info

c 复制代码
context = (struct thread_info *)((unsigned long)stack & (~(THREAD_SIZE - 1)));
  • 通过栈指针找到对应的thread_info
  • THREAD_SIZE - 1是掩码,例如:0x3FFF(16KB栈)
  • stack & ~0x3FFF得到栈的基地址

打印当前上下文堆栈

c 复制代码
ebp = print_context_stack(context, stack, ebp);
  • 调用前面分析的堆栈打印函数
  • 返回更新后的ebp

切换到上一个上下文

c 复制代码
stack = (unsigned long*)context->previous_esp;
  • 通过previous_esp找到上一个执行上下文的栈
  • 实现跨上下文边界的堆栈跟踪

4. dump_stack 包装函数

c 复制代码
void dump_stack(void)
{
    unsigned long stack;
    show_trace(current, &stack);
}

简化接口

  • 为当前任务提供简单的堆栈转储功能
  • &stack: 获取当前栈指针

5. 内核线程堆栈管理

线程堆栈布局

text 复制代码
高地址 +------------------+
      |    栈保护页       |
      +------------------+
      |    线程栈         | 
      |    ...           |
      |    当前栈帧       | ← stack (传入参数)
      |    ...           |
      +------------------+
      |   thread_info    | ← context (栈底)
低地址 +------------------+

thread_info 结构相关:

c 复制代码
struct thread_info {
    struct task_struct *task;      // 指向task_struct
    unsigned long   flags;         // 线程标志
    unsigned long   previous_esp;  // 上一个上下文的栈指针
    // ...
};

6.跨上下文跟踪

上下文切换场景

text 复制代码
场景:中断 -> 软中断 -> 用户进程

堆栈跟踪:
=======================
[中断上下文堆栈]
 [<ffffffff81012345>] do_IRQ+0x45/0x100
 [<ffffffff81012400>] common_interrupt+0x20/0x40
=======================  
[软中断上下文堆栈]
 [<ffffffff81012500>] run_ksoftirqd+0x30/0x50
 [<ffffffff81012600>] smp_apic_timer_interrupt+0x5/0x10
=======================
[进程上下文堆栈]
 [<ffffffff81012700>] schedule+0x10/0x20
 [<ffffffff81012800>] do_syscall_64+0x10/0x20

通过show_tracedump_stack,开发者能够在系统异常时获得完整的执行上下文信息,是Linux内核可调试性的核心组件之一

相关推荐
早起的年轻人2 小时前
CentOS 8系统盘大文件查找方法
linux·运维·centos
心灵宝贝2 小时前
Linux CentOS 7 安装 zip-3.0-11.el7.x86_64.rpm 详细步骤(命令行教程)(附安装包)
linux·运维·centos
挺6的还2 小时前
50.Reactor反应堆模式
linux
Thexhy2 小时前
在Centos的Linux中安装Windows10系统
linux·运维·经验分享·学习·centos
Lzc7743 小时前
Linux的Socket编程之UDP
linux·socket编程之udp
zimoyin4 小时前
Linux 程序使用 STDOUT 打印日志导致程序“假死”?一次线上 Bug 的深度排查与解决
linux·运维·bug
杜子不疼.4 小时前
【Linux】操作系统的认识
linux·运维·服务器
Dovis(誓平步青云)4 小时前
《Gdb 调试实战指南:不同风格于VS下的一种调试模式》
linux·运维·服务器
小-黯4 小时前
Ubuntu离线安装软件包
linux·运维·ubuntu