操作系统——实验lab1 **超详细**实验报告

lab1 操作系统启动过程

实验指导书:
https://chyyuu.gitbooks.io/ucore_os_docs/content/lab1/lab1_1_goals.html
实验中可能使用到的工具:
https://chyyuu.gitbooks.io/ucore_os_docs/content/lab0/lab0_ref_ucore-tools.html
ucore源码以及参考答案
https://github.com/chyyuu/ucore_os_lab
官方问答
https://chyyuu.gitbooks.io/os_course_qa/content/

练习1:理解通过make生成执行文件的过程。

在这里插入图片描述

question 1

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

这个练习只是要大家熟悉makefile以及make,不必完整的掌握makefile里面的源码,只需要知道那些命令是什么意思就够了,了解就好。这东西要你记也记不住,学习makefile和linux系统命令一样,不是一朝一夕久能学好的,需要时间的累积和经验的积累。

下面来进行分析
首先我们查看lab1下的Makefile文件
找到create ucrore.image 大概在169h在这里插入图片描述
看172行 用到了两个块分别叫做kernel和bootblock,我们先展示不管这两个块的内容是什么,假设我们已经有这两个块了,先直接分析下面的代码,先全局再局部分析嘛

dd是一条linux命令指令:
dd:用指定大小的块拷贝一个文件,并在拷贝的同时进行指定的转换。

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

这里的意思就是说先创建一个大小为10000字节的内存块儿,然后再将bootblock和kernel拷贝过去

然后我们再去看看kernel和bootblock的内容
在这里插入图片描述
在这里插入图片描述
这里用到了很多函数 就不一一截图了,太麻烦,这两个部分主要就是通过gcc编译一些源码,源码在你的lab1/boot和lab1/kern中,暂时我们不用去追究源码写的什么内容,后面的实验会慢慢分析的。

在终端中执行输入 make V= 会看到ucore.image 的编译内容
正是我们之前说的先创建一个大小为10000字节的内存块儿,然后再将bootblock和kernel拷贝过去
在这里插入图片描述
其中相关参数的含义为:

  • ggdb 生成可供gdb使用的调试信息

  • m32生成适用于32位环境的代码

  • gstabs 生成stabs格式的调试信息

  • nostdinc 不使用标准库

  • fno-stack-protector 不生成用于检测缓冲区溢出的代码

  • 0s 位减小代码长度进行优化

question 2

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

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

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

练习2 使用qemu执行并调试lab1中的软件。

在这里插入图片描述
在这里插入图片描述
这个实验只是让你练习一下gdb调试 跟着视频上做就好了,这个练习比较简单
1.首先修改 lab1/tools/gdbinit,内容为:


set architecture i8086

target remote :1234

意思是与qemu建立连接
2.在 lab1目录下,执行make debug

3.然后在gdb进行单步调试

4.gdb界面下,可通过如下命令来看BIOS的代码

x /2i $pc

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

在tools/gdbinit结尾加上


b *0x7c00 //在0x7c00处设置断点。此地址是bootloader入口点地址,可看boot/bootasm.S的start地址处

c //continue简称,表示继续执行

x /5i $pc //显示当前eip处的汇编指令

set architecture i386 //设置当前调试的CPU是80386

在lab1目录下,运行make debug,可发现gdb打印出 the target architecture is assumed to be i386。


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

  1. 在tools/gdbinit结尾加上

b *0x7c00

c

x /10i $pc

2.在0x7c00会进行break,然后使用si和x/i $pc 进行单步调试。


si指定 进行单步调试

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

0x00007c01 in ?? ()

(gdb) x/i $pc

=> 0x7c01: cld

(gdb) si

0x00007c02 in ?? ()

(gdb) x/i $pc

=> 0x7c02: xor %eax,%eax

(gdb) si

0x00007c04 in ?? ()

(gdb) x/i $pc

=> 0x7c04: mov %eax,%ds

(gdb)

bootblock.S 中的代码为:


.code16 # Assemble for 16-bit mode

cli # Disable interrupts

cld # String operations increment



# Set up the important data segment registers (DS, ES, SS).

xorw %ax, %ax # Segment number zero

movw %ax, %ds # -> Data Segment

movw %ax, %es # -> Extra Segment

movw %ax, %ss

bootblock.asm


start:

.code16

cli

# 7c00: fa cli

cld

# 7c01: fc cld



xorw %ax, %ax

# 7c02: 31 c0 xor %eax,%eax

movw %ax, %ds

# 7c04: 8e d8 mov %eax,%ds

movw %ax, %es

# 7c06: 8e c0 mov %eax,%es

movw %ax, %ss

# 7c08: 8e d0 mov %eax,%ss

3.对比发现,上述反汇编指令与两个汇编文件相同。

练习3 分析bootloader进入保护模式的过程。

