文章目录
指导书梳理&讲解
注:此处只梳理实验中可能用到的内容,指导书中理论性强的部分补充在相应的Notes当中
内核
Makefile
-
阅读意义:对整个操作系统的布局产生初步了解
-
多种赋值语句
-
=
赋予整个makefile中最后被指定的值VIR_A = A VIR_B = $(VIR_A) B VIR_A = AA # 最后VIR_B的值是AA B
-
:=
赋予当前位置的值VIR_A := A VIR_B := $(VIR_A) B VIR_A := AA # 最后VIR_B的值是A B
-
?=
如果该变量没有被赋值,则赋予等号后的值
-
-
.PHONY
表明列在其后的目标不受修改时间的约束,无视 make 时有关时间戳的性质- 时间戳检查是指在进行编译时,通过比较文件的时间戳来确定是否需要重新生成目标文件。时间戳检查是实现增量编译的核心原理,它能够有效地避免不必要的重复编译,提高编译效率
-
构建整个项目依赖于构建好内核可执行文件 $(mos_elf),后者的构建依赖于所有的模块
顶层Makefile片段
- 内核可执行文件构建:调用链接器将之前构建各模块产生的所有 .o文件在 linker script 的指导下链接到一起
- 每个模块的构建:调用对应模块目录下的 Makefile
ELF (Executable and Linkable Format)文件
-
反汇编并将结果导出至文本文件的命令如下
objdump -DS 要反汇编的目标文件名 > 导出文本文件名
-
ELF 的文件头,就是一个存了关于 ELF 文件信息的结构体。
- 存储ELF 的魔数,以验这是一个有效的 ELF
- 是判断ELF文件的条件,不应该通过扩展名判断
- 存储ELF 的魔数,以验这是一个有效的 ELF
-
有一个随编译工具链提供的工具也名为 readelf
readelf -S
以列表的形式输出文件中各个节的详细信息readelf -l
查看各个段的信息
MIPS 内存布局——寻找内核的正确位置
-
MIPS 内存布局见理论部分
-
内存布局图内存部分
- 为异常处理预留一块空间
- 将内核镜像的 .text 、 .data 、 .bss 这些节安置在 0x80020000
- 为栈预留一块空间,将基地址设置为 0x80400000(栈是从高地址向低地址生长的)
Linker Script —— 控制加载地址
链接器控制了输出文件的内存布局
-
语法
SECTIONS { # . 表示定位计数器,规定当前的位置 . = 0x10000; .text : { *(.text) } . = 0x8000000; .data : { *(.data) } .bss : { *(.bss) } } # 在 Linker Script 中可以通过 ENTRY(symbol) 来设置程序入口(程序执行的第一条指令)
- 注意这里 = 两边有空格
从零开始搭建 MOS
make
targets := $(mos_elf)
all: $(targets)
$(modules):
$(MAKE) --directory=$@
$(mos_elf): $(modules)
$(LD) $(LDFLAGS) -o $(mos_elf) -N -T $(link_script) $(objects)
- 链接命令参数
-T
指定链接脚本
_start 函数
- EXPORT 宏:
EXPORT(_start)
将 _start 函数导出为一个符号,使得链接器可以找到它。可以简单的理解为,它实现了一种在汇编语言中的函数定义
实战 printk
处理变长参数表
-
常用宏和变量类型
va_list
变长参数表的变量类型va_start(va_list ap, lastarg)
初始化变长参数表,lastarg为该函数最后一个命名的形式参数va_arg(va_list ap, 类型)
取变长参数表下一个参数- 由于类型提升的存在,C 语言中的数值类型在作为右值使用(如参与算术运算、作为实参传递等)时往往都会发生提升(表达式中的字符和短整型操作数在使用之前被转换为普通整型)
- 变长参数是通过栈空间实现的。由于需要确定后续参数的地址,所以每次取参数都需要标明类型
va_end(va_list ap)
结束使用变长参数表
void printk(const char *fmt, ...) { // 声明一个类型为 va_list 的变量 ap va_list ap; // 用 va_start 宏进行一次初始化 va_start(ap, fmt); vprintfmt(outputk, NULL, fmt, ap); va_end(ap); }
-
void vprintfmt(fmt_callback_t out, void *data, const char *fmt, va_list ap)
-
out
回调函数,完成输出。实现解析逻辑和输出逻辑的解耦,同时 printk 等上层函数可传入不同的回调函数实现不同的输出行为(比如输出到文件)
-
data
是回调函数 out 需要的额外上下文信息
-
可以类比面向对象语言中的设计,将 out 视为“继承自接口的方法实现”, data 则类似方法中的 this 指针
-
实验代码阅读&常用操作
结构
本章主要使用
-
include目录——存放系统头文件
-
mmu.h
有一张内存布局图,在填写 linker script 的时候需要根据这个图设置相应节的加载地址
-
-
include.mk
-
init目录——初始化内核相关代码
-
start.S
-
_start 函数
是 CPU 控制权被转交给内核后执行的第一个函数,主要工作是初始化 CPU 和栈指针,然后跳转至 MOS 的初始化函数( mips_init )
内核栈空间的地址可以在 include/mmu.h 中看到,注意栈的增长方向
-
-
init.c
-
mips_init 函数
内核中各模块的初始化函数都会在这里被调用
-
-
-
kern目录——存放内核的主体代码
本章中主要存放的是终端输出相关的函数
-
machine.c——往 QEMU 的控制台输出字符
原理为读写某一个特殊的内存地址
-
printk.c——实现了 printk
实际上是把输出字符的函数,接受的输出参数给传递给了 vprintfmt 这个函数
-
-
kernel.lds
-
lib目录——存放一些常用库函数
本章中主要存放用于格式化输出的函数
- print.c——实现了 vprintfmt 函数(实现了格式化输出的主体逻辑)
-
Makefile
-
mk目录
-
tests目录——存放公开的测试用例
-
tools目录
- readelf目录
-
elf.h ——存放解析ELF文件要用的三个关键数据结构
- 包括三个结构体,第一个对应ELF 的文件头,第二个对应节(section)头表,第三个对应段(segment)头表
-
readelf.c ——用于解析ELF文件
- is_elf_format函数——判断输入是否为ELF文件
- readelf 函数——输出 ELF 文件中所有节头中的地址信息
-
- readelf目录
make的使用
编译
-
make
编译完整内核- 需要在init/init.c的mips_init里添加自己的测试代码
-
make test lab=<x>_<y>
编译指定测试点, lab 的第 y 个测试用例eg make test lab=1_2
运行与调试
make run
运行make dbg
使用 QEMU 模拟器以调试模式运行内核,并进入 GDB 调试界面。make objdump
将项目中的目标文件反汇编Ctrl+A+X
退出 QEMU
Lab0实时记录
- 设置32位地址要使用
ori
+lui
指令 - 用gitlab IDE编辑提交之后,切回跳板机运行前记得
git pull
!!
实验报告
难点分析
对“数据类型”的理解
在ELF文件的解析过程中,用到了3个重要的结构体,相当于定义了3种新的数据类型(实际不止3种,其他部分代码里也有定义)。
而在实际编写中,就涉及到数据类型的转换。比如在做Exercise 1.1的时候*,*把 void *类型的 binary 强制转换为了 ELF32_Ehdr * 。
因为此前对于数据类型的理解不够灵活,导致我理解这一部分花了好些时间。我认为这里的理解关键在于,**数据类型并不是某段内存数据的固有属性,而是对它的解释方式。**只要符合类型的大小和对齐要求,就可以把这段数据看做某种类型。
需要注意的是,指针的相关操作与数据类型是密切相关的,比如指针的++操作,增加的是sizeof(数据类型)
较复杂函数的参数表示、实现逻辑与调用关系梳理
在这次lab中指的就是vprintfmt相关函数
-
首先要搞明白每个参数的意义
完成Exercise 1.4花费了我不少时间,因为一开始我对各函数参数的意义完全是一头雾水,就比如vprintfmt参数列表中的data是什么?ap这一可变参数又是哪些参数?
指导书上没有直接的答案,我想课程组正是在锻炼我们在现有资料基础上自主理解代码的能力。
(后续经验总结见“实验体会”部分)
-
然后要明确特定函数的实现逻辑
其实在本次实验中,我认为这不是一个难点,因为指导书中将vprintfmt相关函数的具体实现逻辑(流程)解释得很清楚了,而且代码填空部分的实现步骤也划分得很细,每一步还有详细的注释。
但以后的实验,或者未来我们自己实际开发项目时,很难再有这种“手把手”的指导。我们需要自己学着像指导书一样梳理清楚实现逻辑,像代码填空一样把逻辑拆解为一步步可实现的步骤。
-
还需要搞清楚多模块/函数之间的调用关系
Exercise 1.4 相关函数调用关系
实验体会
实验代码阅读与把握
-
整体结构把握
-
把握好顶层架构
为什么这样划分文件结构?每部分文件的作用是什么?它们怎么协同?弄明白了这些问题,才能更顺利地编码
截止lab1的文件架构梳理
-
用好外部变量
很多时候要用的量和想实现的功能在相应的文件中都有宏定义或者函数定义。所以先阅读文件,找不到再自己写
-
-
具体参数意义理解
-
通过检索的方式在指导书、讲解PPT等多处资源中广泛找思路
-
比如
vprintfmt
参数列表中的data
是什么?指导书中没有说明,但讲解PPT中讲的很清楚。不要纠结与单一资料,学会广泛检索。
-
-
通过阅读并理解更大范围(比如完整函数、上层调用函数)代码段,明白本质,进而把握单个参数意义
-
比如
vprintfmt
参数列表中ap
这一可变参数指的是哪些参数?vprintfmt
函数实现的是格式化输出的主体逻辑,被printk
调用,参数列表中的ap参数也是从上层函数中得来。结合printk
的用法,比如printk("%d%c%ld", a, b, c)
,很容易明白可变参数就是需要输出的一系列变量,而fmt
就是引号中的字符串。
-
-