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

一、背景

大部分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 0x0000000040000000
0x40000000:	0x580000c0	0xaa1f03e1	0xaa1f03e2	0xaa1f03e3
0x40000010:	0x58000084	0xd61f0080	0x48000000	0x00000000
0x40000020:	0x40200000	0x00000000	0x00000000	0x00000000
0x40000030:	0x00000000	0x00000000	0x00000000	0x00000000

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

geek@geek-virtual-machine:~/workspace/linux/linux-6.6.1$ qemu-system-aarch64 --version
QEMU 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.2
v1.6.2
v2.6.2
v6.2.0
v6.2.0-rc0
v6.2.0-rc1
v6.2.0-rc2
v6.2.0-rc3
v6.2.0-rc4
geek@geek-virtual-machine:~/workspace/linux/qemu_src/qemu$ git reset --hard v6.2.0

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

(gdb) si
0x0000000040000010 in ?? ()
=> 0x0000000040000010:	84 00 00 58	ldr	x4, 0x40000020

(gdb) x /x 0x40000020
0x40000020:	0x40200000

ldr	x4, 0x4000002  //把0x40000020 地址存储的值读取到x4,实际就是上面bootloader_aarch64[]数组定义的
                       //FIXUP_ENTRYPOINT_LO + FIXUP_ENTRYPOINT_HI
br	x4    //跳转到x4 并执行

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

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

gdb 已经连接qemu linux kernel
(gdb) x /16x 0x40200000
0x40200000:	0xfa405a4d	0x146a6427	0x00000000	0x00000000
0x40200010:	0x02860000	0x00000000	0x0000000a	0x00000000
0x40200020:	0x00000000	0x00000000	0x00000000	0x00000000
0x40200030:	0x00000000	0x00000000	0x644d5241	0x00000040



gdb vmlinux直接查看_text处的汇编
(gdb) x /16x _text
0x80000000 <_text>:	0xfa405a4d	0x146a6427	0x00000000	0x00000000
0x80000010 <$d+8>:	0x02860000	0x00000000	0x0000000a	0x00000000
0x80000020 <$d+24>:	0x00000000	0x00000000	0x00000000	0x00000000
0x80000030 <$d+40>:	0x00000000	0x00000000	0x644d5241	0x00000040

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

geek@geek-virtual-machine:~/workspace/linux/linux-6.6.1$ aarch64-none-linux-gnu-readelf -S vmlinux
There 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
......

地址映射关系:

section name virtual addr phy addr
.head.text 0xffff800080000000 0x40200000
.text 0xffff800080010000 0x40210000
.rodata 0xffff800081040000 0x40240000
.rodata.text 0xffff800081a94800 0x41C94800
.init.text 0xffff800081aa0000 0x41CA0000
.init.data 0xffff800081b95000 0x41D95000

启动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), 掌握了这个规律也就掌握的调试的入口密码。

相关推荐
内核程序员kevin1 小时前
TCP Listen 队列详解与优化指南
linux·网络·tcp/ip
朝九晚五ฺ5 小时前
【Linux探索学习】第十四弹——进程优先级:深入理解操作系统中的进程优先级
linux·运维·学习
自由的dream5 小时前
Linux的桌面
linux
xiaozhiwise6 小时前
Makefile 之 自动化变量
linux
意疏8 小时前
【Linux 篇】Docker 的容器之海与镜像之岛:于 Linux 系统内探索容器化的奇妙航行
linux·docker
BLEACH-heiqiyihu8 小时前
RedHat7—Linux中kickstart自动安装脚本制作
linux·运维·服务器
一只爱撸猫的程序猿8 小时前
一个简单的Linux 服务器性能优化案例
linux·mysql·nginx
我的K840910 小时前
Flink整合Hudi及使用
linux·服务器·flink
19004310 小时前
linux6:常见命令介绍
linux·运维·服务器