Linux内核的栈回溯与妙用 您所在的位置:网站首页 栈的妙用 Linux内核的栈回溯与妙用

Linux内核的栈回溯与妙用

2024-04-17 07:14| 来源: 网络整理| 查看: 265

Linux内核调试时Linux驱动工程师的必备技能,当内核出现比较严重的错误时,比如Oops错误或者内核认为系统运行状态异常,内核就会打印出当前进程的栈回溯信息,其中包含当前执行代码的位置以及相邻的指令、产生错误的原因、关键寄存器的值以及函数调用关系等信息,这些信息对于调试内核错误非常有用。示例:注:本示例基于Linux-3.14.0的内核,平台为FS4412首先人为制造内核错误,修改drivers/net/ethernet/davicom/dm9000.c,在dm9000_probe的函数中添加自己的信息,比如,在 1450行,解析设备树之后对申请到的资源手动赋值为NULL,如下:1440 db->addr_res = platform_get_resource(pdev, IORESOURCE_MEM, 0);1441 db->data_res = platform_get_resource(pdev, IORESOURCE_MEM, 1);1442 db->irq_res = platform_get_resource(pdev, IORESOURCE_IRQ, 0);14431444 if (db->addr_res == NULL || db->data_res == NULL ||1445 db->irq_res == NULL) {1446 dev_err(db->dev, "insufficient resources\n");1447 ret = -ENOENT;1448 goto out;1449 }1450 printk("db->addr_res :%#x.\n",db->addr_res); //手动添加了三行打印信息1451 printk("db->data_res :%#x.\n",db->data_res);1452 printk("db->irq_res :%#x.\n",db->irq_res);1453 db->addr_res = NULL; //手动给申请到的资源的地址赋值为NULL备注: 设备树信息如下:srom-cs1@5000000 {compatible = "simple-bus";#address-cells = ;#size-cells = ;reg = ;ranges;ethernet@5000000 {compatible = "davicom,dm9000";reg = ;interrupt-parent = ;interrupts = ;davicom,no-eeprom;mac-address = [00 0a 2d a6 55 a2];};};那么接下来我们编译内核和设备树然后拷贝启动内核:$ make uImage$ make dtbs系统启动时的内核打印信息如下:[ 5.075000] brd: module loaded[ 5.085000] loop: module loaded[ 5.090000] db->addr_res :0xee927e80. //这里是我们手动添加的打印信息,打印之后就是我们的内核Oops信息[ 5.090000] db->data_res :0xee927e9c.[ 5.095000] db->irq_res :0xee927eb8.[ 5.100000] Unable to handle kernel NULL pointer dereference at virtual address 00000000[ 5.105000] pgd = c0004000[ 5.110000] [00000000] *pgd=00000000[ 5.115000] Internal error: Oops: 5 [#1] PREEMPT SMP ARM[ 5.115000] Modules linked in:[ 5.115000] CPU: 0 PID: 1 Comm: swapper/0 Not tainted 3.14.0 #15[ 5.115000] task: ee8c0000 ti: ee8be000 task.ti: ee8be000[ 5.115000] PC is at dm9000_probe+0x254/0x900[ 5.115000] LR is at dm9000_probe+0x204/0x900[ 5.115000] pc : [] lr : [] psr: a0000153[ 5.115000] sp : ee8bfe50 ip : 00000003 fp : 00000000[ 5.115000] r10: 00000000 r9 : c05f94d0 r8 : ee0a7150[ 5.115000] r7 : ee9d0200 r6 : ee9d0210 r5 : eead5c80 r4 : eead5800[ 5.115000] r3 : 00000000 r2 : 00000003 r1 : 00000000 r0 : fffffffa[ 5.115000] Flags: NzCv IRQs on FIQs off Mode SVC_32 ISA ARM Segment kernel[ 5.115000] Control: 10c5387d Table: 4000404a DAC: 00000015[ 5.115000] Process swapper/0 (pid: 1, stack limit = 0xee8be240)[ 5.115000] Stack: (0xee8bfe50 to 0xee8c0000)[ 5.115000] fe40: ee0a6f78 00000001 c05f94d0 ee0a8048[ 5.115000] fe60: 00000000 ee9d0210 c062a554 ee9d0210 00000000 c062a554 c05f94d0 c05c14fc[ 5.115000] fe80: 00000000 c026b2e4 c026b2cc c067478c c062a554 c02699d0 ee9d0210 c062a554[ 5.115000] fea0: ee9d0244 00000000 c05da4f4 c0269b6c c062a554 c0269ae0 00000000 c0268324[ 5.115000] fec0: ee804c78 ee927dc0 c062a554 ee072780 c06286d8 c0269190 c0548ae0 c062a554[ 5.115000] fee0: 00000000 c062a554 00000000 c05e5c74 c063a5c0 c026a184 00000000 ee8be000[ 5.115000] ff00: 00000000 c00087b4 ee90ef00 c065f090 60000153 c0609c40 60000100 c0609c40[ 5.115000] ff20: 00000000 00000000 c0609c3c 00000000 c0599df0 ef7fc8bd 0000009f c0034c6c[ 5.115000] ff40: c0550640 c0599400 00000006 00000006 00000000 c05e5c90 c05e5c94 00000006[ 5.115000] ff60: c05e5c74 c063a5c0 0000009f c05c14fc 00000000 c05c1c4c 00000006 00000006[ 5.115000] ff80: c05c14fc c003e0dc 00000000 c040f808 00000000 00000000 00000000 00000000[ 5.115000] ffa0: 00000000 c040f810 00000000 c000e4b8 00000000 00000000 00000000 00000000[ 5.115000] ffc0: 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000[ 5.115000] ffe0: 00000000 00000000 00000000 00000000 00000013 00000000 ff7fffff ffdfdfff[ 5.115000] [] (dm9000_probe) from [] (platform_drv_probe+0x18/0x48)[ 5.115000] [] (platform_drv_probe) from [] (driver_probe_device+0x100/0x210)[ 5.115000] [] (driver_probe_device) from [] (__driver_attach+0x8c/0x90)[ 5.115000] [] (__driver_attach) from [] (bus_for_each_dev+0x58/0x88)[ 5.115000] [] (bus_for_each_dev) from [] (bus_add_driver+0xd8/0x1cc)[ 5.115000] [] (bus_add_driver) from [] (driver_register+0x78/0xf4)[ 5.115000] [] (driver_register) from [] (do_one_initcall+0x30/0x144)[ 5.115000] [] (do_one_initcall) from [] (kernel_init_freeable+0xfc/0x1c8)[ 5.115000] [] (kernel_init_freeable) from [] (kernel_init+0x8/0xe4)[ 5.115000] [] (kernel_init) from [] (ret_from_fork+0x14/0x3c)[ 5.115000] Code: e59f1640 ebff2c17 e59434b4 e3a0a000 (e8930202)[ 5.395000] ---[ end trace cbd2f1e374620c53 ]---[ 5.400000] Kernel panic - not syncing: Attempted to kill init! exitcode=0x0000000b[ 5.400000]分析:1、自己添加的内核打印信息位置:[ 5.090000] db->addr_res :0xee927e80. //这里是我们手动添加的打印信息,打印之后就是我们的内核Oops信息[ 5.090000] db->data_res :0xee927e9c.[ 5.095000] db->irq_res :0xee927eb8.2、内核Oops信息空指针异常造成的错误---很常见[ 5.100000] Unable to handle kernel NULL pointer dereference at virtual address 00000000[ 5.105000] pgd = c0004000[ 5.110000] [00000000] *pgd=00000000[ 5.115000] Internal error: Oops: 5 [#1] PREEMPT SMP ARM3、寄存器信息:关键PC指针的值[ 5.115000] CPU: 0 PID: 1 Comm: swapper/0 Not tainted 3.14.0 #15[ 5.115000] task: ee8c0000 ti: ee8be000 task.ti: ee8be000[ 5.115000] PC is at dm9000_probe+0x254/0x900[ 5.115000] LR is at dm9000_probe+0x204/0x900[ 5.115000] pc : [] lr : [] psr: a0000153[ 5.115000] sp : ee8bfe50 ip : 00000003 fp : 00000000[ 5.115000] r10: 00000000 r9 : c05f94d0 r8 : ee0a7150[ 5.115000] r7 : ee9d0200 r6 : ee9d0210 r5 : eead5c80 r4 : eead5800[ 5.115000] r3 : 00000000 r2 : 00000003 r1 : 00000000 r0 : fffffffa[ 5.115000] Flags: NzCv IRQs on FIQs off Mode SVC_32 ISA ARM Segment kernel[ 5.115000] Control: 10c5387d Table: 4000404a DAC: 00000015[ 5.115000] Process swapper/0 (pid: 1, stack limit = 0xee8be240)当前异常时由于运行在CPU0上的任务引发的异常。那么如果你希望快速定位错误信息,那么只需要获取PC指针所在的函数和PC指针指向的地址就可以了,操作如下:[ 5.115000] PC is at dm9000_probe+0x254/0x900[ 5.115000] pc : []快速定位: 在Linux内核的顶层目录下有一个生成的未压缩的内核vmlinux,反汇编打开它:arm-none-linux-gnueabi-objdump -D vmlinux > vmlinux.dis文件 vmlinux.dis 非常大打开需要一定时间, 从反汇编代码定位到 C 代码并不会如此容易,需要有较强的阅读汇编代码的能力。 你加油。另外一种方法是通过 addr2line 去定位参考链接://http://elinux.org/Addr2line_for_kernel_debugging$ arm-none-linux-gnueabi-addr2line -f -e vmlinux c029bb7c•4、栈回溯信息栈回溯信息是从下往上看,[ 5.115000] [] (dm9000_probe) from [] (platform_drv_probe+0x18/0x48)[ 5.115000] [] (platform_drv_probe) from [] (driver_probe_device+0x100/0x210)[ 5.115000] [] (driver_probe_device) from [] (__driver_attach+0x8c/0x90)[ 5.115000] [] (__driver_attach) from [] (bus_for_each_dev+0x58/0x88)[ 5.115000] [] (bus_for_each_dev) from [] (bus_add_driver+0xd8/0x1cc)[ 5.115000] [] (bus_add_driver) from [] (driver_register+0x78/0xf4)[ 5.115000] [] (driver_register) from [] (do_one_initcall+0x30/0x144)[ 5.115000] [] (do_one_initcall) from [] (kernel_init_freeable+0xfc/0x1c8)[ 5.115000] [] (kernel_init_freeable) from [] (kernel_init+0x8/0xe4)[ 5.115000] [] (kernel_init) from [] (ret_from_fork+0x14/0x3c)[ 5.115000] Code: e59f1640 ebff2c17 e59434b4 e3a0a000 (e8930202)[ 5.395000] ---[ end trace cbd2f1e374620c53 ]---尽可能引导读者将栈回溯的功能用于实际项目调试,栈回溯的功能很强大。打印函数调用关系的函数就是dump_stack(),该函数不仅可以用在系统出问题的时候,我们在调试内核的时候,可以通过dump_stack()函数的打印信息更方便的了解内核代码执行流程。start_kernel-->rest_init--->kernel_thread(kernel_init,xxx)--->kernel_init--->kernel_init_freeable--->do_one_initcall--->driver_register--->bus_add_driver--->bus_for_each_dev--->__driver_attach--->driver_probe_device--->platform_drv_probe---->dm9000_probe随着内核启动,先是系统相关的核心部分的初始化,比如CPU,时钟,内存等,然后不太重要的初始化操作放到rest_init的初始化当中,后来开启内核的第一个内核线程kernel_init,在内核初始化线程当中会去加载Linux下的不同的段当中的内容,比如驱动的初始化调用do_one_initcall,在驱动被初始化时,需要将驱动添加到对应的驱动链表当中driver_register,然后将驱动放到对应的总线上(当前实例的dm9000属于platform设备总线)bus_add_driver,然后由总线负责遍历设备设备链表bus_for_each_dev,当遍历到设备时,由__driver_attach函数负责将设备和驱动进行关联,驱动端的probe函数指向,然后依次回调到对应驱动的probe函数dm9000_probe,这个过程是一个总的框架,那么问题发生在dm9000_probe函数当中,我们可以对dm9000_probe做进一步深入的分析,比如借助dump_stack()函数。dump_stack()函数的实现和系统结构紧密相关,本文介绍ARM体系中dump_stack()函数的实现。该函数定义在arch/arm/kernel/traps.c文件中,调用dump_stack()函数不需要添加头文件,基本上在内核代码任何地方都可以直接使用该函数。关键寄存器介绍:寄存器含义r0-r3用作函数传参,例如函数A调用函数B,如果A需要向B传递参数,则将参数放到寄存器r0-r3中,如果参数个数大于4,则需要借用函数的栈空间。r4-r11变量寄存器,在函数中可以用来保存临时变量。r9(SB)静态基址寄存器。r10(SL)栈界限寄存器。r11(FP)帧指针寄存器,通常用来访问函数栈,帧指针指向函数栈中的某个位置。r12(IP)内部过程调用暂存寄存器。r13(SP)栈指针寄存器,用来指向函数栈的栈顶。r14(LR)链接寄存器,通常用来保存函数的返回地址。内核中的函数栈 内核中,一个函数的代码最开始的指令都是如下形式:mov ip, spstmfd sp!, {r0 - r3} (可选的)stmfd sp!, {..., fp, ip, lr, pc}……从其中两条stmfd(压栈)指令可以看出,一个函数的函数栈的栈底(高地址)的结构基本是固定的,如下图:

