OS-lab1
思考题
Thinking 1.1
请阅读 附录中的编译链接详解,尝试分别使用实验环境中的原生 x86
工具链(gcc
、ld
、readelf
、objdump
等)和 MIPS
交叉编译工具链(带有mips-linux-gnu
前缀),重复其中的编译和解析过程,观察相应的结果,并解释其中向 objdump
传入的参数的含义。
答:
通过阅读附录A.3
,我们可以看到一个简单的printf("Hello World!")
是怎么一步步预处理,编译,链接并最终变成可执行文件的。因为我们想找到printf
到底是在哪里被插入到程序中的,所以我们每次只用gcc
执行一次操作,同时使用objump
进行反汇编。通过找到汇编语言中callq
位置并观察其调用函数的地址来判断该阶段printf
是否插入程序中。
为了使机器码转换回汇编代码,我们使用objdump -参数 要反汇编的目标文件名 > 导出文本文件名
命令进行反汇编,那我们先来介绍一下objump
相关参数的含义:
-S
:用于在反汇编的输出中插入源代码。-D
:用于在每条指令前面添加对应的机器码。通常-D
参数会结合-S
参数一起使用,以便在反汇编内容中同时显示源代码,机器码和汇编语言。
通过进行和附录相同的操作,可以看出在编译阶段,调用printf
函数的地址还是0,而在链接之后,地址变化,所以,printf
实际上是在链接时插入到程序中。
所以,对于拥有多个 c 文件的工程来说,编译器会首先将所有的 c 文件以文件为单位,编译成.o 文件。最后再将所有的.o 文件以及函数库链接在一起,形成最终的可执行文件,整个过程如下图所示:

Thinking 1.2
思考下述问题:
- 尝试使用我们编写的
readelf
程序,解析之前在target
目录下生成的内核ELF
文件。 - 也许你会发现我们编写的
readelf
程序是不能解析readelf
文件本身的,而我们刚才介绍的系统工具readelf
则可以解析,这是为什么呢?(提示:尝试使用readelf -h
,并阅读tools/readelf
目录下的Makefile
,观察readelf
与hello
的不同)
答:通过执行命令./tools/readelf/readelf ./target/mos
,我们可以解析mos文件如下图:

同时readelf
是一个用于查看 ELF 文件信息的工具,但它本身也是一个 ELF 格式的可执行文件。通常情况下,我们自己编写的 readelf
程序是不能解析自身的,这是因为在编写 readelf
程序时,我们使用的是特定的编译器和链接器工具链,生成的可执行文件可能包含一些特定的调试信息或符号表等,这些信息可能超出了我们编写的 readelf
程序所能解析的范围。
而系统提供的 readelf
工具则可以解析自身,这是因为系统提供的 readelf
可执行文件是经过专门的编译、链接和调试处理的,其中包含了更多的调试信息和符号表等,使得它能够解析自身的 ELF 头部以及其他信息。
通过阅读 tools/readelf
目录下的 Makefile
,我发现,target hello
下有-static
,而readelf
没有。

Thinking 1.3
在理论课上我们了解到,MIPS 体系结构上电时,启动入口地址为 0xBFC00000 (其实启动入口地址是根据具体型号而定的,由硬件逻辑确定,也有可能不是这个地址,但 一定是一个确定的地址),但实验操作系统的内核入口并没有放在上电启动地址,而是按照内存布局图放置。思考为什么这样放置内核还能保证内核入口被正确跳转到?(提示:思考实验中启动过程的两阶段分别由谁执行。)
答:MIPS
系统加电起动后, MIPS
处理器默认的程序入口是 0xBFC00000
,此地址在无缓存的 KSEG1
的地址区域内,对应的物理地址是 0x1FC00000
,即 CPU
从 0x1FC00000
开始取第一条指令,这个地址在硬件上已经确定为 FLASH
的位置,然后 Bootloader
执行功能,加载内核到内存。关于内核 ELF
文件的入口地址 (Entry point)
,即 bootloader
移动完内核后,直接跳转到的地址,由 ld
写入 ELF
的头中
难点分析
就我个人而言,我觉得lab1的主要难点是对指导书和相关代码的阅读和理解,下面将分别从四个exercise来分析
Exercise 1.1 : elf
在此之前我们知道了ELF文件结构以及了解了ELF头,节头表和段头表的数据结构,我们的任务是补全代码输出ELF 文件中所有节头中的地址信息。
我觉得本exercise的难点是清楚了解ELF的文件结构,如下图所示

简单来说,ELF头中包含了段头表和节头表的信息,段头表和节头表分别含有各段和各节的信息。了解结构之后,只需要根据elf.h
文件中的数据结构来补全代码即可。
Exercise 1.2 : Linker Script
在了解完ELF之后,我们开始学习内核的相关知识
首先,我要需要知道内核应该被放在哪里。在 本实验的MIPS 体系结构中,4GB虚拟地址空间会被划分为 4 个大区域,如下图所示:
我们的内核便被放置在kseg0
中(具体位置可在include/mmu.h
中的内存布局图中查看),于是,我们就要通过Linker Script
来将内核加载到正确的位置。在Linker Script
我们使用.
这一特殊符号,用来做定位计数器,根据输出节的大小增长,从而将内核.text
、.data
、.bss
,这三个节的信息加载到正确位置。
在本exercise的难点我觉得是了解MIPS内存布局(一开始看到mmu
时属实一脸懵),知道内核的位置。
Exercise 1.3 : _start
第三个exercise还算比较简单,就是首先将 sp
寄存器设置到内核栈空 间的位置上,随后跳转到 mips_init
函数
我觉得需要注意的是,因为 sp
是低地址增长的,所以其栈底地址就在KSTACKTOP
,即在内核的栈在0x8040 0000
以下的位置
Exercise 1.4 : printk
在第四个exercise中,我们需要实现一个printk函数,用来输出,调试(以及评测的方式)
本exercise的难点我觉得有两方面:
变长参数表:在初次接触到变长参数时,我一开始也是看的一头雾水
根据指导书内容,在
stdarg.h
头文件中为处理变长参数表定义了一组宏和变量类型如下:va_list
:变长参数表;va_start(va_list ap, lastarg)
:用于初始化变长参数表的宏;va_arg(va_list ap, 类型)
:用于取变长参数表下一个参数的宏;va_end(va_list ap)
:结束使用变长参数表的宏。
具体应用如下:
void example(const int *fmt, ...) {
va_list ap; //声明变长参数表ap
va_start(ap, fmt); //初始化ap
int num1 = va_arg(ap, int); // 从ap中取出一个int型变量
int num2 = va_arg(ap, int); // 取出下一个int型变量
va_end(ap); //结束变长参数表ap的使用
}代码阅读:本实验中填写代码之前,还需要阅读大量相关代码。而且代码中使用大量指针、宏定义以及文件之间的相互调用,在初次阅读时还是有不小的难度。
主要的输出逻辑是这样,在
printk()
函数中通过vprintfmt(outputk, NULL, fmt, ap);
调用vprintfmt
,接下来在vprintfmt
函数中对输出形式进行判断,最后通过传入的outputk
调用printchar
在最底层以向一个地址写出字符数值的形式输出字符
实验体会
本次实验也是第一次接触到了操作系统的内部架构,对我来说还是有不小的难度。第一次阅读指导书时也是真的很懵,代码无从下手。不过随着反复阅读以及参考学长博客,最终还是顺利完成了exercise任务并基本了解内核启动相关知识。
以后实验指导书和代码的阅读难度肯定会更大,希望也可以一点点理解,一点点深入操作系统的学习。