1. 思考题

  1. 首先利用如下简单C语言程序(hello.c)来研究x86工具链和mips工具链的区别

先利用x86工具链编译并反汇编目标文件代码,并观察

1
2
gcc -c hello.c -o hello.o
objdump -DS hello.o > text.txt

最终text.txt内容如下:

再利用mips工具链编译后反汇编:

1
2
mips-linux-gnu-gcc -c hello.c -o hello.o
mips-linux-gnu-objdump -DS hello.o > text.txt

最终输出如下:

之后再让x86的gcc把hello.c编译成可执行文件,之后再反汇编,代码如下:

1
2
gcc hello.c -o hello
objdump -DS hello > text.txt

结果如下:

可以看到call指令后面填充了 puts@plt,说明在链接的时候这块函数地址被填上了。

再用mips工具链重复以上步骤:

1
2
mips-linux-gnu-gcc hello.c -o hello
mips-linux-gnu-objdump hello > text.txt

结果如下:

可以看到 lui 指令后的立即数不是0x0了,addiu 指令后的里结束也不是0了。此时 t9 寄存器的值不一样了,说明链接时函数地址被填上了。

objdump -DS 中,==-D 表示 –disassemble-all,即反汇编所有部分的内容;-S表示 –source,即显示与反汇编汇合的源代码==。

  1. 先用 make 生成MOS内核的ELF文件,之后利用 ./readelf target/mos 去解析,结果如下:

以下是用系统工具readelf -S 解析的mos内核:

经过尝试发现 ./readelf readelf 命令没有任何反应,但是 ./readelf hello 命令可以正常输出。下面探究原因。通过输出 ./readelf -h readelf 来解析readelf文件的文件头信息,内容如下:

而解析hello可执行文件的内容如下:

再结合makefile中的不同:

可以看到 make hello 中多了 -m32 -static -g 三个参数。因此初步判断是 -m32 使得hello文件是32位的,而readelf默认生成为64位的了。我们的readelf只能解析32位的elf文件,因此无法解析自己。为了证明我们的判断,我们用readelf -h mos去查看mos内核的文件头,内容如下:

可以看到mos内核也是32位的,因此我们的readelf可以解析mos内核。
经查询资料发现,系统工具readelf可以解析32位和64位的ELF文件格式。因此可以确定我们的readelf只能解析32位的程序,由于其本身是64位的,因此其自己无法解析自己,而系统工具readelf可以解析。

  1. 实验中使用QEMU仿真模拟器来模拟硬件,而QEMU支持直接加载ELF文件格式的内核,因此QEMU相当于已经执行了bootloader的stage1过程了,此时硬件已经被初始化了,可以直接使用。而在stage2阶段,QEMU会加载内核到内存,之后跳转到内核的入口,从而完成启动。==因此即使内核入口不是硬件的启动入口地址,也可以通过QEMU来跳转到内核的入口==。

2. 难点分析

  1. 首先是ELF文件格式的理解问题。ELF文件格式包含多种类型的文件,包括可重定位文件、可执行文件、共享对象文件等。ELF文件由几部分组成:首先是文件头,用来说明ELF文件的性质等信息,其中给出了段头表和节头表的偏移、大小等信息。其次是节头表,里面存储了文件的节的信息,地址等,用来在链接的时候把相同的节放在同一段中。还有段头表,也叫程序头表,它的作用是表明每一段的虚拟地址,方便在执行的时候把相应的段放入内存中。

其中可重定位文件和可执行文件的区别如下:

