Exercise-RTSRC

简单的任务练习......喂,气死了,简单个毛!

练习1:

  1. 操作系统镜像文件ucore.img是如何一步一步生成的?(需要比较详细地解释Makefile中每一条相关命令和命令参数的含义,以及说明命令导致的结果)

  2. 一个被系统认为是符合规范的硬盘主引导扇区的特征是什么?

利用make V=可以得到最终的编译顺序,可以看到:

1 + cc kern/init/init.c
2   gcc -Ikern/init/ -march=i686 -fno-builtin -fno-PIC -Wall -ggdb -m32 -gstabs -nostdinc  -fno-stack-protector -Ilibs/ -Ikern/debug/ -Ikern/driver/ -
    Ikern/trap/ -Ikern/mm/ -c kern/init/init.c -o obj/kern/init/init.o

// -I: for directories.
// -fno-builtin: allow same name function declaration
// -march=i686: i686 is a subtype of i386 machine.
// -fno-PIC: possition independent code.
// -Wall: warning for all.
// -ggdb: allow gdb message.
// -m32: i386 is a 32-bit machine.
// -gstabs: more gdb message.
// -nostdinc: Don't search standard position, search directories with "I" marks.
// -fno-stack-protector: Don't provide stack protection.

通过RTFM阅读相关的标志,我们在code block底部简要解析如下,可以看到对于kern文件夹和libs文件夹下对源文件进行了向目标文件的转换。

之后,利用ld链接器将上述源文件链接在一起:链接脚本为kernel.ld.

第三步,编译内核启动源码:方法类似,同时编译了工具sign.c.

第四步,将内核启动源码链接到0x7c00这个位置上,并对这个东西签名。

在完成上面的操作后,利用sign对生成的bootblock文件添上签名再写回。

这个数据很奇怪,不过我们还是找到了相关的资料,是主引导记录扇区的有效位。主引导扇区,即MBR,是计算机访问硬盘后读入的第一个扇区。

MBR结构

最后,采用下述操作:将生成的文件塞到ucore.img这个镜像中。

至于硬盘的主引导扇区的特征,我们已经在表格中提到了,即末尾的特殊字节。

练习2:

  1. 从CPU加电后执行的第一条指令开始,单步跟踪BIOS的执行。

  2. 在初始化位置0x7c00设置实地址断点,测试断点正常。

  3. 从0x7c00开始跟踪代码运行,将单步跟踪反汇编得到的代码与bootasm.S和 bootblock.asm进行比较。

  4. 自己找一个bootloader或内核中的代码位置,设置断点并进行测试。

根据做JOS的经验以及RTFM的体会,加电后第一个位置是0xffff0是一件很了然的事情,然而这里在调试时是有点bug,首先不知道为什么这边的gdb非常操蛋的没有出现相关的指令,需要手动在里头编写一个函数来帮忙:

其次,似乎在这种模式下没有ip寄存器,其对应的其实是eip这个东东。虽然e是extension的意思,但其实架构上是有点小冲突(这应该是386的机器,但是应该能让我去直接找ip寄存器才对)。箭头指向的是ip的位置,打印的也是那部分的地址,但按照常理这显然是出了问题,如果要看下一条指令到底是什么,我们应该把它做一下修正。这下终于可以打印出正确的指令了。

利用sini相关指令稍微跟一下BIOS加电后的指令即可,它们并没有什么太大的意思,而且就人力来说应该是跟不完的,在JOS Lab1中我也尝试跟了一小部分,可以参考,这一部分其实是很dirty的代码,真正开始起作用的其实是0x7c00之后的东东,它是我们之前装载进去的bootblock内容。

练习3:

  • 为何开启A20,以及如何开启A20

  • 如何初始化GDT表

  • 如何使能和进入保护模式

而且,在Makefile中装载是有顺序的,具体来说先装的是bootasm.S,然后再是bootmain.c这一部分。我们尝试对它们进行解读。

首先,我们来看一下A20的开启原因和方法:A20开启是让实模式可以访问高端地址,即高于自身16位的地址空间。如果不开启A20的话,访问高地址会自动回转回低地址,这类似于一个取mod操作。

