Lab1:bootloader操作系统的启动

前言

最近接了一个外包项目再加上填一些之前立的flag,发现好像很久没有发博客了。现在编译原理操作系统算法方面都还有大坑没有填,加上离实习越来越近,应用层方面的学习也要加强了,但反倒是压力越大越想摸鱼

之前暑假的时候跟着书上写过一个玩具操作系统,然后之后还翻了一下Linux0.11一起写了一个系列的博客,但是还是觉得认识的太浅薄,然后最近发现清华大学的一个操作系统课程,就跟着了。准备用博客来记录一下,虽然会和之前的那个系列有些重复,但是也算是加强巩固了

BIOS

对于所有寄存器开机时都有各自的初始值,在X86中,CS和EIP寄存器分别是F000H,0000FFF0H,所以X86开机执行的第一条指令就是位于这个位置的指令,一般这条指令都是长跳转指令,从这里跳到BIOS程序的起始位置。

BIOS的工作是硬件自检和初始化,并且读取该设备的第一扇区,并且转交CPU控制权

bootloader

由BIOS加载的第一扇区一般放的都是bootloader,bootloader的工作就是

  • 切换到保护模式,启用分段机制
  • 读取磁盘中的操作系统内核
  • 将CPU控制权转交到操作系统

实模式

实模式的存在主要是为了兼容老的操作系统(16bit),在这种状态下软件可访问的物理内存空间不能超过1MB,主要的区别就是实模式的寻址能力和方式,实模式将整个物理内存看成分段的区域,程序代码和数据位于不同区域,操作系统和用户程序并没有区别对待,而且每一个指针都是指向实际的物理地址。

段基址 << 4 + 段内偏移

所以对于32位的处理器就有了保护模式。bootloader的一个工作就是将实模式切换为保护模式

保护模式

在保护模式下,对内存的管理就采用了分段机制,当然在后面还会有分页机制

分段机制将内存划分成以起始地址和长度限制这两个二维参数表示的内存块,这些内存块就称之为段

段机制里会有四个概念:

  • 段选择子

在实模式下,段寄存器原本存放的是段基址,但在保护模式下存放的是段选择子,段选择子的本质其实是一个索引,在全局描述符表的索引,但是它还包含其它信息,比如特权级信息

  • 全局描述符表

    全局描述符表,其实就是一个保存着段描述符的数组

  • 段描述符

    至于段描述符就是真正描述物理地址的数据结构了,每个段由如下三个参数进行定义:段基地址(Base Address)、段界限(Limit)和段属性(Attributes)。段属性里包含了一个特权级信息等等

  • 特权级

    至于保护模式为什么叫做保护模式,可能就是因为它指定了代码都是在一个特定的特权级下运行的,从而决定了代码可以做什么,不可以做什么

  1. CPL:当前特权级(Current Privilege Level) 保存在CS段寄存器(选择子)的最低两位,CPL就是当前活动代码段的特权级,并且它定义了当前所执行程序的特权级别)
  2. DPL:描述符特权(Descriptor Privilege Level) 存储在段描述符中的权限位,用于描述对应段所属的特权等级,也就是段本身能被访问的真正特权级。
  • 地址转换

    在没有进入到保护模式的时候,对应的地址就是相应的物理地址,但是在启动了保护模式下,逻辑地址–> (分段地址转换) –>线性地址 == 物理地址

bootloader主要代码

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
void
bootmain(void) {
// 读取第一扇区
readseg((uintptr_t)ELFHDR, SECTSIZE * 8, 0);

// 判断操作系统内核编译后的文件是不是有效的ELF文件
if (ELFHDR->e_magic != ELF_MAGIC) {
goto bad;
}

struct proghdr *ph, *eph;

// 加载内核
ph = (struct proghdr *)((uintptr_t)ELFHDR + ELFHDR->e_phoff);
eph = ph + ELFHDR->e_phnum;
for (; ph < eph; ph ++) {
readseg(ph->p_va & 0xFFFFFF, ph->p_memsz, ph->p_offset);
}

// 启动内核
((void (*)(void))(ELFHDR->e_entry & 0xFFFFFF))();

bad:
outw(0x8A00, 0x8A00);
outw(0x8A00, 0x8E00);

/* do nothing */
while (1);
}

中断和异常

操作系统和CPU一起提供了一种能够打断操作系统和应用的正常执行,让操作系统完成相关处理,然后在恢复操作系统和应用的正常执行。在操作系统中,这种机制称为中断机制。

当CPU收到中断(通过8259A完成)或者异常的事件时,它会暂停执行当前的程序或任务,通过一定的机制跳转到负责处理这个信号的相关处理例程中,在完成对这个事件的处理后再跳回到刚才被打断的程序或任务中。

中断向量和中断服务例程的对应关系主要是由IDT(中断描述符表)负责。操作系统在IDT中设置好各种中断向量对应的中断描述符,留待CPU在产生中断后查询对应中断服务例程的起始地址。而IDT本身的起始地址保存在idtr寄存器中。

IDT的代码实现

1
2
3
4
5
6
7
8
9
10
11
12
void
idt_init(void) {
// __vectors中存放的就是所有的中断处理程序
extern uintptr_t __vectors[];
int i;
for (i = 0; i < sizeof(idt) / sizeof(struct gatedesc); i ++) {
// 设置中断门
SETGATE(idt[i], 0, GD_KTEXT, __vectors[i], DPL_KERNEL);
}
// 加载进寄存器中
lidt(&idt_pd);
}

所有的中断处理程序最后都是进行一些寄存器的保存然后最后交由__alltraps调用trap

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
31
32
33
34
35
36
37
38
39
40
.globl vector0
vector0:
pushl $0
pushl $0
jmp __alltraps
.globl vector1
vector1:
pushl $0
pushl $1
jmp __alltraps


__alltraps:
pushl %ds
pushl %es
pushl %fs
pushl %gs
pushal

movl $GD_KDATA, %eax
movw %ax, %ds
movw %ax, %es

pushl %esp

call trap

popl %esp

.globl __trapret
__trapret:
popal

popl %gs
popl %fs
popl %es
popl %ds

addl $0x8, %esp
iret

最后trap调用trap_dispatch根据不同的服务来执行相关代码

1
2
3
4
void
trap(struct trapframe *tf) {
trap_dispatch(tf);
}