在这里插入图片描述
这个练习涉及到的基础知识比较多,实验配套的视频一定要看 涉及到汇编基础,A20门和ps/2 Controller,这是我网上找的资料,为保护版权也为自己偷点懒,就直接粘贴链接了
https://blog.csdn.net/scnu20142005027/article/details/51147402
https://www.jianshu.com/p/0834d3dc7d1b

总结:
boot.s是一个汇编文件,用于打开A20 gate,A20是一个地址线,打开A20后内存地址位数由20位变到32位

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

练习4:分析bootloader加载ELF格式的OS的过程。

在这里插入图片描述
首先要看bootmain.c的源码
在这里插入图片描述
ELF就是一个可执行文件的意思
代码分析

  • 88L 读取第一个扇区
  • 91L 判断是否是一个有效的elf
  • 97L-103L 加载接下来的扇区
  • 106L 利用强转为函数指针调用函数

详细解读
https://blog.csdn.net/scnu20142005027/article/details/51150601

练习4.1 bootloader如何读取硬盘扇区的?

查看 readsect函数 源码
readsect函数是被readseg里面调用的一个函数 readseg太简单了一看就懂,就不仔细分析了
在这里插入图片描述

在这里插入图片描述
各地址代表的寄存器意义如下:

  • 0x1f0 读数据,当0x1f7不为忙状态时,可以读。
  • 0x1F3 R/W,数据寄存器
  • 0x1F2 R/W,扇区数寄存器,记录操作的扇区数
  • 0x1F3 R/W,扇区号寄存器,记录操作的起始扇区号
  • 0x1F4 R/W,柱面号寄存器,记录柱面号的低 8 位
  • 0x1F5 R/W,柱面号寄存器,记录柱面号的高 8 位
  • 0x1F6 R/W,驱动器/磁头寄存器,记录操作的磁头号,驱动器号,和寻道方式,前 4 位代表逻辑扇区号的高 4 位,DRV = 0/1 代表主/从驱动器,LBA = 0/1 代表 CHS/LBA 方式
  • 0x1F7 R,状态寄存器,第 6、7 位分别代表驱动器准备好,驱动器忙
  • 0x1F8 W,命令寄存器,0x20 命令代表读取扇区

代码分析

  • 48L 设置读取扇区的数目为1
  • 49L-52L
    在这4个字节线联合构成的32位参数中
    29-31位强制设为1
    28位(=0)表示访问"Disk 0"
    0-27位是28位的偏移量
  • 53L 0x20 命令 读取扇区
  • 59L 从0x1F0读取SECTSIZE字节数到dst的位置,每次读四个字节,读取 SECTSIZE/ 4次。

从outb()可以看出这里是用LBA模式的PIO(Program IO)方式来访问硬盘的。从磁盘IO地址和对应功能表可以看出,该函数一次只读取一个扇区。

练习4.2 bootloader是如何加载ELF格式的OS的?

也就是bootmain函数内容
看上文的bootmain中elfhdr、proghdr相关的信息。

下面是关于elfhdr和proghdr的相关信息


#elfhdr:目标文件头

struct elfhdr {

uint32_t e_magic; // must equal ELF_MAGIC

uint8_t e_elf[12]; // 12 字节,每字节对应意义如下:

// 0 : 1 = 32 位程序;2 = 64 位程序

// 1 : 数据编码方式,0 = 无效;1 = 小端模式;2 = 大端模式

// 2 : 只是版本,固定为 0x1

// 3 : 目标操作系统架构

// 4 : 目标操作系统版本

// 5 ~ 11 : 固定为 0



uint16_t e_type; // 1=relocatable, 2=executable, 3=shared object, 4=core image

uint16_t e_machine; // 3=x86, 4=68K, etc.

uint32_t e_version; // file version, always 1

uint32_t e_entry; // 程序入口地址 or 0

uint32_t e_phoff; // 程序段表头相对elfhdr偏移位置

uint32_t e_shoff; // 节头表相对elfhdr偏移量

uint32_t e_flags; // 处理器特定标志, usually 0

uint16_t e_ehsize; // size of this elf header

uint16_t e_phentsize; // 程序头部长度

uint16_t e_phnum; // 段个数

uint16_t e_shentsize; // 节头部长度

uint16_t e_shnum; // 节头部个数

uint16_t e_shstrndx; // 节头部字符索引

};



