操作系统实验报告

练习一

一、操作系统镜像文件ucore.img是如何一步一步生成的?(需要比较详细地解释Makefile中每一条相关命令和命令参数的含义,以及说明命令导致的结果)

首先我们在Makefile文档中找到ucore.img文件生成的代码段:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-DHSL6Ce1-1639279618160)(E:/Typore%E5%9B%BE%E7%89%87/image-20211116160117324.png)]

我们对该代码进行逐行分析:
dd:用指定大小的块拷贝一个文件,并在拷贝的同时进行指定的转换。

if=文件名:输入文件名,缺省为标准输入。即指定源文件。< if=input file >
of=文件名:输出文件名,缺省为标准输出。即指定目的文件。< of=output file >
count=blocks:仅拷贝blocks个块,块大小等于ibs指定的字节数。 conv=conversion:用指定的参数转换文件。 conv=notrunc:不截短输出文件.

上述代码实现了先创建一个大小为10000字节的内存块儿,然后再将bootblock和kernel拷贝过去。

然后我们在终端中执行make V=查看命令执行情况:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5MyOPlwr-1639279618162)(E:/Typore%E5%9B%BE%E7%89%87/image-20211116161226734.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-E4e8TTlW-1639279618162)(E:/Typore%E5%9B%BE%E7%89%87/image-20211116161241558.png)]

这里用到的一些指令:

cc来自于Unix的c语言编译器,是 c compiler 的缩写;这里cc指向gcc,因此在Linux下cc等价于gcc。

makefile中,命令行第一个字符为加号+,则执行命令时不受到make的-n -t -q三个参数的影响。

gcc -c a.c 编译成目标文件a.o
gcc a.c 生成执行文件a.exe
gcc -o a -c a.c 编译成目标文件a
gcc -o a a.c 生成执行文件a.exe
在a.c中引用test.c中的一个函数后:
gcc -c test.c 编译成目标文件test.o
gcc -c a.c 编译成目标文件a.o
gcc -o a test.o a.o 生成执行文件a.exe
gcc -o a test.o a.c 生成执行文件a.exe
gcc -o a test.c a.c 生成执行文件a.exe

-fno-builtin:允许定义函数的时候和C语言的内建函数重名。

-Wall:该选项能发现程序中一系列的常见错误警告。虽然GCC提供了非常丰富的警告,但前提是你已经启用了它们,否则它不会报告这些检测到的警告。

-ggdb:-ggdb产生的debug信息更倾向于给GDB使用的。如果用的GDB调试器,那么使用-ggdb选项,如果是其他调试器,则使用-g。

-m32:-m32 生成32位机器的汇编代码;-m64则生成64位机器汇编代码。

-I(大写i):指定头文件路径(相对路径或绝对路径,建议相对路径)。gcc/g++ 会先在当前目录查找你所制定的头文件,如果没有找到,他回到默认的头文件目录找,如果使用 -I 制定了目录,他会先在你所制定的目录查找, 然后再按常规的顺序去找。

-gstabs:-gstabs,以stabs格式声称调试信息,但是不包括gdb调试信息;-gstabs+,以stabs格式声称调试信息,并且包含仅供gdb使用的额外调试信息。

-nostdinc:不在标准系统文件夹寻找头文件,只在 -I 等参数指定的文件夹中搜索头文件。

-fno-stack-protector:禁用堆栈保护机制。

ld:ld 命令是二进制工具集 GNU Binutils 的一员,是 GNU 链接器,用于将目标文件与库链接为可执行文件或库文件。在上面的ld命令的作用就是分别生成了kernel和bootblock文件。

-m elf_i386:指定模拟仿真链接器为 elf_i386;-nostdlib:只搜索命令行上显式指定的库目录,在链接器脚本中指定的库目录(包括在命令行中指定的链接器脚本)将被忽略;-T tools/kernel.ld:指定链接器的脚本为 tools 目录下的 kernel.ld 文件;-o bin/kernel obj/kern/init/init.o:将转化好的可执行文件放到 bin/ 目录下,并命名为 kernel,链接的文件为 obj/kern/init/ 目录中的 init.o。

