lab1-实验报告
Thingking 1.3
Q: Thinking 1.1 在阅读 附录中的编译链接详解 以及本章内容后,尝试分别使用实验环境中 的原生 x86 工具链(gcc、ld、readelf、objdump 等)和 MIPS 交叉编译工具链(带有 mips-linux-gnu- 前缀,如 mips-linux-gnu-gcc、mips-linux-gnu-ld),重复其中的编译和解析过程,观察相应的结果,并解释其中向objdump传入的参数的含义。
A: 我们对于示例代码test_hello.c
进行测试
1 |
|
使用原生工具链
1
2
3gcc -o test_hello test_hello.c
readelf -h test_hello
objdump -d test_hello利用readelf查看文件头信息得到:
反汇编得到
MIPS 交叉编译工具链
1
2
3mips-linux-gnu-gcc -o test_mips test_hello.c
mips-linux-gnu-readelf -h test_mips
mips-linux-gnu-objdump -d test_mipsobjdump参数用法
-d
:对目标文件中的可执行段进行反汇编,将机器码转换为汇编代码,方便开发者查看程序的指令序列-D
:对目标文件的所有段进行反汇编,包括不可执行段,如数据段等。-h
:显示目标文件的各个段的头部信息,包括段的名称、大小、偏移等。-S
:将源代码和反汇编代码混合显示,方便开发者对照查看源代码和对应的汇编代码。-t
:显示目标文件的符号表信息,包括符号的名称、地址、类型等。
Thinking 1.2
Q 尝试使用我们编写的readelf程序,解析之前在target目录下生成的内核ELF文件。and
编写的readelf
程序不可以解析readelf
文件本身,为什么系统工具可以解析readelf
?
A 自行编写的readelf
文件解析mos得到
1 | 0:0x0 |
利用readelf -h hello
和readelf -h readelf
指令,我们得到ELF头文件中显示的类型,可知hello
是32位,而readelf
是64位的,而我们编写的readelf.c
文件是针对32位文件编写,无法解析64位的readelf
文件(如不能用typedef uint32_t Elf32_Off;
的数据类型取做64位的运算)
Thinking 1.3
Q: 在理论课上我们了解到,MIPS 体系结构上电时,启动入口地址为 0xBFC00000 (其实启动入口地址是根据具体型号而定的,由硬件逻辑确定,也有可能不是这个地址,但 一定是一个确定的地址),但实验操作系统的内核入口并没有放在上电启动地址,而是按照 内存布局图放置。思考为什么这样放置内核还能保证内核入口被正确跳转到? (提示:思考实验中启动过程的两阶段分别由谁执行。)
A实验中使用的QEMU模拟器已经提供了bootloader的引导(启动)功能,并且支持直接加载ELF格式的内核。操作系统启动过程被简化为加载内核到内存(kernel.lds
控制),之后跳转到内核入口(kernel.lds
通过ENTRY(_start)
设置程序入口为_start
函数,链接后程序从_start
函数开始执行)。
难点分析
此次Lab1实验的重难点主要有以下两个方面:
- ELF文件的解析
- **
printk
**函数的实现
ELF文件的解析
ELF
是一种用于可执行文件(executable
)、目标文件(relocatable
)和库(shared object
)的文件格式,而我们编译出的MOS内核文件本质上也是一个ELF文件
ELF的主要结构如下:
从图中可以得到,ELF文件主要由以下几部分构成:
- ELF头:记录程序的基本信息——体系结构与操作系统,同时包含了节头表与段头表相关信息
- 段头表:包含程序各个段的信息,该信息需要在运行和装载时使用
- 节头表:包含程序各个节的信息,该信息需要在程序编译和链接时间使用
- 段头表表项与节头表表项
我们需要学会解析ELF
文件并获取相关信息,同时学习通过链接器等指导其中某些信息的生成与储存。以解析section
为例,
文件头地址
binary
+ 节头表偏移量e_shoff
得到节头表地址,同时从文件头得到节头表表项大小e_shentsize
和 节头表表项数e_shnum
。binary + e_shoff + i * e_shentsize
就是第i个节头表表项的地址。
printk实现逻辑
与实现输出功能相关的主要由三个文件
- kern/machine.c : 其中主要实现了函数
void printcharc(char ch)
, 该函数的功能是向控制台输出字符ch
(本质上是向一块特殊的内存中写入ch
) - kern/printk.c : 其中主要是实现了
void printk(const char*fmt,...)
,该函数实际上是把回调函数void outputk(void *data, const char*buf, size_t len)
与输出参数传递到了vprintfmt
中 - lib/print.c: 其中实现了
vprintfmt(fmt_callback_t out, void *data, const char *fmt, va_list ap)
这个实现格式化输出的主体逻辑
回调函数:是一个只能通过函数指针调用的函数。将某函数的指针作为参数传递给另一个函数,在另一个函数中使用该函数指针,我们认为该函数是一个回调函数。在调用
qsort
函数时间传入的cmp
实际上就是一个回调函数
实验体会
Lab1
主要学习目标是掌握操作系统启动的基本流程、掌握ELF文件的结构和功能以及编写printk函数。本次实验所需要实际编写的代码并不多,可能就是数十行。学习时间主要花费在了阅读源代码上,由于各个文件的依赖文件众多,阅读起来确实稍显麻烦。此外,涉及操作系统内核的众多源文件中含有大量指针运用,使用不当容易出错。
此次实验难度不大,且通过printk
函数的实战让我第一次认真去了解一个C语言库函数的基础实现,我觉得这非常有趣。
实验参考
与回调函数相关内容我主要参考了一下链接https://cookedbear.top/p/44391.html