当前位置:   article > 正文

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

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段名搞混了)

  1. ENTRY(_text)
  2. ...
  3. SECTIONS
  4. {
  5. ...
  6. .head.text : {
  7. _text = .;
  8. HEAD_TEXT
  9. }
  10. .text : ALIGN(SEGMENT_ALIGN) { /* Real text segment */
  11. _stext = .; /* Text and read-only data */
  12. IRQENTRY_TEXT
  13. SOFTIRQENTRY_TEXT
  14. ENTRY_TEXT
  15. TEXT_TEXT
  16. SCHED_TEXT
  17. LOCK_TEXT
  18. KPROBES_TEXT
  19. HYPERVISOR_TEXT
  20. *(.gnu.warning)
  21. }
  22. . = ALIGN(SEGMENT_ALIGN);
  23. _etext = .; /* End of text section */
  24. ...

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

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

  1. (gdb) x /16i 0x0000000040000000
  2. 0x40000000: ldr x0, 0x40000018
  3. 0x40000004: mov x1, xzr
  4. 0x40000008: mov x2, xzr
  5. 0x4000000c: mov x3, xzr
  6. 0x40000010: ldr x4, 0x40000020
  7. 0x40000014: br x4
  8. 0x40000018: stxrh w0, w0, [x0]
  9. 0x4000001c: udf #0
  10. 0x40000020: .inst 0x40200000 ; undefined
  11. 0x40000024: udf #0
  12. 0x40000028: udf #0
  13. 0x4000002c: udf #0
  14. 0x40000030: udf #0
  15. 0x40000034: udf #0
  16. 0x40000038: udf #0
  17. 0x4000003c: udf #0
  18. (gdb) x /16x 0x0000000040000000
  19. 0x40000000: 0x580000c0 0xaa1f03e1 0xaa1f03e2 0xaa1f03e3
  20. 0x40000010: 0x58000084 0xd61f0080 0x48000000 0x00000000
  21. 0x40000020: 0x40200000 0x00000000 0x00000000 0x00000000
  22. 0x40000030: 0x00000000 0x00000000 0x00000000 0x00000000

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

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

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

  1. geek@geek-virtual-machine:~/workspace/linux/qemu_src/qemu$ git tag | grep 6.2
  2. v1.6.2
  3. v2.6.2
  4. v6.2.0
  5. v6.2.0-rc0
  6. v6.2.0-rc1
  7. v6.2.0-rc2
  8. v6.2.0-rc3
  9. v6.2.0-rc4
  10. geek@geek-virtual-machine:~/workspace/linux/qemu_src/qemu$ git reset --hard v6.2.0

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

  1. (gdb) si
  2. 0x0000000040000010 in ?? ()
  3. => 0x0000000040000010: 84 00 00 58 ldr x4, 0x40000020
  4. (gdb) x /x 0x40000020
  5. 0x40000020: 0x40200000
  6. ldr x4, 0x4000002 //0x40000020 地址存储的值读取到x4,实际就是上面bootloader_aarch64[]数组定义的
  7. //FIXUP_ENTRYPOINT_LO + FIXUP_ENTRYPOINT_HI
  8. br x4 //跳转到x4 并执行

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

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

  1. gdb 已经连接qemu linux kernel
  2. (gdb) x /16x 0x40200000
  3. 0x40200000: 0xfa405a4d 0x146a6427 0x00000000 0x00000000
  4. 0x40200010: 0x02860000 0x00000000 0x0000000a 0x00000000
  5. 0x40200020: 0x00000000 0x00000000 0x00000000 0x00000000
  6. 0x40200030: 0x00000000 0x00000000 0x644d5241 0x00000040
  7. gdb vmlinux直接查看_text处的汇编
  8. (gdb) x /16x _text
  9. 0x80000000 <_text>: 0xfa405a4d 0x146a6427 0x00000000 0x00000000
  10. 0x80000010 <$d+8>: 0x02860000 0x00000000 0x0000000a 0x00000000
  11. 0x80000020 <$d+24>: 0x00000000 0x00000000 0x00000000 0x00000000
  12. 0x80000030 <$d+40>: 0x00000000 0x00000000 0x644d5241 0x00000040

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

  1. geek@geek-virtual-machine:~/workspace/linux/linux-6.6.1$ aarch64-none-linux-gnu-readelf -S vmlinux
  2. There are 43 section headers, starting at offset 0x18ff9fb8:
  3. Section Headers:
  4. [Nr] Name Type Address Offset
  5. Size EntSize Flags Link Info Align
  6. [ 0] NULL 0000000000000000 00000000
  7. 0000000000000000 0000000000000000 0 0 0
  8. [ 1] .head.text PROGBITS ffff800080000000 00010000
  9. 0000000000010000 0000000000000000 AX 0 0 65536
  10. [ 2] .text PROGBITS ffff800080010000 00020000
  11. 000000000102b000 0000000000000000 AX 0 0 65536
  12. [ 3] .rodata PROGBITS ffff800081040000 01050000
  13. 00000000009dc8c8 0000000000000000 WA 0 0 4096
  14. ......
  15. [15] .rodata.text PROGBITS ffff800081a94800 01aa4800
  16. 0000000000005800 0000000000000000 AX 0 0 2048
  17. [16] .init.text PROGBITS ffff800081aa0000 01ab0000
  18. 000000000008c6f8 0000000000000000 AX 0 0 8
  19. ......
  20. [19] .init.data PROGBITS ffff800081b95000 01ba5000
  21. 00000000000c551a 0000000000000000 WA 0 0 256
  22. ......

地址映射关系:

section namevirtual addrphy addr
.head.text0xffff8000800000000x40200000
.text0xffff8000800100000x40210000
.rodata0xffff8000810400000x40240000
.rodata.text0xffff800081a948000x41C94800
.init.text0xffff800081aa00000x41CA0000
.init.data0xffff800081b950000x41D95000

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

声明:本文内容由网友自发贡献,转载请注明出处:【wpsshop】
推荐阅读
相关标签
  

闽ICP备14008679号