知识点
- 操作系统的启动知识和中断的建立与初始化
- 涉及到Intel 806386寄存器,AT&T汇编,gcc内联汇编,C函数堆栈,Makefile等知识
笔记主要按照操作系统的启动和中断的建立两个部分来记录
理论课的介绍
系统启动
当CPU刚加电初始化时,CS:IP寄存器根据设定的初始值跳转到BIOS固件处执行第一条指令,根据指令跳转到BIOS数据区执行BIOS代码。BIOS在完成硬件的自检后,会将操作系统的启动代码加载到内存。此时CPU还处于实模式,只能寻址20位,也就是1MB的内存空间(通常处于内存空间的低位地址),所以操作系统的启动代码需要加载在这1MB的寻址空间内。实验环境下,启动代码需加载到内存地址0x7C00处,启动代码再将CPU从实模式转成保护模式,得以获得32位的寻址空间(4GB),去加载代码量庞大的操作系统。理论课视频截图较为系统地展示了这个过程,如下图所示
为什么不利用BIOS直接加载操作系统?原因是不同操作系统可能拥有不同的文件系统,BIOS无法编写所有文件系统的解析代码,所以将加载程序作为操作系统的一部分,让操作系统可以“定制化”实现自己的加载程序。主引导记录和活动分区的存在主要是硬盘分区的原因。
中断建立
中断源的类型
- 系统调用(system call):应用程序主动向操作系统发出的服务请求
- 异常(exception):非法指令或者其它原因导致指令执行失败(如:内存出错)后的处理请求
- 中断(hardware interrupt):来自硬件设备的处理请求
如上图所示,中断向量表可以建立起中断源和服务例程之间的联系,在实操课程中中断向量表的建立也是一项重要的内容。
实验课的一些知识
在Lab1中,代码主要分为bootblock
和kernel
两个部分。bootblock
完成了主引导记录、活动分区文件系统识别以及加载程序的功能,kernel
则是完成系统内核的功能。
bootblock
从make和makefile来看,bootblock
主要涉及到的源文件有bootmain.c
,bootasm.S
,sign.c
,其中,bootmain.c
,bootasm.S
实现了bootblock
的大部分功能,而sign.c
从代码上看来,只是将第二个命令行参数(argv[1])的文件指针指向内容拷贝到argv[2]指向的文件,并在文件结尾加入主引导记录标志0x55, 0xAA,控制拷贝完的文件大小为512字节。在make编译链接的过程中,传入bin/sign
(sign.c生成)的文件为obj/bootblock.out
,输出的文件为bin/bootblock
,bootblock.out
由bootmain.c
和bootasm.S
生成。
bootasm.S
在实模式下,段寄存器和ip寄存器只提供16位的操作空间。为了寻址1MB的内存空间,此时段寄存器存储的基值(16位,base)会左移四位,加上偏移量(IP寄存器,offset)作为逻辑地址。
一开始CPU还处于实模式,段机制还未启动,但是将CPU转成保护模式之前需要将重要的段寄存器设置好,转换前段寄存器DS,ES,SS需置0
xorw %ax, %ax
movw %ax, %ds
movw %ax, %es
movw %ax, %ss
在进入保护模式之前,需要开启A20地址线https://chyyuu.gitbooks.io/ucore_os_docs/content/lab1/lab1_appendix_a20.html,开启步骤为
- 等待8042 Input buffer为空;
- 发送Write 8042 Output Port (P2)命令到8042 Input buffer;
- 等待8042 Input buffer为空;
- 将8042 Output Port(P2)得到字节的第2位置1,然后写入8042 Input buffer;
对应代码为
seta20.1:
inb $0x64, %al # Wait for not busy(8042 input buffer empty).
testb $0x2, %al
jnz seta20.1
movb $0xd1, %al # 0xd1 -> port 0x64
outb %al, $0x64 # 0xd1 means: write data to 8042's P2 port
seta20.2:
inb $0x64, %al # Wait for not busy(8042 input buffer empty).
testb $0x2, %al
jnz seta20.2
movb $0xdf, %al # 0xdf -> port 0x60
outb %al, $0x60 # 0xdf = 11011111, means set P2's A20 bit(the 1 bit) to 1
接下来载入GDT,
lgdt gdtdesc
...
# Bootstrap GDT
.p2align 2 # force 4 byte alignment
gdt:
SEG_NULLASM # null seg
SEG_ASM(STA_X|STA_R, 0x0, 0xffffffff) # code seg for bootloader and kernel
SEG_ASM(STA_W, 0x0, 0xffffffff) # data seg for bootloader and kernel
gdtdesc:
.word 0x17 # sizeof(gdt) - 1
.long gdt # address gdt
从代码上看,CPU会先读取GDT的描述信息,确定GDT的大小(24字节),再跳转到gdt的所在地址,执行内存分段。实验环境中,内存被分为代码段和数据段,大小都为4G(0x0~0xffffffff).设置完成之后,进入32位的保护模式,进行相应的准备工作。
.code32 # Assemble for 32-bit mode
protcseg:
# Set up the protected-mode data segment registers
movw $PROT_MODE_DSEG, %ax # Our data segment selector, $PROT_MODE_DSEG=0x10,ax最高位为1,使ds等段寄存器相应位置1,以支持保护模式
movw %ax, %ds # -> DS: Data Segment
movw %ax, %es # -> ES: Extra Segment
movw %ax, %fs # -> FS
movw %ax, %gs # -> GS
movw %ax, %ss # -> SS: Stack Segment
# Set up the stack pointer and call into C. The stack region is from 0--start(0x7c00)
movl $0x0, %ebp
movl $start, %esp # start为bootasm.S入口地址
一切准备工作就绪后,调用C函数bootmain.c
继续执行以至可以加载操作系统内核call bootmain
,bootmain负责将内核加载到内存0x10000处,并检查是否为合法的ELF文件。
kernel
在Lab1中,提及最多的知识是关于C函数堆栈的内容。见https://chyyuu.gitbooks.io/ucore_os_docs/content/lab1/lab1_3_3_1_function_stack.html,调试的编程参考于此
Lab1关于中断的内容主要是实现LDT的初始化。线索从kern/trap/vector.S
开始,其中以汇编的形式记录了__vectors[].从功能上看,__vectors像是一个函数指针数组,每个数组成员对应某个函数操作的入口。不同类型的中断向量被触发之后都会跳转到kern/trap/trapentry.S
的__alltraps处。目前来讲,这些功能使用C语言都能实现类似的效果,但是操作系统在这里使用了汇编的原因据推断是这里需要获取段寄存器的值,使用汇编会直接很多。__alltraps的目的是辅助C函数trap()的实现。trap的代码如下:
void
trap(struct trapframe *tf) {
trap_dispatch(tf);
}
# trapframe的结构如下
struct trapframe {
struct pushregs tf_regs;
uint16_t tf_gs;
uint16_t tf_padding0;
uint16_t tf_fs;
uint16_t tf_padding1;
uint16_t tf_es;
uint16_t tf_padding2;
uint16_t tf_ds;
uint16_t tf_padding3;
uint32_t tf_trapno;
/* below here defined by x86 hardware */
uint32_t tf_err;
uintptr_t tf_eip;
uint16_t tf_cs;
uint16_t tf_padding4;
uint32_t tf_eflags;
/* below here only when crossing rings, such as from user to kernel */
uintptr_t tf_esp;
uint16_t tf_ss;
uint16_t tf_padding5;
} __attribute__((packed));
汇编代码
.text
.globl __alltraps
__alltraps:
pushl %ds
pushl %es
pushl %fs
pushl %gs
pushal
# load GD_KDATA into %ds and %es to set up data segments for kernel
movl $GD_KDATA, %eax
movw %ax, %ds
movw %ax, %es
pushl %esp
call trap
push语句主要是为了模拟C函数的堆栈形式。最先入栈的是C函数的实际参数,该参数为指向trapframe的指针,但在此之前需要将指针指向内容压栈,可以看到汇编压栈顺序和结构成员声明相反(说明:结构成员tf_paddingx是作为填充消除编译器的内存对齐问题)。之后调用trap_dispatch,根据中断号执行不同的中断例程。
回到LDT的初始化,现在已经大概清楚从中断向量到触发中断例程的过程,但是怎么联系起中断向量__vectors[]和中断源呢?这是idt_init需要实现的东西。中断源用结构体gatedesc描述,再将数组idt[256]对应不同的中断源。idt_init做的很重要的一件事是怎么将__vectors[]和idt[]相同下标的gatedesc成员赋值好。最后利用汇编命令lidt载入LDT.
结尾
笔记写的不是非常严谨,此笔记主要功能是提供自己能够快速回忆操作系统知识。有其他重要知识点就等以后意识到再补充吧