摘要:现在hack linux kernel的人很多,而调试工具很少,Bochs的最大的好处就是它的调试功能,它能仿真IA-32的全部指令,所有这些都是用C++写成的,并且开源。
名词:Bochs
Bochs是Kevin Lawton 用C++语言开发的一个仿真软件系统。主要用来仿真x86的系统软件。和VMware,Virtual PC 仿真软件相比,速度很慢,但Bochs的最大好处是它的单步调试功能。
下面我们主要来讲述如何用bochs的单步调试功能对linux的启动过程进行调试,从而了解linux是如何启动的。
在讲述之前,首先我搭配了一个Bochs环境:
1) Bochs的执行文件
2) Linux映像文件
我选用了linux 早期的内核版本进行调试,0.11版,主要原因是0.11版网上有很多已经编译制作好的内核映像文件,而其0.11版的引导启动程序结构和最版内核基本上是一样的。
当机器启动的时候,i386系统中BIOS做最初的引导工作。就是把可启动设备的第一个分区的第一个扇区加载到实模式地址0x7c00上,然后将控制转交给它。所以我们可以在地址0x7c00处设置断点,然后让系统执行到这个地方
命令:vbreak 0x0000:0x7c00 或则(b 0x00007c00),设置断点。
C,继续执行,遇到断点,停止
这个时候mov ax,0x7c0指令正好是linux启动程序bootsect.S的第一个有效指令(在bootsect.S中为
start:
movax,#BOOTSEG)。说明BIOS在执行到0x7c00的时候,把控制权转移到了linux启动程序了。Linux 启动程序包括三个文件。他们分别是:
Bootsect.S ,setup.S,head.s,他们都是由汇编写成的,这三个程序按一定的顺序执行各自不同的功能能来完成整个从引导到启动的整个过程。
启动顺序是:BIOS--Bootsect.S--setup.S--head.s---main.c
Bootsect.S代码是磁盘引导块程序。它的主要功能是:
1)把自己移到内存绝对地址0x90000的开始处,并跳到那里执行,也就是地址0x9000:0x0000处
2)用13号BIOS中断来读磁盘的从第2个扇区开始的连续4个扇区内的内容,这部分内容就是setup程序模块,然后把这个内容放到bootsect紧接的后面。也就是地址0x90200.为什么是0x90200呢?后面要说道。
3)把system模块读到内存0x10000地方。这个system模块大小必须小于0x90000-0x10000=0x80000 也就是512k字节。
关于setup.S和head.s两个文件的功能和作用等调试到的时候在介绍。
继续往下调试,我们用反汇编从现在开始的10条指令。
指令介绍:u /10 ,u是反汇编指令,这个指令意思反汇编当前开始的10条指令并打印出来。注意这并不是程序执行的指令。
从0x00007c0s到0x00007c11的几条指令是把ds:[offset]开始的内容复制0x0100个字到es:0000开始的内存空间内。也就是从0x7c00处复制256字到0x9000.这就把bootsect搬家到了0x9000:0000地方了。长度是256个字,也就是 0x0200个字节。这就是为什么bootsect的后面是0x90200.
继续调试,由于256个字需要256个循环的指令。所以我们可以跳过去用s 256,然后把跳到0x9000:0x0018处继续执行bootsect引导程序。代码段由0x0000,变成了0x9000。
利用调试命令dump_cpu可以看到当时的cpu各个寄存器的内容。
继续调试,下面就开始了bootsect的第二个功能。继续用step指令。下面这几个指令是
把ds,es,ss都设置成移动后的代码所在的段(0x90000)并设置堆栈在0x9000:0xff00
设置它主要是以后要用到call这个指令时要push,pop.
下面就是开始把setup模块从磁盘第二个扇区读到0x90200开始处,一共4个扇区。用BIOS中断INT0x13.
这里要说明已下INT 13的用法。
读扇:
Ah=0x02(bootsect用的就是值)
Al=需要读出的扇区数量。
Ch=磁道低8位
Cl=磁道高8位
Dh=磁头号
Dl=驱动号
Es:bx数据缓冲区。
这里es以前在上面设置成0x9000.
以上的寄存器在下面均有对应的值,就不一一介绍了。我们主要侧重于讲述linux的启动。
好,setup模块已经移到0x90200处,程序到这里,bootsect已经完成了他的使命。接下来就是开始执行setup模块了。所以上面的图最后一个指令是jmp far 9020:0000.把控制权移交给setup模块。
Setup.s的主要作用是收集信息,他是系统加载程序,setup模块利用BIOS中断读取机器的系统数据然后保存起来,共保护模式下的代码来读取。这些信息包括内存容量,磁盘信息等各种硬件信息。另外setup一个重要的作用是开始了实模式向32位保护模式转变,因此需要Intel 32位保护模式编程方面的知识。
开始调试,为了能捕捉到setup.s的开始部分。我们在9020:0000出设置一个断点。如图:
利用BIOS INT 0x10读取当前的光标位置保存在ds:0x0处,也就是0x9000:0x0000处。这个位置本来是bootsect引导程序的位置,不过他的使命已经完成,没有用了,利用这段内存位置开始保存系统信息。
INT 0x15 取得扩展内存的大小,也就是从0x100000(1M)以上的内存大小。返回值放在寄存器ax里面
然后move的ds:0x2的位置。也就是0x9000:0x0002处,站位一个字节。
INT 0x10 取得显示卡的当前模式
返回值分别放在ds:0x4,ds:0x6
检查显示方式并取参数。
读取硬盘的信息,这些信息都保存在BIOS里面了,叫做硬盘的参数表,只要把这些表里信息读出来放到内存就可以。第一块硬盘信息放到0x90080,第二块放到0x90090处。
程序到这里实模式结束,开始先保护模式向转变。0x0009027c:cli这条指令是关中断,再设置保护模式下的中断向量表之前,应该把中断机制关掉。然后开始system模块的转移。Bootsect引导程序把system模块读入到0x10000(64kb)开始的位置,由于bootsect要把自己移到0x90000处,所以system模块不能超过0x8000(512k)。接下来把这不超过512k的system模块移到0x00000位置,也就是内存的首地址。
接下来就进入了比较复杂的保护模式中了。在进入保护之前。要做几个准备工作。
1) 设置gdt,idt寄存器值
2) 打开保护模式开关
由于前面移动代码的时候ds寄存器已经变了。所以应该再设置一下,让它指向本程序段(setup.S).
Ds=0x9020.
Lidt ds:0x12c
Lgdt ds:0x132
我们可以用内存察看指令来察看一下ds:0x12c和ds:0x132两个地方的内存里lidt和lgdt要加载的指。
然后把程序运行到0x000902a2,验证一下cpu的信息是不是加载的那个值。
对于lidt的内容可以看出。中断向量表的长度是0,表的32位线形地址基地址也是0。这里表示进入实模式之前先设置一个中断向量空表。
我们来看一下,全局描述表GDT的寄存器。表长:0x0800,也就是2048个字节,每8个字节是一项,一共256项。
基地址是:0x00090314.
也就是说有一个256项的表,每一项有8个字节。这个表的基地址在0x00090314的位置。也就是gdt寄存器指的地方。
我们知道了地址可以看看这个表里到底有什么内容。
调试指令x /8 0x00090314 ,打印这个地址连续的8个表项内容。8个字节是一项,这个命令打印了两个表项,第一个内容是:0x00000000,第一表项不用。第二和第三表项分别作为内核代码段和内核数据段,
保护模式的准备工作已经作好。
上面就是开启保护模式,lmsw加载机器状态字,并且跳转到ds=0x0008
Offset=0x0000处,这是一个什么地方呢?我们知道ds已经不是代码段了,在保护模式下它是段选择符,如图:这16位的代码段分成三个部分4 到16位,可以理解成表述表的下标。在这里ds=0x0008(0b0000 0000 0000 1000) 也就是表的下标识。也就是表项的第二个。有上面的表项值可以看出。线形基地址是0x0000.也就是下面这个jmp的指令是跳转到机器地址 0x0000:0x0000地方。这正好 是system模块的开始地址。
在继续调试system模块之前,介绍一下head.s这个程序。其实system已经超过启动模块的范围。Head.s在编译生成目标文件之后与内核一起连接成system模块,它位于system模块最前面。也就是内存绝对地址0x0000:0x0000的地方。head.s的功能很单一,就是重新设置GDT 和IDT表,设置页目录和页表,除此之外,最主要的,也是最有意思的是它如何调用main()函数的。
程序从0地址开始执行,前面几条指令把ds,es,fs,gs这几个段寄存器都设置成0x10.也就是全局表述表中第三项,这个表是内核数据段表述符项,线形基地址是0x0000.
然后用call指令调用两个子程序。
两个子程序分别是:setup_idt,setup_gdt
下面介绍main的调用
前三个push到堆栈的值都是main()的参数。
第四个push到堆栈的值是main()函数返回的地址。
第五个push到堆栈的值是main()函数地址。
这时候执行jmp指令到子函数setup_paging,
在setup_paging里面,子程序执行完毕以后,又一个iret指令。
这个指令会把main()pop出来执行,这时候开始执行main()函数里的内容。
到这里整个启动就结束了,上面牵扯到很多内核的移动情况,很容易高糊涂。
如果需要继续往下调试,最好找一个当前内核的system.map表。
0.11内核和新内核的启动仍然区别,但是基本是一样的,比如bootsect基本没有改动,setup.S功能没有变,新内核心的setup.S还需要把video.s的代码包含近来,检测显示器和设置显示模式。区别最大的还是system的head模块,由于随着计算机硬件的不断提高,system变的非常大,已经大大超过了521k,所以就出现了压缩过的内核vmlinux.所以system模块为了适应这种变化,代码有了很大的改动。