写点什么

qemu 调试 kernel 启动(从第一行汇编开始)

作者:无人知晓
  • 2023-12-09
    广东
  • 本文字数:2911 字

    阅读完需:约 10 分钟

一、背景

大部分 qemu 调试 kernel 都是讲解从 start_kernel 开始设置断点,然后开启调试; 但是我们熟悉 linux 启动流程的伙伴肯定知道,在 start_kernel 之前还有一段汇编,包括初始化页表及 mmu 等操作, 这部分如何调试呢?

二、如何从第一行代码开始调试?

无论是 gdb 调试还是 JTAG 调试,其中最重要的一个就是加载 symbols 到正确的物理/虚拟地址(是物理地址还是虚拟地址取决于此时 mmu 是否有打开); 我们需要知道 kernel 的第一行地址是什么? 对应的 symbols 的区域在 vmlinux 的哪里?

qemu 启动 kernel 的物理地址:

qemu 启动增加-S 选项时(启动时停止等待 gdb 连接,这时会显示一个地址,这个就是当前启动的物理地址)


vmlinux 中的的起始地址(虚拟地址):

在源码的 System.map 或者通过 gdb 打开 vmlinux 查看,内核的入口是_text,虚拟地址 0xffff800080000000


_text 的定义在 arch/arm64/kernel/vmlinux.lds.S (注意,这里的 section name:.head.text 放的是_text 不要和.text 段名搞混了)

