内核调试技术之x86_64

437 阅读5分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第5天,点击查看活动详情

X86使用的qemu和arm不同,x86使用的是qemu-system-x86_64

  1. sudo apt-get install qemu libncurses5-dev gcc-arm-linux-gnueabi buildessent qemu-system-x86安装编译需要的工具包。

  2. 下载linux源码,和下载busybox工具包:busybox.net/downloads/b…

  3. 编译linux的x86_64内核。

    编译之前需要打开内核调试功能:Kernel hacking --> Compile-time checks and compiler options --> Compile the kernel with debug info

    make x86_64_defconfig    #生成x86_64版本配置文件.
    make bzImage   #编译内核
    make modules   #编译内核模块
    
  4. 启动内核.

    qemu-system-x86_64 -m 512M -smp 4 -kernel ./bzImage
    

    上述命令假设编译好的 bzImage 内核文件就存放在当前目录下。不出意外的话,就可以在启动窗口中看到内核的启动日志了。在内核启动的最后,会出现一条 panic 日志:

    Kernel panic - not syncing: VFS: Unable to mount root fs on unknown-block(0, 0)
    

    从日志内容可以看出,内核启动到一定阶段后尝试加载根文件系统,但我们没有指定任何磁盘设备,所以无法挂载根文件系统。而且上一节中编译出来的内核模块现在也没有用上,内核模块也需要存放到文件系统中供内核需要的时候进行加载。所以,接下来需要制作一个磁盘镜像文件供内核作为根文件系统加载。

  5. 创建磁盘镜像文件,使用 qemu-img创建一个 512M 的磁盘镜像文件:

    qemu-img create -f raw disk.raw 512M
    

    现在 disk.raw 文件就相当于一块磁盘,为了在里面存储文件,需要先进行格式化,创建文件系统。

    mkfs -t ext4 ./disk.raw
    
  6. 挂载磁盘镜像文件。

    sudo mount -o loop ./disk.raw ./img
    
  7. 安装内核模块。 将之前编译好的内核模块安装到磁盘镜像中,命令如下:

    sudo make modules_install INSTALL_MOD_PATH=./img  # 指定安装路径
    

    完成后会在./img/lib/modules/下看到安装好的内核模块。

  8. 使用磁盘文件启动qemu:

    qemu-system-x86_64 -m 512M -smp 4 -kernel ./arch/x86_64/boot/bzImage -drive format=raw,file=./disk.raw -append "root=/dev/sda"
    

    file=./disk.raw : 指定文件作为磁盘。 root=/dev/sda :内核启动参数,指定根文件系统所在设备。

    不出意外的话,会显示:

    Kernel panic - not syncing: No working init found. Try passing init= option to Kernel. See Linux Documentation/admin-guide/init.rst for guidance.
    

    说明启动参数里边没有指定init选项,磁盘镜像中也没有相应的init程序。

  9. 编译busybox:

    make defconfig
    make menuconfig
    

    这里有一个重要的配置,因为 busybox 将被用作 init 程序,而且我们的磁盘镜像中没有任何其它库,所以 busybox 需要被静态编译成一个独立、无依赖的可执行文件,以免运行时发生链接错误。配置路径如下:

    Busybox Settings --->
          --- Build Options
          [*] Build BusyBox as a static binary (no shared libs)
    

    最后,编译并安装到磁盘镜像中:

    make
    make CONFIG_PREFIX=需要安装的文件夹路径 install
    
  10. 加入init内核启动参数来指定busybox作为init进程,再次尝试启动。

    qemu-system-x86_64 -m 512M -smp 4 -kernel ./bzImage -drive format=raw,file=./disk.raw -append "init=/linuxrc root=/dev/sda"
    

    还是有问题,会出现:

    can't run '/etc/init.d/rcS': No such file or directory
    can't open /dev/tty3: No such file or directory
    can't open /dev/tty4: No such file or directory
    

    init进程执行报错,需要配置。

  11. init 启动后会扫描/etc/inittab配置文件,这个配置文件决定了init程序的行为。而busybox init再没有/etc/inittab文件的情况下也能工作,因为它有默认行为。它的默认行为相当于如下配置:

    ::sysinit:/etc/init.d/rcS
    ::askfirst:/bin/sh
    ::ctrlaltdel:/sbin/reboot
    ::shutdown:/sbin/swapoff -a
    ::shutdown:/bin/umount -a -r
    ::restart:/sbin/init
    tty2::askfirst:/bin/sh
    tty3::askfirst:/bin/sh
    tty4::askfirst:/bin/sh
    

    但是不能这样用,需要去掉后边的三行。 如下:

    ::sysinit:/etc/init.d/rcS
    ::askfirst:/bin/ash
    ::ctrlaltdel:/sbin/reboot
    ::shutdown:/sbin/swapoff -a
    ::shutdown:/bin/umount -a -r
    ::restart:/sbin/init
    
  12. 创建可执行文件/etc/init.d/rcS 内容如下(暂时啥都不做,后边要加):

    #!/bin/sh
    

    这次应该可以进入控制台。 但是/dev 这些都不能访问。

  13. 创建并在脚本中挂载/dev,/proc, /sys 文件系统:

    mkdir ./img/dev ./img/proc ./img/sys
    

    并修改etc/init.d/rcS :

    #!/bin/sh
    mount -t proc proc /proc
    mount -t sysfs sysfs /sys
    

    重启系统,可以查看/dev,/proc,/sys挂载点都有了相应的内容。

  14. 到此处qemu已经可以启动linux内核了, 下来介绍启动时使用GDB调试并打断点到方法。

    开始启动内核,并在启动时暂停:

    sudo umount /dev/loop0  #先将上面挂载的img文件夹卸载掉
    
    qemu-system-x86_64 -m 512M -smp 4 -kernel ./bzImage -drive format=raw,file=./disk.raw -append "init=/linuxrc rw root=/dev/sda nokaslr" -S -s
    

    nokaslr:不加nokaslr可能导致断点不生效。因为kernel address space layout randomation(内核地址空间布局随机化),这样内核地址不就不一致了,禁掉就好了。

    -S :Do not start CPU at startup (you must type 'c' in the monitor).

    -s:Shorthand for -gdb tcp::1234, i.e. open a gdbserver on TCP port 1234(see gdb_usage).

    qemu参数介绍参考文档blog.csdn.net/wj_j2ee/art…

  15. 步骤14会在qemu启动时暂停,必须使用另一个终端启动gdb并加载未压缩内核(内核符号表),然后链接通过tcp链接本地1234端口,然后加断点,最后使用c,开始执行到断点处,后边使用和GDB调试相同,不做赘述。命令如下:

    sh> gdb vmlinux
    (gdb)target remote localhost:1234
    (gdb)b start_main
    (gdb)c
    

    断点调试效果如下图所示:

8.png