前言

通常而言,调试应用程序是很简单的,调试驱动要复杂的多,而调试BIOS / Boot Loader则要复杂的多。JTag技术由于其独立于OS,拥有更强大的能力。GDK7是基于英特尔Skylake微架构研发的一套调试套件,通过DCI(Direct Connect Interface)协议实现对目标机的高速调试。本文尝试探索利用GDK7套件实现对Grub的调试。

本文用到如下工具:

  • GDK7套件 (GDK7-100)
  • Nano Code v1.0.8
  • Ubuntu上的hexedit
  • PEViewer

本文用到的环境是Ubuntu 19.04 x64,GPT分区,EFI模式boot,boot loader为grub2 64位(v2.04)。

背景

阅读本文需要读者具备Grub2,EFI,GPT等相关背景知识。相关知识很容易搜索到,所以本文不在赘述。

本文所用到的调试对象为GDK7,分区如下:

sda2分区即为EFI 分区,已经被mount到/boot/efi:

/boot/efi/EFI/下有三个文件,bootx64.efi即是bootloader的stage2的主image,也是本文的主目标:

我们现来看看efi文件

文件头两个字节, “MZ”以及0x80位置的“PE”说明这是一个PE32+格式文件。下面我们用PEviewer看一下它的更多信息:

准备环境

连接USB线,启动Nano Code

USB线很好连,用GDK7套件里的蓝色USB线,一头连接GDK7的USB3 port(在机箱后面),一头连接主机上的USB3 port即可。

Nano Code需要在调试主机上运行。它的图标是马踏飞燕,据说点一下那马就可以以光速飞起来(其实没有)。Nano Code的启动速度中规中矩,没有传说中的“纳秒级”。Nano Code启动后,选择“去调试”,然后在菜单里选择File->Kernel Debugging …

然后在弹出窗口上选择相应配置,如下图即可:

然后按“确定”就进入调试主界面了。这个过程还是很简单明白的。

小试牛刀

首先看一下是不是可以在BIOS和Boot Loader阶段是不是能把它停下来单步执行。

调试BIOS和boot loader需要眼疾手快,需要同时操作GDK7和调试主机上的Nano Code。

实验1:单步调试BIOS

BIOS时间很短,所以这一个实验需要手疾眼快:左手按在GDK7电源键上,右手拿着调试主机的鼠标,对准Nano Debugger的工具条上的break 按钮。当左手按下GDK7的按钮,听到悦耳的GDK7启动声音之后,右手需要迅速按下break按钮。

很成功,顺利断了下来:

单步没有障碍:

实验二:单步调试boot loader

既然BIOS阶段都能够做到,那boot loader阶段肯定没有问题了。在boot menu这里,按下break,就如图这样了:

第一阶段的测试表现良好。

更进一步

在boot loader阶段暂停下来,我们看看还能干点啥:

堆栈是这样的:

反汇编:

寄存器:

看看符号?

到这儿一脸蒙圈,我是谁?我在哪儿?我要到哪儿去?

好吧,我们尝试看看能更进一步做点啥吧!

先尝试看一下bootx64.efi被载入到内存的位置。

这一步应该不难,在准备部分我们已经了解到了这个文件PE结构概要信息,到内存里去搜索 4D 5A 90,即文件头的前三个字节,用s 命令搜索即可。在我的GDK7上搜索出来如下三处:

这说明至少有三个efi文件被载入了(其实就是 /boot/efi/EFI/Boot/下的三个文件),那么哪一个是bootx64.efi呢?这就需要进一步匹配了。这时候可以用到两个调试命令:

 !dh <base-address> 可以查看模块的头信息

 db <base-address> [range] 可以打印出模块的内存数据

经过逐个测试,第三个模块(基地址 00000000`8545c000)即bootx64.efi。

我们对一下眼神试试:

 db 00000000`854dc000 l100,结果跟hexdump出来的一致。

  !dh 00000000`854dc000:结果跟PEViewer内容匹配上:

同时 !dh 命令也告诉我们一个重要信息:它的入口函数的偏移量是25000,即在内存中的位置是:

   00000000`854dc000(起始位置)+ 25000(偏移量),即00000000`85501000处。

我们反汇编一下,看看他的入口长的啥模样吧:

显然,入口函数很简单,就是做一些准备工作,设置一些参数,然后调用一个实体函数实现具体功能。

尝试在入口函数设置中断,调试boot loader的功能

好像很简单,在入口函数的地方设置个断点,跑起来就是了,难道不是吗?对于应用程序而言,的确如此,对于boot loader来说,却是有不少曲折的。最主要的挑战就是在恰当的时间下断点,因为下的早了的话无效(image还没有载入到内存),载入的晚的话则已经运行过去了。用到关键技术点即:截获bootx64.efi载入的过程,当完整载入到内存以后再下断点。

下面来重点讲一下如何实现这个目标。

  1. 开机,在滴滴声后break
  2. 通过写内存中断截获模块的载入,可以用 ba w<长度> <地址>下断点。实际跟踪发现,应该用entry point的地址来做断点,因为该模块是分多次载入进来的,所以如果用模块基地址来下断点要操作更复杂。
    ba w8 00000000`85501000,然后go,触发断点

此时关键寄存器 rsi,rdi,rcx如下:

检查entry point处内存如下:

说明此时已经拷贝了8个字节。

按f10单步执行,再看内存则module已经被载入:

在entrypoint处下断点,用bl指令检查,断点编号为1:

按F5继续,则命中断点:

此时堆栈如下:

到此,我们已经顺利在bootx64.efi的入口函数下了断点,接下来就可以开心(痛苦)地真正调试了!

备注

  1. 作者做此尝试主要在于探索一种新的调试boot loader的方法,而不是具体调试boot loader,所以本文所记录的尝试,其实只是到了bootx64.efi的入口函数,尚未深入调试具体功能,登堂尚未完成,入室更谈不上。

  2. 作者尝试载入symbol,尚没有找到合适的方法,可能Nano Code还没有实现对boot loader的symbol的自动载入的支持。

  3. 本文提到的一些地址、偏移等数据,依环境差异可能不一样。

作者:沈根成  创建时间:2024-03-20 15:34
最后编辑:沈根成  更新时间:2024-04-26 11:30