二、一个被系统认为是符合规范的硬盘主引导扇区的特征是什么?

查看lab1/tools/sign.c源代码:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8eFxATuI-1639279618162)(E:/Typore%E5%9B%BE%E7%89%87/image-20211116162003592.png)]

由31,32行代码看出、符合规范的硬盘主引导扇区的大小为512个字节,且最后两个字节为0x55、0xAA。

练习二

1、从CPU加电后执行的第一条指令开始,单步跟踪BIOS的执行。

查看lab1/tools/gdbinit内容如下:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-FpcsxxV4-1639279618163)(E:/Typore%E5%9B%BE%E7%89%87/image-20211116164106345.png)]

将其进行修改:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-PyQCodNH-1639279618163)(E:/Typore%E5%9B%BE%E7%89%87/image-20211116164300649.png)]

意思是与qemu建立连接,然后我们在lab1目录下进行调试:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1N9I2hUV-1639279618164)(E:/Typore%E5%9B%BE%E7%89%87/image-20211116164515772.png)]

si单步执行,x/2i $pc可以查看BIOS的代码

2 、在初始化位置0x7c00设置实地址断点,测试断点正常。

对gdbinit文件进行修改:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-GDInKBxO-1639279618164)(E:/Typore%E5%9B%BE%E7%89%87/image-20211116193613022.png)]

然后运行测试:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-DC0W8NfF-1639279618165)(E:/Typore%E5%9B%BE%E7%89%87/image-20211116193720453.png)]

由gdb打印出 the target architecture is assumed to be i386知验证成功。

3、从0x7c00开始跟踪代码运行,将单步跟踪反汇编得到的代码与bootasm.S和 bootblock.asm进行比较

单步跟踪反汇编代码如图:

si:执行下一步

x/i $pc : 将当前指令进行反汇编

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-I9iVOVBz-1639279618165)(E:/Typore%E5%9B%BE%E7%89%87/image-20211116195413319.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Jkif3GXy-1639279618165)(E:/Typore%E5%9B%BE%E7%89%87/image-20211116195454585.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-H7YcWstc-1639279618166)(E:/Typore%E5%9B%BE%E7%89%87/image-20211116195523298.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-UJDw7dKz-1639279618167)(E:/Typore%E5%9B%BE%E7%89%87/image-20211116195544977.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-LDRJ3vu2-1639279618168)(E:/Typore%E5%9B%BE%E7%89%87/image-20211116195925673.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-R6pup474-1639279618168)(E:/Typore%E5%9B%BE%E7%89%87/image-20211116195946560.png)]

bootblock.S 中的代码:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-E2RpVGDL-1639279618169)(E:/Typore%E5%9B%BE%E7%89%87/image-20211116200411260.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Ys7Tomnk-1639279618169)(E:/Typore%E5%9B%BE%E7%89%87/image-20211116200454737.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jonycH1c-1639279618170)(E:/Typore%E5%9B%BE%E7%89%87/image-20211116200506641.png)]

bootblock.asm:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-YHk1LT5x-1639279618171)(E:/Typore%E5%9B%BE%E7%89%87/image-20211116200538396.png)]

对比可以发现相应位置的代码一致。

练习三

1、为何要开启A20以及如何开启A20

boot.s是一个汇编文件,用于打开A20 gate,A20是一个地址线,打开A20后内存地址位数由20位变到32位,BIOS 将通过读取硬盘主引导扇区到内存,并转跳到对应内存中的位置执行 bootloader。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-W2NdQ8fp-1639279618171)(E:/Typore%E5%9B%BE%E7%89%87/image-20211119102150786.png)]

2、如何初始化GDT表

把gdt表的起始位置和界限装入GDTR寄存器

lgdt gdtdesc            #把gdt表的起始位置和界限装入GDTR寄存器 movl %cr0, %eax          orl $CR0_PE_ON, %eax    
movl %eax, %cr0         #把保护模式位开启

