思考题

Thinking 1.1

请阅读 附录中的编译链接详解,尝试分别使用实验环境中的原生 x86 工具 链(gcc、ld、readelf、objdump 等)和 MIPS 交叉编译工具链(带有 mips-linux-gnu前缀),重复其中的编译和解析过程,观察相应的结果,并解释其中向 objdump 传入的参数 的含义。

使用x86工具链

命令行代码如下

1
2
3
4
5
6
7
8
9
10
touch hello.c
vim hello.c
gcc -E hello.c -o hello.i
vim hello.i
gcc -c hello.c -o hello.o
objdump -DS hello.o > re1
vim re1
gcc hello.c -o hello
objdump -DS hello > re2
vim re2

只进行预处理gcc -E hello.c -o hello.i,结果如下(截取hello.i中部分内容)

image-20240318233044056

生成hello.o并进行反汇编之后的文件如下:

image-20240318233515384

直接编译成可执行文件hello后,进行反汇编结果如下(截取main部分):

image-20240318234042449

使用MIPS交叉编译工具链

命令行代码如下:

1
2
3
4
5
6
7
8
mips-linux-gnu-gcc -E hello.c -o hello.i
vim hello.i
mips-linux-gnu-gcc -c hello.c -o hello.o
mips-linux-gnu-objdump -DS hello.o > re1
vim re1
mips-linux-gnu-gcc hello.c -o hello
mips-linux-gnu-objdump -DS hello > re2
vim re2

只进行预处理mips-linux-gnu-gcc -E hello.c -o hello.i,结果如下(截取hello.i中部分内容)

image-20240321112840272

hello.o反汇编后如下

image-20240321113053075

hello反汇编如下:

image-20240321113236677

objdump传入参数的含义

在指令中,使用了objdump -DS

  • -S:将代码段反汇编的同时,将反汇编代码与源代码交替显示
  • -D:反汇编所有的section

Thinking 1.2

尝试使用我们编写的 readelf 程序,解析之前在 target 目录下生成的内核 ELF 文件。

也许你会发现我们编写的 readelf 程序是不能解析 readelf 文件本身的,而我们刚才介绍的系统工具 readelf 则可以解析,这是为什么呢?(提示:尝试使用 readelf -h,并阅读 tools/readelf 目录下的 Makefile,观察 readelf 与 hello 的不同)

先说结论:

  • 我们实现的readelf是一个简易的对 32-bit little-endian ELF 文件的解析程序
  • readelf文件本身是一个 64-bit little-endian ELF 文件
  • 我们编写的readelf不支持对64-bit文件的解析,而系统工具是支持的

下面是题目要求的解析过程:

解析在 target 目录下生成的内核 ELF 文件 mos:

image-20240321145245681

下面尝试解析readelf文件。发现./readelf readelf指令没有反应,而readelf -S readelf指令打印出了readelf文件的信息。

image-20240321145510992

使用readelf -h helloreadelf -h readelf分别观察结果,发现hello的类别是ELF32,而readelf的类别是ELF64;除此之外,两文件的ABI、类型和系统架构也都不一样。

image-20240321145840616 image-20240321145856365

阅读Makefile文件,发现两者的编译指令也有区别,具体内容如下:

image-20240321155142872

可以看到hello.c被指定为编译为32-bit的目标文件,因此我们的readelf可以解析

Thinking 1.3

在理论课上我们了解到,MIPS 体系结构上电时,启动入口地址为 0xBFC00000 (其实启动入口地址是根据具体型号而定的,由硬件逻辑确定,也有可能不是这个地址,但一定是一个确定的地址),但实验操作系统的内核入口并没有放在上电启动地址,而是按照内存布局图放置。思考为什么这样放置内核还能保证内核入口被正确跳转到? (提示:思考实验中启动过程的两阶段分别由谁执行。)

实验中的启动过程

由于QEMU支持加载ELF格式内核,所以启动流程被简化为 加载内核到内存,之后跳转到内核入口。在链接过程中,目标文件被看成节的集合。而链接脚本,也就是Linker Script记录了各个节应该如何映射到段,以及各个段应该被加载到的位置。

实验中,我们的实现方式是:

  1. 通过编写Linker Script,来控制ELF文件各节被加载的位置,实现将内核ELF文件加载到内存指定位置
  2. kernel.lds 中通过 ENTRY(_start) 来设置程序入口为 _start,也就是说:链接后的程序从_start开始执行

难点分析

最大的难点应该是对整体架构的理解。

由于实验对不同部分进行了拆分,所以在基础知识的学习方面不存在什么问题,但是要理解各个模块之间是怎么联系起来的,还是有些困难。(特别是由于lab0对于Makefile的指导书教学比较基础,要看懂lab1的顶层Makefile还是需要去查阅不少资料,而且在不理解每个模块的具体功能的时候,就算看明白了语法可能也不会很明白整个项目是干什么的)

按照具体题目来讲的话,我认为主要难点在EX4,也就是实现printk函数。首先得理解变长参数,然后得搞明白machine.cprintk.cprint.c这几个文件之间的调用关系,看懂每个函数的具体功能和调用格式,然后才能对printk进行补全。

而且由于后几个部分只有一个测试点,bug的定位也是一个问题,得学会看报错信息,然后定位bug去debug。我是在填写kernel.lds时出现了语法错误,某个地方少了一个分号,当时不会看报错信息还苍蝇乱撞了半天。

实验体会

虽然根据提示填写代码并不是特别困难,但是要理解整个过程还是需要花费一定时间的。因为涉及到的内容比较多,如何很好的理解每个细节,并最终将这些细节串联在一起,在脑内形成对整个流程的主体框架,是一个有一定挑战性的过程。

但是在通过指导书和查阅资料,逐渐理解之后,再回过头去看实验代码,有一种豁然开朗的感觉。特别是好好研究过思考题之后 x 感觉搞明白了很多,如果只是单纯完成实验可能不会搞的很明白的地方。