思考题

Thinking 1.1

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


首先,我们使用objdump --help命令来查看-DS参数的含义,输出结果如下:

1
2
3
4
-D, --disassemble-all    Display assembler contents of all sections
// 含义为展示所有section的汇编内容
-S, --source Intermix source code with disassembly
// 同时显示机器源码与反汇编得到的汇编代码

也就是说,这里参数的含义是:展示二进制文件所有section的反汇编结果,并同时显示机器源码和反汇编得到的汇编代码

使用原生x86工具链的操作流程与结果在指导书中已经十分清晰了,于是我在此不做复述,仅对使用mips交叉编译工具链的实验过程进行记录。

首先我在空文件夹下创建C文件hello.c,内容如下:

1
2
3
4
5
6
#include<stdio.h>

int main(){
printf("Hello World!");
return 0;
}

对其使用命令mips-linux-gnu-gcc -E hello.c > cache1,得到的预处理后的文件里只是出现了头文件的内容(只包含printf()的函数头),并没有出现printf()函数本尊的身影,部分文件内容摘录如下:

1
2
3
4
5
6
7
extern int printf (const char *__restrict __format, ...);

"hello.c"
int main(){
printf("Hello World!");
return 0;
}

再键入命令mips-linux-gnu-gcc -c hello.chello.c文件进行编译(不链接),使用mips-linux-gnu-objdump -DS hello.o > cache1进行反汇编,cache1中的main函数反汇编结果为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
 7 00000000 <main>:
8 0: 27bdffe0 addiu sp,sp,-32
9 4: afbf001c sw ra,28(sp)
10 8: afbe0018 sw s8,24(sp)
11 c: 03a0f025 move s8,sp
12 10: 3c1c0000 lui gp,0x0
13 14: 279c0000 addiu gp,gp,0
14 18: afbc0010 sw gp,16(sp)
15 1c: 3c020000 lui v0,0x0
16 20: 24440000 addiu a0,v0,0
17 24: 8f820000 lw v0,0(gp)
18 28: 0040c825 move t9,v0
19 2c: 0320f809 jalr t9
20 30: 00000000 nop
21 34: 8fdc0010 lw gp,16(s8)
22 38: 00001025 move v0,zero
23 3c: 03c0e825 move sp,s8
24 40: 8fbf001c lw ra,28(sp)
25 44: 8fbe0018 lw s8,24(sp)
26 48: 27bd0020 addiu sp,sp,32
27 4c: 03e00008 jr ra
28 50: 00000000 nop

而键入命令mips-linux-gnu-gcc -o hello hello.c
,对源文件hello.c进行编译和链接。再执行命令mips-linux-gnu-objdump -DS hello > cache3,其中的main函数结果反汇编结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
380 004006e0 <main>:
381 4006e0: 27bdffe0 addiu sp,sp,-32
382 4006e4: afbf001c sw ra,28(sp)
383 4006e8: afbe0018 sw s8,24(sp)
384 4006ec: 03a0f025 move s8,sp
385 4006f0: 3c1c0042 lui gp,0x42
386 4006f4: 279c9010 addiu gp,gp,-28656
387 4006f8: afbc0010 sw gp,16(sp)
388 4006fc: 3c020040 lui v0,0x40
389 400700: 24440830 addiu a0,v0,2096
390 400704: 8f828030 lw v0,-32720(gp)
391 400708: 0040c825 move t9,v0
392 40070c: 0320f809 jalr t9
393 400710: 00000000 nop
394 400714: 8fdc0010 lw gp,16(s8)
395 400718: 00001025 move v0,zero
396 40071c: 03c0e825 move sp,s8
397 400720: 8fbf001c lw ra,28(sp)
398 400724: 8fbe0018 lw s8,24(sp)
399 400728: 27bd0020 addiu sp,sp,32
400 40072c: 03e00008 jr ra
401 400730: 00000000 nop