3、如何进入保护模式

通过长跳转指令进入了保护模式。

ljmp $PROT_MODE_CSEG, $protcseg 

实模式: cs:ip寻址模式 也就是cs乘以16(左移4位)+ip 最大寻址空间1M
保护模式: 保护模式与实模式相比,主要是两个差别:一是提供了段间的保护机 制,防止程序间胡乱访问地址带来的问题,二是访问的内存空间变大。

练习四

1、bootloader如何读取硬盘扇区的?

首先查看bootmain.c中的实现代码:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-vVFY6q4F-1639279618172)(E:/Typore%E5%9B%BE%E7%89%87/image-20211119102842014.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-t7CFs7IE-1639279618172)(E:/Typore%E5%9B%BE%E7%89%87/image-20211119102934502.png)]

代码分析

45行:readsect函数读取了一个512字节的扇区,两个参数是扇区号和写入地址

50-53行,将secno的32位写入设备ID寄存器,0~27位是偏移量,第28位是主副通道,剩下的3位被强置设为1.

54行:读取磁盘

57行:等待磁盘准备好

60行:读到dst地址, 因为这里是以DW(双字)为单位,所以要/4

68行:readseg函数从设备中读count个字节到va这个地址,offset是指想读的位置距离开始的偏移 static void

72行:计算当前偏移在那个扇区的位置,并且让va地址向前偏移这些字节,读完之后,用户开始传入的地址va的内容就是偏移所在内容。

2、bootloader是如何加载ELF格式的OS的?

代码实现及分析:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-nV4AMvD3-1639279618173)(E:/Typore%E5%9B%BE%E7%89%87/image-20211119104456322.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-YCQzi4ka-1639279618173)(E:/Typore%E5%9B%BE%E7%89%87/image-20211119104509925.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-nRxwhU3I-1639279618173)(E:/Typore%E5%9B%BE%E7%89%87/image-20211119104522212.png)]

练习五

1、实现函数调用堆栈跟踪函数

函数堆栈:

栈是一个很重要的编程概念,与编译器和编程语言有紧密的联系。理解调用栈最重要的两点是:栈的结构,EBP寄存器的作用。一个函数调用动作可分解为:零到多个PUSH指令(用于参数入栈),一个CALL指令。CALL指令内部其实还暗含了一个将返回地址(即CALL指令下一条指令的地址)压栈的动作(由硬件完成)。几乎所有本地编译器都会在每个函数体之前插入类似如下的汇编指令:

pushl	%ebp

movl	%esp	,	%ebp

这样在程序执行到一个函数的实际指令前,已经有以下数据顺序入栈:参数、返回地址、ebp寄存器。由此得到类似如下的栈结构(参数入栈顺序跟调用方式有关,这里以C语言默认的CDECL为例): 关于栈的生长方向,大多数编译器实现的都是向下生长。也就是栈底为高地址

+|	栈底方向	|	高位地址

|	...	|

|	...	|

|	参数3	|

|	参数2	|

|	参数1	|

|	返回地址	|

|	上一层[ebp]	|<--------	[ebp]

|	局部变量	|	低位地址

这两条汇编指令的含义是:首先将ebp寄存器入栈,然后将栈顶指针esp赋值给ebp。“mov ebp esp”这条指令表面上看是用esp覆盖ebp原来的值,其实不然。因为给ebp赋值之前,原ebp值已经被压栈(位于栈顶),而新的ebp又恰恰指向栈顶。此时ebp寄存器就已经处于一个非常重要的地位,该寄存器中存储着栈中的一个地址(原ebp入栈后的栈顶),从该地址为基准,向上(栈底方向)能获取返回地址、参数值,向下(栈顶方向)能获取函数局部变量值,而该地址处又存储着上一层函数调用时的ebp值。

