进入内核前的苦力活[linux源码趣读]
GPT摘要
计算机系统启动及内存管理流程主要包括以下几个关键步骤: 1. 初始启动阶段 - CPU从ROM的
0xFFFF0
地址(BIOS入口)开始执行。 - BIOS加载硬盘第一个扇区的代码(bootsect)到内存的0x7C00
地址,随后将其复制到0x90000
。 - 跳转到go
代码,设置cs
、ds
、ss
、sp
等寄存器,完成早期初始化。 - 将操作系统核心代码(setup.s和head.s)加载到内存,bootsect.s的使命结束。 2. setup.s阶段 - 通过int
指令初始化硬件(如光标、内存、显卡、磁盘信息),并存储到原先bootsect.s所在的位置。 - 将system代码复制到内存的0x0
地址,准备后续切换至保护模式。 3. 实模式与保护模式 - 实模式:早期8086处理器的基本工作模式,直接访问物理内存,无内存保护或分页机制。 - 保护模式(cr0.PE=1
):现代处理器模式,支持分页、虚拟内存和内存保护。特权级(CPL)通过CS段选择子确定,分为Ring 0(内核态)至Ring 3(用户态)。 4. 保护模式初始化(setup.s完成) - 设置IDT(初始化键盘、时钟等中断)和GDT(定义代码段、数据段描述符)。 - 将cr0.PE
置1,切换到保护模式后,地址转换通过段选择子从GDT获取段基址。 - 跳转到cs\:ip 0\:0
(system代码入口)。 - 重新设置IDT和GDT,指向新内存空间。 5. 分页机制启用 - 设置cr0.PG=1
开启分页,MMU通过二级页表(页目录表PDE和页表PTE)将线性地址转为物理地址。 - Linux 0.11设计支持最大16MB内存,通过1个页目录表和4个页表管理。 - 分页机制实现内存隔离和按需分配,而分段机制(保护模式必需)主要用于权限控制和逻辑地址转换。 6. 关键寄存器作用 - CR3:指向页目录表,进程切换时更新。 - 分段机制:CS/EIP(代码执行)、DS(数据访问…
总体
加载代码
- pc指针初始指向0xFFFF0(ROM) 代表BIOS的地址
- 加载硬盘第一扇区代码(bootsetct)到0x7c00
- 复制到0x90000
- 跳转到go代码,设置好cs ds ss sp
- 把全部os代码搬入内存(setup.s 2~5; head.s 240扇区),至此bootsect.s使命完成
- setup.s 使用
int
指令初始化光标、内存、显卡、磁盘等信息放到bootsect.s的位置,并把system代码复制到0位置
进入保护模式
实模式:
- 处理器基本工作模式,主要用于兼容早期的8086处理器
- 直接访问物理内存,没有内存保护机制, 没有虚拟内存和分页机制
保护模式 cr0中PE=1
现代处理器
支持分页以及虚拟内存以及内存保护机制
保护模式 CS下最后两位 代表CPL 此外倒数第三位决定从GDT还是LDT ├── Ring 0 --> 内核态 ├── Ring 1 --> 设备驱动(较少使用) ├── Ring 2 --> 设备驱动(较少使用) └── Ring 3 --> 用户态
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
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
setup.s
1. 设置初始化IDT(键盘、时钟、串口、鼠标),GDT(包含代码段、数据段描述符)并设置寄存器指向他们的地址
2. 将cr0中PE置为1,切换到保护模式(地址转换也响应变换)
- 原来实模式下地址转换的方法
- 保护模式下转换方法:需要转换一下段基址(**段寄存器(比如 ds、ss、cs)里存储的是段选择子,段选择子去全局描述符表中寻找段描述符,从中取出段基址**)

3. 跳转到cs:ip 0:0位置,**从现在开始进入system代码**
4. 重新设置idt、gdt,指向新的空间

### 开启分页
#### 地址转换
**cr0**中的PG设置为1
没有开启分页机制的时候,只需要经过这一步转换即可得到最终的物理地址了,但是在开启了分页机制后,又会**多一步转换**。

二级页表线性地址转物理地址(第一级叫**页目录表 PDE**,第二级叫**页表 PTE**),**MMU**负责转换
0000000011_0100000000_000000000000

> 当时 linux-0.11 认为,总共可以使用的内存不会超过 **16M**,也即最大地址空间为 **0xFFFFFF**。
>
> 而按照当前的页目录表和页表这种机制,1 个页目录表最多包含 1024 个页目录项(也就是 1024 个页表),1 个页表最多包含 1024 个页表项(也就是 1024 个页),1 页为 4KB(因为有 12 位偏移地址),因此,16M 的地址空间可以用 1 个页目录表 + 4 个页表搞定。
>
> 4(页表数)* 1024(页表项数) * 4KB(一页大小)= 16MB
> 在早期的x86保护模式中,分段机制用于控制内存访问,给不同段分配不同的权限级别。但在现代操作系统中,分段机制主要用于基础的内存地址分段,而特权控制和内存保护主要依赖分页机制。因此,Linux等操作系统通常将分段简化为“扁平模式”(flat model),即所有段的基地址设为0,段限长覆盖整个虚拟地址空间,这样就可以忽略分段的复杂性,专注于分页。
#### 全局结构

CR3寄存器是虚拟内存管理的核心部分,与操作系统的内存管理紧密相关。当操作系统需要切换当前的内存映射时(如进程切换时),它会更新CR3寄存器的值来指向新的页目录表。
#### 小结
Intel 体系结构的**内存管理**可以分成两大部分,也就是标题中的两板斧,**分段**和**分页**。
**分段机制**在之前几回已经讨论过多次了,其目的是为了为每个程序或任务提供单独的代码段(cs)、数据段(ds)、栈段(ss),使其不会相互干扰。(保护模式必须开启)
**分页机制**是本回讲的内容,开机后分页机制默认是关闭状态,需要我们手动开启,并且设置好页目录表(PDE)和页表(PTE)。其目的在于可以**按需使用**物理内存,同时也可以在**多任务时起到隔离**的作用,这个在后面将多任务时将会有所体会。
- **逻辑地址**:我们程序员写代码时给出的地址叫逻辑地址,其中包含段选择子和偏移地址两部分。
- **线性地址**:通过分段机制,将逻辑地址转换后的地址,叫做线性地址。而这个线性地址是有个范围的,这个范围就叫做线性地址空间,32 位模式下,线性地址空间就是 4G。
- **物理地址**:就是真正在内存中的地址,它也是有范围的,叫做物理地址空间。那这个范围的大小,就取决于你的内存有多大了。
最后进入main函数是利用ret指令,ret会将栈顶作为下一条指令的地址,所以只需要将main函数提前入栈即可
### 总结
前五节:载入代码

之后包含进入保护模式(分段)以及开启分页,初始化了IDT、GDT、页表,并且设置响应寄存器指向他们:idtr 寄存器指向了 idt,这个就是中断的设置;gdtr 寄存器指向了 gdt,这个就是全局描述符表的设置,可以简单理解为分段机制的设置;cr3 寄存器指向了页目录表的位置

> Intel 本身对于访问内存就分成三类:
>
> - **代码**
> - **数据**
> - **栈**
>
> 而 Intel 也提供了三个段寄存器来分别对应着三类内存:
>
> - **代码段寄存器(cs)**
> - **数据段寄存器(ds)**
> - **栈段寄存器(ss)**
>
> 具体来说:
>
> - **cs:eip** 表示了我们要执行哪里的代码。
> - **ds:xxx** 表示了我们要访问哪里的数据。
> - **ss:esp** 表示了我们的栈顶地址在哪里。
>
> 而第一部分的代码,也做了如下工作:
>
> - 将 **ds** 设置为了 0x10,表示指向了索引值为 2 的全局描述符,即数据段描述符。
> - 将 **cs** 通过一次长跳转指令设置为了 8,表示指向了索引值为 1 的全局描述符,即代码段描述符。
> - 将 **ss:esp** 这个栈顶地址设置为 user_stack 数组的末端。
>
> 你看,分段和分页,以及这几个寄存器的设置,其实本质上就是安排我们今后访问内存的方式,做了一个初步规划,**包括去哪找代码、去哪找数据、去哪找栈,以及如何通过分段和分页机制将逻辑地址转换为最终的物理地址**。
>
> 而所有上面说的这一切,和 Intel CPU 这个硬件打交道比较多,设置了一些最最最最基础的环境和内存布局,为之后进入 main 函数做了充分的准备,因为 c 语言虽然很底层了,但也有其不擅长的事情,就交给第一部分的汇编语言来做,所以我称第一部分为**进入内核前的苦力活**。
```c
void main(void) {
ROOT_DEV = ORIG_ROOT_DEV;
drive_info = DRIVE_INFO;
memory_end = (1<<20) + (EXT_MEM_K<<10);
memory_end &= 0xfffff000;
if (memory_end > 16*1024*1024)
memory_end = 16*1024*1024;
if (memory_end > 12*1024*1024)
buffer_memory_end = 4*1024*1024;
else if (memory_end > 6*1024*1024)
buffer_memory_end = 2*1024*1024;
else
buffer_memory_end = 1*1024*1024;
main_memory_start = buffer_memory_end;
mem_init(main_memory_start,memory_end);
trap_init();
blk_dev_init();
chr_dev_init();
tty_init();
time_init();
sched_init();
buffer_init(buffer_memory_end);
hd_init();
floppy_init();
sti();
move_to_user_mode();
if (!fork()) {
init();
}
for(;;) pause();
}