# 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 结果如下

image-20220328215605426

如果加入 -c 选项,使之编译出 .o 文件,反汇编结果摘录如下

image-20220328215707794

如果直接执行 mips_4KC-gcc simpleC.c -o simpleC 则会报错,看起来好像是链接过程出现问题

image-20220328215742161

如果这样编译,会出现警告,但可以出结果,原因未知

image-20220402210101935

然后反汇编结果如下

image-20220402210159650

# Thinking 1.2

也许你会发现我们的 readelf 程序是不能解析之前生成的内核文件 (内 核文件是可执行文件) 的,而我们刚才介绍的工具 readelf 则可以解析,这是为什么呢?(提示:尝试使用 readelf -h ,观察不同)

答案

对编译得到的 vmlinux 执行 readelf 得到下图

image-20220328220054766

testELF 执行 readelf 得到下图

image-20220328220148520

发现不同之处在于一个为 big endian,另一个为 little endian,故我们写的 readelf.c 仅支持小端,所以不能解析目标文件

# Thinking 1.3

在理论课上我们了解到,MIPS 体系结构上电时,启动入口地址为 0xBFC00000(其实启动入口地址是根据具体型号而定的,由硬件逻辑确定,也有可能不是这个地址,但一定是一个确定的地址),但实验操作系统的内核入口并没有放在上电启动地址,而是按照内存布局图放置。思考为什么这样放置内核还能保证内核入口被正确跳转到?

答案

启动的第一阶段是初始化硬件设备,在 ROM 中由 bootloader 执行,第二阶段在 RAM 中,初始化该阶段硬件设备,读取并载入内核,执行 grub 这样的引导程序,因此启动入口地址未必是内核入口地址,而 grub 的存在会把内核载入内存并跳转,保证内核入口被正确跳转到

# Thinking 1.4

与内核相比,普通进程的 sg_sizebin_size 的区别在于它的开始加载位置并非页对齐,同时 bin_size 的结束位置( va+i ,其中 i 为计算出的该段在 ELF 文件中的大小)也并非页对齐,最终整个段加载完毕的 sg_size 末尾的位置也并非页对齐。请思考,为了保证页面不冲突(不重复为同一地址申请多个页,以及页上数据尽可能减少冲突),这样一个程序段应该怎样加载内存空间中。

彻底并透彻地理解上图能帮助大家在后续实验中节约很多时间

            va(加载起始地址)                                           va+i
|                                            |                          |
|_ _ _|___|___BY2PG___|___BY2PG___|___BY2PG__|____|____|___BY2PG___|___ |_ _ _|
offset|                                      |                    |
      |<---            .text & .data           --->|<---    .bss    --->|
      |<---              bin_size              --->|                    |
      |<---                           sg_size                       --->|

答案

在加载程序时,避免发生冲突页面现象。首先,不同程序段的占用空间不能够有重合,然后,尽量避免一个页面同时被多个程序段所占用。即若前面的程序段末地址所占用的页面地址为viv_i,则后续的程序段首地址应从下一页面vi+1v_{i+1} 开始占用。

# Thinking 1.5

内核入口在什么地方?main 函数在什么地方?我们是怎么让内核进入到想要的 main 函数的呢?又是怎么进行跨文件调用函数的呢?

答案

内核入口是 _start 函数在 boot/start.S_start 函数地址在 0x80010000main 函数在 init/main.cmain 函数在 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

目的:

  1. 设置 SR 寄存器来使 CPU 进入工作状态,而硬件一般是复位后使许多寄存器的位为未定义行为;
  2. CONFIG 寄存器的后三位为可写为,用来决定固定的 kseg0 区是否经过高速缓存和其确切行为如何

# 实验难点图示

# 操作系统启动流程

boot

  • 启动流程理论课上分 MIPS 和 x86 两种,讲的非常复杂
  • 结合代码就清楚了很多

# ELF 文件结构

elf

  • 感觉刚开始上来填写 readelf 时,还不是很清楚想让我们干什么
  • 所以根据理论课课件里面的图片去理解代码就清晰了很多

# 体会与感想

  • 本实验相比于 lab-0 来说,难度略有上升,但总体来说仍然偏简单。更多的是训练我们阅读指导书,阅读代码的能力
  • 需要填充的代码信息大多可在本目录或其他目录的文件中找到
  • 总体而言,本实验的内容仍然处于一个较浅的层面
  • 但是,操作系统实验是逐层深入的,本次实验会为我们之后较难的 lab 打下良好基础
  • 这次实验中遇到的困难主要是 printf 函数调试时出现了问题,负号的输出忘记判断
  • 因此需要练习在没有 IDE 条件下利用 gdb 的调试能力
  • (虽然但是,我自己在 WSL 上装了交叉编译器和 Clion,可以有图形化界面调试:))