一般而言,ss:[ebp+4]处为返回地址,ss:[ebp+8]处为第一个参数值(最后一个入栈的参数值,此处假设其占用4字节内存),ss:[ebp-4]处为第一个局部变量,ss:[ebp]处为上一层ebp值。由于ebp中的地址处总是“上一层函数调用时的ebp值”,而在每一层函数调用中,都能通过当时的ebp值“向上(栈底方向)”能获取返回地址、参数值,“向下(栈顶方向)”能获取函数局部变量值。如此形成递归,直至到达栈底。这就是函数调用栈。

代码实现:

void print_stackframe(void) {
	uint32_t t_ebp = read_ebp();
	uint32_t t_eip = read_eip();
	int i,j;
	for(i = 0;i< STACKFRAME_DEPTH && t_ebp!=0;i++)
	{
		cprintf("ebp=%08x,eip=%08x,args:",t_ebp,t_eip);
		uint32_t *args = (uint32_t *)t_ebp +2;
		for(j = 0;j<4;j++)
		{
			cprintf("0x%08x",args[j]);
		}
		cprintf("\n");
		print_debuginfo(t_eip-1);
		t_eip = ((uint32_t *)t_ebp)[1];
		t_ebp = ((uint32_t *)t_ebp)[0];
	}
}

验证:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kusYg5H0-1639279618174)(E:/Typore%E5%9B%BE%E7%89%87/image-20211119114323873.png)]

练习六

1、中断描述符表(也可简称为保护模式下的中断向量表)中一个表项占多少字节?其中哪几位代表中断处理代码的入口?

中断向量表一个表项占用8字节,其中2-3字节是段选择子,0-1字节和6-7字节拼成位移, 两者联合便是中断处理程序的入口地址。

2、请编程完善 kern/trap/trap.c 中对中断向量表进行初始化的函数 idt_init。
void
idt_init(void) {
  extern uintptr_t __vectors[];
     int i;
     //初始化idt
     for(i=0;i<256;i++)
     {
         SETGATE(idt[i],0,GD_KTEXT,__vectors[i],DPL_KERNEL);
     }
     SETGATE(idt[T_SWITCH_TOK],0,GD_KTEXT,__vectors[T_SWITCH_TOK],DPL_USER);
     SETGATE(idt[T_SWITCH_TOU],0,GD_KTEXT,__vectors[T_SWITCH_TOU],DPL_KERNEL);
     lidt(&idt_pd);
}

首先引入中断处理函数的入口地址__vectors[],这个变量在vector.s里面生成的,然后初始化idt中断描述符表,最后根据提示用lidt函数告知cpu IDT表的位置。

3、请编程完善trap.c中的中断处理函数trap
void
trap(struct trapframe *tf) {
    // dispatch based on what type of trap occurred
	

     ticks ++; //每一次时钟信号会使变量ticks加1
        if (ticks==TICK_NUM) {//TICK_NUM已经被预定义成了100,每到100便调用print_ticks()函数打印
            ticks=0;
            print_ticks();
        }
}

验证结果:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-f0jXzjnN-1639279618174)(E:/Typore%E5%9B%BE%E7%89%87/image-20211119151529733.png)]

实验总结

1、重要知识点:

qemu环境的搭建,对qemu命令的理解和掌握,对gdb调试要有一定的掌握,对汇编知识的掌握,对Intel 80386寄存器的理解,以及了解ucore编程方法和通用数据结构。实验整体做起来非常难,大多数知识点以前只是略有涉及,没有实际结果代码使用过,以及也有许多知识点没有学过,需要从网上搜索学习理解。

2、对应的OS知识点:

感觉在实验中使用最多的知识点就是地址空间和地址转换,无论是gdb调试还是汇编语言的书写和阅读,都离开地址空间和地址跳转,地址重定位。

3、需要拓展的知识点:

A20 Gate,即Intel早期的8086 CPU提供的20根地址线,我们需要了解它的每一位表示着什么。

熟悉Linux的各种指令也会对实验有很大的帮助。

Logo

为开发者提供学习成长、分享交流、生态实践、资源工具等服务,帮助开发者快速成长。

更多推荐