首先我们约定被调用的函数称为callee函数,而调用者函数称为caller函数。 在进行函数调用的回溯时,内核中的dump_stack()函数需要做以下尝试:1、首先读取系统中的FP寄存器的值,我们知道帧指针是指向函数栈的某个位置的,所以通过FP的值可以直接找到当前函数的函数栈的地址。 2、得到当前函数的代码段地址,这个很容易,因为当前正在执行的代码(可通过PC寄存器获得)就处在函数的代码段中。在函数栈中保存了一个PC寄存器的备份,通过这个PC寄存器的值可以定位到函数的第一条指令,即函数的入口地址。 3、得到当前函数的入口地址后,内核中保存了所有函数地址和函数名的对应关系,所以可以打印出函数名(详见另一篇博客:内核符号表的查找过程)。 4、在当前函数的函数栈中还保存了caller函数的帧指针(FP寄存器的值),所以我们就可以找到caller函数的函数栈的位置。 5、继续执行2-4步,直到某个函数的函数栈中保存的帧指针(FP寄存器的值)为0或非法。 发生函数调用时,函数栈和代码段的关系如下图所示:dump_stack()函数 接下来我们就来看一下dump_stack()函数的实现。 dump_stack()主要是调用了下面的函数c_backtrace(fp, mode);两个参数的含义为: fp : current进程栈的fp寄存器。 mode: ptrace 用到的PSR模式,在这里我们不关心。dump_stack传入的值为0x10。 这两个参数分别赋值给r0, r1寄存器传给c_backtrace()函数。 c_backtrace函数定义如下(arch/arm/lib/backtrace.S):@ 定义几个局部变量#define frame r4#define sv_fp r5#define sv_pc r6#define mask r7#define offset r8@ 当前处于dump_backtrace函数的栈中ENTRY(c_backtrace)stmfd sp!, {r4 - r8, lr} @ 将r4-r8和lr压入栈中,我们要使用r4-r8,所以备份一下原来的值。sp指向最后压入的数据movs frame, r0 @ frame=r0。r0为传入的第一个参数,即fp寄存器的值beq no_frame @ 如果frame为0,则退出tst r1, #0x10 @ 26 or 32-bit mode? 判断r1的bit4是否为0moveq mask, #0xfc000003 @ mask for 26-bit 如果是,即r1=0x10,则mask=0xfc000003,即pc地址只有低26bit有效,且末两位为0movne mask, #0 @ mask for 32-bit 如果不是,即r1!=0x10,则mask=0@ 下面是一段和该函数无关的代码,用来计算pc预取指的偏移,一般pc是指向下两条指令,所以offset一般等于81: stmfd sp!, {pc} @ 存储pc的值到栈中,sp指向pc。ldr r0, [sp], #4 @ r0=sp的值,即刚刚存的pc的值(将要执行的指令),sp=sp+4即还原spadr r1, 1b @ r1 = 标号1的地址,即指令 stmfd sp!, {pc} 的地址sub offset, r0, r1 @ offset=r0-r1,即pc实际指向的指令和读取pc的指令之间的偏移/** Stack frame layout:* optionally saved caller registers (r4 - r10)* saved fp* saved sp* saved lr* frame => saved pc @ frame即上面的fp,每个函数的fp都指向这个位置* optionally saved arguments (r0 - r3)* saved sp => ** Functions start with the following code sequence:* mov ip, sp* stmfd sp!, {r0 - r3} (optional)* corrected pc => stmfd sp!, {..., fp, ip, lr, pc} //将pc压栈的指令*/@ 函数主流程:开始查找并打印调用者函数for_each_frame: tst frame, mask @ Check for address exceptionsbne no_frame@ 由sv_pc找到将pc压栈的那条指令,因为这条指令在代码段中的位置有特殊性,可用于定位函数入口。1001: ldr sv_pc, [frame, #0] @ 获取保存在callee栈里的sv_pc,它指向callee的代码段的某个位置1002: ldr sv_fp, [frame, #-12] @ get saved fp,这个fp就是caller的fp,指向caller的栈中某个位置sub sv_pc, sv_pc, offset @ sv_pc减去offset,找到将pc压栈的那条指令,即上面注释提到的corrected pc。bic sv_pc, sv_pc, mask @ mask PC/LR for the mode 清除sv_pc中mask为1的位,例如,mask=0x4,则清除sv_pc的bit2。@ 定位函数的第一条指令,即函数入口地址1003: ldr r2, [sv_pc, #-4] @ if stmfd sp!, {args} exists, 如果在函数最开始压入了r0-r3ldr r3, .Ldsi+4 @ adjust saved 'pc' back one. r3 = 0xe92d0000 >> 10teq r3, r2, lsr #10 @ 比较stmfd指令机器码是否相同(不关注是否保存r0-r9),目的是判断是否为stmfd指令subne r0, sv_pc, #4 @ allow for mov: 如果sv_pc前面只有mov ip, spsubeq r0, sv_pc, #8 @ allow for mov + stmia: 如果sv_pc前面有两条指令@ 至此,r0为callee函数的第一条指令的地址,即callee函数的入口地址@ 打印r0地址对应的符号名,传给dump_backtrace_entry三个参数:@ r0:函数入口地址,@ r1:返回值即caller中的地址,@ r2:callee的fpldr r1, [frame, #-4] @ get saved lrmov r2, framebic r1, r1, mask @ mask PC/LR for the modebl dump_backtrace_entry@ 打印保存在栈里的寄存器,这跟栈回溯没关系,本文中不太关心ldr r1, [sv_pc, #-4] @ if stmfd sp!, {args} exists, sv_pc前一条指令是否是stmfd指令ldr r3, .Ldsi+4teq r3, r1, lsr #10ldreq r0, [frame, #-8] @ get sp。frame-8指向保存的IP寄存器,由于mov ip, sp,所以caller的sp=ip@ 所以r0=caller的栈的低地址。subeq r0, r0, #4 @ point at the last arg. r0+4就是callee的栈的高地址。@ 由于参数的压栈顺序为r3,r2,r1,r0,所以这里栈顶实际上是最后一个参数。bleq .Ldumpstm @ dump saved registers@ 打印保存在栈里的寄存器,这跟栈回溯没关系,本文中不太关心1004: ldr r1, [sv_pc, #0] @ if stmfd sp!, {..., fp, ip, lr, pc}ldr r3, .Ldsi @ instruction exists, 如果指令为frame指向的指令为stmfd sp!, {..., fp, ip, lr, pc}teq r3, r1, lsr #10subeq r0, frame, #16 @ 跳过fp, ip, lr, pc,即找到保存的r4-r10bleq .Ldumpstm @ dump saved registers,打印出来r4-r10@ 对保存在当前函数栈中的caller的fp做合法性检查teq sv_fp, #0 @ zero saved fp means 判断获取的caller的fp的值beq no_frame @ no further frames 如果caller fp=0,则停止循环@ 更新frame变量指向caller函数栈的位置,将上面注释中的Stack frame layoutcmp sv_fp, frame @ sv_fp-framemov frame, sv_fp @ frame=sv_fpbhi for_each_frame @ cmp的结果,如果frame@ 这时frame指向caller栈的fp,由于函数中不会修改fp的值,所以这个fp肯定是指向caller保存的pc的位置的。1006: adr r0, .Lbad @ 否则就打印bad frame提示mov r1, framebl printkno_frame: ldmfd sp!, {r4 - r8, pc}ENDPROC(c_backtrace)@ c_backtrace函数结束。@ 将上面的代码放到__ex_table异常表中。其中1001b ... 1006b是指上面的1001-1006标号。.section __ex_table,"a".align 3.long 1001b, 1006b.long 1002b, 1006b.long 1003b, 1006b.long 1004b, 1006b.previous#define instr r4#define reg r5#define stack r6@ 打印寄存器值.Ldumpstm: stmfd sp!, {instr, reg, stack, r7, lr}mov stack, r0mov instr, r1mov reg, #10mov r7, #01: mov r3, #1tst instr, r3, lsl regbeq 2fadd r7, r7, #1teq r7, #6moveq r7, #1moveq r1, #'\n'movne r1, #' 'ldr r3, [stack], #-4mov r2, regadr r0, .Lfpbl printk2: subs reg, reg, #1bpl 1bteq r7, #0adrne r0, .Lcrblne printkldmfd sp!, {instr, reg, stack, r7, pc}.Lfp: .asciz "%cr%d:%08x".Lcr: .asciz "\n".Lbad: .asciz "Backtrace aborted due to bad frame pointer \n".align.Ldsi:@ 用来判断是否是stmfd sp!指令,并且参数包含fp, ip, lr, pc,不包含r10.word 0xe92dd800 >> 10 @ stmfd sp!, {... fp, ip, lr, pc}@ 用来判断是否是stmfd sp!指令,并且参数不包含r10, fp, ip, lr, pc.word 0xe92d0000 >> 10 @ stmfd sp!, {}对嵌入式物联网感兴趣的小伙伴,可以多了解一下相关信息。



【本文地址】

公司简介

联系我们

今日新闻

    推荐新闻

    专题文章
      CopyRight 2018-2019 实验室设备网 版权所有