方法的话参考下面这个网址所提的内容:写的很好

A20 relative info

我们根据80386手册对于初始化的描述章节可以得知,在从实模式转换为保护模式的时候,需要把全局描述符表也做好初始化。这意味着GDTR需要指向一个合法的GDT位置。

为了理解这个,我们先RTFM一下,在李忠老师的书《x86汇编语言:从实模式到保护模式》中也可以看到类似的表述:

GDT Info

也就是说,GDTR寄存器首先要规定好limit值,然后再把gdt的地址放在base处,至于limit的值,它应该被设置成8N-1这个值。我们看到此处的gdt共有三个项,所以设置limit0x17是很合理的选择。

此外,和在前置知识中我们提到的那样,第一项是作为NULL特殊处理的,这边也就得到了一个不错的解释。

如要了解剩下的两个项干了什么,请参考这个资料。

GDT项资料

我们尝试对SEG_ASM逐项分析:

首先,前16位是Limit的低16位,而输入的lim值是0xffffffff,严格意义上的lim是20 bit,所以需要作12位的右移工作,把多余的12位删掉。base则是同理,但它自然便是32 bit,所以右移便是不需要的了。

之后,第32-39位正好是一个字节,它反映的是Base的第16-23位。下一个字节则是Access Byte,其中按照下面的方式分成六个信息组件,我们已经在前置知识中提过了。这边仅给出表格结构。

程序中让type与作0x90或操作,这很显然是把Present位和S位设置成1了。Present位自不必说,S位设置为1表示我们要做的是代码和数据段。

有了这个知识后,底下的几个Define基本是把功能写在了脸上。于是我们可以确定,GDT表中其中允许读和执行的描述符是代码段,而仅允许修改的是数据段。

对于第48-51位,这是Limit的高四位所在地,让它右移一下再与0xf与一下就行了,而52-55位,则是flag,这边设置成了1100这个样子

于是我们清楚了Limit的粒度是4 KiB,这是一个很重要的信息。

对于最高的那个比特,太简单了,不提。

在完成了GDT表的装载之后,下一步是通过使能进入保护模式。在前置知识我们提到,CR0寄存器中有一位用于控制保护模式与否。

哦,对了,这个东西和刚刚的GDT写在一起:我们刚刚建立了GDT表,所以对于代码段和数据段的地址现在也非常清楚了。CR0的控制寄存器只要修改最后一位,就能通知硬件时刻准备好进入保护模式。

之后,只要轻轻一跃,我们就进入了保护模式。

练习4:

  • bootloader如何读取硬盘扇区的?

  • bootloader是如何加载ELF格式的OS?

我们先给上一节的小尾巴做一个切断。

现在我们进入函数bootmain,该文件位于bootmain.c中。

该函数调用了readseg函数,同时检查ELFHDR头是否是合法的,具体是检查魔数。

先看到readseg函数,以及内部的readsect函数。我们在注释中做了更详细的解析工作。

特别的,我们要注意按照先前makefile的解析,第二个扇区是kernel,第一个扇区是bootloader,因此,在计算secno之时,sector number会有一个加一。

readseg函数把从第二个扇区开始的内核ELF文件加载到地址0x10000处,并做相关的解析工作。查看源码如下:

对于ELF的格式,其实是很大的一个篇幅,不过我们这边简单看一下WIKI是怎么介绍的。

ELF结构

可以看到作为头结构的其实是两个文件,因此于lib/elf.h中的内容对应上了。在wiki上提到,为了能够把相关的代码段完整的装载进来,需要找到代码头表格的每一项的地址,并把他们拉进来。

ELF info

因此,我们看到了前述代码的for loop + readseg将代码读进来,最后还进入ELF的起始运行地址entry,从而运行内核源码这一整个流程。

然而,针对这里的0xffffff我并没有太多的想法,如果你对这个东东有更多的理解,请联系我的邮箱:songlinke18@mails.ucas.ac.cn

Last updated