ENTRY(_text)...SECTIONS{...	.head.text : {		_text = .;		HEAD_TEXT	}	.text : ALIGN(SEGMENT_ALIGN) {	/* Real text segment		*/		_stext = .;		/* Text and read-only data	*/			IRQENTRY_TEXT			SOFTIRQENTRY_TEXT			ENTRY_TEXT			TEXT_TEXT			SCHED_TEXT			LOCK_TEXT			KPROBES_TEXT			HYPERVISOR_TEXT			*(.gnu.warning)	}
. = ALIGN(SEGMENT_ALIGN); _etext = .; /* End of text section */...
复制代码

qemu 启动的物理地址和 vmlinux 中启动地址(_text 虚拟地址)的关系

先来看 qemu 的启动地址 0x0000000040000000 附近内容

(gdb) x /16i 0x0000000040000000   0x40000000:	ldr	x0, 0x40000018   0x40000004:	mov	x1, xzr   0x40000008:	mov	x2, xzr   0x4000000c:	mov	x3, xzr   0x40000010:	ldr	x4, 0x40000020   0x40000014:	br	x4   0x40000018:	stxrh	w0, w0, [x0]   0x4000001c:	udf	#0   0x40000020:	.inst	0x40200000 ; undefined   0x40000024:	udf	#0   0x40000028:	udf	#0   0x4000002c:	udf	#0   0x40000030:	udf	#0   0x40000034:	udf	#0   0x40000038:	udf	#0   0x4000003c:	udf	#0
(gdb) x /16x 0x00000000400000000x40000000: 0x580000c0 0xaa1f03e1 0xaa1f03e2 0xaa1f03e30x40000010: 0x58000084 0xd61f0080 0x48000000 0x000000000x40000020: 0x40200000 0x00000000 0x00000000 0x000000000x40000030: 0x00000000 0x00000000 0x00000000 0x00000000
复制代码

注意:不同的 qemu 版本可能起始的物理地址不同,本人电脑使用 ubuntu22.04 自带版本,6.2.0

geek@geek-virtual-machine:~/workspace/linux/linux-6.6.1$ qemu-system-aarch64 --versionQEMU emulator version 6.2.0 (Debian 1:6.2+dfsg-2ubuntu6.15)Copyright (c) 2003-2021 Fabrice Bellard and the QEMU Project developers
复制代码

源码路径:https://gitlab.com/qemu-project/qemu.git ,切换到 6.2.0 版本

geek@geek-virtual-machine:~/workspace/linux/qemu_src/qemu$ git tag | grep 6.2v1.6.2v2.6.2v6.2.0v6.2.0-rc0v6.2.0-rc1v6.2.0-rc2v6.2.0-rc3v6.2.0-rc4geek@geek-virtual-machine:~/workspace/linux/qemu_src/qemu$ git reset --hard v6.2.0
复制代码

qemu 启动 kernel 的部分在 qemu 源码路径:hw/arm/boot.c

(gdb) si0x0000000040000010 in ?? ()=> 0x0000000040000010:	84 00 00 58	ldr	x4, 0x40000020
(gdb) x /x 0x400000200x40000020: 0x40200000
ldr x4, 0x4000002 //把0x40000020 地址存储的值读取到x4,实际就是上面bootloader_aarch64[]数组定义的 //FIXUP_ENTRYPOINT_LO + FIXUP_ENTRYPOINT_HIbr x4 //跳转到x4 并执行
复制代码

通过 qemu 代码的注释也可以看到,在这个版本的 qemu 中 arm64 的 kernel 起始地址是放在 0x40200000,并从这里开始执行第一条指令;

所以我们要在 qemu 中做的就是将物理地址 0x40200000 与 vmlinux 中的第一条指令地址 0xffff800080000000 (_text) 对齐即可;

gdb 已经连接qemu linux kernel(gdb) x /16x 0x402000000x40200000:	0xfa405a4d	0x146a6427	0x00000000	0x000000000x40200010:	0x02860000	0x00000000	0x0000000a	0x000000000x40200020:	0x00000000	0x00000000	0x00000000	0x000000000x40200030:	0x00000000	0x00000000	0x644d5241	0x00000040


gdb vmlinux直接查看_text处的汇编(gdb) x /16x _text0x80000000 <_text>: 0xfa405a4d 0x146a6427 0x00000000 0x000000000x80000010 <$d+8>: 0x02860000 0x00000000 0x0000000a 0x000000000x80000020 <$d+24>: 0x00000000 0x00000000 0x00000000 0x000000000x80000030 <$d+40>: 0x00000000 0x00000000 0x644d5241 0x00000040
复制代码

通过 readelf 确认那些段需要映射

geek@geek-virtual-machine:~/workspace/linux/linux-6.6.1$ aarch64-none-linux-gnu-readelf -S vmlinuxThere are 43 section headers, starting at offset 0x18ff9fb8:
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 ffff800080000000 00010000 0000000000010000 0000000000000000 AX 0 0 65536 [ 2] .text PROGBITS ffff800080010000 00020000 000000000102b000 0000000000000000 AX 0 0 65536 [ 3] .rodata PROGBITS ffff800081040000 01050000 00000000009dc8c8 0000000000000000 WA 0 0 4096...... [15] .rodata.text PROGBITS ffff800081a94800 01aa4800 0000000000005800 0000000000000000 AX 0 0 2048 [16] .init.text PROGBITS ffff800081aa0000 01ab0000 000000000008c6f8 0000000000000000 AX 0 0 8...... [19] .init.data PROGBITS ffff800081b95000 01ba5000 00000000000c551a 0000000000000000 WA 0 0 256......
复制代码

地址映射关系:

启动 gdb 时不要加载 vmlinux, 通过 add-symbol-file 指定 section 要加载的物理地址

add-symbol-file vmlinux -s .head.text 0x40200000 -s .text 0x40210000 -s .rodata 0x40240000 -s .rodata.text 0x41C94800 -s .init.text 0x41CA0000 -s .init.data 0x41D95000

设置断点:b _text

然后就可以单步调试:

三、总结

其实不管使用什么调试器(gdb/T32/Crash/lldb),第一步要做的都是将 elf 和调试 target 的执行地址做一个对齐,当然这个对齐可能是物理地址对齐(无 mmu,如 bootloader,elf 编译的地址就是代码运行的物理地址),也有可能是虚拟地址对齐(开启了 mmu 比如 kernel start_kernel 之后部分),也有可能是物理地址与虚拟地址对齐(比如本文中的_text 到 start_kernel), 掌握了这个规律也就掌握的调试的入口密码。

发布于: 刚刚阅读数: 4
用户头像

无人知晓

关注

记录我的linux学习和总结 2023-07-31 加入

知乎帐号《无人知晓》同步更新:

评论

发布
暂无评论
qemu调试kernel启动(从第一行汇编开始)_qemu_无人知晓_InfoQ写作社区