lab1_report
lab1_report
lab_thinkings
Thinking 1.1
question
请阅读附录中的编译链接详解,尝试分别使用实验环境中的原生
x86工具链(gcc、ld、readelf、objdump等)和 MIPS 交叉编译工具链(带有mips-linux-gnu-前缀),重复其中的编译和解析过程,观察相应的结果,并解释其中向objdump传入的参数的含义。
answer
尝试使用x86工具链和MIPS交叉编译工具链
1. 创建新文件main.c
1 | int main(){ |
2.使用x86工具链
首先使用gcc -c main.c指令形成main.o文件,之后使用objdump -DS main.o进行反汇编。就能得到如下结果:
1 | Disassembly of section .text: |
3.使用MIPS交叉编译工具链
首先使用mips-linux-gnu-gcc -c main.c指令形成main.o文件,之后使用mips-linux-gnu-objdump -DS main.o进行反汇编。就能得到如下结果:
1 | main.o: 文件格式 elf32-tradbigmips |
对objdump的参数简单介绍
objdump命令的常用参数有以下几个:
| 参数 | 含义 |
|---|---|
| -d | 将代码段反汇编 反汇编那些应该还有指令机器码的section |
| -D | 与 -d 类似,但反汇编所有section |
| -S | 将代码段反汇编的同时,将反汇编代码和源代码交替显示,源码编译时需要加-g参数,即需要调试信息 |
| -C | 将C++符号名逆向解析 |
| -l | 反汇编代码中插入源代码的文件名和行号 |
| -j | section: 仅反编译所指定的section,可以有多个-j参数来选择多个section |
因此,指导书上的objdump -DS [executable file]这一命令中-DS表示将所有的section反汇编,并将反汇编代码和原码交替显示。
Thinking 1.2
question
思考下述问题:
尝试使用我们编写的
readelf程序,解析之前在target目录下生成的内核 ELF 文件。也许你会发现我们编写的
readelf程序是不能解析readelf文件本身的,而我们刚才介绍的系统工具readelf则可以解析,这是为什么呢?(提示:尝试使用readelf -h,并阅读tools/readelf目录下的Makefile,观察readelf与hello的不同)
answer
解析target/mos
在target目录下运行readelf -h mos的结果如下:

用我们自己编写的readelf解析如下:
解析tools/readelf/hello和tools/readelf/readelf并比较区别
在tools/readelf目录下运行readelf -h hello的结果如下:
在tools/readelf目录下运行readelf -h readelf的结果如下:
通过观察发现,两个程序的类别不同,且readelf文件的类型为DYN而非EXEC,所以合理推断这是readelf不能解析自己的原因,文件类型不匹配。
Thinking 1.3
question
在理论课上我们了解到,
MIPS体系结构上电时,启动入口地址为0xBFC00000(其实启动入口地址是根据具体型号而定的,由硬件逻辑确定,也有可能不是这个地址,但一定是一个确定的地址),但实验操作系统的内核入口并没有放在上电启动地址,而是按照内存布局图放置。思考为什么这样放置内核还能保证内核入口被正确跳转到?(提示:思考实验中启动过程的两阶段分别由谁执行。)
answer
因为我们在kernel.lds中设置好了各个节被加载的位置,即最终的segment地址,同时使用了ENTRY(_start)指定了程序入口(即内核入口),最终保证了内核入口能够被正确跳转。
lab_difficulties
lab1本身题目不难,难度在于如何理解源码乃至其背后的原理。主要难点包括如何使用指针来获取ELF文件中各个section和segment中的数据和printk实现逻辑的梳理。
lab1叫做操作系统的启动,但由于GXemul本身提供了bootloader的功能,所以lab1的任务可以简化为加载内核到内存和跳转到内核的入口两个任务。
常规的启动流程涉及 bootloader 对软硬件的初始化,十分复杂。但 MOS 运行的环境是 GXemul 模拟器。 GXemul 支持直接加载 ELF 格式的内核,因此我们只需要把内核编译成正确的 ELF 可执行文件就可以启动了
从操作系统角度理解 MIPS 体系结构
首先是对于lab1非常重要的一张图:
1 | /* |
这张图记录了我们lab1实验中的内存布局图,也是我们lab1的最大难点的解决利器,我们要学会利用这张图链接到正确的位置上。
掌握 ELF 文件的结构和功能
ELF 是一种用于可执行文件、目标文件和库的文件格式。ELF 格式是是 UNIX 系统实验室作为 ABI(Application Binary Interface)而开发和发布的,现在早已经是 Linux 下的标准格式了。我们在之前曾经看见过的.o 文件就是 ELF 所包含的三种文件类型中的一种,称为可重定位 (relocatable) 文件,其它两种文件类型分别是可执行 (executable) 文件和共享对象 (shared object) 文件,这两种文件都需要链接器对可重定位文件进行处理才能生成。下图时ELF文件的基本结构——
这里,我们再补充一下关于 ELF 文件中节(section)的概念。在链接过程中,目标文件被看成节的集合,并使用节头表来描述各个节的组织。换句话说,节记录了在链接过程中所需要的必要信息。
| 节名称 | 功能 |
|---|---|
.text |
保存可执行文件的操作指令。 |
.data |
保存已初始化的全局变量和静态变量。 |
.bss |
保存未初始化的全局变量和静态变量。 |
完成 printk 函数的编写
可能是被OO影响,感觉printk的编写特别像递归下降的文法读入,所以实际编写时注重细节对每个参数分类处理即可。具体流程可以参考下面这张流程图。
我们的printk()函数时通过调用print.c文件中定义的 vprintfmt()函数来实现打印的,这个vprintfmt()函数实现了printf的核心逻辑,虽然函数很长,但是逻辑并不复杂。实际上就是,遍历一遍格式字符串fmt,如果没有遇到\0或者%就直接打印到终端,如果遇到%就按照%[flags][width][length]<specifier>的形式进行输出格式的解析,并以这种输出格式打印从可变参数列表获得的参数;如果遇到\n就结束整个打印过程。
lab_summary
MOS内核文件组织
| 目录 | 功能 |
|---|---|
| init 目录 | 内核初始化相关代码 |
| include 目录 | 存放系统头文件 |
| lib 目录 | 存放一些常用的库函数,包括 vprintfmt |
| kern 目录 | 存放内核的主体代码 |
| tests 目录 | 存放测试程序 |
| tools 目录 | 存放一些实用工具,包括 readelf该目录下的 C 程序使用原生工具链构建(而非交叉编译),在宿主环境(而非 GXemul)下运行 |
| target 目录 | 存放编译的产物 |
| Makefile | 用于组装 MOS 内核的 Makefile 文件 |
make用法
| 指令 | 功能 |
|---|---|
| make run | 使用 GXemul 仿真内核 |
| make dbg | 以调试模式启动 GXemul |
| make objdump | 将内核镜像反汇编并输出到 target/mos.objdump 中 |
| make clean | 清空编译器构建的文件,以待重新编译 |
总结
Lab1主要让我们学习操作系统启动的基本流程、掌握ELF文件的结构和功能,以及最终完成一个printf函数的书写。本次Lab花费时间大约为8个小时,大多数时间都花在阅读源代码和思考源代码的逻辑上。由于完成每个功能依赖的代码文件不止一个,而且在vim中阅读代码终归不如在vscode等图形化编辑器上方便,因此给代码阅读和理解带来了不小的麻烦。此外,课程组提供的源代码中大量使用了“指针”,也给代码的理解带来了一定的困难。
- Title: lab1_report
- Author: Charles
- Created at : 2023-03-07 12:38:53
- Updated at : 2023-11-05 21:36:18
- Link: https://charles2530.github.io/2023/03/07/lab1-report/
- License: This work is licensed under CC BY-NC-SA 4.0.