# lab-1 实验总结
# 思考题
# Thinking 1.1
请查阅并给出前述 objdump
中使用的参数的含义。使用其它体系结构的编译器(如课程平台的 MIPS 交叉编译器)重复上述各步编译过程,观察并在 实验报告中提交相应的结果。
答案
下面的结果摘自 objdump --help
-D, --disassemble-all Display assembler contents of all sections | |
--disassemble=<sym> Display assembler contents from <sym> | |
-S, --source Intermix source code with disassembly | |
--source-comment[=<txt>] Prefix lines of source code with <txt> |
从中可以看出, -D
代表反汇编, -S
代表把源代码和反汇编代码一同显示出来
编写一个简单的 C 程序:
int main() { | |
int a = 1; | |
int b = 2; | |
int c = (a+b)*(a-b); | |
return 0; | |
} |
执行 mips_4KC-gcc -E simpleC.c
结果如下
如果加入 -c
选项,使之编译出 .o
文件,反汇编结果摘录如下
如果直接执行 mips_4KC-gcc simpleC.c -o simpleC
则会报错,看起来好像是链接过程出现问题
如果这样编译,会出现警告,但可以出结果,原因未知
然后反汇编结果如下
# Thinking 1.2
也许你会发现我们的 readelf
程序是不能解析之前生成的内核文件 (内 核文件是可执行文件) 的,而我们刚才介绍的工具 readelf
则可以解析,这是为什么呢?(提示:尝试使用 readelf -h
,观察不同)
答案
对编译得到的 vmlinux
执行 readelf
得到下图
对 testELF
执行 readelf
得到下图
发现不同之处在于一个为 big endian,另一个为 little endian,故我们写的 readelf.c
仅支持小端,所以不能解析目标文件
# Thinking 1.3
在理论课上我们了解到,MIPS 体系结构上电时,启动入口地址为 0xBFC00000(其实启动入口地址是根据具体型号而定的,由硬件逻辑确定,也有可能不是这个地址,但一定是一个确定的地址),但实验操作系统的内核入口并没有放在上电启动地址,而是按照内存布局图放置。思考为什么这样放置内核还能保证内核入口被正确跳转到?
答案
启动的第一阶段是初始化硬件设备,在 ROM 中由 bootloader 执行,第二阶段在 RAM 中,初始化该阶段硬件设备,读取并载入内核,执行 grub 这样的引导程序,因此启动入口地址未必是内核入口地址,而 grub 的存在会把内核载入内存并跳转,保证内核入口被正确跳转到
# Thinking 1.4
与内核相比,普通进程的 sg_size
和 bin_size
的区别在于它的开始加载位置并非页对齐,同时 bin_size
的结束位置( va+i
,其中 i
为计算出的该段在 ELF 文件中的大小)也并非页对齐,最终整个段加载完毕的 sg_size
末尾的位置也并非页对齐。请思考,为了保证页面不冲突(不重复为同一地址申请多个页,以及页上数据尽可能减少冲突),这样一个程序段应该怎样加载内存空间中。
彻底并透彻地理解上图能帮助大家在后续实验中节约很多时间
va(加载起始地址) va+i
| | |
|_ _ _|___|___BY2PG___|___BY2PG___|___BY2PG__|____|____|___BY2PG___|___ |_ _ _|
offset| | |
|<--- .text & .data --->|<--- .bss --->|
|<--- bin_size --->| |
|<--- sg_size --->|
答案
在加载程序时,避免发生冲突页面现象。首先,不同程序段的占用空间不能够有重合,然后,尽量避免一个页面同时被多个程序段所占用。即若前面的程序段末地址所占用的页面地址为,则后续的程序段首地址应从下一页面 开始占用。
# Thinking 1.5
内核入口在什么地方?main 函数在什么地方?我们是怎么让内核进入到想要的 main 函数的呢?又是怎么进行跨文件调用函数的呢?
答案
内核入口是 _start
函数在 boot/start.S
, _start
函数地址在 0x80010000
; main
函数在 init/main.c
, main
函数在 0x80010050
处。
内核启动先执行 _start
入口函数,然后从这个函数设置堆栈后直接跳转到 main
函数,从 start.S
文件中 jal main
命令可以看出。这样内核启动的入口地址就可以固定下来,只需要传递 main 函数的地址就可以实现不同位置的 main 函数的调用。
跨文件调用函数:通过 include 头文件的方式。将需要函数写入.h 文件,然后其他文件需要使用这些函数时 include 相应头文件即可,每个函数会有一个固定的地址,调用过程为将需要存储的值进行进栈等保护,再用 jal 跳转到相应函数的地址。
# Thinking 1.6
查阅《See MIPS Run Linux》一书相关章节,解释 boot/start.S
中下面几行对 CP0
协处理器寄存器进行读写的意义。具体而言,它们分别读 / 写了哪些寄存器的哪些特定位,从而达到什么目的?
/* Disable interrupts */
mtc0 zero, CP0_STATUS
......
/* disable kernel mode cache */
mfc0 t0, CP0_CONFIG
and t0, ~0x7
ori t0, 0x2
mtc0 t0, CP0_CONFIG
答案
将宏定义进行转换后如下
/* Disable interrupts */
mtc0 $0, $12 # 将sr寄存器清零
......
/* disable kernel mode cache */
mfc0 $t0, $16
and $t0, ~0x7
ori $t0, 0x2
mtc0 $t0, $16 #将CP0_CONFIG寄存器的0号位和2号位置0,将1号位置1
目的:
- 设置 SR 寄存器来使 CPU 进入工作状态,而硬件一般是复位后使许多寄存器的位为未定义行为;
CONFIG
寄存器的后三位为可写为,用来决定固定的kseg0
区是否经过高速缓存和其确切行为如何
# 实验难点图示
# 操作系统启动流程
- 启动流程理论课上分 MIPS 和 x86 两种,讲的非常复杂
- 结合代码就清楚了很多
# ELF 文件结构
- 感觉刚开始上来填写
readelf
时,还不是很清楚想让我们干什么 - 所以根据理论课课件里面的图片去理解代码就清晰了很多
# 体会与感想
- 本实验相比于 lab-0 来说,难度略有上升,但总体来说仍然偏简单。更多的是训练我们阅读指导书,阅读代码的能力
- 需要填充的代码信息大多可在本目录或其他目录的文件中找到
- 总体而言,本实验的内容仍然处于一个较浅的层面
- 但是,操作系统实验是逐层深入的,本次实验会为我们之后较难的 lab 打下良好基础
- 这次实验中遇到的困难主要是
printf
函数调试时出现了问题,负号的输出忘记判断 - 因此需要练习在没有 IDE 条件下利用
gdb
的调试能力 - (虽然但是,我自己在 WSL 上装了交叉编译器和 Clion,可以有图形化界面调试:))