文章目录
1. 前言
限于作者能力水平,本文可能存在谬误,因此而给读者带来的损失,作者不做任何承诺。
2. 调试环境
本文分析调试环境为:
主机 : Ubuntu 16.04.4 LTS
VM 客户机:QEMU + ARM vexpress-a9 (ARM32 架构)
linux-4.14.315 内核 (用交叉编译器 gcc-linaro-5.3-2016.02-x86_64_arm-linux-gnueabihf 编译)
rootfs 基于 ubuntu-base-16.04-core-armhf.tar.gz 制作
交叉编译器:gcc-linaro-5.3-2016.02-x86_64_arm-linux-gnueabihf
3. 调试内核启动代码的步骤
3.1 工具准备
调试分析 需用到 ARM 交叉编译器
、ARM 平台 GDB
、QEMU
。这里给出 ARM 交叉编译器 和 GDB 下载链接 ,如果该链接无法访问(建议通过 Google Chrome
浏览器访问),可以通过命令 sudo apt-get install gcc-arm-linux-gnueabi
和 sudo apt install gdb-multiarch
分别安装 ARM 交叉编译器
和 跨平台 GDB 调试器
,QEMU
使用命令 sudo apt-get install qemu
安装。
3.2 构建客户端系统
3.2.1 编译可调试内核
启用内核配置项 CONFIG_DEBUG_KERNEL
和 CONFIG_DEBUG_INFO
,用 ARM 交叉编译器
编译内核。适配 QEMU + ARM vexpress-a9 内核
的过程,网上有很多资料,读者可自行查找。
3.2.2 构建根文件系统
本文测试过程中,使用基于 ubuntu 官方基础包构建 根文件系统(rootfs)
,读者不必使用相同的构建方式。事实上,根文件系统
对于本文的调试过程也不是必须的
,毕竟我们仅仅是调试内核启动阶段的代码。
构建跟文件系统的方法,可以参考链接 https://gitee.com/jimokuangxiangqu/arm32-linux/tree/master/rootfs/arm-ubuntu,这是笔者测试中使用的根文件系统。
3.3 调试客户端系统内核
3.3.1 ARM32 Linux 内核启动过程
整个调试过程,需要对 ARM32 Linux 内核启动过程
的有所了解;而了解 zImage
的构建过程,可以帮助理解 Linux 内核的启动过程。zImage
构建过程如下:
编译+链接 objcopy 压缩(gzip,lzo,lzma,...)
1. linux源代码 ---------> vmlinux(elf文件) -------> arch/arm/boot/Image -----------------------> piggy_data
编译
2. piggy.S(包含 piggy_data 压缩内核) ------> piggy.o
链接 objcopy
3. (head.o,misc.o,decompress.o,...) + piggy.o ----> arch/arm/boot/compressed/vmlinux ------> arch/arm/boot/zImage
从上面看到,实际上 zImage
包含两部分
内容:
. 内核解压代码: head.o, misc.o, decompress.o
. 压缩的内核: piggy.o
BootLoader
(如 U-BOOT) 结束时,将 内核解压程序
加载到内存执行,然后 内核解压程序
将压缩的内核(piggy.o)
解压缩到内存执行:
BootLoader -> 内核解压程序 -> 内核
本文测试的客户端系统是在 QEMU
模拟环境下加载执行,QEMU 在这里充当 BootLoader 的角色
,它先做些准备工作,然后跳转到 内核解压程序
开始执行。如果项了解内核解压的更多细节,可参考博文 Linux:内核解压缩过程简析 。
3.3.2 内核启动过程调试
了解了 Linux 内核的启动过程后,我们来分别看 内核解压程序
和 内核启动代码
的调试。
3.3.2.1 调试 内核解压程序
首先用 QEMU
启动 3.2
小节中构建的 客户机系统
,并指示 QEMU 冻结程序执行(通过 QEMU -S 参数)
,启动 QEMU 内置的 GDB 服务端(通过 QEMU -s 参数)
,等待 GDB 客户端
连接 和 进一步调试指令:
sudo qemu-system-arm \
-M vexpress-a9 -smp 4 -m 512M \
-kernel linux-4.14.315/output/arch/arm/boot/zImage \
-dtb linux-4.14.315/output/arch/arm/boot/dts/vexpress-v2p-ca9.dtb \
-nographic \
-append "root=/dev/mmcblk0 rw rootfstype=ext4 console=ttyAMA0" \
-sd rootfs/arm-ubuntu-16.04.img \
-S -s
通过主机系统 Ubuntu 16.04.4 LTS
下 ARM 交叉编译工具链 自带的 GDB 客户端程序
,连接 QEMU GDB 服务端
:
$ arm-linux-gnueabihf-gdb linux-4.14.315/output/vmlinux
上图中,左边的窗口是 QEMU
启动 客户机系统
执行结果窗口,右边的窗口是 arm-linux-gnueabihf-gdb
执行结果窗口。arm-linux-gnueabihf-gdb
在启动后,执行了
set disassemble-next-line on
target remote :1234
GDB
指令 target remote :1234
,连接到了 QEMU GDB 服务端
,现在可以进一步通过 arm-linux-gnueabihf-gdb
向 QEMU GDB 服务端
发起请求,接下来可以进一步调试 内核解压程序
了。
QEMU
在跳转到 内核解压程序
前,还做了一些其它操作,来看一下:
接下来通过 GDB
的 si 4
命令,跳转到 内核解压程序
入口:
怎么确定地址 0x60010000
处的代码是 内核解缩程序
入口?一方面,可以通过 QEMU 源码确定,这种情况读者可以自行阅读 QEMU
源码;另一方面,可以通过反汇编 zImage
文件确认:
$ arm-linux-gnueabihf-objdump -D -marm -b binary linux-4.14.315/output/arch/arm/boot/zImage | head -n 23
linux-4.14.315/output/arch/arm/boot/zImage: file format binary
Disassembly of section .data:
00000000 <.data>:
0: e1a00000 nop ; (mov r0, r0)
4: e1a00000 nop ; (mov r0, r0)
8: e1a00000 nop ; (mov r0, r0)
c: e1a00000 nop ; (mov r0, r0)
10: e1a00000 nop ; (mov r0, r0)
14: e1a00000 nop ; (mov r0, r0)
18: e1a00000 nop ; (mov r0, r0)
1c: e1a00000 nop ; (mov r0, r0)
20: ea000003 b 0x34
24: 016f2818 cmneq pc, r8, lsl r8 ; <UNPREDICTABLE>
28: 00000000 andeq r0, r0, r0
2c: 003d7148 eorseq r7, sp, r8, asr #2
30: 04030201 streq r0, [r3], #-513 ; 0xfffffdff
34: e10f9000 mrs r9, CPSR
38: eb000d28 bl 0x34e0
3c: e1a07001 mov r7, r1
可以看到,对 zImage
的反汇编代码 对应到了 地址 0x60010000
之处的反汇编代码,从而确定了 地址 0x60010000
为 内核解压程序
入口。
接下来,加载 内核解压程序
的符号表,这样可以方便的通过对标号等符号设置断点
,在需要的时候不必一步一步的执行 si
调断肠。
通过 GDB
命令 add-symbol-file
加载 内核解压程序 (linux-4.14.315/output/arch/arm/boot/compressed/vmlinux)
的符号表
。其中 内核解压程序
的 当前加载地址 0x60010000
是关键参数,只有正确指定了该地址,才能正确的解析符号。有了符号表,接下来就可以对 restart
标号设置一个断点,接着运行 GDB
的 c 指令
继续执行 内核解压程序
,然后 内核解压程序
会停在标号 restart
处,这时候查看寄存 r4
的内容,就可以知道 内核解压后的加载地址
:
从上图可以知道,内核解压后的加载地址 为 0x60008000
。我们不能直接在 __enter_kernel
标号处设置断点,因为解压缩过程中,可能由于 内核的加载地址
和 内核解压代码
彼此之间存在重叠,要重定位
当前正在执行的 内核解压程序的解压代码 到其它地址,所以 __enter_kernel
在整个过程中可能产生变化,导致设置在 __enter_kernel
处的断点失效。
如果想了解 内核解压
、以及 解压代码重定位
更深入的细节,就需要一步步的执行 si
指令。如果中间有发生 解压代码重定位
,需要重新通过 add-symbol-file
加载 内核解压程序
的符号表到对应的新地址,这样才能正确的解析 内核解压程序
的符号。
如果不想了解更多的细节,可以直接对 解压后的内核入口地址
设置断点,然后执行 c
指令,就会跳转到内核入口并停止执行:
=> 0x600100c8 <restart+0>: 5f 0f 8f e2 add r0, pc, #380 ; 0x17c
(gdb) info reg r4
r4 0x60008000 1610645504
(gdb) b *0x60008000
Breakpoint 2 at 0x60008000
(gdb) c
Continuing.
Thread 1 hit Breakpoint 2, 0x60008000 in ?? ()
=> 0x60008000: ae 36 04 eb bl 0x60115ac0
(gdb)
3.3.2.2 调试 内核启动代码
在进一步调试内核启动代码前,需要先通过指令 remove-symbol-file
移除 内核解压程序
的符号表
:
(gdb) remove-symbol-file -a 0x60010000
Remove symbol table from file "/home/XXX/Study/qemu-lab/linux-4.14.315/output/arch/arm/boot/compressed/vmlinux"? (y or n) y
接下来调试内核的方法 和 调试 内核解压程序 的方法类似,可以通过 si
和 ni
指令,也可以设置标号、函数断点。
如果想直接跳转到内核 C 代码入口 start_kernel()
,也可以对该函数直接设置断点,然后执行 c
指令进入:
Thread 1 hit Breakpoint 1, 0x60008000 in ?? ()
=> 0x60008000: ae 36 04 eb bl 0x60115ac0
(gdb) disassemble 0x60008000,+64
Dump of assembler code from 0x60008000 to 0x60008040:
=> 0x60008000: bl 0x60115ac0
0x60008004: mrs r9, CPSR
0x60008008: eor r9, r9, #26
0x6000800c: tst r9, #31
0x60008010: bic r9, r9, #31
0x60008014: orr r9, r9, #211 ; 0xd3
0x60008018: bne 0x60008030
0x6000801c: orr r9, r9, #256 ; 0x100
0x60008020: add lr, pc, #12
0x60008024: msr SPSR_fsxc, r9
0x60008028: msr ELR_hyp, lr
0x6000802c: eret
0x60008030: msr CPSR_c, r9
0x60008034: mrc 15, 0, r9, cr0, cr0, {0}
0x60008038: bl 0x60101aac
0x6000803c: movs r10, r5
End of assembler dump.
(gdb) b start_kernel
Breakpoint 2 at 0x80a00a44: file ../init/main.c, line 514.
(gdb) info c
Ambiguous info command "c": checkpoints, classes, common, copying.
(gdb) c
Continuing.
Thread 1 hit Breakpoint 2, start_kernel () at ../init/main.c:514
514 {
=> 0x80a00a44 <start_kernel+0>: 0d c0 a0 e1 mov r12, sp
0x80a00a48 <start_kernel+4>: f0 df 2d e9 push {r4, r5, r6, r7, r8, r9, r10, r11, r12, lr, pc}
0x80a00a4c <start_kernel+8>: 04 b0 4c e2 sub r11, r12, #4
0x80a00a50 <start_kernel+12>: 24 d0 4d e2 sub sp, sp, #36 ; 0x24
0x80a00a54 <start_kernel+16>: 04 e0 2d e5 push {lr} ; (str lr, [sp, #-4]!)
0x80a00a58 <start_kernel+20>: 54 3e dc eb bl 0x801103b0 <__gnu_mcount_nc>
(gdb)
通过内核的 System.map
文件,内核函数 start_kernel()
正是
4. 参考资料
https://blog.printk.io/2019/06/arm-linux-kernel-early-startup-code-debugging/
https://sourceware.org/gdb/current/onlinedocs/gdb.html/
https://developer.arm.com/documentation/101471/2023-0/Arm-Debugger-commands/Arm-Debugger-commands-listed-in-alphabetical-order/add-symbol-file?lang=en