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.