特性 可重定位文件(Relocatable Files) 可执行文件(Executable Files)
文件类型 ET_REL(1) ET_EXEC(2)
主要用途 链接阶段,生成可执行文件或共享库 直接由操作系统加载并执行
节(Sections) 包含 .text.data.bss 通常不包含节,而是包含段(Segments)
段(Segments) 无程序头表 包含程序头表,描述如何加载到内存
重定位信息 包含重定位信息(如 .rel.text 通常不包含重定位信息(除非动态链接)
符号表 包含完整的符号表(.symtab 可能不包含完整的符号表
入口点地址 无入口点地址(e_entry 为 0) 包含入口点地址(e_entry
2. 注意指针的加法。如果指针是void* 类型,例如 void * p = 0x0,则 p + 3 = 0x3,即p移动1* num个字节。==而如果指针是其他类型,例如 int * p =0x0,则 p + 3 = 0xb,即p移动sizeof(int)* num个字节==。因此我们想让指针只移动num个字节,需要保证指针是 void * 类型,或者指针指向的类型是一字节的。
  1. 在Linker Script 中书写段的内存定向时,要先确定放置的地址,此时需要用 . = address;来表示接下来的段放在哪里。注意 = 两边要有空格,以及行尾要有分号。之后可以接着写
    .text : {* (.text)}
    .data : {* (.data)}
    等,用来表示把所有.text节组成的段放在相应地址上。

  2. 内核入口是在 Linker Script 中用ENTRY( _ start )定义的,即内核执行的第一行代码是_start函数。在_start函数中,我们设置了栈指针的位置,并跳转到了mips_init函数完成内核的C语言部分。

  3. 在printk函数的书写部分,我们需要注意每一个符号的判断,要记得初始化数据。最终的代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
 19         for (;;) {
20 /* scan for the next '%' */
21 /* Exercise 1.4: Your code here. (1/8) */
22 while ((*fmt)!='\0' && (*fmt)!='%') {
23 out(data, fmt, 1);
24 fmt++;
25 }
26 /* flush the string found so far */
27 /* Exercise 1.4: Your code here. (2/8) */
28 if ((*fmt) == '\0') {
29 break;
30 }
31 /* check "are we hitting the end?" */
32 /* Exercise 1.4: Your code here. (3/8) */
33 fmt++;
34 /* we found a '%' */
35 /* Exercise 1.4: Your code here. (4/8) */
36 ladjust = 0;
37 padc = ' ';
38 if ((*fmt) == '-') {
39 ladjust = 1;
40 fmt++;
41 } else if ((*fmt) == '0') {
42 padc = '0';
43 fmt++;
44 }
45 /* check format flag */
46 /* Exercise 1.4: Your code here. (5/8) */
47 width = 0;
48 while ((*fmt) >= '0' && (*fmt) <= '9') {
49 width = width*10 + (*fmt) - '0';
50 fmt++;
51 }
52 /* get width */
53 /* Exercise 1.4: Your code here. (6/8) */
54 long_flag = 0;
55 while ((*fmt) == 'l') {
56 long_flag = 1;
57 fmt++;
58 }
59 /* check for long */
60 /* Exercise 1.4: Your code here. (7/8) */
61
62 neg_flag = 0;
63 switch (*fmt) {
64 case 'b':
65 if (long_flag) {
66 num = va_arg(ap, long int);
67 } else {
68 num = va_arg(ap, int);
69 }
70 print_num(out, data, num, 2, 0, width, ladjust, padc, 0);
71 break;
72
73 case 'd':
74 case 'D':
75 if (long_flag) {
76 num = va_arg(ap, long int);
77 } else {
78 num = va_arg(ap, int);
79 }
80
81 /*
82 * Refer to other parts (case 'b', case 'o', etc.) and func 'print_num' to
83 * complete this part. Think the differences between case 'd' and the
84 * others. (hint: 'neg_flag').
85 */
86 /* Exercise 1.4: Your code here. (8/8) */
87 if (num < 0) {
88 neg_flag = 1;
89 num = -num;
90 }
91 print_num(out, data, num, 10, neg_flag, width, ladjust, padc, 0);
92 break;
93
94 case 'o':
95 case 'O':
96 if (long_flag) {
97 num = va_arg(ap, long int);
98 } else {
99 num = va_arg(ap, int);
100 }
101 print_num(out, data, num, 8, 0, width, ladjust, padc, 0);
102 break;

同时注意变长参数的使用方法,如下图所示:


需要先用 va_list ap;声明一个变量。再用 va_start(ap, lastarg);进行初始化。
每次获取参数可以用 va_arg(ap, int);获取。其中int为该变量的类型。
最后结束的时候要用 va_end(ap);来结束变量。

3. 实验体会

本次实验需要认真体会,反复观看,才能很好的掌握整体逻辑。我在前几次看指导书的时候,都有种不知所云的感觉,感觉读了但不知道在说什么;对于做题也只是简单的完成本题的任务,但不清楚这个题目在整体启动过程中是哪一步骤,发挥了什么作用。这使得我对于整体开机的顺序还是模糊的。

在初次完成任务后,我又在写实验报告的时候认真重温了一遍指导书,这才稍有眉目,多了解了一些内容,并明确了每一部分是在干什么。

我自己理解了一点lab1的内容,不知道对不对,想请大家斧正一下。真正的计算机启动主要分为:硬件启动和内核启动。其中硬件启动在Linux系统上是,当按下电源后,计算机先运行BIOS的代码,这部分代码是存储在断电不丢失的ROM上的,因为这是最最最开始运行的代码,不会被其他方式加载,只能存储在硬件上。之后BIOS进行硬件自检后加载MBR,并运行MBR中的bootloader部分,Linux系统中主要是GRUB或LILO。bootloader分为两部分:stage1先简单初始化硬件,之后初始化RAM,加载stage2代码到RAM中,跳转到stage2入口;stage2再初始化相关硬件,加载内核到内存中(这部分是lab1实验完成的部分),配置内核参数,然后跳转到内核的入口,即把CPU控制权给内核,之后便运行内核代码完成进一步初始化。我们的lab1实验主要是聚焦于加载内核到内存中这一阶段?我们用的是QEMU模拟器,支持直接加载ELF格式的内核,我理解的意思就是QEMU自动完成了加载内核到内存这一步之前的所有操作?我们在实验中通过make来编译链接出mos内核,也就是说内核在make之前是不存在的,那真实启动中,是bootloader来完成make这一任务来创生出内核么?还是通过给定路径去解压下载(这样的话内核一开始是存在的)?这部分完成后只是有了内核,但是还没放到内存中,内核本质是ELF文件格式,它不能自己把自己放到内存中,只是通过段头表给出了每段的虚拟地址而已,那是不是OS根据其段头表的地址把内核的段放到内存中的?ELF中有内核的入口地址en_entry,是不是OS在配置好内核参数后,跳转到entry运行,从而完成跳转到内核入口这一过程?

如果以上内容大致方向理解正确,那我们lab1实验其实没有干任何OS要干的事(应该)只是在完成内核信息而已。我们写的readelf.c跟内核本身没关系,只是为了帮助我们理解ELF文件格式而已;我们写的 LinkerScript 的链接地址,在make后会决定mos内核的段头表的地址,等待后续被OS参考并放入内存;我们补充的_start.S的代码,是内核的入口函数的代码,也是CPU控制权交给内核后运行的代码,之后的 mips_init 会进一步初始化内核。这么多工作其实都只是在完善mos内核而已,我们也没有真正把内核放入内存中,只是给出了地址。所以OS的工作我们还没做?

4. 原创声明

以上内容大部分为原创。少部分参考了北航操作系统课程lab1实验报告 - 南风北辰 - 博客园 (cnblogs.com)该博客。