本篇文章发布于微信公众号: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 1
Key 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底层小工,欢迎关注,获取更多原创技术文章!