Thingking 1.3

Q: Thinking 1.1 在阅读 附录中的编译链接详解 以及本章内容后,尝试分别使用实验环境中 的原生 x86 工具链(gcc、ld、readelf、objdump 等)和 MIPS 交叉编译工具链(带有 mips-linux-gnu- 前缀,如 mips-linux-gnu-gcc、mips-linux-gnu-ld),重复其中的编译和解析过程,观察相应的结果,并解释其中向objdump传入的参数的含义。

A: 我们对于示例代码test_hello.c进行测试

1
2
3
4
5
6
#include <stdio.h>

int main() {
printf("Hello, World!\n");
return 0;
}
  • 使用原生工具链

    1
    2
    3
    gcc -o test_hello test_hello.c
    readelf -h test_hello
    objdump -d test_hello

    利用readelf查看文件头信息得到:

    xxxx

    反汇编得到

  • MIPS 交叉编译工具链

    1
    2
    3
    mips-linux-gnu-gcc -o test_mips test_hello.c
    mips-linux-gnu-readelf -h test_mips
    mips-linux-gnu-objdump -d test_mips

    image-20250328221617592

    image-20250328221207291

  • objdump参数用法

    • -d:对目标文件中的可执行段进行反汇编,将机器码转换为汇编代码,方便开发者查看程序的指令序列
    • -D:对目标文件的所有段进行反汇编,包括不可执行段,如数据段等。
    • -h:显示目标文件的各个段的头部信息,包括段的名称、大小、偏移等。
    • -S:将源代码和反汇编代码混合显示,方便开发者对照查看源代码和对应的汇编代码。
    • -t:显示目标文件的符号表信息,包括符号的名称、地址、类型等。

Thinking 1.2

Q 尝试使用我们编写的readelf程序,解析之前在target目录下生成的内核ELF文件。and 编写的readelf程序不可以解析readelf文件本身,为什么系统工具可以解析readelf ?

A 自行编写的readelf文件解析mos得到

1
2
3
4
5
6
7
8
0:0x0
1:0x80400000
2:0x804016f0
3:0x80401708
4:0x80401720
...
16:0x0
17:0x0

利用readelf -h helloreadelf -h readelf指令,我们得到ELF头文件中显示的类型,可知hello是32位,而readelf是64位的,而我们编写的readelf.c文件是针对32位文件编写,无法解析64位的readelf文件(如不能用typedef uint32_t Elf32_Off; 的数据类型取做64位的运算)

Thinking 1.3

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

A实验中使用的QEMU模拟器已经提供了bootloader的引导(启动)功能,并且支持直接加载ELF格式的内核。操作系统启动过程被简化为加载内核到内存(kernel.lds控制),之后跳转到内核入口(kernel.lds通过ENTRY(_start)设置程序入口为_start函数,链接后程序从_start函数开始执行)。

难点分析

此次Lab1实验的重难点主要有以下两个方面:

  • ELF文件的解析
  • **printk**函数的实现

ELF文件的解析

ELF 是一种用于可执行文件(executable)、目标文件(relocatable)和库(shared object)的文件格式,而我们编译出的MOS内核文件本质上也是一个ELF文件

ELF的主要结构如下:

image-20250328204826046

从图中可以得到,ELF文件主要由以下几部分构成:

  • ELF头:记录程序的基本信息——体系结构与操作系统,同时包含了节头表与段头表相关信息
  • 段头表:包含程序各个段的信息,该信息需要在运行和装载时使用
  • 节头表:包含程序各个节的信息,该信息需要在程序编译和链接时间使用
  • 段头表表项与节头表表项

我们需要学会解析ELF文件并获取相关信息,同时学习通过链接器等指导其中某些信息的生成与储存。以解析section为例,

文件头地址binary + 节头表偏移量e_shoff 得到节头表地址,同时从文件头得到节头表表项大小e_shentsize 和 节头表表项数e_shnumbinary + e_shoff + i * e_shentsize就是第i个节头表表项的地址。

printk实现逻辑

与实现输出功能相关的主要由三个文件

  • kern/machine.c : 其中主要实现了函数void printcharc(char ch) , 该函数的功能是向控制台输出字符ch(本质上是向一块特殊的内存中写入ch
  • kern/printk.c : 其中主要是实现了void printk(const char*fmt,...),该函数实际上是把回调函数void outputk(void *data, const char*buf, size_t len)与输出参数传递到了vprintfmt
  • lib/print.c: 其中实现了vprintfmt(fmt_callback_t out, void *data, const char *fmt, va_list ap)这个实现格式化输出的主体逻辑

回调函数:是一个只能通过函数指针调用的函数。将某函数的指针作为参数传递给另一个函数,在另一个函数中使用该函数指针,我们认为该函数是一个回调函数。在调用qsort函数时间传入的cmp实际上就是一个回调函数

实验体会

Lab1主要学习目标是掌握操作系统启动的基本流程、掌握ELF文件的结构和功能以及编写printk函数。本次实验所需要实际编写的代码并不多,可能就是数十行。学习时间主要花费在了阅读源代码上,由于各个文件的依赖文件众多,阅读起来确实稍显麻烦。此外,涉及操作系统内核的众多源文件中含有大量指针运用,使用不当容易出错。

此次实验难度不大,且通过printk函数的实战让我第一次认真去了解一个C语言库函数的基础实现,我觉得这非常有趣。

实验参考

与回调函数相关内容我主要参考了一下链接https://cookedbear.top/p/44391.html