MIT6.828LAB1 (3)

本文讲述了通过GDB学习和分析BootLoader的工作原理,包括设置断点、追踪代码、理解ebp和esp寄存器的作用,以及跟进readsect函数的执行过程。作者还展示了如何使用GDB的x/i命令进行反汇编和代码比较。
摘要由CSDN通过智能技术生成

LAB1_Part2 The Boot Loader


前言

记录一下自己的学习过程
实验内容翻译:
https://gitee.com/cherrydance/mit6.828
该翻译仅供参考
接上篇博客后续

练习3

查看实验工具指南,特别是关于GDB命令的部分。即使你对GDB已经很熟悉,这部分内容也包含了一些对操作系统工作很有用的冷门GDB命令。
设置一个断点,地址为0x7c00,这是引导扇区将被加载的位置。继续执行直到达到该断点。通过使用源代码和反汇编文件obj/boot/boot.asm来追踪boot/boot.S中的代码。还可以使用GDB中的x/i命令来反汇编引导加载程序中的指令序列,并将原始引导加载程序源代码与obj/boot/boot.asm和GDB中的反汇编结果进行比较。
进入boot/main.c中的bootmain()函数,然后进入readsect()函数。确定每个readsect()语句对应的具体汇编指令。继续跟踪readsect()的其余部分,然后返回到bootmain()函数,并确定读取剩余内核扇区的for循环的起始和结束位置。找出循环结束后将执行的代码,设置一个断点,并继续执行到该断点。然后逐步执行剩余的引导加载程序代码。

按照要求先打开gdb调试,设置断点0x7c00,运行到断点处。练习让我们比较三个地方的指令的区别,我们先使用x /20i 0x7c00查看前20条命令,如图所示:
在这里插入图片描述
将结果与boot.S和boot.asm对比,可以发现基本一致,事实上,boot.asm可以说是反汇编结果与boot.S的合并展示结果。我感觉这并不是重点,看一下就行了。

进入boot/main.c中的bootmain()函数,然后进入readsect()函数。

从上篇博客我们就能得到进入bootmain函数是从指令call bootmain开始的
在这里插入图片描述
从boot.asm中我们可以得到该指令的地址,设置新断点b *0x7c45跳转到该指令的位置,使用si命令逐步执行。其实直接在boot.asm中就可以找到代码对应的位置,比gdb得到的结果更加清晰,因为将汇编与c语言一一对应,gdb用来验证结果更合适。

    7d25:	f3 0f 1e fb          	endbr32 
    #检查是否跳转到了预期地址
    7d29:	55                   	push   %ebp
    #将当前的ebp压栈,此时会导致esp地址变化
    7d2a:	89 e5                	mov    %esp,%ebp
    #将栈底置为当前栈顶
    7d2c:	56                   	push   %esi
    7d2d:	53                   	push   %ebx
    #将esi和ebx压栈,也是保存寄存器的值,说实话看这些寄存器头都大
    #之后具体涉及到再看吧,现在只知道寄存器功能,但没法连贯起来

在进行接下的操作前我们首先要了解ebp esp eip这三个寄存器的含义。eip很好理解,就是当前命令的地址。所以我们的重点是ebp和esp这两个寄存器代表的含义。ebp是栈的基地址,而esp是栈顶指针。如下图,我将最开始的ebp和esp值都进行了打印输出,可以对照看一下。
在这里插入图片描述
首先是call命令执行前esp是0x7c00,这是boot代码运行的起点,也就是说栈和boot代码以0x7c00为分界一个地址向下增长,一个向上增长。ebp为0是因为在boot启动阶段没有栈帧,ebp没有含义,因此设置为0。
在这里插入图片描述
我们注意到esp的内容由0x7c00变成了0x7bfc,这是因为在call命令调用之后会将函数的返回地址压栈,所以栈指针下移一个位置,打印0x7dfc的值可以得到0x7c4a,这就是call命令的返回地址。
在这里插入图片描述
执行命令push %ebp将ebp压栈,因此esp继续下移。然后执行的mov %esp,%ebp将ebp的值变成了当前的栈顶,这两条指令是函数调用之后一定会运行的两条指令,目的就是为保存旧函数的栈底和设置新函数栈底。

	readseg((uint32_t) ELFHDR, SECTSIZE*8, 0);
    7d2e:	52                   	push   %edx
    #为什么要将edx压栈?
    7d2f:	6a 00                	push   $0x0
    7d31:	68 00 10 00 00       	push   $0x1000
    7d36:	68 00 00 01 00       	push   $0x10000
    7d3b:	e8 a2 ff ff ff       	call   7ce2 <readseg>