可以观察到:首先,main函数入口在链接后被分配了在0x4006e0的地址,其次,被用于存储跳转到printf()函数的地址的t9寄存器的值也不再是默认值了。

Thinking 1.2

思考下述问题:

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

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


使用命令./readelf ../../target/mos对内核ELF文件进行解析,得到如下结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
0:0x0
1:0x80010000
2:0x80011df0
3:0x80011e08
4:0x80011e20
5:0x0
6:0x0
7:0x0
8:0x0
9:0x0
10:0x0
11:0x0
12:0x0
13:0x0
14:0x0
15:0x0
16:0x0
17:0x0

对于我们编写的readelf程序无法解析readelf文件本身的原因,我先放结论:这是因为我们编写的readelf程序只能够解析32位的ELF文件,而我们编写的readelf程序本身是64位的ELF文件,因此无法被解析。

首先,我们使用readelf -h命令分别对helloreadelf文件进行解析,得到的结果如下:

1
2
3
4
5
6
7
8
9
// hello
ELF 头:
Magic: 7f 45 4c 46 01 01 01 03 00 00 00 00 00 00 00 00
类别: ELF32

// readelf
ELF 头:
Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
类别: ELF64

而目录下Makefile中对hello的编译命令为:

1
2
hello: hello.c
$(CC) $^ -o $@ -m32 -static -g

其中-m32参数的作用是在64位操作系统下编译生成32位可执行文件,这更加佐证了我的观点:hello是一个32位的可执行文件。

为了进一步佐证我的观点,我编写了一个完全相同的hello2.c文件,并使用gcc -o hello2 hello2.c,接着用./readelf hello2命令尝试对hello2文件进行解析,未果。再次使用readelf -h hello2,得到如下结果:

1
2
3
ELF 头:
Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
类别: ELF64

可知,我们编写的readelf程序无法解析自身的原因是该程序只能解析32位的elf文件。

Thinking 1.3

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


根据指导书附录,我们可以知道,真实操作系统启动的时候,在内核被加载运行之前是会经过两个由bootloader控制进行的阶段(stage1,stage2)的。其中,stage1完成的工作为:基础的硬件初始化,为stage2初始化RAM,载入stage2到RAM,设置堆栈并跳转到stage2入口;而stage2完成的工作为:初始化该阶段所需的硬件设备,载入内核和根文件系统,最后才会为内核设置启动参数并跳转到内核入口。

也就是说,真实的操作系统的启动流程中,内核并不是在一开始就直接被加载到内存上,而是先由bootloader逐步对硬件设备进行控制与初始化,在硬件初始化完成后再将硬件的控制权交给操作系统的。

而我们的实验编写的MOS操作系统的运行和现实不同的一点是,它是在GXemul仿真器上运行的。而GXemul仿真器支持直接加载ELF格式的内核文件,也就是说我们在实验中,因为仿真器已经提供了本由bootloader的引导功能(即硬件已经被初始化),更通俗点讲,就是需要bootloader程序去做的 “盘活硬件”的脏活儿 已经不用做了,启动的时候也不需要从 bootloader程序的入口 开始运行了。所以在启动时我们只需要通过Linker Script保证将内核ELF文件加载到内存的合适位置上就好。

本次实验的难点

我认为本次实验就代码实现上并不算困难。那么难点在哪里呢?我想难主要难在 “带着镣铐跳舞”。本次实验让我明白操作系统实验不是像以往课程实验那样,自己从头到尾实现一个程序,而是在别人的代码框架上做补全。这就要求我设计程序的基础不是我的架构设计,而是这套代码本身的架构设计。我的程序设计是建立在对涉及到的相关代码提供的“基础设施”以及设计思路的基础之上的,而不是我想怎么来就怎么来。

我的心得体会

跨多个代码文件进行阅读并总结思考编码方案,这对我这样的没有接触过大项目的人而言是一个很有挑战性的过程,希望我的工程设计和把控能力能在本学期的操作系统课程中取得锻炼和进步。