BUAA-OS lab1实验报告,关于elf,mos系统内核结构,系统启动流程,和printf函数的实现。
lab1_log
一个关于ssh权限的小插曲
- 我按照OS的教程配置本地连接实验的跳板机,但是在配置好ssh的config之后到ssh连接这一步出现了问题,报错
bad ownership
。 - 在同学助教的帮助下发现.ssh文件夹的权限开大了反而有问题,应该开小一点,打开属性,点击安全,把权限删到只剩下用户就行。
思考题
Thinking1.1
Thinking 1.1 请阅读附录中的编译链接详解,尝试分别使用实验环境中的原生 x86 工具链(gcc、ld、readelf、objdump 等)和 MIPS 交叉编译工具链(带有 mips-linux-gnu-前缀),重复其中的编译和解析过程,观察相应的结果,并解释其中向 objdump 传入的参数的含义。
- 我们知道C语言从源代码到可执行文件需要四个步骤,预处理 -> 编译 -> 汇编 -> 链接,分别通过命令
gcc -E/S/c/I
来完成。我的复现过程如下: - 通过
man objdump
我们知道,其中objdump传递的-DS(disassemble source)的意义是将后面传入的所有文件的section反汇编并输出结果(而且大写的S的意义是将反汇编的代码与原有机器码一同显示),但是我们将其重定向至另一个文件中,结果如下(只放出了main函数):
1 | # 链接前: |
- 虽然我们生成的程序是x86的,但是我们能够明显的看出来,我们最后比较未链接的反汇编文件与完整编译后的反汇编文件中,原本出现printf函数的位置,地址从0变成了一段有意义的地址。
- 当然,我们也可以通过readelf指令来看两个可执行文件之间的节头表的差异这里就先不再展示了。
Thinking1.2
Thinking 1.2 思考下述问题:
• 尝试使用我们编写的 readelf 程序,解析之前在 target 目录下生成的内核 ELF 文件。
• 也许你会发现我们编写的 readelf 程序是不能解析 readelf 文件本身的,而我们刚才介绍的系统工具 readelf 则可以解析,这是为什么呢?(提示:尝试使用 readelf-h,并阅读 tools/readelf 目录下的 Makefile,观察 readelf 与 hello 的不同)
- 我们自己实现的readelf函数的作用是输出ELF文件中所有节头的地址,我用它来解析target目录下的内核,产生的结果如下:
- readelf目录下的Makefile中,它们俩分别长这样: (hello文件被编译成了32位
- 而我的尝试确实也表明readelf无法解析本身但是readelf命令可以:
- 我们也用readelf命令来解析第一问中我们可以用自己的readelf解析的内核文件,发现结果如下:
- 比较两个的结果,除地址上的不同外,只有系统架构不一致,所以我认为原因是:是我们自己实现的readelf不能解析x86-64架构的文件。
Thinking1.3
Thinking 1.3 在理论课上我们了解到,MIPS 体系结构上电时,启动入口地址为 0xBFC00000(其实启动入口地址是根据具体型号而定的,由硬件逻辑确定,也有可能不是这个地址,但一定是一个确定的地址),但实验操作系统的内核入口并没有放在上电启动地址,而是按照内存布局图放置。思考为什么这样放置内核还能保证内核入口被正确跳转到?
(提示:思考实验中启动过程的两阶段分别由谁执行。)
- 系统启动分为两个阶段,分别在ROM或FLASH上和RAM上执行,然后在ROM上的系统引导程序Bootloader会将Stage2的代码复制到RAM上,并跳转至入口函数(也就是内核入口),所以只要Bootloader跳转正确,内核放置在哪无所谓。
难点分析
Exercise 1.1
- 主要是要补全readelf的c程序,它的功能是输出ELF文件中所有节头所在的地址,所以做这一题的核心是理解节头表的结构原理,以及看懂elf.h中结构体的意思。
- 我们从ehdr这个结构体的代码中可以知道,节头表的偏移、节头项的数目、每个节头项的大小。
- 所以我们只需要获得节头表的首地址,然后在它的基础上累加,对于得到的每一项,需要注意的是我们仅仅只得到了表项,也就是一个Elf32_Shdr的示例,我们需要访问结构体的sh_addr属性获得最终的地址。即:
1 | *(sh_table + i*sh_entry_size).sh_addr |
Exercise 1.2
- 第二道题的难点主要在于理解内核的节的结构,并通过Linker Script建立起链接。
- 剩下的比较的简单,按照题中给的示例及include/mmu.h 中的内存布局图做就行
Exercise 1.3
- .S文件代表汇编文件,所以需要用汇编写出。
- 只需要填写两行代码:
- 首先建立起栈,将sp移动到初始栈顶的位置,看内存布局图我们知道,在0x8040_0000,所以可以用
lui
指令赋值。 - 然后就是跳转,由于跳转之后不用再回退,所以用
j
指令即可。
Exercise 1.4
- 然后就到了最具难度的一题:实现一个printk函数,最主要的是看懂指导书中关于几个关联的库的关系,还是比较清晰的。
- 但是关于变长参数这一块,我本来还有点看不太懂(可能是我的问题),这里需要重点辨析的是传入的fmt指针,指向的是栈的最后一个参数(栈顶地址),由于压栈是从右向左压,所以最后一个参数就是形式化字符串,而ap的意义就是后面的参数表,只是需要初始化,以及通过
va_arg(va_list ap, 类型)
去访问。 - 另外最重要的是指导书并没有详细给出我们需要在print.c中做的工作,所以一定要先看懂源代码再开始补全!!!(这可能也是指导书信息不对称的考虑吧,因为看懂源码的能力还是挺重要的,移植或补全别的东西可不会有指导书教你)
实验体会
- 本次实验核心内容就是通过elf了解内核的结构以及它的形成,掌握思考系统启动的流程,完成一个printk的函数。
- 这是MOS操作系统运行起来的基础,大体上做到了与理论课程的内容相匹配,收获颇多。
- 另外感慨的就是读懂源码的能力了,这个上面已经感慨过了,总之就是十分的重要(read the F**king source code!)