思考题

Thinking 1.1

请阅读 附录中的编译链接详解,尝试分别使用实验环境中的原生 x86 工具链(gccldreadelfobjdump 等)和 MIPS 交叉编译工具链(带有mips-linux-gnu前缀),重复其中的编译和解析过程,观察相应的结果,并解释其中向 objdump 传入的参数的含义。

答:

通过阅读附录A.3,我们可以看到一个简单的printf("Hello World!")是怎么一步步预处理,编译,链接并最终变成可执行文件的。因为我们想找到printf到底是在哪里被插入到程序中的,所以我们每次只用gcc执行一次操作,同时使用objump进行反汇编。通过找到汇编语言中callq位置并观察其调用函数的地址来判断该阶段printf是否插入程序中。

为了使机器码转换回汇编代码,我们使用objdump -参数 要反汇编的目标文件名 > 导出文本文件名命令进行反汇编,那我们先来介绍一下objump相关参数的含义:

  • -S :用于在反汇编的输出中插入源代码。

  • -D :用于在每条指令前面添加对应的机器码。通常 -D 参数会结合 -S 参数一起使用,以便在反汇编内容中同时显示源代码,机器码和汇编语言。

通过进行和附录相同的操作,可以看出在编译阶段,调用printf函数的地址还是0,而在链接之后,地址变化,所以,printf实际上是在链接时插入到程序中。

所以,对于拥有多个 c 文件的工程来说,编译器会首先将所有的 c 文件以文件为单位,编译成.o 文件。最后再将所有的.o 文件以及函数库链接在一起,形成最终的可执行文件,整个过程如下图所示:

image-20240324173448949

Thinking 1.2

思考下述问题:

  • 尝试使用我们编写的 readelf 程序,解析之前在 target 目录下生成的内核 ELF 文件。
  • 也许你会发现我们编写的 readelf 程序是不能解析 readelf 文件本身的,而我们刚才介绍的系统工具 readelf 则可以解析,这是为什么呢?(提示:尝试使用 readelf -h,并阅读 tools/readelf 目录下的 Makefile,观察 readelfhello 的不同)

答:通过执行命令./tools/readelf/readelf ./target/mos,我们可以解析mos文件如下图:

image-20240324180945778

​ 同时readelf 是一个用于查看 ELF 文件信息的工具,但它本身也是一个 ELF 格式的可执行文件。通常情况下,我们自己编写的 readelf 程序是不能解析自身的,这是因为在编写 readelf 程序时,我们使用的是特定的编译器和链接器工具链,生成的可执行文件可能包含一些特定的调试信息或符号表等,这些信息可能超出了我们编写的 readelf 程序所能解析的范围。

​ 而系统提供的 readelf 工具则可以解析自身,这是因为系统提供的 readelf 可执行文件是经过专门的编译、链接和调试处理的,其中包含了更多的调试信息和符号表等,使得它能够解析自身的 ELF 头部以及其他信息。

​ 通过阅读 tools/readelf 目录下的 Makefile,我发现,target hello下有-static,而readelf没有。

image-20240324181111480

Thinking 1.3

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

答:MIPS 系统加电起动后, MIPS 处理器默认的程序入口是 0xBFC00000 ,此地址在无缓存的 KSEG1 的地址区域内,对应的物理地址是 0x1FC00000 ,即 CPU0x1FC00000 开始取第一条指令,这个地址在硬件上已经确定为 FLASH 的位置,然后 Bootloader 执行功能,加载内核到内存。关于内核 ELF 文件的入口地址 (Entry point) ,即 bootloader 移动完内核后,直接跳转到的地址,由 ld 写入 ELF 的头中

难点分析

就我个人而言,我觉得lab1的主要难点是对指导书和相关代码的阅读和理解,下面将分别从四个exercise来分析

Exercise 1.1 : elf

在此之前我们知道了ELF文件结构以及了解了ELF头,节头表和段头表的数据结构,我们的任务是补全代码输出ELF 文件中所有节头中的地址信息。

我觉得本exercise的难点是清楚了解ELF的文件结构,如下图所示

image-20240325102203773

简单来说,ELF头中包含了段头表和节头表的信息,段头表和节头表分别含有各段和各节的信息。了解结构之后,只需要根据elf.h文件中的数据结构来补全代码即可。

Exercise 1.2 : Linker Script

在了解完ELF之后,我们开始学习内核的相关知识

首先,我要需要知道内核应该被放在哪里。在 本实验的MIPS 体系结构中,4GB虚拟地址空间会被划分为 4 个大区域,如下图所示:

image-20240325102830042

我们的内核便被放置在kseg0中(具体位置可在include/mmu.h 中的内存布局图中查看),于是,我们就要通过Linker Script来将内核加载到正确的位置。在Linker Script我们使用.这一特殊符号,用来做定位计数器,根据输出节的大小增长,从而将内核.text.data.bss,这三个节的信息加载到正确位置。

在本exercise的难点我觉得是了解MIPS内存布局(一开始看到mmu时属实一脸懵),知道内核的位置。

Exercise 1.3 : _start

第三个exercise还算比较简单,就是首先将 sp 寄存器设置到内核栈空 间的位置上,随后跳转到 mips_init 函数

我觉得需要注意的是,因为 sp 是低地址增长的,所以其栈底地址就在KSTACKTOP,即在内核的栈在0x8040 0000以下的位置

Exercise 1.4 : printk

在第四个exercise中,我们需要实现一个printk函数,用来输出,调试(以及评测的方式)

本exercise的难点我觉得有两方面:

  • 变长参数表:在初次接触到变长参数时,我一开始也是看的一头雾水

    根据指导书内容,在stdarg.h 头文件中为处理变长参数表定义了一组宏和变量类型如下:

    1. va_list:变长参数表;
    2. va_start(va_list ap, lastarg):用于初始化变长参数表的宏;
    3. va_arg(va_list ap, 类型):用于取变长参数表下一个参数的宏;
    4. va_end(va_list ap):结束使用变长参数表的宏。

    具体应用如下:

    void example(const int *fmt, ...) {   
    va_list ap; //声明变长参数表ap
    va_start(ap, fmt); //初始化ap
    int num1 = va_arg(ap, int); // 从ap中取出一个int型变量
    int num2 = va_arg(ap, int); // 取出下一个int型变量
    va_end(ap); //结束变长参数表ap的使用
    }
  • 代码阅读:本实验中填写代码之前,还需要阅读大量相关代码。而且代码中使用大量指针、宏定义以及文件之间的相互调用,在初次阅读时还是有不小的难度。

    主要的输出逻辑是这样,在printk()函数中通过vprintfmt(outputk, NULL, fmt, ap);调用vprintfmt,接下来在vprintfmt函数中对输出形式进行判断,最后通过传入的outputk调用printchar在最底层以向一个地址写出字符数值的形式输出字符

实验体会

本次实验也是第一次接触到了操作系统的内部架构,对我来说还是有不小的难度。第一次阅读指导书时也是真的很懵,代码无从下手。不过随着反复阅读以及参考学长博客,最终还是顺利完成了exercise任务并基本了解内核启动相关知识。

以后实验指导书和代码的阅读难度肯定会更大,希望也可以一点点理解,一点点深入操作系统的学习。