第3章 Linux内核调试手段之二
发布日期:2021-06-30 18:59:04 浏览次数:2 分类:技术文章

本文共 10366 字,大约阅读时间需要 34 分钟。

640?wx_fmt=png

640?wx_fmt=png

640?wx_fmt=png

640?wx_fmt=png

640?wx_fmt=png

640?wx_fmt=png

640?wx_fmt=png

640?wx_fmt=png

===================

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\n",a);

   return 0;

}

static void my_oops_exit(void)

{

   printk(KERN_ALERT "Goodbye, oops\n");

}

module_init(my_oops_init);

module_exit(my_oops_exit)

Makefile 文件,这个文件跟上面的文件有些不同,加入了一个 FLAG ,这个FLAG 为了方便我们调试内核。

ifneq ($(KERNELRELEASE),)

EXTRA_CFLAGS = -Wall -g

obj-m := oops.o

else

PWD  := $(shell pwd)

KVER := $(shell uname -r)

KDIR := /lib/modules/$(KVER)/build

all:

$(MAKE) -C $(KDIR) M=$(PWD) modules

clean:

rm -rf .*.cmd *.o *.mod.c *.ko .tmp_versions

endif

我们执行 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 oops

oops 13418 1 - Loading 0xffffffffa0358000 (OX+)

root@ubuntu:~/linuxBook/oopsmodules#

使用 addr2line 找到 oops 位置

知道了基地址和偏移地址,我们就可以知道偏移量了 offset = 0xffffffffa0358014 - 0xffffffffa0358000 = 0x14

root@ubuntu:~/linuxBook/oopsmodules# addr2line -e oops.o 0x14

/home/linux/linuxBook/oopsmodules/oops.c:12

root@ubuntu:~/linuxBook/oopsmodules#

这样知道代码导致 oops 的位置是第12 行。

通过objdump 来查找oops 位置

root@ubuntu:~/linuxBook/oopsmodules# objdump -dS --adjust-vma=0xffffffffa0358000 oops.ko

oops.ko:     file format elf64-x86-64

Disassembly 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\n",a);

ffffffffa0358006: be 33 33 00 00       mov    $0x3333,%esi

ffffffffa035800b: 48 c7 c7 00 00 00 00 mov    $0x0,%rdi

ffffffffa0358012: 31 c0                 xor    %eax,%eax

static int my_oops_init(void)

{

int *a;

a = (int *)0x00003333;

*a = 3;

ffffffffa0358014: c7 04 25 33 33 00 00 movl   $0x3,0x3333

ffffffffa035801b: 03 00 00 00

MODULE_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\n",a);

ffffffffa0358022: e8 00 00 00 00       callq  ffffffffa0358027 <init_module+0x27>

   return 0;

}

ffffffffa0358027: 31 c0                 xor    %eax,%eax

ffffffffa0358029: 5d                   pop    %rbp

ffffffffa035802a: 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\n");

ffffffffa0358036: 48 c7 c7 00 00 00 00 mov    $0x0,%rdi

ffffffffa035803d: 31 c0                 xor    %eax,%eax

*a = 3;

   printk(KERN_ALERT "oops %d\n",a);

   return 0;

}

static void my_oops_exit(void)

{

ffffffffa035803f: 48 89 e5             mov    %rsp,%rbp

   printk(KERN_ALERT "Goodbye, oops\n");

ffffffffa0358042: e8 00 00 00 00       callq  ffffffffa0358047 <cleanup_module+0x17>

}

ffffffffa0358047: 5d                   pop    %rbp

ffffffffa0358048: 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\n");

   return 0;

}

static void hello_exit(void)

{

   printk(KERN_ALERT "Goodbye, cruel world\n");

}

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里面的实现。

640?wx_fmt=png

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+0x100

0xc07cd05c 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_CALA02

1848                    platform_device_register(&pmic_mpp_leds_pdev_skud);

1849        #endif

1850                    /* 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/vmlinux

GNU gdb (GDB) 7.3.1-gg2

Copyright (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/vmlinux

weiqifa@dev:~/rk3399_7in1$ ./prebuilts/gcc/linux-x86/aarch64/aarch64-linux-android-4.9/bin/aarch64-linux-android-addr2line -f -e kernel/vmlinux ffffff8008459f3c

rk_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_list

proc应该重点关注

proc下面的文件系统应该重点关注,内核的调试信息很多都在这里面,特别是初学者,把下面的每个文件夹都看看,作用还是非常明显的。

下周开始,我们可能进去封闭开发了,时间会更加紧张,可能会分享一些心得,连载的话我还是会写,只会迟到不会缺席,这样也是对自己的一种鞭策。

共勉,加油~

======================

640?wx_fmt=png

转载地址:https://linus.blog.csdn.net/article/details/96793785 如侵犯您的版权,请留言回复原文章的地址,我们会给您删除此文章,给您带来不便请您谅解!

上一篇:介绍一个我创业的朋友
下一篇:一点小思考

发表评论

最新留言

能坚持,总会有不一样的收获!
[***.219.124.196]2024年04月19日 22时27分25秒