Linux: 内核启动代码调试

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 平台 GDBQEMU。这里给出 ARM 交叉编译器 和 GDB 下载链接 ,如果该链接无法访问(建议通过 Google Chrome 浏览器访问),可以通过命令 sudo apt-get install gcc-arm-linux-gnueabisudo apt install gdb-multiarch 分别安装 ARM 交叉编译器跨平台 GDB 调试器QEMU 使用命令 sudo apt-get install qemu 安装。

3.2 构建客户端系统

3.2.1 编译可调试内核

启用内核配置项 CONFIG_DEBUG_KERNELCONFIG_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 LTSARM 交叉编译工具链 自带的 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-gdbQEMU GDB 服务端 发起请求,接下来可以进一步调试 内核解压程序 了。
QEMU 在跳转到 内核解压程序 前,还做了一些其它操作,来看一下:
在这里插入图片描述
接下来通过 GDBsi 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 标号设置一个断点,接着运行 GDBc 指令继续执行 内核解压程序,然后 内核解压程序 会停在标号 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

接下来调试内核的方法 和 调试 内核解压程序 的方法类似,可以通过 sini 指令,也可以设置标号、函数断点。
如果想直接跳转到内核 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

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值