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。
有问题欢迎大家指出。