struct	proghdr	{

uint	type;	//	段类型

// 1 PT_LOAD : 可载入的段

// 2 PT_DYNAMIC : 动态链接信息

// 3 PT_INTERP :

// 指定要作为解释程序调用的以空字符结尾的路径名的位置和大小

// 4 PT_NOTE : 指定辅助信息的位置和大小

// 5 PT_SHLIB : 保留类型,但具有未指定的语义

// 6 PT_PHDR : 指定程序头表在文件及程序内存映像中的位置和大小

// 7 PT_TLS : 指定线程局部存储模板



uint	offset;	//	段相对文件头的偏移值

uint	va;	//	段的第一个字节将被放到内存中的虚拟地址

uint	pa;	//段的第一个字节在内存中的物理地址

uint	filesz; //段在文件中的长度

uint	memsz;	//	段在内存映像中占用的字节数

uint	flags; //可读可写可执行标志位。

uint	align; //段在文件及内存的对齐方式

};



练习5:实现函数调用堆栈跟踪函数 (需要编程)

在这里插入图片描述

函数堆栈

栈是一个很重要的编程概念,与编译器和编程语言有紧密的联系。理解调用栈最重要的两点是:栈的结构,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值“向上(栈底方向)”能获取返回地址、参数值,“向下(栈顶方向)”能获取函数局部变量值。如此形成递归,直至到达栈底。这就是函数调用栈。

有了上述关于函数堆栈的内容,我们就可以开始写代码啦
在这里插入图片描述
运行效果
在这里插入图片描述

练习6 完善中断初始化和处理 (需要编程)

在这里插入图片描述

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

一个表项占八个字节。入口地址为:gd_off_31_16 << 16 + gd_off_15_0 ;

struct gatedesc {

unsigned gd_off_15_0 : 16; // 低16位为段内偏移

unsigned gd_ss : 16; // 段选择子占16位

unsigned gd_args : 5; // # args, 0 for interrupt/trap gates ,also 0

unsigned gd_rsv1 : 3; // reserved(should be zero I guess)

unsigned gd_type : 4; // type(STS_{TG,IG32,TG32}) interrupt or trap

unsigned gd_s : 1; // must be 0 (system)

unsigned gd_dpl : 2; // descriptor(meaning new) privilege level

unsigned gd_p : 1; // Present

unsigned gd_off_31_16 : 16; // 作为高16位的段内偏移。

};

练习6.2 请编程完善kern/trap/trap.c中对中断向量表进行初始化的函数idt_init。在idt_init函数中,依次对所有中断入口进行初始化。使用mmu.h中的SETGATE宏,填充idt数组内容。每个中断的入口由tools/vectors.c生成,使用trap.c中声明的vectors数组即可。

在这里插入图片描述
我的源代码

     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表的位置。

1.写完后,发现这中断向量表的代码贼短,其实里面涉及到的东西不少哟。

我们看,第一句,声明一个vectors数组,这个vector在vector.S里面定义的,意思是通过这个指针可以跳转到该中断处理的地点。


.globl vector20

vector20:

pushl $0

pushl $20

jmp __alltraps

举个例子,上面的代码是中断号为20的跳转代码,首先会把该中断信息(中断号)进行压栈,在进入到统一的中断预处理函数。接下来跳转到__alltraps,该函数的作用很简单:保存好用户空间的上下文(也就是一些寄存器变量)。并切换到内核的上下文。

2.setgate这个函数的作用是设置正确的interrupt/trap gate 描述符。接下来,我来介绍一下该函数的参数。


//gate: Gate descriptors for interrupts and traps

//istrap : 0 is interrupts ,else is traps

//sel : the off's segment ,usual kernel text,the value is GD_KTEXT

//off : offset in segment ,函数入口地址。

//dpl : 特权级。

#define SETGATE(gate, istrap, sel, off, dpl) { \

(gate).gd_off_15_0 = (uint32_t)(off) & 0xffff; \

(gate).gd_ss = (sel); \

(gate).gd_args = 0; \

(gate).gd_rsv1 = 0; \

(gate).gd_type = (istrap) ? STS_TG32 : STS_IG32; \

(gate).gd_s = 0; \

(gate).gd_dpl = (dpl); \

(gate).gd_p = 1; \

(gate).gd_off_31_16 = (uint32_t)(off) >> 16; \

}



/* Gate descriptors for interrupts and traps */

struct gatedesc {

unsigned gd_off_15_0 : 16; // low 16 bits of offset in segment

unsigned gd_ss : 16; // segment selector

unsigned gd_args : 5; // # args, 0 for interrupt/trap gates

unsigned gd_rsv1 : 3; // reserved(should be zero I guess)

unsigned gd_type : 4; // type(STS_{TG,IG32,TG32})

unsigned gd_s : 1; // must be 0 (system)

unsigned gd_dpl : 2; // descriptor(meaning new) privilege level

unsigned gd_p : 1; // Present

unsigned gd_off_31_16 : 16; // high bits of offset in segment

};

3.注意需要对T_SWITCH_TOK的发生时机是在用户空间的,所以对应的dpl需要修改为DPL_USER。

4.lidt将idt的首地址和size装进idtr寄存器。


版权声明:本文为qq_40832456原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。