OS_lab1_实验报告
1. 思考题
首先利用如下简单C语言程序(hello.c)来研究x86工具链和mips工具链的区别
先利用x86工具链编译并反汇编目标文件代码,并观察
1 | gcc -c hello.c -o hello.o |
最终text.txt内容如下:
再利用mips工具链编译后反汇编:
1 | mips-linux-gnu-gcc -c hello.c -o hello.o |
最终输出如下:
之后再让x86的gcc把hello.c编译成可执行文件,之后再反汇编,代码如下:
1 | gcc hello.c -o hello |
结果如下:
可以看到call指令后面填充了 puts@plt,说明在链接的时候这块函数地址被填上了。
再用mips工具链重复以上步骤:
1 | mips-linux-gnu-gcc hello.c -o hello |
结果如下:
可以看到 lui 指令后的立即数不是0x0了,addiu 指令后的里结束也不是0了。此时 t9 寄存器的值不一样了,说明链接时函数地址被填上了。
objdump -DS 中,==-D 表示 –disassemble-all,即反汇编所有部分的内容;-S表示 –source,即显示与反汇编汇合的源代码==。
- 先用 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可以解析。
- 实验中使用QEMU仿真模拟器来模拟硬件,而QEMU支持直接加载ELF文件格式的内核,因此QEMU相当于已经执行了bootloader的stage1过程了,此时硬件已经被初始化了,可以直接使用。而在stage2阶段,QEMU会加载内核到内存,之后跳转到内核的入口,从而完成启动。==因此即使内核入口不是硬件的启动入口地址,也可以通过QEMU来跳转到内核的入口==。
2. 难点分析
- 首先是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 * 类型,或者指针指向的类型是一字节的。 |
在Linker Script 中书写段的内存定向时,要先确定放置的地址,此时需要用 . = address;来表示接下来的段放在哪里。注意 = 两边要有空格,以及行尾要有分号。之后可以接着写
.text : {* (.text)}
.data : {* (.data)}
等,用来表示把所有.text节组成的段放在相应地址上。内核入口是在 Linker Script 中用ENTRY( _ start )定义的,即内核执行的第一行代码是_start函数。在_start函数中,我们设置了栈指针的位置,并跳转到了mips_init函数完成内核的C语言部分。
在printk函数的书写部分,我们需要注意每一个符号的判断,要记得初始化数据。最终的代码如下所示:
1 | 19 for (;;) { |
同时注意变长参数的使用方法,如下图所示:
需要先用 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)该博客。