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);
1443
1444 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 = <1>;
#size-cells = <1>;
reg = <0x5000000 0x1000000>;
ranges;
ethernet@5000000 {
compatible = "davicom,dm9000";
reg = <0x5000000 0x2 0x5000004 0x2>;
interrupt-parent = <&gpx0>;
interrupts = <6 4>;
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 : [<c029bb7c>] lr : [<c029bb2c>] 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] [<c029bb7c>] (dm9000_probe) from [<c026b2e4>] (platform_drv_probe+0x18/0x48)
[ 5.115000] [<c026b2e4>] (platform_drv_probe) from [<c02699d0>] (driver_probe_device+0x100/0x210)
[ 5.115000] [<c02699d0>] (driver_probe_device) from [<c0269b6c>] (__driver_attach+0x8c/0x90)
[ 5.115000] [<c0269b6c>] (__driver_attach) from [<c0268324>] (bus_for_each_dev+0x58/0x88)
[ 5.115000] [<c0268324>] (bus_for_each_dev) from [<c0269190>] (bus_add_driver+0xd8/0x1cc)
[ 5.115000] [<c0269190>] (bus_add_driver) from [<c026a184>] (driver_register+0x78/0xf4)
[ 5.115000] [<c026a184>] (driver_register) from [<c00087b4>] (do_one_initcall+0x30/0x144)
[ 5.115000] [<c00087b4>] (do_one_initcall) from [<c05c1c4c>] (kernel_init_freeable+0xfc/0x1c8)
[ 5.115000] [<c05c1c4c>] (kernel_init_freeable) from [<c040f810>] (kernel_init+0x8/0xe4)
[ 5.115000] [<c040f810>] (kernel_init) from [<c000e4b8>] (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 ARM
3、寄存器信息:關鍵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 : [<c029bb7c>] lr : [<c029bb2c>] 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 : [<c029bb7c>]
快速定位: 在Linux內核的頂層目錄下有一個生成的未壓縮的內核vmlinux,反匯編打開它:
arm-none-linux-gnueabi-objdump -D vmlinux > vmlinux.dis
文件 vmlinux.dis 非常大打開需要一定時間, 從反匯編代碼定位到 C 代碼并不會如此容易,需要有較強的閱讀匯編代碼的能力。 你加油。
另外一種方法是通過 addr2line 去定位
參考鏈接://elinux.org/Addr2line_for_kernel_debugging
$ arm-none-linux-gnueabi-addr2line -f -e vmlinux c029bb7c•
4、棧回溯信息
棧回溯信息是從下往上看,
[ 5.115000] [<c029bb7c>] (dm9000_probe) from [<c026b2e4>] (platform_drv_probe+0x18/0x48)
[ 5.115000] [<c026b2e4>] (platform_drv_probe) from [<c02699d0>] (driver_probe_device+0x100/0x210)
[ 5.115000] [<c02699d0>] (driver_probe_device) from [<c0269b6c>] (__driver_attach+0x8c/0x90)
[ 5.115000] [<c0269b6c>] (__driver_attach) from [<c0268324>] (bus_for_each_dev+0x58/0x88)
[ 5.115000] [<c0268324>] (bus_for_each_dev) from [<c0269190>] (bus_add_driver+0xd8/0x1cc)
[ 5.115000] [<c0269190>] (bus_add_driver) from [<c026a184>] (driver_register+0x78/0xf4)
[ 5.115000] [<c026a184>] (driver_register) from [<c00087b4>] (do_one_initcall+0x30/0x144)
[ 5.115000] [<c00087b4>] (do_one_initcall) from [<c05c1c4c>] (kernel_init_freeable+0xfc/0x1c8)
[ 5.115000] [<c05c1c4c>] (kernel_init_freeable) from [<c040f810>] (kernel_init+0x8/0xe4)
[ 5.115000] [<c040f810>] (kernel_init) from [<c000e4b8>] (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, sp
stmfd 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是否為0
moveq mask, #0xfc000003 @ mask for 26-bit 如果是,即r1=0x10,則mask=0xfc000003,即pc地址只有低26bit有效,且末兩位為0
movne mask, #0 @ mask for 32-bit 如果不是,即r1!=0x10,則mask=0
@ 下面是一段和該函數無關的代碼,用來計算pc預取指的偏移,一般pc是指向下兩條指令,所以offset一般等于8
1: stmfd sp!, {pc} @ 存儲pc的值到棧中,sp指向pc。
ldr r0, [sp], #4 @ r0=sp的值,即剛剛存的pc的值(將要執行的指令),sp=sp+4即還原sp
adr 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 => <next word>
*
* 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 exceptions
bne 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-r3
ldr r3, .Ldsi+4 @ adjust saved 'pc' back one. r3 = 0xe92d0000 >> 10
teq r3, r2, lsr #10 @ 比較stmfd指令機器碼是否相同(不關注是否保存r0-r9),目的是判斷是否為stmfd指令
subne r0, sv_pc, #4 @ allow for mov: 如果sv_pc前面只有mov ip, sp
subeq r0, sv_pc, #8 @ allow for mov + stmia: 如果sv_pc前面有兩條指令
@ 至此,r0為callee函數的第一條指令的地址,即callee函數的入口地址
@ 打印r0地址對應的符號名,傳給dump_backtrace_entry三個參數:
@ r0:函數入口地址,
@ r1:返回值即caller中的地址,
@ r2:callee的fp
ldr r1, [frame, #-4] @ get saved lr
mov r2, frame
bic r1, r1, mask @ mask PC/LR for the mode
bl dump_backtrace_entry
@ 打印保存在棧里的寄存器,這跟棧回溯沒關系,本文中不太關心
ldr r1, [sv_pc, #-4] @ if stmfd sp!, {args} exists, sv_pc前一條指令是否是stmfd指令
ldr r3, .Ldsi+4
teq r3, r1, lsr #10
ldreq 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 #10
subeq r0, frame, #16 @ 跳過fp, ip, lr, pc,即找到保存的r4-r10
bleq .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 layout
cmp sv_fp, frame @ sv_fp-frame
mov frame, sv_fp @ frame=sv_fp
bhi for_each_frame @ cmp的結果,如果frame<sv_fp,即當前fp小于caller的fp,則繼續循環
@ 這時frame指向caller棧的fp,由于函數中不會修改fp的值,所以這個fp肯定是指向caller保存的pc的位置的。
1006: adr r0, .Lbad @ 否則就打印bad frame提示
mov r1, frame
bl printk
no_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, r0
mov instr, r1
mov reg, #10
mov r7, #0
1: mov r3, #1
tst instr, r3, lsl reg
beq 2f
add r7, r7, #1
teq r7, #6
moveq r7, #1
moveq r1, #'\n'
movne r1, #' '
ldr r3, [stack], #-4
mov r2, reg
adr r0, .Lfp
bl printk
2: subs reg, reg, #1
bpl 1b
teq r7, #0
adrne r0, .Lcr
blne printk
ldmfd sp!, {instr, reg, stack, r7, pc}
.Lfp: .asciz "%cr%d:%08x"
.Lcr: .asciz "\n"
.Lbad: .asciz "Backtrace aborted due to bad frame pointer <%p>\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!, {}
參考:https://blog.csdn.net/jasonchen_gbd/article/details/45585133