OS-Lab1

一.内核的物理位置

操作系统最重要的部分是操作系统内核,因为内核需要直接与硬件交互管理各个硬件,从而利用硬件的功能为用户进程提供服务。

启动操作系统,我们就需要将内核代码在计算机结构上运行起来,一个程序要能够运行,其代码必须能够被 CPU 直接访问,所以不能在磁盘上,因为 CPU 无法直接访问磁盘。

CPU可以直接从硬盘里调用数据,然而这样太慢了,而内存则比硬盘快得多,把用有的东西先放入内存里面,CPU调用起来就快得。

所以不可能将内核代码保存在内存中。所以直观上可以认识到:

(1) 磁盘不能直接访问

(2) 内存掉电易失,内核文件有可能放置的位置只能是 CPU 能够直接访问的非易失性存储器——ROM 或 FLASH 中。

将硬件初始化的相关工作从操作系统中抽出放在bootloader中实现,意味着通过这种方式实现了硬件启动和软件启动的分离。 因此需要存储在非易失性存储器中的硬件启动相关指令不需要很多,能够很容易地保存在ROM或FLASH中。

bootloader在硬件初始化完后,需要为软件启动(即操作系统内核的功能)做相应的准备, 比如需要将内核镜像文件从存放它的存储器(比如磁盘)中读到RAM中。既然bootloader需要将内核镜像文件加载到内存中, 那么它就能选择使用哪一个内核镜像进行加载,即实现多重开机的功能。使用bootloader后,我们就能够在一个硬件上运行多个操作系统了

二.Bootloader

而当内存被初始化,bootloader将后续代码载入到内存中后,位于内存中的代码便能完整地使用C语言的各类功能了。 所以说,内存中的代码拥有了一个正常的C环境。

在 stage 1 时,需要初始化硬件设备,包括watchdog timer、中断、时钟、内存等。需要注意的一个细节是,此时内存 RAM 尚未初始化完成, 因而 stage 1 直接运行在存放 bootloader 的存储设备上(比如FLASH)。由于当前阶段不能在内存 RAM 中运行,其自身运行会受诸多限制, 比如有些 flash 程序不可写,即使程序可写的 flash 也有存储空间限制。这就是为什么需要stage 2的原因。 stage 1除了初始化基本的硬件设备以外,会为加载stage 2准备RAM空间,然后将stage 2的代码复制到RAM空间,并且设置堆栈,最后跳转到stage 2的入口函数。

stage 2运行在RAM中,此时有足够的运行环境,所以可以用C语言来实现较为复杂的功能。 这一阶段的工作包括,初始化这一阶段需要使用的硬件设备以及其他功能,然后将内核镜像文件从存储器读到RAM中,并为内核设置启动参数, 最后将CPU指令寄存器的内容设置为内核入口函数的地址,即可将控制权从bootloader转交给操作系统内核。

gxemul支持加载elf格式内核,所以启动流程被简化为加载内核到内存,之后跳转到内核的入口。启动完毕

三.编译和链接

printf的实现是在链接(Link)这一步骤中被插入到最终的可执行文件中的。那么,了解这个细节究竟有什么用呢? 作为一个库函数,printf被大量的程序所使用。因此,每次都将其编译一遍实在太浪费时间了。printf的实现其实早就被编译成了二进制形式。

但此时,printf并未链接到程序中,它的状态与我们利用-c选项产生的hello.o相仿,都还处于未链接的状态。而在编译的最后,链接器(Linker)会将所有的目标文件链接在一起,将之前未填写的地址等信息填上,形成最终的可执行文件,这就是链接的过程。

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

链接器通过哪些信息来链接多个目标文件呢?答案就在于在目标文件(也就是我们通过-c选项生成的.o文件)。 在目标文件中,记录了代码各个段的具体信息。链接器通过这些信息来将目标文件链接到一起。而ELF(Executable and Linkable Format)正是Unix上常用的一种目标文件格式。 其实,不仅仅是目标文件,可执行文件也是使用ELF格式记录的。

四.va_list、va_start和va_end三个宏的用法。

1.c语言提供了函数的不定长参数使用,比如 void func(int a, …)。三个省略号,表示了不定长参数。

注意:c标准规定了,函数必须至少有一个明确定义的参数,因此,省略号前面必须有至少一个参数。

2.va_list宏定义了一个指针类型,这个指针类型指向参数列表中的参数。

3.void va_start(va_list ap, last_arg),修改了用va_list申明的指针,比如ap,使这个指针指向了不定长参数列表省略号前的参数。

4.type va_arg(va_list, type),获取参数列表的下一个参数,并以type的类型返回。

5.void va_end(va_list ap), 参数列表访问完以后,参数列表指针与其他指针一样,必须收回,否则出现野指针。一般va_start 和va_end配套使用。

5.函数的参数一般从右至左先后入栈,根据栈的特性,也就是,最左边的参数最先出栈。贴一段代码介绍下va_list、va_start和va_end的使用。

感兴趣的,可以把函数的整型换成char或者int,参数列表判断条件为NUL,还可以为每个参数指定类型。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <stdio.h>
#include <stdarg.h>
void functestarg(int, ...);
int main ()
{
functestarg(1,2,3,4,5,6,7,8,9,10,0);
return 0;
}
void functestarg(int a, ...)
{
va_list argpointer;
va_start(argpointer, a);
int argument;
int count = 0;
while(0 != (argument = va_arg(argpointer, int)))
{
printf("parameter%d:%d\n", ++count, argument);
}
}

五.ELF文件的结构

ELF文件的解析

需要输出 ELF ⽂件的所有 section header 的序号和地址信息
在这里插入图片描述

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
typedef struct {
unsigned char e_ident[EI_NIDENT]; /* Magic number and other info */
// 存放魔数以及其他信息
Elf32_Half e_type; /* Object file type */
// 文件类型
Elf32_Half e_machine; /* Architecture */
// 机器架构
Elf32_Word e_version; /* Object file version */
// 文件版本
Elf32_Addr e_entry; /* Entry point virtual address */
// 入口点的虚拟地址
Elf32_Off e_phoff; /* Program header table file offset */
// 程序头表所在处与此文件头的偏移
Elf32_Off e_shoff; /* Section header table file offset */
// 段头表所在处与此文件头的偏移
Elf32_Word e_flags; /* Processor-specific flags */
// 针对处理器的标记
Elf32_Half e_ehsize; /* ELF header size in bytes */
// ELF文件头的大小(单位为字节)
Elf32_Half e_phentsize; /* Program header table entry size */
// 程序头表入口大小
Elf32_Half e_phnum; /* Program header table entry count */
// 程序头表入口数
Elf32_Half e_shentsize; /* Section header table entry size */
// 段头表入口大小
Elf32_Half e_shnum; /* Section header table entry count */
// 段头表入口数
Elf32_Half e_shstrndx; /* Section header string table index */
// 段头字符串编号
} Elf32_Ehdr;

六.lp_Print()函数流程图

在这里插入图片描述


OS-Lab1
http://example.com/2022/07/17/OS-Lab1/
作者
Wei Xia
发布于
2022年7月17日
许可协议