RISC-V篇-qemu+gdb调试Linux kernel源码

本篇文章发布于微信公众号:Linux底层小工,欢迎关注,获取更多原创技术文章!

调试Linux kernel源码要分两部分,分别是MMU开启之前与MMU开启之后,这是因为在没有打开MMU之前,CPU直接访问物理内存,而一旦MMU开启,CPU对memory系统的访问需要通过一系列的Translation table进行翻译,即访问的是虚拟地址空间。在MMU开启之前,内核代码是位置无关的代码(Position Independent Code, PIC),可以在任意地址上运行,也就导致了运行地址与链接地址不一致的情况,需要加载symbol到相应的位置才能进行debug。而在MMU开启之后,内核开始运行在虚拟地址上,此时运行地址和链接地址是一致的。

01 确定kernel的加载地址

要调试kernel需要知道kernel被OpenSBI加载到何处,即需要知道kernel的运行地址,这里说的是物理地址,可以分析OpenSBI源码得到,或者还有一个更简单的方法,就是修改OpenSBI源码,直接将kernel的运行地址打印出来

这里我们找到sbi_hart_switch_mode函数:

void __attribute__((noreturn))sbi_hart_switch_mode(unsigned long arg0, unsigned long arg1,                     unsigned long next_addr, unsigned long next_mode,                     bool next_virt){#if __riscv_xlen == 32        unsigned long val, valH;#else        unsigned long val;#endif        switch (next_mode) {        case PRV_M:                break;        case PRV_S:                if (!misa_extension('S'))                        sbi_hart_hang();                break;        case PRV_U:                if (!misa_extension('U'))                        sbi_hart_hang();                break;        default:                sbi_hart_hang();        }        val = csr_read(CSR_MSTATUS);        val = INSERT_FIELD(val, MSTATUS_MPP, next_mode);        val = INSERT_FIELD(val, MSTATUS_MPIE, 0);#if __riscv_xlen == 32        if (misa_extension('H')) {                valH = csr_read(CSR_MSTATUSH);                valH = INSERT_FIELD(valH, MSTATUSH_MPV, next_virt);                csr_write(CSR_MSTATUSH, valH);        }#else        if (misa_extension('H'))                val = INSERT_FIELD(val, MSTATUS_MPV, next_virt);#endif        csr_write(CSR_MSTATUS, val);        csr_write(CSR_MEPC, next_addr);        if (next_mode == PRV_S) {                if (next_virt) {                        csr_write(CSR_VSTVEC, next_addr);                        csr_write(CSR_VSSCRATCH, 0);                        csr_write(CSR_VSIE, 0);                        csr_write(CSR_VSATP, 0);                } else {                        csr_write(CSR_STVEC, next_addr);                        csr_write(CSR_SSCRATCH, 0);                        csr_write(CSR_SIE, 0);                        csr_write(CSR_SATP, 0);                }        } else if (next_mode == PRV_U) {                if (misa_extension('N')) {                        csr_write(CSR_UTVEC, next_addr);                        csr_write(CSR_USCRATCH, 0);                        csr_write(CSR_UIE, 0);                }        }        register unsigned long a0 asm("a0") = arg0;        register unsigned long a1 asm("a1") = arg1;        __asm__ __volatile__("mret" : : "r"(a0), "r"(a1));        __builtin_unreachable();}

该函数就是用来从OpenSBI跳转到kernel执行的一段代码,因为从OpenSBI跳转到kernel执行需要从Machine模式切换到Supervisor模式,所以需要mret指令,而mret指令执行时,会将CSR_MEPC的值复制到PC中,也就是说CSR_MEPC中存放了mret指令返回之后需要执行的地址,而CSR_MEPC的值就是next_addr,也就是kernel的加载地址,我们把next_addr打印出来就可以确定kernel的地址了,关于代码的详解,后面我会专门写文章进行分析,现在我们来加一句打印,如下图:

图片

前面加一串1是为了醒目一点,也便于在输出的log中进行搜索,重新编译OpenSBI并运行,可以看到输出的kernel address为0x80200000:

图片

02 MMU开启之前的调试

知道了kernel的运行地址之后,我们就可以调试kernel了,先来调试MMU开启之前的代码,基本都是汇编代码,调试之前需要对kernel进行配置

执行如下命令:

make ARCH=riscv CROSS_COMPILE=riscv64-linux-gnu- menuconfig

然后进行如下配置:

Kernel hacking  --->  Compile-time checks and compiler options  --->      Debug information         Rely on the toolchain's implicit default DWARF version

‍然后重新编译kernel:

make ARCH=riscv CROSS_COMPILE=riscv64-linux-gnu- -j12

编译完成后运行qemu:

./run.sh -S -s

此时qemu等待gdb连接

在启动gdb之前我们还需要看一下kernel的各个段的地址,在linux源码目录下执行如下命令:

readelf -S vmlinux

得到如下段信息:

There are 40 section headers, starting at offset 0x10fd0368:Section Headers:  [Nr] Name              Type             Address           Offset       Size              EntSize          Flags  Link  Info  Align  [ 0]                   NULL             0000000000000000  00000000       0000000000000000  0000000000000000           0     0     0  [ 1] .head.text        PROGBITS         ffffffff80000000  00001000       0000000000001e9c  0000000000000000  AX       0     0     4096  [ 2] .text             PROGBITS         ffffffff80002000  00003000       0000000000973fdc  0000000000000000  AX       0     0     4  [ 3] .init.text        PROGBITS         ffffffff80a00000  00a00000       000000000004a4a2  0000000000000000  AX       0     0     2097152  [ 4] .exit.text        PROGBITS         ffffffff80a4a4a8  00a4a4a8       00000000000027a8  0000000000000000  AX       0     0     2  [ 5] .init.data        PROGBITS         ffffffff80c00000  00a4d000       0000000000024320  0000000000000000  WA       0     0     4096  [ 6] .init.pi          PROGBITS         ffffffff80c24320  00a71320       0000000000002533  0000000000000000 WAX       0     0     8  [ 7] .init.bss         NOBITS           ffffffff80c26858  00a73853       0000000000000048  0000000000000000  WA       0     0     8  [ 8] .data..percpu     PROGBITS         ffffffff80c27000  00a74000       000000000000c0a8  0000000000000000  WA       0     0     64  [ 9] .alternative      PROGBITS         ffffffff80c330a8  00a800a8       00000000000017c0  0000000000000000   A       0     0     1  [10] .rodata           PROGBITS         ffffffff80e00000  00a82000       00000000002c6ff0  0000000000000000  WA       0     0     256  [11] .pci_fixup        PROGBITS         ffffffff810c6ff0  00d48ff0       0000000000004068  0000000000000000   A       0     0     8  [12] __ksymtab         PROGBITS         ffffffff810cb058  00d4d058       000000000001dc70  0000000000000000   A       0     0     8  [13] __ksymtab_gpl     PROGBITS         ffffffff810e8cc8  00d6acc8       0000000000025260  0000000000000000   A       0     0     8  [14] __ksymtab_strings PROGBITS         ffffffff8110df28  00d8ff28       0000000000036833  0000000000000001 AMS       0     0     1  [15] __param           PROGBITS         ffffffff81144760  00dc6760       0000000000003340  0000000000000000   A       0     0     8  [16] __modver          PROGBITS         ffffffff81147aa0  00dc9aa0       0000000000000240  0000000000000000  WA       0     0     8  [17] __ex_table        PROGBITS         ffffffff81147ce0  00dc9ce0       0000000000002040  0000000000000000   A       0     0     4  [18] .notes            NOTE             ffffffff81149d20  00dcbd20       0000000000000054  0000000000000000   A       0     0     4  [19] .srodata          PROGBITS         ffffffff81200000  00dcc000       0000000000001738  0000000000000000   A       0     0     8  [20] .data             PROGBITS         ffffffff81400000  00dce000       00000000000f7b80  0000000000000000  WA       0     0     4096  [21] __bug_table       PROGBITS         ffffffff814f7b80  00ec5b80       000000000001b378  0000000000000000  WA       0     0     1  [22] .sdata            PROGBITS         ffffffff81512ef8  00ee0ef8       0000000000002140  0000000000000000  WA       0     0     8  [23] .got              PROGBITS         ffffffff81515038  00ee3038       0000000000000020  0000000000000008  WA       0     0     8  [24] .pecoff_edat[...] PROGBITS         ffffffff81515058  00ee3058       00000000000001a8  0000000000000000   A       0     0     1  [25] .sbss             NOBITS           ffffffff81516000  00ee3200       0000000000002895  0000000000000000  WA       0     0     64  [26] .bss              NOBITS           ffffffff81519000  00ee3200       00000000000778f0  0000000000000000  WA       0     0     4096  [27] .debug_aranges    PROGBITS         0000000000000000  00ee3200       0000000000023720  0000000000000000           0     0     16  [28] .debug_info       PROGBITS         0000000000000000  00f06920       000000000a7aac35  0000000000000000           0     0     1  [29] .debug_abbrev     PROGBITS         0000000000000000  0b6b1555       00000000004ecafe  0000000000000000           0     0     1  [30] .debug_line       PROGBITS         0000000000000000  0bb9e053       0000000001e4ad57  0000000000000000           0     0     1  [31] .debug_frame      PROGBITS         0000000000000000  0d9e8db0       00000000002d8e50  0000000000000000           0     0     8  [32] .debug_str        PROGBITS         0000000000000000  0dcc1c00       000000000031e645  0000000000000001  MS       0     0     1  [33] .debug_line_str   PROGBITS         0000000000000000  0dfe0245       00000000000137bc  0000000000000001  MS       0     0     1  [34] .debug_loclists   PROGBITS         0000000000000000  0dff3a01       00000000020a1d35  0000000000000000           0     0     1  [35] .debug_rnglists   PROGBITS         0000000000000000  10095736       000000000091383b  0000000000000000           0     0     1  [36] .comment          PROGBITS         0000000000000000  109a8f71       000000000000002b  0000000000000001  MS       0     0     1  [37] .symtab           SYMTAB           0000000000000000  109a8fa0       000000000041b7b8  0000000000000018          38   154951     8  [38] .strtab           STRTAB           0000000000000000  10dc4758       000000000020ba72  0000000000000000           0     0     1  [39] .shstrtab         STRTAB           0000000000000000  10fd01ca       0000000000000198  0000000000000000           0     0     1Key to Flags:  W (write), A (alloc), X (execute), M (merge), S (strings), I (info),  L (link order), O (extra OS processing required), G (group), T (TLS),  C (compressed), x (unknown), o (OS specific), E (exclude),  D (mbind), p (processor specific)

我们需要知道如下段的地址,其他的暂时可以不用管:

.head.text ffffffff80000000.text      ffffffff80002000.init.text ffffffff80a00000.rodata    ffffffff80e00000

知道这些段地址之后,就可以启动gdb了,在新终端上输入如下命令:

gdb-multiarch

然后在gdb命令行中输入如下内容加载symbol:

add-symbol-file vmlinux 0x80202000 -s .head.text 0x80200000 -s .init.text 0x80c00000 -s .rodata 0x81000000

然后输入y,如下图:

图片

上面symbol各个段加载的地址的计算方法是:

kernel运行地址+相对于0xffffffff80000000的偏移

0x80200000+(addr-0xffffffff80000000)

然后连接qemu:

target remote:1234

之后就可以设置断点进行调试了,注意只能设置MMU开启之前的断点:

图片

到此,MMU开启之前的代码就可以使用gdb单步进行跟踪调试了,下面我们来看MMU开启之后的调试步骤。

03 MMU开启之后的调试

其实,MMU开启之后的调试比开启之前的调试简单很多,因为MMU开启之后,CPU访问的都是虚拟地址,而kernel链接地址就是按照虚拟地址进行的,也就是说运行地址和链接地址是一致的,这种情况下,直接按照vmlinux中的symbol进行加载即可

依然是先运行qemu:

./run.sh -S -s

重新开启一个终端,输入如下命令:

gdb-multiarch vmlinux

之后在gdb命令行执行:

target remote:1234

此时就可以设置断点进行调试了,注意只能设置MMU开启之后的断点:

图片

注意上图中的断点是start_kernel不是MMU开启之前的_start_kernel,不带下划线

终于熬夜写完了qemu+gdb调试系列~

到这里,qemu+gdb调试OpenSBI和kernel都已经完结了,恭喜你,你可以进行OpenSBI和Linux kernel代码的研究了,看不懂的地方不放gdb调试一下。

本篇文章发布于微信公众号:Linux底层小工,欢迎关注,获取更多原创技术文章!

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Linux底层小工

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值