第3章 Linux內核調試手段之二 ===================gdb 和 addr2line 調試內核模塊 內核模塊插入內核鏈表的時候,會調用 init 裡面的程序,我們上面給的那個常式的程序因為是經過多年風吹雨打的,但是如果你是一個萌新的碼農,你能保證自己寫的內核模塊沒有問題嗎?所以就需要調試方法,如果你寫了一個內核模塊載入不成功,這時候就會產生 oops ,內核不會有影響,就好像你拿了一個偽造的車票想上高鐵,結果被列車員發現了,把你踢下車了,列車不受影響繼續運行。我們用個示例來說明下調試 oops 的問題,調試的最終目的就是要找到出現問題的地方。oops.c 源代碼,相比HelloWorld的代碼,這個代碼增加了一些東西,一個是增加了模塊描述 MODULE_DESCRIPTION 和模塊作者 MODULE_AUTHOR ,這些都不是必須的,但是作為一個標準的內核開發者,把自己的模塊寫得越規範是越好的,不僅代碼看起來比較美觀,而且別人也可以從這些代碼裡面看到一些有用的信息。#include <linux/init.h>#include <linux/module.h>MODULE_LICENSE("Dual BSD/GPL");MODULE_DESCRIPTION ("Oops"); MODULE_AUTHOR("weiqifa");static int my_oops_init(void){ int *a; a = (int *)0x00003333; *a = 3; printk(KERN_ALERT "oops %d ",a); return 0;}static void my_oops_exit(void) { printk(KERN_ALERT "Goodbye, oops ");}module_init(my_oops_init);module_exit(my_oops_exit)Makefile 文件,這個文件跟上面的文件有些不同,加入了一個 FLAG ,這個FLAG 為了方便我們調試內核。ifneq ($(KERNELRELEASE),)EXTRA_CFLAGS = -Wall -g obj-m := oops.oelsePWD := $(shell pwd)KVER := $(shell uname -r)KDIR := /lib/modules/$(KVER)/buildall: $(MAKE) -C $(KDIR) M=$(PWD) modulesclean: rm -rf .*.cmd *.o *.mod.c *.ko .tmp_versionsendif 我們執行 insmod oops.ko 的時候,會出現 kernel panic,再通過 dmesg 來查看 panic 內容,其中看到的 kernel 日誌如下root@ubuntu:~/linuxBook/oopsmodules# dmesg |tail -20[ 815.844634] RSP: 0018:ffff88003b933de8 EFLAGS: 00010246[ 815.844635] RAX: 0000000000000000 RBX: 0000000000000000 RCX: 0000000000000001[ 815.844637] RDX: 0000000000040770 RSI: 0000000000003333 RDI: ffffffffa0359024[ 815.844638] RBP: ffff88003b933e68 R08: 00000000000030d4 R09: 0000000000000100[ 815.844639] R10: 8000000000000000 R11: 0000000000000000 R12: ffffffffa0358000[ 815.844640] R13: 0000000000000000 R14: 0000000001005010 R15: 0000000000000000 [ 815.844641] FS: 00007fb80a1f6700(0000) GS:ffff88003d600000(0000) knlGS:0000000000000000[ 815.844643] CS: 0010 DS: 0000 ES: 0000 CR0: 000000008005003b[ 815.844643] CR2: 0000000000003333 CR3: 0000000036198000 CR4: 00000000000407f0[ 815.844687] Stack:[ 815.844689] ffff88003b933e68 ffffffff8100215a 0000000001005010 0000000000000000[ 815.844691] ffff88003b933e38 ffffffff8105ec93 0000000000000000 0000000000000000[ 815.844693] 0000000001005010 ffffffffa0020000 ffff88003b933e68 00000000181edb8b[ 815.844695] Call Trace:[ 815.844734] [<ffffffff8100215a>] ? do_one_initcall+0xfa/0x1b0[ 815.844740] [<ffffffff8105ec93>] ? set_memory_nx+0x43/0x50 [ 815.844752] [<ffffffff8175a462>] do_init_module+0x80/0x1d1[ 815.844759] [<ffffffff810ec66d>] load_module+0x4ed/0x620[ 815.844761] [<ffffffff810e9f10>] ? show_initstate+0x50/0x50[ 815.844763] [<ffffffff810ec854>] SyS_init_module+0xb4/0x100[ 815.844798] [<ffffffff8177b55d>] system_call_fastpath+0x1a/0x1f[ 815.844800] Code: <c7> 04 25 33 33 00 00 03 00 00 00 48 89 e5 e8 3c 16 40 e1 31 c0 5d[ 815.844809] RIP [<ffffffffa0358014>] my_oops_init+0x14/0x30 [oops][ 815.844812] RSP <ffff88003b933de8>[ 815.844813] CR2: 0000000000003333[ 815.844818] ---[ end trace f8f9b64af5078acc ]---看日誌也是一個比較考驗程序員的事情,從上面的日誌看,我們可以看到 oops 發生的關鍵日誌如下,其中還有一些函數的堆棧調用,但是這個不是重點。[ 815.844809] RIP [<ffffffffa0358014>] my_oops_init+0x14/0x30 [oops][ 815.844812] RSP <ffff88003b933de8>查看模塊的載入地址上面出現 oops 的是從模塊的基地址偏移出來的地址,我們要找到基地址,然後再用基地址和偏移地址運算,就可以知道出現問題的偏移量了。root@ubuntu:~/linuxBook/oopsmodules# cat /proc/modules |grep oopsoops 13418 1 - Loading 0xffffffffa0358000 (OX+)root@ubuntu:~/linuxBook/oopsmodules#使用 addr2line 找到 oops 位置知道了基地址和偏移地址,我們就可以知道偏移量了 offset = 0xffffffffa0358014 - 0xffffffffa0358000 = 0x14root@ubuntu:~/linuxBook/oopsmodules# addr2line -e oops.o 0x14/home/linux/linuxBook/oopsmodules/oops.c:12root@ubuntu:~/linuxBook/oopsmodules#這樣知道代碼導致 oops 的位置是第12 行。通過objdump 來查找oops 位置root@ubuntu:~/linuxBook/oopsmodules# objdump -dS --adjust-vma=0xffffffffa0358000 oops.kooops.ko: file format elf64-x86-64Disassembly of section .text:ffffffffa0358000 <init_module>:MODULE_LICENSE("Dual BSD/GPL");MODULE_DESCRIPTION ("Oops");MODULE_AUTHOR("weiqifa");static int my_oops_init(void){ffffffffa0358000: e8 00 00 00 00 callq ffffffffa0358005 <init_module+0x5>ffffffffa0358005: 55 push %rbp int *a; a = (int *)0x00003333; *a = 3; printk(KERN_ALERT "oops %d ",a);ffffffffa0358006: be 33 33 00 00 mov $0x3333,%esiffffffffa035800b: 48 c7 c7 00 00 00 00 mov $0x0,%rdiffffffffa0358012: 31 c0 xor %eax,%eaxstatic int my_oops_init(void){ int *a; a = (int *)0x00003333; *a = 3;ffffffffa0358014: c7 04 25 33 33 00 00 movl $0x3,0x3333ffffffffa035801b: 03 00 00 00MODULE_LICENSE("Dual BSD/GPL");MODULE_DESCRIPTION ("Oops");MODULE_AUTHOR("weiqifa");static int my_oops_init(void){ffffffffa035801f: 48 89 e5 mov %rsp,%rbp int *a; a = (int *)0x00003333; *a = 3; printk(KERN_ALERT "oops %d ",a);ffffffffa0358022: e8 00 00 00 00 callq ffffffffa0358027 <init_module+0x27> return 0;}ffffffffa0358027: 31 c0 xor %eax,%eaxffffffffa0358029: 5d pop %rbpffffffffa035802a: c3 retq ffffffffa035802b: 0f 1f 44 00 00 nopl 0x0(%rax,%rax,1)ffffffffa0358030 <cleanup_module>:static void my_oops_exit(void){ffffffffa0358030: e8 00 00 00 00 callq ffffffffa0358035 <cleanup_module+0x5>ffffffffa0358035: 55 push %rbp printk(KERN_ALERT "Goodbye, oops ");ffffffffa0358036: 48 c7 c7 00 00 00 00 mov $0x0,%rdiffffffffa035803d: 31 c0 xor %eax,%eax *a = 3; printk(KERN_ALERT "oops %d ",a); return 0;}static void my_oops_exit(void){ffffffffa035803f: 48 89 e5 mov %rsp,%rbp printk(KERN_ALERT "Goodbye, oops ");ffffffffa0358042: e8 00 00 00 00 callq ffffffffa0358047 <cleanup_module+0x17>}ffffffffa0358047: 5d pop %rbpffffffffa0358048: c3 retq ffffffffa0358049: 00 00 add %al,(%rax) ...root@ubuntu:~/linuxBook/oopsmodules#這樣看就更加清晰了,我們知道出現 oops 的位置是 ffffffffa0358014 直接在上面找就可以看到了。 *a = 3;ffffffffa0358014: c7 04 25 33 33 00 00 movl $0x3,0x3333使用函數dump_stack()調試內核不知道大家有沒有跟我一樣的困惑,Linux 代碼非常多,也非常大,有時候為了找到這個函數是從哪裡調用的,需要花費非常多的時間去查找代碼,而且很多宏函數還會蒙蔽你的雙眼,讓你淪陷在Linux 內核裡面,這時候就需要這麼一個函數,讓你撥開層層烏雲,看清它的真面目,這時候,你就會有一個感覺,我從哪裡來,我要到哪裡去?都是一清二楚了。廢話不多說,dump_stack()這個函數可以列印當前函數的上下文調用,讓你更加直觀的知道你的函數調用關係,這樣,你就更清楚的知道,你的爸爸的媽媽的妹妹的姑姑的姐姐的兒子的弟弟是你的什麼關係了。代碼#include <linux/init.h>#include <linux/module.h>MODULE_LICENSE("Dual BSD/GPL");static int hello_init(void){ dump_stack(); printk(KERN_ALERT "Hello, world "); return 0;}static void hello_exit(void){ printk(KERN_ALERT "Goodbye, cruel world ");}module_init(hello_init);module_exit(hello_exit);執行sudo insmod hello.ko後的kernel日誌如下[176360.807755] [<ffffffff81765bf5>] dump_stack+0x64/0x82[176360.807776] [<ffffffffa0275000>] ? 0xffffffffa0274fff[176360.807779] [<ffffffffa027500e>] hello_init+0xe/0x20 [hello][176360.807931] [<ffffffff8100215a>] do_one_initcall+0xfa/0x1b0[176360.808709] [<ffffffff8105ec93>] ? set_memory_nx+0x43/0x50[176360.808717] [<ffffffff8175a462>] do_init_module+0x80/0x1d1[176360.809093] [<ffffffff810ec66d>] load_module+0x4ed/0x620[176360.809097] [<ffffffff810e9f10>] ? show_initstate+0x50/0x50[176360.809100] [<ffffffff810ec854>] SyS_init_module+0xb4/0x100[176360.809187] [<ffffffff8177b55d>] system_call_fastpath+0x1a/0x1f[176360.809308] Hello, world可以看到,裡面的地址還有堆棧的調用,使用dump_stack函數不用外加什麼頭文件,使用起來非常方便,也不用指定說,只有在oops的時候才調用,正常的時候,我們想看函數調用關係的時候,也可以調用,就像我上面那樣。實現源碼位置/lib/dump_stack.c幾個比較關鍵的函數調用關係主要想研究的同學可以去看看dump_stack_print_info 和show_stack裡面的實現。 dump_stack總結我們知道,CPU工作的時候,有不同的工作模式,不同的工作模式,代表有不同的許可權,就比如我是老闆,我才可能有保險柜的鑰匙。這裡有一個用戶模式,超級用戶模式,和中斷模式,我們知道,每個進程分配的內存空間是不一樣的,他們的堆棧有一個task_struct來維護,每個進程不能互相訪問相互的內存,進程切換的時候,SP指針去執行要執行進程的堆棧地址,所以,我們可以知道一個事情,SP這個東東啊,是可以知道所有進程的東西的。但是呢,操作系統在運行的時候,CPU肯定有需要從用戶模式跳轉到中斷模式運行,進入中斷的時候,SP指針這個東東,就體現出當前的堆棧了,所以dump_stack就是從這些不斷的切換中,把堆棧地址給保存列印出來,得到一個上下文的調用關係。使用vmlinux調試內核我們知道,使用objdump反編譯調試的是動態載入的內核模塊,但是我們需要調試那些靜態編譯進入內核的那些代碼,不能使用這個方法,這時候就需要使用vmlinux,vmlinux是每次編譯內核的時候生成的內核符號表,裡面包含了所有編譯到內核的函數名,還有偏移地址。使用方法(高通平台)arm-eabi-gdb out/target/product/msm8625/obj/KERNEL_OBJ/vmlinux在內核的.config裡面要打開 DEBUG_INFO和DEBUG_VM定位故障方法(gdb) l * qrd7627a_add_io_devices+0x1000xc07cd05c is in qrd7627a_add_io_devices (/home/yejialong/GH700C/kernel/arch/arm/mach-msm/msm8x25/goso-msm7627a-io.c:1851).1846 } else if (machine_is_msm8625q_skud() || machine_is_msm8625q_evbd()) {1847 #ifndef CONFIG_CALA021848 platform_device_register(&pmic_mpp_leds_pdev_skud);1849 #endif1850 /* enable the skud flash and torch by gpio leds driver */1851 platform_device_register(&gpio_flash_skud);1852 } else if (machine_is_msm8625q_skue()) {1853 /* enable the skue flashlight by gpio leds driver */1854 platform_device_register(&gpio_flash_skue);1855 }使用方法(MTK平台)weiqifa@weiqifa-Inspiron-3847:~/weiqifa/tm100$ ./prebuilts/gcc/linux-x86/arm/arm-eabi-4.7/bin/arm-eabi-gdb ./out/target/product/tm100/obj/KERNEL_OBJ/vmlinuxGNU gdb (GDB) 7.3.1-gg2Copyright (C) 2011 Free Software Foundation, Inc.License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>This is free software: you are free to change and redistribute it.There is NO WARRANTY, to the extent permitted by law. Type "show copying"and "show warranty" for details.This GDB was configured as "--host=x86_64-linux-gnu --target=arm-linux-android".For bug reporting instructions, please see:<http://source.android.com/source/report-bugs.html>...Reading symbols from /home/weiqifa/weiqifa/tm100/out/target/product/tm100/obj/KERNEL_OBJ/vmlinux...done.(gdb)使用方法(rockchip平台)./prebuilts/gcc/linux-x86/aarch64/aarch64-linux-android-4.9/bin/aarch64-linux-android-addr2line -f -e kernel/vmlinuxweiqifa@dev:~/rk3399_7in1$ ./prebuilts/gcc/linux-x86/aarch64/aarch64-linux-android-4.9/bin/aarch64-linux-android-addr2line -f -e kernel/vmlinux ffffff8008459f3crk_iommu_domain_free/data/weiqifa/rk3399_7in1/kernel/drivers/iommu/rockchip-iommu.c:1005 (discriminator 2)weiqifa@dev:~/rk3399_7in1$一些調試相關的命令查看中斷linux@ubuntu:/usr/src/linux-headers-3.13.0-117/kernel$ cat /proc/interrupts CPU0 0: 26 IO-APIC-edge timer 1: 12200 IO-APIC-edge i8042 8: 1 IO-APIC-edge rtc0 9: 0 IO-APIC-fasteoi acpi 12: 110704 IO-APIC-edge i8042 14: 0 IO-APIC-edge ata_piix 15: 0 IO-APIC-edge ata_piix 16: 21165 IO-APIC-fasteoi vmwgfx, snd_ens1371 17: 76243 IO-APIC-fasteoi ehci_hcd:usb1, ioc0 18: 657 IO-APIC-fasteoi uhci_hcd:usb2 19: 197583 IO-APIC-fasteoi eth0 40: 0 PCI-MSI-edge PCIe PME, pciehp這個可以在嵌入式調試的時候查看中斷是否被觸發,非常有作用。查看工作隊列cat /proc/sched_debug查看內核定時器cat /proc/timer_listproc應該重點關注proc下面的文件系統應該重點關注,內核的調試信息很多都在這裡面,特別是初學者,把下面的每個文件夾都看看,作用還是非常明顯的。下周開始,我們可能進去封閉開發了,時間會更加緊張,可能會分享一些心得,連載的話我還是會寫,只會遲到不會缺席,這樣也是對自己的一種鞭策。共勉,加油~ ====================== 推薦閱讀: 相关文章 {{#data}} {{title}} {{/data}}