内核中提供 dump_stack 函数,能够用来回溯打印调用栈信息。oops 时打印的栈回溯调用信息也是通过调用此函数来完成的。
dump_stack 函数如何使用?
使用 dump_stack 函数时不需要添加头文件,直接调用即可。下面我通过修改一个 proc 文件的内核模块 demo 来演示。
我在自己实现的 proc 文件的写函数中添加了一行对 dump_stack 函数的调用。这个函数会在我向 proc 文件写入数据时被调用。
相关的代码如下:
static ssize_t my_proc_write(struct file *filp, const char __user *buffer, size_t count, loff_t *pos)
{
int i;
char *data = PDE_DATA(file_inode(filp));
dump_stack();
if (count > DATA_SIZE) {
return -EFAULT;
}
...
}
static struct file_operations proc_fops = {
.read = my_proc_read,
.write = my_proc_write,
};
向 proc 文件中写入数据后,dmesg 查看,发现有如下内容:
[25109.178081] Call Trace:
[25109.178096] dump_stack+0x5c/0x80
[25109.178104] my_proc_write+0x27/0x44 [example_of_proc]
[25109.178110] proc_reg_write+0x39/0x60
[25109.178115] vfs_write+0xa5/0x1a0
[25109.178117] ksys_write+0x57/0xd0
[25109.178123] do_syscall_64+0x53/0x110
[25109.178132] entry_SYSCALL_64_after_hwframe+0x44/0xa9
[25109.178134] RIP: 0033:0x7f454f355504
[25109.178137] Code: 00 f7 d8 64 89 02 48 c7 c0 ff ff ff ff eb b3 0f 1f 80 00 00 00 00 48 8d 05 f9 61 0d 00 8b 00 85 c0 75 13 b8 01 00 00 00 0f 05 <48> 3d 00 f0 ff ff 77 54 c3 0f 1f 00 41 54 49 89 d4 55 48 89 f5 53
[25109.178138] RSP: 002b:00007ffe650d2418 EFLAGS: 00000246 ORIG_RAX: 0000000000000001
[25109.178140] RAX: ffffffffffffffda RBX: 000000000000000a RCX: 00007f454f355504
[25109.178141] RDX: 000000000000000a RSI: 0000563e11c7dac0 RDI: 0000000000000001
[25109.178142] RBP: 0000563e11c7dac0 R08: 000000000000000a R09: 00007f454f3e5e80
[25109.178143] R10: 000000000000000a R11: 0000000000000246 R12: 00007f454f427760
[25109.178144] R13: 000000000000000a R14: 00007f454f422760 R15: 000000000000000a
上面的信息就是 dump_stack 打印的栈回溯信息。我们可以看到调用从顶端一直调到 my_proc_write 函数,最终调用到的函数就是 dump_stack 函数。
dump_stack 函数的原理浅析
函数调用依赖栈来完成。栈被用于函数调用中参数的传递、创建局部变量、返回信息的保存。函数的执行过程中栈帧会动态的增减,这不需要额外的工作,只需要拨动栈顶指针即可。
我用下面的图示为例,讲讲 dump_stack 函数回溯栈帧的原理。
首先注明上图来源于《深入理解计算机系统》一书的第三章。
上图中我们能看到主要的两个栈帧,Caller’s frame 是调用者函数的栈帧,Current frame 是被调用者函数的栈帧。
在 ia32 架构中 ebp 指向当前栈帧的起始位置,这个位置保存着旧的 ebp 的值。我们可以看到在旧的 ebp 保存的位置上方保存着返回地址。这个返回地址是调用者函数中 call 指令的下一条指令的地址,子函数执行完成后会返回,旧的 ebp 首先出栈并赋值给 ebp 寄存器,同时返回地址也要出栈并赋值给 pc。
上面的过程可以递归的用于多层函数调用上。
我们可以将 dump_stack 函数的栈帧看做 Current frame,当前 pc 的值保存的是 dump_stack 中的某条指令的地址,内核先根据这个地址查询 map 获取到 dump_stack 函数的名称与当前指令县相对于 dump_stack 函数起始位置的偏移量,然后通过访问 ebp 寄存器指向的旧 ebp 的值来获取到调用 dump_stack 函数的栈帧指针的值,有了这个值就可以不断的回溯上方的栈帧,一个栈帧就是一个调用层次。
同时返回地址的位置就在旧的 ebp 存储位置的上方,根据这样的特点 dump_stack 也就能回溯不同调用层次中返回地址的值。根据返回地址就可以获取到返回地址的上一条调用语句的地址,对该地址进行寻址,获取到指令的编码,就能够获取到调用函数的入口地址。这里可以使用如下公式:
call 指令调用函数的地址 = call 指令码后面的偏移量 + 返回地址
这之后使用入口地址查询 System-map 获取到函数的名称,同时计算出返回地址相对于函数入口的偏移量就准备好了打印的内容,调用打印函数打印信息,然后继续重复这一过程直到找不到一个合法的栈帧为止。
总结
dump_stack 函数的实现与体系结构直接相关,不同的硬件架构对应的实现也不同。我上面对 dump_stack 原理的分析是以 ia32 架构为例进行的。其它的架构上会有不同的实现,但原理大致相同。