文章目录
- 一、内核符号打印函数`__print_symbol`
-
- [1. 函数原型和参数](#1. 函数原型和参数)
- [2. 缓冲区定义](#2. 缓冲区定义)
- [3. 符号查找](#3. 符号查找)
- [4. 格式化输出](#4. 格式化输出)
- [5. 格式说明](#5. 格式说明)
- [6. 最终输出](#6. 最终输出)
- 二、模块文本地址查找函数`__module_text_address`
-
- [1. 函数原型和目的](#1. 函数原型和目的)
- [2. 辅助函数:`within`](#2. 辅助函数:
within
) - [3. 模块链表遍历](#3. 模块链表遍历)
- [4. 地址范围检查](#4. 地址范围检查)
- 三、内核文本地址判断函数`__kernel_text_address`
- 四、内核栈回溯函数`print_context_stack`
-
- [1. `print_symbol` 宏](#1.
print_symbol
宏) - [2. `valid_stack_ptr` 函数](#2.
valid_stack_ptr
函数) - [3. `print_context_stack` 函数](#3.
print_context_stack
函数) - [4. 两种方法的对比](#4. 两种方法的对比)
- [1. `print_symbol` 宏](#1.
- 五、内核堆栈跟踪函数`show_trace`
-
- [1. `show_trace` 函数分析](#1.
show_trace
函数分析) - [2. 任务处理逻辑](#2. 任务处理逻辑)
- [3. 多上下文堆栈遍历](#3. 多上下文堆栈遍历)
- [4. `dump_stack` 包装函数](#4.
dump_stack
包装函数) - [5. 内核线程堆栈管理](#5. 内核线程堆栈管理)
- 6.跨上下文跟踪
- [1. `show_trace` 函数分析](#1.
参考博客
内核模块符号查找函数
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缓冲区:
这是一个精确计算的缓冲区,避免内存浪费和溢出:
组成部分:
- 基础格式字符串 :
sizeof("%s+%#lx/%#lx [%s]")
- 符号名称 :
KSYM_NAME_LEN
(通常128) - 两个地址数值 :
2*(BITS_PER_LONG*3/10)
BITS_PER_LONG
:长整型的位数(32或64)*3/10
:计算一个long
类型十六进制数的最大字符长度 (包括0x
前缀)- 64位系统:
2*(64*3/10) = 2*19 = 38
- 模块名称 :
MODULE_NAME_LEN
(通常64) - 结束符 :
+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内核的栈回溯功能,用于在系统崩溃或调试时显示函数调用链
1. print_symbol
宏
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
:为安全余量,防止访问栈外
3. print_context_stack
函数
这个函数有两种实现,取决于是否启用帧指针
配置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
+------------------+
| 局部变量 |
低地址 +------------------+
回溯过程:
- 从当前
ebp
获取返回地址 (ebp + 4
) - 打印符号信息
- 移动到上一个栈帧 (
*ebp
) - 重复直到栈边界
配置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_trace
和dump_stack
,开发者能够在系统异常时获得完整的执行上下文信息,是Linux内核可调试性的核心组件之一