OS_Lab1_实验报告
Lab1_实验报告
一、思考题
Thinking 1.1
- 尝试分别使用实验环境中的原生 x86 工具链和 MIPS 交叉编译工具链,重复其中的编译和解析过程,观察相应的结果
(1)原生 x86 工具链
1.只进行预处理gcc -E hello.c
,这 里一阶段并没有 printf 这一函数的定义
1 | /* 保留部分输出 */ |
2.只编译而不链接 gcc -c hello.c
并进行反汇编 objdump -DS hello.o
,printf 的具体实现依然不在我们的程序中
1 | 0000000000000000 <main>: |
3.正常编译gcc hello.c -o hello
并进行反汇编 objdump -DS hello
,call
后填入地址,被标记为 puts@plt
的位置
1 | 0000000000001149 <main>: |
(2)MIPS 交叉编译工具链
1.mips-linux-gnu-gcc -E hello.c
结果同上
2.mips-linux-gnu-gcc -c hello.c
并mips-linux-gnu-objdump -DS hello.o
1 | 00000000 <main>: |
3. mips-linux-gnu-gcc hello.c -o hello
并 mips-linux-gnu-objdump -DS hello
1 | 004006e0 <main>: |
2和3的主要区别在于gp、v0、t9
寄存器的值
在链接阶段,全局数据的布局可能会发生变化。因此,在链接时,gp
寄存器的值可能会被重新计算
此时v0
的值改变主要是为了改变t9
的值,也就是跳转的地址,在jalr t9
步骤完成printf
的调用
- 解释其中向 objdump 传入的参数 的含义
1 | -D, --disassemble-all Display assembler contents of all sections |
所以objdump -DS ......
中传入参数含义是显示所有section的反汇编,并反汇编出源代码,将源代码与汇编代码混合显示
Thinking 1.2
尝试使用我们编写的 readelf 程序,解析之前在 target 目录下生成的内核 ELF 文 件
也许你会发现我们编写的 readelf 程序是不能解析 readelf 文件本身的,而我们刚 才介绍的系统工具 readelf 则可以解析,这是为什么呢?(提示:尝试使用 readelf -h,并阅读 tools/readelf 目录下的 Makefile,观察 readelf 与 hello 的不同)
使用
readelf-h
来 显示 ELF 文件头,发现hello
的类别是ELF32
,readelf
的类别是ELF64
阅读 tools/readelf
目录下的Makefile
, 使用了 -m32
参数来生成 32 位的hello
可执行文件
我们编写的readelf程序使用的数据类型均为32位,只能解析32位的elf文件。
而我们编写的readelf文件是64位,不能解析;而hello是32位,可以解析。
Thinking 1.3
**在理论课上我们了解到,MIPS 体系结构上电时,启动入口地址为 0xBFC00000 (其实启动入口地址是根据具体型号而定的,由硬件逻辑确定,也有可能不是这个地址,但 一定是一个确定的地址),但实验操作系统的内核入口并没有放在上电启动地址,而是按照 内存布局图放置。思考为什么这样放置内核还能保证内核入口被正确跳转到? (提示:思考实验中启动过程的两阶段分别由谁执行。) **
本实验使用的QEMU 模拟器支持直接加载 ELF 格式的内核,提供了 bootloader 的引导(启动)功能,故启动流程被简化为加载内核到内存,跳转到内核的入口。
QEMU 模拟器通过
kernel.lds
控制各节被加载到的位置,使得顶层Makefile生成的内核加载到内存指定位置kernel.lds
中通过ENTRY(_start)
来设置程序入口为_start
函数(该符号通过EXPORT(_start)
在init\start.S
被导出),链接后的程序从_start
函数开始执行内核自己通过
init\start.S
初始化sp,跳转到mips_init
1
2li sp, 0x80400000
j mips_init
二、难点分析
1.C语言指针使用
结构体指针的使用
1
2
3
4
5
6
7typedef struct {
Elf32_Off e_shnum;
...
} Elf32_Ehdr;
Elf32_Ehdr *ehdr = ....
Elf32_Off sh_entry_count = (*ehdr).e_shnum;//表示方法1
Elf32_Off sh_entry_count = ehdr->e_shnum;//表示方法2void类型指针强制转换
(Elf32_Shdr *)p + 1
表示的地址与(void *)p + sizeof(Elf32_Shdr)
相同shdr =(Elf32_Shdr *)(sh_table + i*sh_entry_size)
要先在void*
下计算偏移再强制转换
2.C语言变长参数使用
va_list
,定义变长参数表的变量类型,代码中的ap
就是va_list
类型的va_start(va_list ap, lastarg)
,用于初始化变长参数表的宏va_arg(va_list ap, 类型)
,用于取变长参数表下一个参数的宏va_end(va_list ap)
,结束使用变长参数表的宏
1 | //从可变参数列表中获取一个 int 类型参数的地址,并将其存储在指针变量 ip 中 |
3.Makefile的深入学习
1 | lab ?= $(shell cat .mos-this-lab 2>/dev/null || echo 6) |
不同Lab不同测试样例
lab的值会影响$(user_modules)
1 | include include.mk |
顶层Makefile
,Make
之后会产生target/mos
内核文件,具体会发生下面的事:
$(targets)
->$(mos_elf)
$(mos_elf)
->$(modules)
进入
$(modules)
也就是lib init kern
三个目录下,make
分别将.c文件编译为.o文件$(mos_elf)
->$(target_dir)
生成
target_dir
$(LD) -o $(mos_elf) -N -T $(link_script) $(objects)
使用
kernel.lds
将$(objects)
(所以的.o文件)链接, 输出到$(mos_elf)
位置target/mos
4.printk()
include/stdarg.h
C标准库,用于支持可变参数的接收
include/print.h
1 |
|
kern/printk.c
1 |
|
kern/machine.c
1 |
|
lib/print.c
1 |
|
三、实验体会
Lab1实验的学习让我体验了一把无头苍蝇的感觉,即使有顶层Makefile
作为地图,我依然在不同文件的穿梭中狠狠迷路,繁杂的代码如何上手抽丝剥茧,我总结以下三点:
- 整体把握
如同我难点分析的第四点一样,我们应该对文件之间的依赖关系有一个整体的把握。通过仔细阅读Makefile
理解实验代码究竟是如何层层运作的,脑子里模拟一下怎么make run
(lab2
更难把握了!!!!!T_T)
- 刨根问底
这次顶层Makefile
简直为我打开了新世界的大门原来还可以这么写,看不懂的地方不能无视跳过得过且过,勤快点粘给chatgpt
辅助理解是个不错的选择。对于新知识比如可变参数列表,应该刨根问底追本溯源。因为懒惰我写课下的时候没有看cppreference
,在课上extra
涉及到它的“灵活使用“差点就寄了T_T
- 注重细节
我在课下的时候发现自己阴差阳错的通过了printk()
测试
1 | if(*fmt == '-'){ |
然后课上没注意,再次重演悲剧差点就寄
1 | if(ch == '-'){ |
由此可见注重细节不要复现习惯性错误的必要性