这几条指令是将调用参数入栈。我们很容易就发现0x0是参数0的值,0x1000是参数SECTSIZE*8的值,0x10000则是ELFHDR的值,这里就是将被调用函数的参数依次入栈。然后调用call 指令跳转到readseg函数,同样的,call指令会将函数返回地址压入栈中。

    7ce2:	f3 0f 1e fb          	endbr32 
    7ce6:	55                   	push   %ebp
    7ce7:	89 e5                	mov    %esp,%ebp
    7ce9:	57                   	push   %edi
    7cea:	56                   	push   %esi
    7ceb:	53                   	push   %ebx
    #后续用到了这三个寄存器,因此要先保存寄存器的值

函数调用时的基本指令

7cec:	83 ec 0c             	sub    $0xc,%esp
	#将esp减去0xc,将栈顶指针向上移动0xc。为啥?
	offset = (offset / SECTSIZE) + 1;
    7cef:	8b 7d 10             	mov    0x10(%ebp),%edi
    #将0x10(%ebp)处的值赋给edi,0x0
{
    7cf2:	8b 5d 08             	mov    0x8(%ebp),%ebx
    #将0x8(%ebp)处的值赋给edi,0x10000
	end_pa = pa + count;
    7cf5:	8b 75 0c             	mov    0xc(%ebp),%esi
    #将0xc(%ebp)处的值赋给edi,0x1000

使用gdb打印ebp之后的几个值,这是在执行mov 0xc(%ebp),%esi命令之后打印的结果。
在这里插入图片描述
0x7bf8是上一个栈帧的ebp地址 0x7d40是返回地址,接下来三个数是函数调用时的参数。所以这三行代码将三个参数的值赋给三个寄存器。因为在这里用到了这些寄存器,所在要将之前寄存器的值入栈保存。

	offset = (offset / SECTSIZE) + 1;
    7cf8:	c1 ef 09             	shr    $0x9,%edi
    #将寄存器edi的值右移9位,2^9=512,即0ffset/SECTSIZE
	end_pa = pa + count;
    7cfb:	01 de                	add    %ebx,%esi
    #寄存器esi的值加上寄存器ebx存到esi
	offset = (offset / SECTSIZE) + 1;
    7cfd:	47                   	inc    %edi
    #edi++,实现加一
	pa &= ~(SECTSIZE - 1);
    7cfe:	81 e3 00 fe ff ff    	and    $0xfffffe00,%ebx
    #0x200-1=0x1FF,~0x1ff=0xfffffe00,再&预算

循环之前的准备工作

	while (pa < end_pa) {
    7d04:	39 f3                	cmp    %esi,%ebx
    7d06:	73 15                	jae    7d1d <readseg+0x3b>
    #比较esi的值和ebx的值,符合条件则跳转到readseg+0x3b
    #0x7ce2+0x3b = 0x7d1d,即跳出循环
		readsect((uint8_t*) pa, offset);
    7d08:	50                   	push   %eax
    7d09:	50                   	push   %eax
    7d0a:	57                   	push   %edi
    #保存对应的寄存器值,不理解为啥两个push %eax
		offset++;
    7d0b:	47                   	inc    %edi
    #edi++
		readsect((uint8_t*) pa, offset);
    7d0c:	53                   	push   %ebx
		pa += SECTSIZE;
    7d0d:	81 c3 00 02 00 00    	add    $0x200,%ebx
    #调用readsect,参数入栈
		readsect((uint8_t*) pa, offset);
    7d13:	e8 64 ff ff ff       	call   7c7c <readsect>
    #跳转到readsect
		offset++;
    7d18:	83 c4 10             	add    $0x10,%esp
    #栈指针加0x10,释放栈的一部分空间
    7d1b:	eb e7                	jmp    7d04 <readseg+0x22>
    回到循环开始的位置
}

循环主体,直接复制的boot.asm,c语言和汇编没有一一对应上,但不影响理解。下面看readsect函数的内容。

readsect(void *dst, uint32_t offset)
{
    7c7c:	f3 0f 1e fb          	endbr32 
    7c80:	55                   	push   %ebp
    7c81:	89 e5                	mov    %esp,%ebp
    7c83:	57                   	push   %edi
    7c84:	50                   	push   %eax
    7c85:	8b 4d 0c             	mov    0xc(%ebp),%ecx
	// wait for disk to be ready
	waitdisk();
    7c88:	e8 dd ff ff ff       	call   7c6a <waitdisk>
}

前面的还是函数调用时的基本操作,然后直接调用了waitdisk()函数。这个函数的主要作用就是等待磁盘空闲。然后就是一系列的outb函数调用。该函数的作用就是往对应端口里写数据。查看端口https://bochs.sourceforge.io/techspec/PORTS.LST可以得到0x1f2到0x1f7的作用。如图。
在这里插入图片描述
然后又调用waitdisk等待磁盘,最后调用insl(0x1F0, dst, SECTSIZE/4);该函数的原型是static inline void insl(int port, void *addr, int cnt)

    7cce:	8b 7d 08             	mov    0x8(%ebp),%edi
    #获得dst的值存入edi
    7cd1:	b9 80 00 00 00       	mov    $0x80,%ecx
    #0x80 = 512/4=128
    7cd6:	ba f0 01 00 00       	mov    $0x1f0,%edx
    #端口为0x1f0,查阅得到01F0	r/w	data register的信息
    7cdb:	fc                   	cld    
    7cdc:	f2 6d                	repnz insl (%dx),%es:(%edi)

详细解释最后一条指令repnz insl (%dx),%es:(%edi)的含义,前缀repnz表示重复执行指令直到条件不满足,即直到寄存器%ecx 的值为 0 。insl会从(%dx)读取数据到%es:(%edi)位置。因为一次会读取32bit即4byte,所以读取128次,就是ecx寄存器的数字。

然后就是函数返回的一些指令,直接跳过了。直接看返回bootmain之后循环

	for (; ph < eph; ph++)
    7d66:	39 f3                	cmp    %esi,%ebx
    #ebx存放的ph,esi存放eph
    7d68:	73 17                	jae    7d81 <bootmain+0x5c>
    #满足条件跳出循环
		readseg(ph->p_pa, ph->p_memsz, ph->p_offset);
    7d6a:	50                   	push   %eax
    #eax入栈,不明白入栈啥
	for (; ph < eph; ph++)
    7d6b:	83 c3 20             	add    $0x20,%ebx
    #ph++
		readseg(ph->p_pa, ph->p_memsz, ph->p_offset);
    7d6e:	ff 73 e4             	pushl  -0x1c(%ebx)
    7d71:	ff 73 f4             	pushl  -0xc(%ebx)
    7d74:	ff 73 ec             	pushl  -0x14(%ebx)
    7d77:	e8 66 ff ff ff       	call   7ce2 <readseg>
    #三个参数入栈,调用函数
	for (; ph < eph; ph++)
    7d7c:	83 c4 10             	add    $0x10,%esp
    #之前入栈的三个参数加上call的返回地址出栈
    7d7f:	eb e5                	jmp    7d66 <bootmain+0x41>
    #跳到循环开头

总结

感觉其实这里的核心就是ebp和esp这两个寄存器的值的变化,这两个东西我过了一遍也还是不太清晰,只是稍微了解了一点。后续再继续学习吧!
练习三的学习就到这里了,orz。
有问题欢迎大家指出。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值