mirror of
https://github.com/MintCN/linux-insides-zh.git
synced 2026-04-26 11:41:53 +08:00
Merge branch 'master' of https://github.com/MintCN/linux-insides-zh
This commit is contained in:
578
Booting/linux-bootstrap-2.md
Executable file
578
Booting/linux-bootstrap-2.md
Executable file
@@ -0,0 +1,578 @@
|
||||
# 在内核安装代码的第一步
|
||||
|
||||
内核启动的第一步
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
在[上一节中](https://0xax.gitbooks.io/linux-insides/content/Booting/linux-bootstrap-1.html)我们开始接触到内核启动代码,并且分析了初始化部分,最后我们停在了对`main`函数(`main`函数是第一个用C写的函数)的调用(`main`函数位于[arch/x86/boot/main.c](https://github.com/torvalds/linux/blob/master/arch/x86/boot/main.c))。
|
||||
|
||||
在这一节中我们将继续对内核启动过程的研究,我们将
|
||||
* 认识`保护模式`
|
||||
* 如何从实模式进入保护模式
|
||||
* 堆和控制台初始化
|
||||
* 内存检测,cpu验证,键盘初始化
|
||||
* 还有更多
|
||||
|
||||
现在让我们开始我们的旅程
|
||||
|
||||
保护模式
|
||||
--------------------------------------------------------------------------------
|
||||
在操作系统可以使用Intel 64位CPU的[长模式](http://en.wikipedia.org/wiki/Long_mode)之前,内核必须首先将CPU切换到保护模式运行。
|
||||
|
||||
什么是[保护模式](https://en.wikipedia.org/wiki/Protected_mode)?保护模式于1982年被引入到Intel CPU家族,并且从那之后,直到Intel 64出现,保护模式都是Intel CPU的主要运行模式。
|
||||
|
||||
淘汰[实模式](http://wiki.osdev.org/Real_Mode)的主要原因是因为在实模式下,系统能够访问的内存非常有限。如果你还记得我们在上一节说的,在实模式下,系统最多只能访问1M内存,而且在很多时候,实际能够访问的内存只有640K。
|
||||
|
||||
保护模式带来了很多的改变,不过主要的改变都集中在内存管理方法。在保护模式中,实模式的20位地址线被替换成32位地址线,因此系统可以访问多大4GB的地址空间。另外,在保护模式中引入了[内存分页](http://en.wikipedia.org/wiki/Paging)功能,在后面的章节中我们将介绍这个功能。
|
||||
|
||||
保护模式提供了2种完全不同的内存管理机制:
|
||||
|
||||
* 段式内存管理
|
||||
* 内存分页
|
||||
|
||||
在这一节中,我们只介绍段式内存管理,内存分页我们将在后面的章节进行介绍。
|
||||
|
||||
在上一节中我们说过,在实模式下,一个物理地址是由2个部分组成的:
|
||||
|
||||
* 内存段的基地址
|
||||
* 从基地址开始的偏移
|
||||
|
||||
使用这2个信息,我们可以通过下面的公式计算出对应的物理地址
|
||||
|
||||
```
|
||||
PhysicalAddress = Segment * 16 + Offset
|
||||
```
|
||||
|
||||
在保护模式中,内存段的定义和实模式完全不同。在保护模式中,每个内存段不再是64K大小,段的大小和起始位置是通过一个叫做`段描述符`的数据结构进行描述。所有内存段的段描述符存储在一个叫做`全局描述符表`(GDT)的内存结构中。
|
||||
|
||||
`全局描述符表`这个内存数据结构在内存中的位置并不是固定的,它的地址保存在一个特殊寄存器 `GDTR` 中。在后面的章节中,我们将在Linux内核代码中看到全局描述符表的地址是如何被保存到 `GDTR` 中的。具体的汇编代码看起来是这样的:
|
||||
|
||||
```assembly
|
||||
lgdt gdt
|
||||
```
|
||||
|
||||
`lgdt` 汇编代码将把全局描述符表的基地址和大小保存到 `GDTR` 寄存器中。`GDTR` 是一个48位的寄存器,这个寄存器中的保存了2部分的内容:
|
||||
|
||||
* 全局描述符表的大小 (16位)
|
||||
* 全局描述符表的基址 (32位)
|
||||
|
||||
就像前面的段落说的,全局描述符表包含了所有内存段的`段描述符`。每个段描述符长度是64位,结构如下图描述:
|
||||
|
||||
```
|
||||
31 24 19 16 7 0
|
||||
------------------------------------------------------------
|
||||
| | |B| |A| | | | |0|E|W|A| |
|
||||
| BASE 31:24 |G|/|L|V| LIMIT |P|DPL|S| TYPE | BASE 23:16 | 4
|
||||
| | |D| |L| 19:16 | | | |1|C|R|A| |
|
||||
------------------------------------------------------------
|
||||
| | |
|
||||
| BASE 15:0 | LIMIT 15:0 | 0
|
||||
| | |
|
||||
------------------------------------------------------------
|
||||
```
|
||||
|
||||
粗粗一看,上面的结构非常吓人,不过实际上这个结构是非常容易理解的。比如在上图中的 LIMIT 15:0 表示这个数据结构的0到15位保存的是内存段的大小的0到15位。相似的 LIMITE 19:16 表示上述数据结构的16到19位保存的是内存段大小的16到19位。从这个分析中,我们可以看出每个内存段的大小是通过20位进行描述的。下面我们将对这个数据结构进行仔细分析:
|
||||
|
||||
1. Limit[20位] 被保存在上述内存结构的0-15和16-19位。根据上述内存结构中`G`位的设置,这20位内存定义的内存长度是不一样的。下面是一些具体的例子:
|
||||
|
||||
* 如果`G` = 0, 并且Limit = 0, 那么表示段长度是1 byte
|
||||
* 如果`G` = 1, 并且Limit = 0, 那么表示段长度是4K bytes
|
||||
* 如果`G` = 0,并且Limit = 0xfffff,那么表示段长度是1M bytes
|
||||
* 如果`G` = 1,并且Limit = 0xfffff,那么表示段长度是4G bytes
|
||||
|
||||
从上面的例子我们可以看出:
|
||||
|
||||
* 如果G = 0, 那么内存段的长度是按照1 byte进行增长的 ( Limit每增加1,段长度增加1 byte ),最大的内存段长度将是1M bytes;
|
||||
* 如果G = 1, 那么内存段的长度是按照4K bytes进行增长的 ( Limit每增加1,段长度增加4K bytes ),最大的内存段长度将是4G bytes;
|
||||
* 段长度的计算公司是 base_seg_length * ( LIMIT + 1)。
|
||||
|
||||
2. Base[32-bits] 被保存在上述地址结构的0-15, 32-39以及56-63位。Base定义了段基址。
|
||||
|
||||
3. Type/Attribute (40-47 bits) 定义了内存段的类型以及支持的操作。
|
||||
* `S` 标记( 第44位 )定义了段的类型,`S` = 0说明这个内存段是一个系统段;`S` = 1说明这个内存段是一个代码段或者是数据段( 堆栈段是一种特使类型的数据段,堆栈段必须是可以进行读写的段 )。
|
||||
|
||||
在`S` = 1的情况下,上述内存结构的第43位决定了内存段是数据段还是代码段。如果43位 = 0,说明是一个数据段,否则就是一个代码段。
|
||||
|
||||
对于数据段和代码段,下面的表格给出了段类型定义
|
||||
|
||||
```
|
||||
| Type Field | Descriptor Type | Description
|
||||
|-----------------------------|-----------------|------------------
|
||||
| Decimal | |
|
||||
| 0 E W A | |
|
||||
| 0 0 0 0 0 | Data | Read-Only
|
||||
| 1 0 0 0 1 | Data | Read-Only, accessed
|
||||
| 2 0 0 1 0 | Data | Read/Write
|
||||
| 3 0 0 1 1 | Data | Read/Write, accessed
|
||||
| 4 0 1 0 0 | Data | Read-Only, expand-down
|
||||
| 5 0 1 0 1 | Data | Read-Only, expand-down, accessed
|
||||
| 6 0 1 1 0 | Data | Read/Write, expand-down
|
||||
| 7 0 1 1 1 | Data | Read/Write, expand-down, accessed
|
||||
| C R A | |
|
||||
| 8 1 0 0 0 | Code | Execute-Only
|
||||
| 9 1 0 0 1 | Code | Execute-Only, accessed
|
||||
| 10 1 0 1 0 | Code | Execute/Read
|
||||
| 11 1 0 1 1 | Code | Execute/Read, accessed
|
||||
| 12 1 1 0 0 | Code | Execute-Only, conforming
|
||||
| 14 1 1 0 1 | Code | Execute-Only, conforming, accessed
|
||||
| 13 1 1 1 0 | Code | Execute/Read, conforming
|
||||
| 15 1 1 1 1 | Code | Execute/Read, conforming, accessed
|
||||
```
|
||||
|
||||
从上面的表格我们可以看出,当第43位是`0`的时候,这个段描述符对应的是一个数据段,如果该位是`1`,那么表示这个段描述符对应的是一个代码段。对于数据段,第42,41,40位表示的是(*E*扩展,*W*可写,*A*可访问);对于代码段,第42,41,40位表示的是(*C*一致,*R*可读,*A*可访问)。
|
||||
|
||||
* 如果`E` = 0,数据段是向上扩展数据段,反之为向下扩展数据段。关于向上扩展和向下扩展数据段,可以参考下面的[链接](http://www.sudleyplace.com/dpmione/expanddown.html)。在一般情况下,应该是不会使用向下扩展数据段的。
|
||||
* 如果`W` = 1,说明这个数据段是可写的,否则不可写。所有数据段都是可读的。
|
||||
* A位表示该内存段是否已经被CPU访问。
|
||||
* 如果`C` = 1,说明这个代码段可以被低优先级的代码访问,比如可以被用户态代码访问。反之如果`C` = 0,说明只能同优先级的代码段可以访问。
|
||||
* 如果`R` = 1,说明该代码段可读。代码段是永远没有写权限的。
|
||||
|
||||
4. DPL(2-bits, bit 45 和 46)定义了该段的优先级。具体数值是0-3。
|
||||
|
||||
5. P 标志(bit 47) - 说明该内存段是否已经存在于内存中。如果`P` = 0,那么在访问这个内存段的时候将报错。
|
||||
|
||||
6. AVL 标志(bit 52) - 这个位在Linux内核中没有被使用。
|
||||
|
||||
7. L 标志(bit 53) - 只对代码段有意义,如果`L` = 1,说明该代码段需要运行在64位模式下。
|
||||
|
||||
8. D/B flag(bit 54) - 根据段描述符描述的是一个可执行代码段、下扩数据段还是一个堆栈段,这个标志具有不同的功能。(对于32位代码和数据段,这个标志应该总是设置为1;对于16位代码和数据段,这个标志被设置为0。)。
|
||||
|
||||
* 可执行代码段。此时这个标志称为D标志并用于指出该段中的指令引用有效地址和操作数的默认长度。如果该标志置位,则默认值是32位地址和32位或8位的操作数;如果该标志为0,则默认值是16位地址和16位或8位的操作数。指令前缀0x66可以用来选择非默认值的操作数大小;前缀0x67可用来选择非默认值的地址大小。
|
||||
* 栈段(由SS寄存器指向的数据段)。此时该标志称为B(Big)标志,用于指明隐含堆栈操作(如PUSH、POP或CALL)时的栈指针大小。如果该标志置位,则使用32位栈指针并存放在ESP寄存器中;如果该标志为0,则使用16位栈指针并存放在SP寄存器中。如果堆栈段被设置成一个下扩数据段,这个B标志也同时指定了堆栈段的上界限。
|
||||
* 下扩数据段。此时该标志称为B标志,用于指明堆栈段的上界限。如果设置了该标志,则堆栈段的上界限是0xFFFFFFFF(4GB);如果没有设置该标志,则堆栈段的上界限是0xFFFF(64KB)。
|
||||
|
||||
在保护模式下,段寄存器保存的不再是一个内存段的基地址,而是一个称为`段选择子`的结构。每个段描述符都对应一个`段选择子`。`段选择子`是一个16位的数据结构,下图显示了这个数据结构的内容:
|
||||
|
||||
```
|
||||
-----------------------------
|
||||
| Index | TI | RPL |
|
||||
-----------------------------
|
||||
```
|
||||
|
||||
其中,
|
||||
* **Index** 表示在GDT中,对应段描述符的索引号。
|
||||
* **TI** 表示要在GDT还是LDT中查找对应的段描述符
|
||||
* **RPL** 表示请求者优先级。这个优先级将和段描述符中的优先级协同工作,共同确定访问是否合法。
|
||||
|
||||
在保护模式下,每个段寄存器实际上包含下面2部分内容:
|
||||
* 可见部分 - 段选择子
|
||||
* 隐藏部分 - 段描述符
|
||||
|
||||
在保护模式中,cpu是通过下面的步骤来找到一个具体的物理地址的:
|
||||
|
||||
* 代码必须将相应的`段选择子`装入某个段寄存器
|
||||
* CPU根据`段选择子`从GDT中找到一个匹配的段描述符,然后将段描述符放入段寄存器的隐藏部分
|
||||
* 在没有使用向下扩展段的时候,那么内存段的基地址就是`段描述符中的基地址`,段描述符的`limit + 1`就是内存段的长度。如果你知道一个内存地址的`偏移`,那么在没有开启分页机制的情况下,这个内存的物理地址就是`基地址+偏移`
|
||||
|
||||

|
||||
|
||||
当代码要从实模式进入保护模式的时候,需要执行下面的操作:
|
||||
|
||||
* 禁止中断发生
|
||||
* 使用命令 `lgdt` 将GDT表装入 `GDTR` 寄存器
|
||||
* 设置CR0寄存器的PE位为1,是CPU进入保护模式
|
||||
* 跳转开始执行保护模式代码
|
||||
|
||||
在后面的章节中,我们将看到Linux 内核中完整的转换代码。不过在系统进入保护模式之前,内核有很多的准备工作需要进行。
|
||||
|
||||
让我们代开C文件 [arch/x86/boot/main.c](https://github.com/torvalds/linux/blob/master/arch/x86/boot/main.c)。这个文件包含了很多的函数,这些函数分别会执行键盘初始化,内存堆初始化等等操作...,下面让我们来具体看一些重要的函数。
|
||||
|
||||
将启动参数拷贝到"zeropage"
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
让我们从`main`函数开始看起,这个函数中,首先调用了[`copy_boot_params(void)`](https://github.com/torvalds/linux/blob/master/arch/x86/boot/main.c#L30)。
|
||||
|
||||
这个函数将内核设置信息拷贝到`boot_params`结构的相应字段。大家可以在[arch/x86/include/uapi/asm/bootparam.h](https://github.com/torvalds/linux/blob/master/arch/x86/include/uapi/asm/bootparam.h#L113)找到`boot_params`结构的定义。
|
||||
|
||||
1. 将[header.S](https://github.com/torvalds/linux/blob/master/arch/x86/boot/header.S#L281)中定义的 `hdr` 结构中的内容拷贝到 `boot_params` 结构的字段 `struct setup_header hdr` 中。
|
||||
|
||||
2. 如果内核是通过老的命令行协议运行起来的,那么就更新内核的命令行指针。
|
||||
|
||||
这里需要注意的是拷贝 `hdr` 数据结构的 `memcpy` 函数不是C语言中的函数,而是定义在 [copy.S](https://github.com/torvalds/linux/blob/master/arch/x86/boot/copy.S)。让我们来具体分析一下这段代码:
|
||||
|
||||
```assembly
|
||||
GLOBAL(memcpy)
|
||||
pushw %si ;push si to stack
|
||||
pushw %di ;push di to stack
|
||||
movw %ax, %di ;move &boot_param.hdr to di
|
||||
movw %dx, %si ;move &hdr to si
|
||||
pushw %cx ;push cx to stack ( sizeof(hdr) )
|
||||
shrw $2, %cx
|
||||
rep; movsl ;copy based on 4 bytes
|
||||
popw %cx ;pop cx
|
||||
andw $3, %cx ;cx = cx % 4
|
||||
rep; movsb ;copy based on one byte
|
||||
popw %di
|
||||
popw %si
|
||||
retl
|
||||
ENDPROC(memcpy)
|
||||
```
|
||||
|
||||
在`copy.S`文件中,你可以看到所有的方法都开始于 `GLOBAL` 宏定义,而结束于 `ENDPROC` 宏定义。
|
||||
|
||||
你可以在 [arch/x86/include/asm/linkage.h](https://github.com/torvalds/linux/blob/master/arch/x86/include/asm/linkage.h)中找到 `GLOBAL` 宏定义。这个宏给代码段分配了一个名字标签,并且让这个名字全局可用。
|
||||
|
||||
```assembly
|
||||
#define GLOBAL(name) \
|
||||
.globl name; \
|
||||
name:
|
||||
```
|
||||
|
||||
你可以在[include/linux/linkage.h](https://github.com/torvalds/linux/blob/master/include/linux/linkage.h)中找到 `ENDPROC` 宏的定义。 这个宏通过 `END(name)` 代码标识了汇编函数的结束,同时将函数名输出,从而静态分析工具可以找到这个函数。
|
||||
|
||||
```assembly
|
||||
#define ENDPROC(name) \
|
||||
.type name, @function ASM_NL \
|
||||
END(name)
|
||||
```
|
||||
|
||||
`memcpy` 的实现代码是很容易理解的。首先,代码将 `si` 和 `di` 寄存器的值压入堆栈进行保存,这么做的原因是因为后续的代码将修改 `si` 和 `di` 寄存器的值。`memcpy` 函数(也包括其他定义在copy.s中的其他函数)使用了 `fastcall` 调用规则,意味着所有的函数调用参数是通过 `ax`, `dx`, `cx`寄存器传入的,而不是传统的通过堆栈传入。因此在使用下面的代码调用 `memcpy` 函数的时候
|
||||
|
||||
```c
|
||||
memcpy(&boot_params.hdr, &hdr, sizeof hdr);
|
||||
```
|
||||
|
||||
函数的参数是这样传递的
|
||||
|
||||
* `ax` 寄存器指向 `boot_param.hdr` 的内存地址
|
||||
* `dx` 寄存器指向 `hdr` 的内存地址
|
||||
* `cx` 寄存器包含 `hdr` 结构的大小
|
||||
|
||||
`memcpy` 函数在将 `si` 和 `di` 寄存器压栈之后,将 `boot_param.hdr` 的地址放入 `di` 寄存器,将 `hdr` 的地址放入 `si` 寄存器,并且将 `hdr` 数据结构的大小压栈。 接下来代码首先以4个字节为单位,将 `si` 寄存器指向的内存内容拷贝到 `di` 寄存器指向的内存。当剩下的字节数不足4字节的时候,代码将原始的 `hdr` 数据结构大小出栈放入 `cx` ,然后对 `cx` 的值对4求模,接下来就是根据 `cx` 的值,以字节为单位将 `si` 寄存器指向的内存内容拷贝到 `di` 寄存器指向的内存。当拷贝操作完成之后,将保留的 `si` 以及 `di` 寄存器值出栈,函数返回。
|
||||
|
||||
控制台初始化
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
在 `hdr` 结构体被拷贝到 `boot_params.hdr` 成员之后,系统接下来将进行控制台的初始化。控制台初始化时通过调用[arch/x86/boot/early_serial_console.c](https://github.com/torvalds/linux/blob/master/arch/x86/boot/early_serial_console.c)中定义的 `console_init` 函数实现的。
|
||||
|
||||
这个函数首先查看命令行参数是否包含 `earlyprintk` 选项。如果命令行参数包含该选项,那么函数将分析这个选项的内容。得到控制台将使用的串口信息,然后进行串口的初始化。以下是 `earlyprintk` 选项可能的取值:
|
||||
|
||||
* serial,0x3f8,115200
|
||||
* serial,ttyS0,115200
|
||||
* ttyS0,115200
|
||||
|
||||
当串口初始化成功之后,如果命令行参数包含 `debug` 选项,我们将看到如下的输出。
|
||||
|
||||
```C
|
||||
if (cmdline_find_option_bool("debug"))
|
||||
puts("early console in setup code\n");
|
||||
```
|
||||
|
||||
`puts` 函数定义在[tty.c](https://github.com/torvalds/linux/blob/master/arch/x86/boot/tty.c)。这个函数只是简单的调用 `putchar` 函数将输入字符串中的内容按字节输出。下面让我们来看看 `putchar`函数的实现:
|
||||
|
||||
```C
|
||||
void __attribute__((section(".inittext"))) putchar(int ch)
|
||||
{
|
||||
if (ch == '\n')
|
||||
putchar('\r');
|
||||
|
||||
bios_putchar(ch);
|
||||
|
||||
if (early_serial_base != 0)
|
||||
serial_putchar(ch);
|
||||
}
|
||||
```
|
||||
|
||||
`__attribute__((section(".inittext")))` 说明这段代码将被放入 `.inittext` 代码段。关于 `.inittext` 代码段的定义你可以在 [setup.ld](https://github.com/torvalds/linux/blob/master/arch/x86/boot/setup.ld#L19)中找到。
|
||||
|
||||
如果需要输出的字符是 `\n` ,那么 `putchar` 函数将调用自己首先输出一个字符 `\r`。接下来,就调用 `bios_putchar` 函数将字符输出到显示器(使用bios int10中断):
|
||||
|
||||
```C
|
||||
static void __attribute__((section(".inittext"))) bios_putchar(int ch)
|
||||
{
|
||||
struct biosregs ireg;
|
||||
|
||||
initregs(&ireg);
|
||||
ireg.bx = 0x0007;
|
||||
ireg.cx = 0x0001;
|
||||
ireg.ah = 0x0e;
|
||||
ireg.al = ch;
|
||||
intcall(0x10, &ireg, NULL);
|
||||
}
|
||||
```
|
||||
|
||||
在上面的代码中 `initreg` 函数接受一个 `biosregs` 结构的地址作为输入参数,该函数首先调用 `memset` 函数将 `biosregs` 结构体所有成员清0。
|
||||
|
||||
```C
|
||||
memset(reg, 0, sizeof *reg);
|
||||
reg->eflags |= X86_EFLAGS_CF;
|
||||
reg->ds = ds();
|
||||
reg->es = ds();
|
||||
reg->fs = fs();
|
||||
reg->gs = gs();
|
||||
```
|
||||
|
||||
下面让我们来看看[memset](https://github.com/torvalds/linux/blob/master/arch/x86/boot/copy.S#L36)函数的实现 :
|
||||
|
||||
```assembly
|
||||
GLOBAL(memset)
|
||||
pushw %di
|
||||
movw %ax, %di
|
||||
movzbl %dl, %eax
|
||||
imull $0x01010101,%eax
|
||||
pushw %cx
|
||||
shrw $2, %cx
|
||||
rep; stosl
|
||||
popw %cx
|
||||
andw $3, %cx
|
||||
rep; stosb
|
||||
popw %di
|
||||
retl
|
||||
ENDPROC(memset)
|
||||
```
|
||||
|
||||
首先你会发现,`memset` 函数和 `memcpy` 函数一样使用了 `fastcall` 调用规则,因此函数的参数是通过 `ax`,`dx` 以及 `cx` 寄存器传入函数内部的。
|
||||
|
||||
就像memcpy函数一样,`memset` 函数一开始将 `di` 寄存器入栈,然后将 `biosregs` 结构的地址从 `ax` 寄存器拷贝到`di`寄存器。接下来,使用 `movzbl` 指令将 `dl` 寄存器的内容拷贝到 `ax` 寄存器的低字节,到这里 `ax` 寄存器就包含了需要拷贝到 `di` 寄存器所指向的内存的值。
|
||||
|
||||
接下来的 `imull` 指令将 `eax` 寄存器的值乘上 `0x01010101`。这么做的原因是代码每次将尝试拷贝4个字节内存的内容。下面让我们来看一个具体的例子,假设我们需要将 `0x7` 这个数值放到内存中,在执行 `imull` 指令之前,`eax` 寄存器的值是 `0x7`,在 `imull` 指令被执行之后,`eax` 寄存器的内容变成了 `0x07070707`(4个字节的 `0x7`)。在 `imull` 指令之后,代码使用 `rep; stosl` 指令将 `eax` 寄存器的内容拷贝到 `es:di` 指向的内存。
|
||||
|
||||
在 `bisoregs` 结构体被 `initregs` 函数正确填充之后,`bios_putchar` 调用中断 [0x10](http://www.ctyme.com/intr/rb-0106.htm) 在显示器上输出一个字符。接下来 `putchar` 函数检查是否初始化了串口,如果串口被初始化了,那么将调用[serial_putchar](https://github.com/torvalds/linux/blob/master/arch/x86/boot/tty.c#L30)将字符输出到串口。
|
||||
|
||||
堆初始化
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
当堆栈和bss段在[header.S](https://github.com/torvalds/linux/blob/master/arch/x86/boot/header.S)中被初始化之后 (细节请参考上一篇[part](linux-bootstrap-1.md)), 内核需要初始化全局堆,全局堆的初始化是通过 [`init_heap`](https://github.com/torvalds/linux/blob/master/arch/x86/boot/main.c#L116) 函数实现的。
|
||||
|
||||
代码首先检查内核设置头中的[`loadflags`](https://github.com/torvalds/linux/blob/master/arch/x86/boot/header.S#L321)是否设置了 [`CAN_USE_HEAP`](https://github.com/torvalds/linux/blob/master/arch/x86/include/uapi/asm/bootparam.h#L21)标志。 如果该标记被设置了,那么代码将计算堆栈的结束地址::
|
||||
|
||||
```C
|
||||
char *stack_end;
|
||||
|
||||
//%P1 is (-STACK_SIZE)
|
||||
if (boot_params.hdr.loadflags & CAN_USE_HEAP) {
|
||||
asm("leal %P1(%%esp),%0"
|
||||
: "=r" (stack_end) : "i" (-STACK_SIZE));
|
||||
```
|
||||
|
||||
换言之`stack_end = esp - STACK_SIZE`.
|
||||
|
||||
在计算了堆栈结束地址之后,代码计算了堆的结束地址:
|
||||
|
||||
```c
|
||||
|
||||
//heap_end = heap_end_ptr + 512
|
||||
heap_end = (char *)((size_t)boot_params.hdr.heap_end_ptr + 0x200);
|
||||
```
|
||||
|
||||
接下来代码判断 `heap_end` 是否大于 `stack_end`,如果条件成立,将 `stack_end` 设置成 `heap_end`(这么做是因为在大部分系统中全局堆和堆栈是相邻的,但是增长方向是相反的)。
|
||||
|
||||
到这里为止,全局堆就被正确初始化了。在全局堆被初始化之后,我们就可以使用 `GET_HEAP` 方法。至于这个函数的实现和使用,我们将在后续的章节中看到。
|
||||
|
||||
检查CPU类型
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
在堆栈初始化之后,内核代码通过调用[arch/x86/boot/cpu.c](https://github.com/torvalds/linux/blob/master/arch/x86/boot/cpu.c)提供的 `validate_cpu` 方法检查CPU级别以确定系统是否能够在当前的CPU上运行。
|
||||
|
||||
`validate_cpu` 调用了[`check_cpu`](https://github.com/torvalds/linux/blob/master/arch/x86/boot/cpucheck.c#L102)方法得到当前系统的CPU级别,并且和系统预设的最低CPU级别进行比较。如果不满足条件,则不允许系统运行。
|
||||
|
||||
```c
|
||||
/*from cpu.c*/
|
||||
check_cpu(&cpu_level, &req_level, &err_flags);
|
||||
/*after check_cpu call, req_level = req_level defined in cpucheck.c*/
|
||||
if (cpu_level < req_level) {
|
||||
printf("This kernel requires an %s CPU, ", cpu_name(req_level));
|
||||
printf("but only detected an %s CPU.\n", cpu_name(cpu_level));
|
||||
return -1;
|
||||
}
|
||||
```
|
||||
|
||||
除此之外,`check_cpu` 方法还做了大量的其他检测和设置工作,下面就简单介绍一些:1)检查cpu标志,如果cpu是64位cpu,那么就设置[long mode](http://en.wikipedia.org/wiki/Long_mode), 2) 检查CPU的制造商,根据制造商的不同,设置不同的CPU选项。比如对于AMD出厂的cpu,如果不支持 `SSE+SSE2`,那么就禁止这些选项。
|
||||
|
||||
内存分布侦测
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
接下来,内核调用 `detect_memory` 方法进行内存侦测,以得到系统当前内存的使用分布。该方法使用多种编程接口,包括 `0xe820`(获取全部内存分配),`0xe801` 和 `0x88`(获取临近内存大小),进行内存分布侦测。在这里我们只介绍[arch/x86/boot/memory.c](https://github.com/torvalds/linux/blob/master/arch/x86/boot/memory.c)中提供的 `detect_memory_e820` 方法。
|
||||
|
||||
该方法首先调用 `initregs` 方法初始化 `biosregs` 数据结构,然后向该数据结构填入 `0xe820` 编程接口所要求的参数:
|
||||
|
||||
```assembly
|
||||
initregs(&ireg);
|
||||
ireg.ax = 0xe820;
|
||||
ireg.cx = sizeof buf;
|
||||
ireg.edx = SMAP;
|
||||
ireg.di = (size_t)&buf;
|
||||
```
|
||||
|
||||
* `ax` 固定为 `0xe820`
|
||||
* `cx` 包含数据缓冲区的大小,该缓冲区将包含系统内存的信息数据
|
||||
* `edx` 必须是 `SMAP` 这个魔术数字,就是 `0x534d4150`
|
||||
* `es:di` 包含数据缓冲区的地址
|
||||
* `ebx` 必须为0.
|
||||
|
||||
接下来就是通过一个循环来收集内存信息了。每个循环都开始于一个 `0x15` 中断调用,这个中断调用返回地址分配表中的一项,接着程序将返回的 `ebx` 设置到 `biosregs` 数据结构中,然后进行下一次的 `0x15` 中断调用。那么循环什么时候结束呢?直到 `0x15` 调用返回的eflags包含标志 `X86_EFLAGS_CF`:
|
||||
|
||||
```C
|
||||
intcall(0x15, &ireg, &oreg);
|
||||
ireg.ebx = oreg.ebx;
|
||||
```
|
||||
|
||||
在循环结束之后,整个内存分配信息将被写入到 `e820entry` 数组中,这个数组的每个元素包含下面3个信息:
|
||||
|
||||
* 内存段的起始地址
|
||||
* 内存段的大小
|
||||
* 内存段的类型(类型可以是reserved, usable等等)。
|
||||
|
||||
你可以在 `dmesg` 输出中看到这个数组的内容:
|
||||
|
||||
```
|
||||
[ 0.000000] e820: BIOS-provided physical RAM map:
|
||||
[ 0.000000] BIOS-e820: [mem 0x0000000000000000-0x000000000009fbff] usable
|
||||
[ 0.000000] BIOS-e820: [mem 0x000000000009fc00-0x000000000009ffff] reserved
|
||||
[ 0.000000] BIOS-e820: [mem 0x00000000000f0000-0x00000000000fffff] reserved
|
||||
[ 0.000000] BIOS-e820: [mem 0x0000000000100000-0x000000003ffdffff] usable
|
||||
[ 0.000000] BIOS-e820: [mem 0x000000003ffe0000-0x000000003fffffff] reserved
|
||||
[ 0.000000] BIOS-e820: [mem 0x00000000fffc0000-0x00000000ffffffff] reserved
|
||||
```
|
||||
|
||||
键盘初始化
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
接下来内核调用[`keyboard_init()`](https://github.com/torvalds/linux/blob/master/arch/x86/boot/main.c#L65) 方法进行键盘初始化操作。 首先,方法调用`initregs`初始化寄存器结构,然后调用[0x16](http://www.ctyme.com/intr/rb-1756.htm)中断来获取键盘状态。
|
||||
|
||||
```c
|
||||
initregs(&ireg);
|
||||
ireg.ah = 0x02; /* Get keyboard status */
|
||||
intcall(0x16, &ireg, &oreg);
|
||||
boot_params.kbd_status = oreg.al;
|
||||
```
|
||||
|
||||
在获取了键盘状态之后,代码再次调用[0x16](http://www.ctyme.com/intr/rb-1757.htm)中断来设置键盘的按键检测频率。
|
||||
|
||||
```c
|
||||
ireg.ax = 0x0305; /* Set keyboard repeat rate */
|
||||
intcall(0x16, &ireg, NULL);
|
||||
```
|
||||
|
||||
系统参数查询
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
接下来内核将进行一系列的参数查询。我们在这里将不深入介绍所有这些查询,我们将在后续章节中在进行详细介绍。在这里我们将简单介绍一些系统参数查询:
|
||||
|
||||
[query_mca](https://github.com/torvalds/linux/blob/master/arch/x86/boot/mca.c#L18) 方法调用[0x15](http://www.ctyme.com/intr/rb-1594.htm)中断来获取机器的型号信息,BIOS版本以及其他一些硬件相关的属性:
|
||||
|
||||
```c
|
||||
int query_mca(void)
|
||||
{
|
||||
struct biosregs ireg, oreg;
|
||||
u16 len;
|
||||
|
||||
initregs(&ireg);
|
||||
ireg.ah = 0xc0;
|
||||
intcall(0x15, &ireg, &oreg);
|
||||
|
||||
if (oreg.eflags & X86_EFLAGS_CF)
|
||||
return -1; /* No MCA present */
|
||||
|
||||
set_fs(oreg.es);
|
||||
len = rdfs16(oreg.bx);
|
||||
|
||||
if (len > sizeof(boot_params.sys_desc_table))
|
||||
len = sizeof(boot_params.sys_desc_table);
|
||||
|
||||
copy_from_fs(&boot_params.sys_desc_table, oreg.bx, len);
|
||||
return 0;
|
||||
}
|
||||
```
|
||||
|
||||
这个方法设置 `ah` 寄存器的值为 `0xc0`,然后调用 `0x15` BIOS中断。中断返回之后代码检查 [carry flag](http://en.wikipedia.org/wiki/Carry_flag)。如果它被置位,说明BIOS不支持[**MCA**](https://en.wikipedia.org/wiki/Micro_Channel_architecture)。如果CF被设置成0,那么 `ES:BX` 指向系统信息表。这个表的内容如下所示:
|
||||
|
||||
```
|
||||
Offset Size Description
|
||||
00h WORD number of bytes following
|
||||
02h BYTE model (see #00515)
|
||||
03h BYTE submodel (see #00515)
|
||||
04h BYTE BIOS revision: 0 for first release, 1 for 2nd, etc.
|
||||
05h BYTE feature byte 1 (see #00510)
|
||||
06h BYTE feature byte 2 (see #00511)
|
||||
07h BYTE feature byte 3 (see #00512)
|
||||
08h BYTE feature byte 4 (see #00513)
|
||||
09h BYTE feature byte 5 (see #00514)
|
||||
---AWARD BIOS---
|
||||
0Ah N BYTEs AWARD copyright notice
|
||||
---Phoenix BIOS---
|
||||
0Ah BYTE ??? (00h)
|
||||
0Bh BYTE major version
|
||||
0Ch BYTE minor version (BCD)
|
||||
0Dh 4 BYTEs ASCIZ string "PTL" (Phoenix Technologies Ltd)
|
||||
---Quadram Quad386---
|
||||
0Ah 17 BYTEs ASCII signature string "Quadram Quad386XT"
|
||||
---Toshiba (Satellite Pro 435CDS at least)---
|
||||
0Ah 7 BYTEs signature "TOSHIBA"
|
||||
11h BYTE ??? (8h)
|
||||
12h BYTE ??? (E7h) product ID??? (guess)
|
||||
13h 3 BYTEs "JPN"
|
||||
```
|
||||
|
||||
接下来代码调用 `set_fs` 方法,将 `es` 寄存器的值写入 `fs` 寄存器:
|
||||
|
||||
```c
|
||||
static inline void set_fs(u16 seg)
|
||||
{
|
||||
asm volatile("movw %0,%%fs" : : "rm" (seg));
|
||||
}
|
||||
```
|
||||
|
||||
在[boot.h](https://github.com/torvalds/linux/blob/master/arch/x86/boot/boot.h) 存在很多类似于 `set_fs` 的方法, 比如 `set_gs`。
|
||||
|
||||
在 `query_mca` 的最后,代码将 `es:bx` 指向的内存地址的内容拷贝到 `boot_params.sys_desc_table`。
|
||||
|
||||
接下来,内核调用 `query_ist` 方法获取[Intel SpeedStep](http://en.wikipedia.org/wiki/SpeedStep)信息。这个方法首先检查CPU类型,然后调用 `0x15` 中断获得这个信息并放入 `boot_params` 中。
|
||||
|
||||
接下来,内核会调用[query_apm_bios](https://github.com/torvalds/linux/blob/master/arch/x86/boot/apm.c#L21) 方法从BIOS获得 [高级电源管理](http://en.wikipedia.org/wiki/Advanced_Power_Management) 信息。`query_apm_bios` 也是调用 `0x15` 中断,只不过将 `ax` 设置成 `0x5300` 以得到APM设置信息。中断调用返回之后,代码将检查 `bx` 和 `cx` 的值,如果 `bx` 不是 `0x504d` ( PM 标记 ),或者 `cx` 不是 `0x02` (0x02,表示支持32位模式),那么代码直接返回错误。否则,将进行下面的步骤。
|
||||
|
||||
接下来,代码使用 `ax = 0x5304` 来调用 `0x15` 中断,以断开 `APM` 接口;然后使用 `ax = 0x5303` 调用 `0x15` 中断,使用32位接口重新连接 `APM`;最后使用 `ax = 0x5300` 调用 `0x15` 中断再次获取APM设置,然后将信息写入 `boot_params.apm_bios_info`。
|
||||
|
||||
需要注意的是,只有在 `CONFIG_APM` 或者 `CONFIG_APM_MODULE` 被设置的情况下,`query_apm_bios` 方法才会被调用:
|
||||
|
||||
```C
|
||||
#if defined(CONFIG_APM) || defined(CONFIG_APM_MODULE)
|
||||
query_apm_bios();
|
||||
#endif
|
||||
```
|
||||
|
||||
最后是[`query_edd`](https://github.com/torvalds/linux/blob/master/arch/x86/boot/edd.c#L122) 方法调用, 这个方法从BIOS中查询 `Enhanced Disk Drive` 信息。下面让我们看看 `query_edd` 方法的实现。
|
||||
|
||||
首先,代码检查内核命令行参数是否设置了[edd](https://github.com/torvalds/linux/blob/master/Documentation/kernel-parameters.txt#L1023) 选项,如果edd选项设置成 `off`,`query_edd` 不做任何操作,直接返回。
|
||||
|
||||
如果EDD被激活了,`query_edd` 遍历所有BIOS支持的硬盘,并获取相应硬盘的EDD信息:
|
||||
|
||||
```C
|
||||
for (devno = 0x80; devno < 0x80+EDD_MBR_SIG_MAX; devno++) {
|
||||
if (!get_edd_info(devno, &ei) && boot_params.eddbuf_entries < EDDMAXNR) {
|
||||
memcpy(edp, &ei, sizeof ei);
|
||||
edp++;
|
||||
boot_params.eddbuf_entries++;
|
||||
}
|
||||
...
|
||||
...
|
||||
...
|
||||
```
|
||||
|
||||
在代码中 `0x80` 是第一块硬盘,`EDD_MBR_SIG_MAX` 是一个宏,值为16。代码把获得的信息放入数组[edd_info](https://github.com/torvalds/linux/blob/master/include/uapi/linux/edd.h#L172)中。`get_edd_info` 方法通过调用 `0x13` 中断调用(设置 `ah = 0x41` ) 来检查EDD是否被硬盘支持。如果EDD被支持,代码将再次调用 `0x13` 中断,在这次调用中 `ah = 0x48`,并且 `si` 指向一个数据缓冲区地址。中断调用之后,EDD信息将被保存到 `si` 指向的缓冲区地址。
|
||||
|
||||
结束语
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
本章到此就结束了,在下一章我们将讲解显示模式设置,以及在进入保护模式之前的其他准备工作,在下一章的最后我们将成功进入保护模式。
|
||||
|
||||
如果你有任何的问题或者建议,你可以留言,也可以直接发消息给我[twitter](https://twitter.com/0xAX).
|
||||
|
||||
**英文不是我的母语。如果你发现我的英文描述有任何问题,请提交一个PR到[linux-insides](https://github.com/0xAX/linux-internals).**
|
||||
|
||||
相关链接
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
* [Protected mode](http://en.wikipedia.org/wiki/Protected_mode)
|
||||
* [Protected mode](http://wiki.osdev.org/Protected_Mode)
|
||||
* [Long mode](http://en.wikipedia.org/wiki/Long_mode)
|
||||
* [Nice explanation of CPU Modes with code](http://www.codeproject.com/Articles/45788/The-Real-Protected-Long-mode-assembly-tutorial-for)
|
||||
* [How to Use Expand Down Segments on Intel 386 and Later CPUs](http://www.sudleyplace.com/dpmione/expanddown.html)
|
||||
* [earlyprintk documentation](http://lxr.free-electrons.com/source/Documentation/x86/earlyprintk.txt)
|
||||
* [Kernel Parameters](https://github.com/torvalds/linux/blob/master/Documentation/kernel-parameters.txt)
|
||||
* [Serial console](https://github.com/torvalds/linux/blob/master/Documentation/serial-console.txt)
|
||||
* [Intel SpeedStep](http://en.wikipedia.org/wiki/SpeedStep)
|
||||
* [APM](https://en.wikipedia.org/wiki/Advanced_Power_Management)
|
||||
* [EDD specification](http://www.t13.org/documents/UploadedDocuments/docs2004/d1572r3-EDD3.pdf)
|
||||
* [TLDP documentation for Linux Boot Process](http://www.tldp.org/HOWTO/Linux-i386-Boot-Code-HOWTO/setup.html) (old)
|
||||
* [Previous Part](linux-bootstrap-1.md)
|
||||
* [BIOS Interrupt](http://wiki.osdev.org/BIOS)
|
||||
@@ -9,27 +9,27 @@
|
||||
|
||||
2. 通过如下命令从你的 Github 帐号 `clone` 库:
|
||||
|
||||
```
|
||||
git clone git@github.com:your_github_username/linux-insides.git
|
||||
```
|
||||
```
|
||||
git clone git@github.com:your_github_username/linux-insides-zh.git
|
||||
```
|
||||
|
||||
3. 通过如下命令创建分支 (`branch`) :
|
||||
|
||||
```
|
||||
```
|
||||
git checkout -b "linux-insides-zh-fix"
|
||||
# linux-insides-zh-fix is just an example
|
||||
```
|
||||
```
|
||||
|
||||
4. 对本地库进行修改。
|
||||
|
||||
5. 提交自己的修改,然后推送 (`push`) 到远端。
|
||||
|
||||
```
|
||||
```
|
||||
git add your_changed_files
|
||||
git commit -m "your comment"
|
||||
git push --set-upstream origin linux-insides-zh-fix
|
||||
```
|
||||
```
|
||||
|
||||
6. 点击 `New pull request` 按钮,将 your_github_username 库的 linux-insides-zh-fix 分支的修改提交到 MintCN 的 `linux-insides-zh` 库中。
|
||||
6. 点击 `New pull request` 按钮,将 your_github_username 的 `linux-insides-zh` 库的 linux-insides-zh-fix 分支的修改提交到 MintCN 的 `linux-insides-zh` 库中。
|
||||
|
||||
Thank you.
|
||||
十分感谢!
|
||||
|
||||
20
README.md
20
README.md
@@ -13,21 +13,21 @@ Linux Insides
|
||||
| 章节|译者|翻译进度|
|
||||
| ------------- |:-------------:| -----:|
|
||||
|Booting||正在进行|
|
||||
|├1.1|[@xinqiu](https://github.com/xinqiu)|正在进行|
|
||||
|├1.2||未开始|
|
||||
|├1.3||未开始|
|
||||
|├1.1|[@hailincai](https://github.com/hailincai)|正在进行|
|
||||
|├1.2|[@hailincai](https://github.com/hailincai)|正在进行|
|
||||
|├1.3|[@hailincai](https://github.com/hailincai)|正在进行|
|
||||
|├1.4||未开始|
|
||||
|└1.5||未开始|
|
||||
|Initialization|[@lijiangsheng1](https://github.com/lijiangsheng1)|正在进行|
|
||||
|Interrupts||正在进行|
|
||||
|├3.1|[@littleneko](https://github.com/littleneko)|正在进行|
|
||||
|├3.2||未开始|
|
||||
|├3.3||未开始|
|
||||
|├3.4||未开始|
|
||||
|├3.5||未开始|
|
||||
|├3.6||未开始|
|
||||
|├3.7||未开始|
|
||||
|├3.8||未开始|
|
||||
|├3.2|[@cloudusers](https://github.com/cloudusers)|正在进行|
|
||||
|├3.3|[@cloudusers](https://github.com/cloudusers)|正在进行|
|
||||
|├3.4|[@cloudusers](https://github.com/cloudusers)|正在进行|
|
||||
|├3.5|[@cloudusers](https://github.com/cloudusers)|正在进行|
|
||||
|├3.6|[@cloudusers](https://github.com/cloudusers)|正在进行|
|
||||
|├3.7|[@cloudusers](https://github.com/cloudusers)|正在进行|
|
||||
|├3.8|[@cloudusers](https://github.com/cloudusers)|正在进行|
|
||||
|├3.9||未开始|
|
||||
|└3.10||未开始|
|
||||
|System calls|[@qianmoke](https://github.com/qianmoke)|正在进行|
|
||||
|
||||
84
SUMMARY.md
84
SUMMARY.md
@@ -1,52 +1,54 @@
|
||||
### Summary
|
||||
# Summary
|
||||
|
||||
* [Introduction](README.md)
|
||||
* [引导](Booting/README.md)
|
||||
* [从引导加载程序内核](Booting/linux-bootstrap-1.md)
|
||||
* [在内核安装代码的第一步](Booting/linux-bootstrap-2.md)
|
||||
* [视频模式初始化和转换到保护模式](Booting/linux-bootstrap-3.md)
|
||||
* [过渡到64位模式](Booting/linux-bootstrap-4.md)
|
||||
* [内核解压缩](Booting/linux-bootstrap-5.md)
|
||||
* [从引导加载程序内核](Booting/linux-bootstrap-1.md)
|
||||
* [在内核安装代码的第一步](Booting/linux-bootstrap-2.md)
|
||||
* [视频模式初始化和转换到保护模式](Booting/linux-bootstrap-3.md)
|
||||
* [过渡到64位模式](Booting/linux-bootstrap-4.md)
|
||||
* [内核解压缩](Booting/linux-bootstrap-5.md)
|
||||
* [Initialization](Initialization/README.md)
|
||||
* [First steps in the kernel](Initialization/linux-initialization-1.md)
|
||||
* [Early interrupts handler](Initialization/linux-initialization-2.md)
|
||||
* [Last preparations before the kernel entry point](Initialization/linux-initialization-3.md)
|
||||
* [Kernel entry point](Initialization/linux-initialization-4.md)
|
||||
* [Continue architecture-specific boot-time initializations](Initialization/linux-initialization-5.md)
|
||||
* [Architecture-specific initializations, again...](Initialization/linux-initialization-6.md)
|
||||
* [End of the architecture-specific initializations, almost...](Initialization/linux-initialization-7.md)
|
||||
* [Scheduler initialization](Initialization/linux-initialization-8.md)
|
||||
* [RCU initialization](Initialization/linux-initialization-9.md)
|
||||
* [End of initialization](Initialization/linux-initialization-10.md)
|
||||
* [First steps in the kernel](Initialization/linux-initialization-1.md)
|
||||
* [Early interrupts handler](Initialization/linux-initialization-2.md)
|
||||
* [Last preparations before the kernel entry point](Initialization/linux-initialization-3.md)
|
||||
* [Kernel entry point](Initialization/linux-initialization-4.md)
|
||||
* [Continue architecture-specific boot-time initializations](Initialization/linux-initialization-5.md)
|
||||
* [Architecture-specific initializations, again...](Initialization/linux-initialization-6.md)
|
||||
* [End of the architecture-specific initializations, almost...](Initialization/linux-initialization-7.md)
|
||||
* [Scheduler initialization](Initialization/linux-initialization-8.md)
|
||||
* [RCU initialization](Initialization/linux-initialization-9.md)
|
||||
* [End of initialization](Initialization/linux-initialization-10.md)
|
||||
* [Interrupts](interrupts/README.md)
|
||||
* [Introduction](interrupts/interrupts-1.md)
|
||||
* [Start to dive into interrupts](interrupts/interrupts-2.md)
|
||||
* [Interrupt handlers](interrupts/interrupts-3.md)
|
||||
* [Initialization of non-early interrupt gates](interrupts/interrupts-4.md)
|
||||
* [Implementation of some exception handlers](interrupts/interrupts-5.md)
|
||||
* [Handling Non-Maskable interrupts](interrupts/interrupts-6.md)
|
||||
* [Dive into external hardware interrupts](interrupts/interrupts-7.md)
|
||||
* [Initialization of external hardware interrupts structures](interrupts/interrupts-8.md)
|
||||
* [Introduction](interrupts/interrupts-1.md)
|
||||
* [Start to dive into interrupts](interrupts/interrupts-2.md)
|
||||
* [Interrupt handlers](interrupts/interrupts-3.md)
|
||||
* [Initialization of non-early interrupt gates](interrupts/interrupts-4.md)
|
||||
* [Implementation of some exception handlers](interrupts/interrupts-5.md)
|
||||
* [Handling Non-Maskable interrupts](interrupts/interrupts-6.md)
|
||||
* [Dive into external hardware interrupts](interrupts/interrupts-7.md)
|
||||
* [Initialization of external hardware interrupts structures](interrupts/interrupts-8.md)
|
||||
* [Memory management](mm/README.md)
|
||||
* [Memblock](mm/linux-mm-1.md)
|
||||
* [Fixmaps and ioremap](mm/linux-mm-2.md)
|
||||
* [vsyscalls and vdso]()
|
||||
* [SMP]()
|
||||
* [Memblock](mm/linux-mm-1.md)
|
||||
* [Fixmaps and ioremap](mm/linux-mm-2.md)
|
||||
* vsyscalls and vdso
|
||||
* SMP
|
||||
* [Concepts](Concepts/README.md)
|
||||
* [Per-CPU variables](Concepts/per-cpu.md)
|
||||
* [Cpumasks](Concepts/cpumask.md)
|
||||
* [Per-CPU variables](Concepts/per-cpu.md)
|
||||
* [Cpumasks](Concepts/cpumask.md)
|
||||
* [Data Structures in the Linux Kernel](DataStructures/README.md)
|
||||
* [Doubly linked list](DataStructures/dlist.md)
|
||||
* [Radix tree](DataStructures/radix-tree.md)
|
||||
* [Doubly linked list](DataStructures/dlist.md)
|
||||
* [Radix tree](DataStructures/radix-tree.md)
|
||||
* [理论](Theory/README.md)
|
||||
* [分页](Theory/Paging.md)
|
||||
* [Elf64 格式](Theory/ELF.md)
|
||||
* [CPUID]()
|
||||
* [MSR]()
|
||||
* [Initial ram disk]()
|
||||
* [initrd]()
|
||||
* [分页](Theory/Paging.md)
|
||||
* [Elf64 格式](Theory/ELF.md)
|
||||
* CPUID
|
||||
* MSR
|
||||
* Initial ram disk
|
||||
* initrd
|
||||
* [Misc](Misc/README.md)
|
||||
* [How kernel compiled]()
|
||||
* [Write and Submit your first Linux kernel Patch]()
|
||||
* [Data types in the kernel]()
|
||||
* How kernel compiled
|
||||
* Write and Submit your first Linux kernel Patch
|
||||
* Data types in the kernel
|
||||
* [Useful links](LINKS.md)
|
||||
* [Contributors](contributors.md)
|
||||
|
||||
|
||||
164
Theory/Paging.md
164
Theory/Paging.md
@@ -1,39 +1,39 @@
|
||||
分页
|
||||
================================================================================
|
||||
|
||||
Introduction
|
||||
简介
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
In the fifth [part](http://0xax.gitbooks.io/linux-insides/content/Booting/linux-bootstrap-5.html) of the series `Linux kernel booting process` we learned about what the kernel does in its earliest stage. In the next step the kernel will initialize different things like `initrd` mounting, lockdep initialization, and many many others things, before we can see how the kernel runs the first init process.
|
||||
在 Linux 内核启动过程中的[第五部分](http://0xax.gitbooks.io/linux-insides/content/Booting/linux-bootstrap-5.html),我们学到了内核在最早的阶段做的工作。在接下来的步骤中,在我们明白内核如何运行第一个 init 进程,内核初始化不同的事,比如 `initrd` 加载, lockdep 初始化,以及许多许多其他的事。
|
||||
|
||||
Yeah, there will be many different things, but many many and once again many work with **memory**.
|
||||
是的,那将有很多不同的事,但是还有更多更多更多关于**内存**的工作。
|
||||
|
||||
In my view, memory management is one of the most complex part of the linux kernel and in system programming in general. This is why before we proceed with the kernel initialization stuff, we need to get acquainted with paging.
|
||||
在我看来,一般而言,内存管理是 Linux 内核和系统编程最复杂的部分之一。这就是为什么在我们处理内核初始化事情之前,我们需要了解分页。
|
||||
|
||||
`Paging` is a mechanism that translates a linear memory address to a physical address. If you have read the previous parts of this book, you may remember that we saw segmentation in real mode when physical addresses are calculated by shifting a segment register by four and adding an offset. We also saw segmentation in protected mode, where we used the descriptor tables and base addresses from descriptors with offsets to calculate the physical addresses. Now that we are in 64-bit mode, will see paging.
|
||||
分页是将线性地址转换为物理地址的机制。如果我们已经读过了这本书之前的部分,你可能记得我们在实模式下有分段机制,当时物理地址是由左移四位段寄存器加上偏移算出来的。我们也看了保护模式下的分段机制,其中我们使用描述符表和从描述符得到的基地址加上偏移得到物理地址。由于我们在 64 位模式,我们将看分页机制。
|
||||
|
||||
As the Intel manual says:
|
||||
正如 Intel 手册中说的:
|
||||
|
||||
> Paging provides a mechanism for implementing a conventional demand-paged, virtual-memory system where sections of a program’s execution environment are mapped into physical memory as needed.
|
||||
> 分页机制提供一种机制,为了实现一种常见的页需求,虚拟内存系统,其中一个程序执行环境中的段将要按照需求被映射到物理地址。
|
||||
|
||||
So... In this post I will try to explain the theory behind paging. Of course it will be closely related to the `x86_64` version of the linux kernel for, but we will not go into too much details (at least in this post).
|
||||
所以... 在这个帖子中我将尝试解释分页背后的理论。当然它将与64位版本的 Linux 内核关系密切,但是我们将不会深入太多细节(至少在这个帖子里面)。
|
||||
|
||||
Enabling paging
|
||||
开启分页
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
There are three paging modes:
|
||||
有三种分页模式:
|
||||
|
||||
* 32-bit paging;
|
||||
* PAE paging;
|
||||
* IA-32e paging.
|
||||
* 32 位分页模式;
|
||||
* PAE 分页;
|
||||
* IA-32e 分页。
|
||||
|
||||
We will only explain the last mode here. To enable the `IA-32e paging` paging mode we need to do following things:
|
||||
我们这里将只解释最后一种模式。为了开启 `IA-32e 分页模式`,我们需要做如下事情:
|
||||
|
||||
* set the `CR0.PG` bit;
|
||||
* set the `CR4.PAE` bit;
|
||||
* set the `IA32_EFER.LME` bit.
|
||||
* 设置 `CR0.PG` 位;
|
||||
* 设置 `CR4.PAE` 位;
|
||||
* 设置 `IA32_EFER.LME` 位。
|
||||
|
||||
We already saw where those this bits were set in [arch/x86/boot/compressed/head_64.S](https://github.com/torvalds/linux/blob/master/arch/x86/boot/compressed/head_64.S):
|
||||
我们已经在 [arch/x86/boot/compressed/head_64.S](https://github.com/torvalds/linux/blob/master/arch/x86/boot/compressed/head_64.S) 中看见了这些位被设置了:
|
||||
|
||||
```assembly
|
||||
movl $(X86_CR0_PG | X86_CR0_PE), %eax
|
||||
@@ -49,17 +49,17 @@ btsl $_EFER_LME, %eax
|
||||
wrmsr
|
||||
```
|
||||
|
||||
Paging structures
|
||||
分页数据结构
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
Paging divides the linear address space into fixed-size pages. Pages can be mapped into the physical address space or even external storage. This fixed size is `4096` bytes for the `x86_64` linux kernel. To perform the linear address translation to a physical address special structures are used. Every structure is `4096` bytes size and contains `512` entries (this only for `PAE` and `IA32_EFER.LME` modes). Paging structures are hierarchical and the linux kernel uses 4 level of paging in the `x86_64` architecture. The CPU uses a part of the linear address to identify the entry in another paging structure which is at the lower level or physical memory region (`page frame`) or physical address in this region (`page offset`). The address of the top level paging structure located in the `cr3` register. We already saw this in [arch/x86/boot/compressed/head_64.S](https://github.com/torvalds/linux/blob/master/arch/x86/boot/compressed/head_64.S):
|
||||
分页将线性地址分为固定尺寸的页。页会被映射进入物理地址空间或外部存储设备。这个固定尺寸在 `x86_64` 内核中是 `4096` 字节。为了将线性地址转换位物理地址,特殊的结构会被用到。每个结构都是 `4096` 字节并包含 `512` 项(这只为 `PAE` 和 `IA32_EFER.LME` 模式)。分页结构是层次级的, Linux 内核在 `x86_64` 框架中使用4层的分层机制。CPU使用一部分线性地址去确定另一个分页结构中的项,这个分页结构可能在最低层,物理内存区域(页框),在这个区域的物理地址(页偏移)。最高层的分页结构的地址存储在 `cr3` 寄存器中。我们已经从 [arch/x86/boot/compressed/head_64.S](https://github.com/torvalds/linux/blob/master/arch/x86/boot/compressed/head_64.S) 这个文件中已经看到了。
|
||||
|
||||
```assembly
|
||||
leal pgtable(%ebx), %eax
|
||||
movl %eax, %cr3
|
||||
```
|
||||
|
||||
We built the page table structures and put the address of the top-level structure in the `cr3` register. Here `cr3` is used to store the address of the top-level structure, the `PML4` or `Page Global Directory` as it is called in the linux kernel. `cr3` is 64-bit register and has the following structure:
|
||||
我们构建页表结构并且将这个最高层结构的地址存放在 `cr3` 寄存器中。这里 `cr3` 用于存储最高层结构的地址,在 Linux 内核中被称为 `PML4` 或 `Page Global Directory` 。 `cr3` 是一个64位的寄存器,并且有着如下的结构:
|
||||
|
||||
```
|
||||
63 52 51 32
|
||||
@@ -76,26 +76,26 @@ We built the page table structures and put the address of the top-level structur
|
||||
--------------------------------------------------------------------------------
|
||||
```
|
||||
|
||||
These fields have the following meanings:
|
||||
这些字段有着如下的意义:
|
||||
|
||||
* Bits 2:0 - ignored;
|
||||
* Bits 51:12 - stores the address of the top level paging structure;
|
||||
* Bit 3 and 4 - PWT or Page-Level Writethrough and PCD or Page-level cache disable indicate. These bits control the way the page or Page Table is handled by the hardware cache;
|
||||
* Reserved - reserved must be 0;
|
||||
* Bits 63:52 - reserved must be 0.
|
||||
* 第 0 到第 2 位 - 忽略;
|
||||
* 第 12 位到第 51 位 - 存储最高层分页结构的地址;
|
||||
* 第 3 位 到第 4 位 - PWT 或 Page-Level Writethrough 和 PCD 或 Page-level Cache Disable 显示。这些位控制页或者页表被硬件缓存处理的方式;
|
||||
* 保留位 - 保留,但必须为 0 ;
|
||||
* 第 52 到第 63 位 - 保留,但必须为 0 ;
|
||||
|
||||
The linear address translation address is following:
|
||||
线性地址转换过程如下所示:
|
||||
|
||||
* A given linear address arrives to the [MMU](http://en.wikipedia.org/wiki/Memory_management_unit) instead of memory bus.
|
||||
* 64-bit linear address splits on some parts. Only low 48 bits are significant, it means that `2^48` or 256 TBytes of linear-address space may be accessed at any given time.
|
||||
* `cr3` register stores the address of the 4 top-level paging structure.
|
||||
* `47:39` bits of the given linear address stores an index into the paging structure level-4, `38:30` bits stores index into the paging structure level-3, `29:21` bits stores an index into the paging structure level-2, `20:12` bits stores an index into the paging structure level-1 and `11:0` bits provide the byte offset into the physical page.
|
||||
* 一个给定的线性地址到达 [MMU](http://en.wikipedia.org/wiki/Memory_management_unit) 而不是存储器总线;
|
||||
* 64位线性地址分为很多部分。只有低 48 位是有意义的,它意味着 `2^48` 或 256TB 的线性地址空间在任意给定时间内都可以被访问;
|
||||
* `cr3` 寄存器存储这个最高层分页数据结构的地址;
|
||||
* 给定的线性地址中的第 39 位到第 47 位存储一个第 4 级分页结构的索引,第 30 位到第 38 位存储一个第3级分页结构的索引,第 29 位到第 21 位存储一个第 2 级分页结构的索引,第 12 位到第 20 位存储一个第 1 级分页结构的索引,第 0 位到第 11 位提供物理页的字节偏移;
|
||||
|
||||
schematically, we can imagine it like this:
|
||||
按照图示,我们可以这样想象它:
|
||||
|
||||

|
||||

|
||||
|
||||
Every access to a linear address is either a supervisor-mode access or a user-mode access. This access is determined by the `CPL` (current privilege level). If `CPL < 3` it is a supervisor mode access level otherwise, otherwise it is a user mode access level. For example, the top level page table entry contains access bits and has the following structure:
|
||||
每一个对线性地址的访问不是一个管态访问就是用户态访问。这个访问是被 `CPL (Current Privilege Level)` 所决定。如果 `CPL < 3` ,那么它是管态访问级,否则,它就是用户态访问级。比如,最高级页表项包含访问位和如下的结构:
|
||||
|
||||
```
|
||||
63 62 52 51 32
|
||||
@@ -112,33 +112,33 @@ Every access to a linear address is either a supervisor-mode access or a user-mo
|
||||
--------------------------------------------------------------------------------
|
||||
```
|
||||
|
||||
Where:
|
||||
其中:
|
||||
|
||||
* 63 bit - N/X bit (No Execute Bit) - presents ability to execute the code from physical pages mapped by the table entry;
|
||||
* 62:52 bits - ignored by CPU, used by system software;
|
||||
* 51:12 bits - stores physical address of the lower level paging structure;
|
||||
* 12:9 bits - ignored by CPU;
|
||||
* MBZ - must be zero bits;
|
||||
* Ignored bits;
|
||||
* A - accessed bit indicates was physical page or page structure accessed;
|
||||
* PWT and PCD used for cache;
|
||||
* U/S - user/supervisor bit controls user access to the all physical pages mapped by this table entry;
|
||||
* R/W - read/write bit controls read/write access to the all physical pages mapped by this table entry;
|
||||
* P - present bit. Current bit indicates was page table or physical page loaded into primary memory or not.
|
||||
* 第 63 位 - N/X 位(不可执行位)显示被这个页表项映射的所有物理页执行代码的能力;
|
||||
* 第 52 位到第 62 位 - 被CPU忽略,被系统软件使用;
|
||||
* 第 12 位到第 51 位 - 存储低级分页结构的物理地址;
|
||||
* 第 9 位到第 11 位 - 被 CPU 忽略;
|
||||
* MBZ - 必须为 0 ;
|
||||
* 忽略位;
|
||||
* A - 访问位暗示物理页或者页结构被访问;
|
||||
* PWT 和 PCD 用于缓存;
|
||||
* U/S - 用户/管理位控制对被这个页表项映射的所有物理页用户访问;
|
||||
* R/W - 读写位控制着被这个页表项映射的所有物理页的读写权限
|
||||
* P - 存在位。当前位表示页表或物理页是否被加载进内存;
|
||||
|
||||
Ok, we know about the paging structures and their entries. Now let's see some details about 4-level paging in the linux kernel.
|
||||
好的,我们知道了分页结构和它们的表项。现在我们来看一下 Linux 内核中的 4 级分页机制的一些细节。
|
||||
|
||||
Paging structures in the linux kernel
|
||||
Linux 内核中的分页结构
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
As we've seen, the linux kernel in `x86_64` uses 4-level page tables. Their names are:
|
||||
就如我们已经看到的那样, `x86_64`Linux 内核使用4级页表。它们的名字是:
|
||||
|
||||
* Page Global Directory
|
||||
* Page Upper Directory
|
||||
* Page Middle Directory
|
||||
* Page Table Entry
|
||||
* 页全局目录
|
||||
* 页上层目录
|
||||
* 页中间目录
|
||||
* 页表项
|
||||
|
||||
After you've compiled and installed the linux kernel, you can see the `System.map` file which stores the virtual addresses of the functions that are used by the kernel. For example:
|
||||
在你已经编译和安装 Linux 内核,你可以看到存储内核中使用的函数的虚拟地址的 `System.map` 文件。例如:
|
||||
|
||||
```
|
||||
$ grep "start_kernel" System.map
|
||||
@@ -146,7 +146,7 @@ ffffffff81efe497 T x86_64_start_kernel
|
||||
ffffffff81efeaa2 T start_kernel
|
||||
```
|
||||
|
||||
We can see `0xffffffff81efe497` here. I doubt you really have that much RAM installed. But anyway, `start_kernel` and `x86_64_start_kernel` will be executed. The address space in `x86_64` is `2^64` size, but it's too large, that's why a smaller address space is used, only 48-bits wide. So we have a situation where the physical address space is limited to 48 bits, but addressing still performed with 64 bit pointers. How is this problem solved? Look at this diagram:
|
||||
这里我们可以看见 `0xffffffff81efe497` 。我怀疑你是否真的有安装这么多内存。但是无论如何, `start_kernel` 和 `x86_64_start_kernel` 将会被执行。在 `x86_64` 中,地址空间的大小是 `2^64` ,但是它太大了,这就是为什么我们使用一个较小的地址空间,只是 48 位的宽度。所以一个情况出现,虽然物理地址空间限制到 48 位,但是寻址仍然使用 64 位指针。 这个问题是如何解决的?看下面的这个表。
|
||||
|
||||
```
|
||||
0xffffffffffffffff +-----------+
|
||||
@@ -166,12 +166,12 @@ We can see `0xffffffff81efe497` here. I doubt you really have that much RAM inst
|
||||
0x0000000000000000 +-----------+
|
||||
```
|
||||
|
||||
This solution is `sign extension`. Here we can see that the lower 48 bits of a virtual address can be used for addressing. Bits `63:48` can be either only zeroes or only ones. Note that the virtual address space is split in 2 parts:
|
||||
这个解决方案是 `sign extension` 。这里我们可以看到一个虚拟地址的低 48 位可以被用于寻址。第 48 位到第 63 位全是 0 或 1 。注意这个虚拟地址空间被分为两部分:
|
||||
|
||||
* Kernel space
|
||||
* Userspace
|
||||
* 内核空间
|
||||
* 用户空间
|
||||
|
||||
Userspace occupies the lower part of the virtual address space, from `0x000000000000000` to `0x00007fffffffffff` and kernel space occupies the highest part from `0xffff8000000000` to `0xffffffffffffffff`. Note that bits `63:48` is 0 for userspace and 1 for kernel space. All addresses which are in kernel space and in userspace or in other words which higher `63:48` bits are zeroes or ones are called `canonical` addresses. There is a `non-canonical` area between these memory regions. Together these two memory regions (kernel space and user space) are exactly `2^48` bits wide. We can find the virtual memory map with 4 level page tables in the [Documentation/x86/x86_64/mm.txt](https://github.com/torvalds/linux/blob/master/Documentation/x86/x86_64/mm.txt):
|
||||
用户空间占用虚拟地址空间的低部分,从 `0x000000000000000` 到 `0x00007fffffffffff` ,而内核空间占据从 `0xffff8000000000` 到 `0xffffffffffffffff` 的高部分。注意,第 48 位到第 63 位是对于用户空间是 0 ,对于内核空间是 1 。内核空间和用户空间中的所有地址是标准地址,而在这些内存区域中间有非标准区域。这两块内存区域(内核空间和用户空间)合起来是 48 位宽度。我们可以在 [Documentation/x86/x86_64/mm.txt](https://github.com/torvalds/linux/blob/master/Documentation/x86/x86_64/mm.txt) 找到 4 级页表下的虚拟内存映射:
|
||||
|
||||
```
|
||||
0000000000000000 - 00007fffffffffff (=47 bits) user space, different per mm
|
||||
@@ -193,21 +193,21 @@ ffffffffff600000 - ffffffffffdfffff (=8 MB) vsyscalls
|
||||
ffffffffffe00000 - ffffffffffffffff (=2 MB) unused hole
|
||||
```
|
||||
|
||||
We can see here the memory map for user space, kernel space and the non-canonical area in-between them. The user space memory map is simple. Let's take a closer look at the kernel space. We can see that it starts from the guard hole which is reserved for the hypervisor. We can find the definition of this guard hole in [arch/x86/include/asm/page_64_types.h](https://github.com/torvalds/linux/blob/master/arch/x86/include/asm/page_64_types.h):
|
||||
这里我们可以看到用户空间,内核空间和非标准空间的内存映射。用户空间的内存映射很简单。让我们来更近地查看内核空间。我们可以看到它始于为管理程序 (hypervisor) 保留的防卫空洞 (guard hole) 。我们可以在 [arch/x86/include/asm/page_64_types.h](https://github.com/torvalds/linux/blob/master/arch/x86/include/asm/page_64_types.h) 这个文件中看到防卫空洞的概念!
|
||||
|
||||
```C
|
||||
#define __PAGE_OFFSET _AC(0xffff880000000000, UL)
|
||||
```
|
||||
|
||||
Previously this guard hole and `__PAGE_OFFSET` was from `0xffff800000000000` to `0xffff80ffffffffff` to prevent access to non-canonical area, but was later extended by 3 bits for the hypervisor.
|
||||
以前防卫空洞和 `__PAGE_OFFSET` 是从 `0xffff800000000000` 到 `0xffff80ffffffffff` ,来防止对非标准区域的访问,但是后来为了管理程序扩展了 3 位。
|
||||
|
||||
Next is the lowest usable address in kernel space - `ffff880000000000`. This virtual memory region is for direct mapping of the all physical memory. After the memory space which maps all physical addresses, the guard hole. It needs to be between the direct mapping of all the physical memory and the vmalloc area. After the virtual memory map for the first terabyte and the unused hole after it, we can see the `kasan` shadow memory. It was added by [commit](https://github.com/torvalds/linux/commit/ef7f0d6a6ca8c9e4b27d78895af86c2fbfaeedb2) and provides the kernel address sanitizer. After the next unused hole we can see the `esp` fixup stacks (we will talk about it in other parts of this book) and the start of the kernel text mapping from the physical address - `0`. We can find the definition of this address in the same file as the `__PAGE_OFFSET`:
|
||||
紧接着是内核空间中最低的可用空间 - `ffff880000000000` 。这个虚拟地址空间是为了所有的物理内存的直接映射。在这块空间之后,还是防卫空洞。它位于所有物理内存的直接映射地址和被 vmalloc 分配的地址之间。在第一个 1TB 的虚拟内存映射和无用的空洞之后,我们可以看到 `ksan` 影子内存 (shadow memory) 。它是通过 [commit](https://github.com/torvalds/linux/commit/ef7f0d6a6ca8c9e4b27d78895af86c2fbfaeedb2) 提交到内核中,并且保持内核空间无害。在紧接着的无用空洞之后,我们可以看到 `esp` 固定栈(我们会在本书其他部分讨论它)。内核代码段的开始从物理地址 - `0` 映射。我们可以在相同的文件中找到将这个地址定义为 `__PAGE_OFFSET` 。
|
||||
|
||||
```C
|
||||
#define __START_KERNEL_map _AC(0xffffffff80000000, UL)
|
||||
```
|
||||
|
||||
Usually kernel's `.text` start here with the `CONFIG_PHYSICAL_START` offset. We saw it in the post about [ELF64](https://github.com/0xAX/linux-insides/blob/master/Theory/ELF.md):
|
||||
通常内核的 `.text` 段开始于 `CONFIG_PHYSICAL_START` 偏移。我们已经在 [ELF64](https://github.com/0xAX/linux-insides/blob/master/Theory/ELF.md) 相关帖子中看见。
|
||||
|
||||
```
|
||||
readelf -s vmlinux | grep ffffffff81000000
|
||||
@@ -216,43 +216,41 @@ readelf -s vmlinux | grep ffffffff81000000
|
||||
90766: ffffffff81000000 0 NOTYPE GLOBAL DEFAULT 1 startup_64
|
||||
```
|
||||
|
||||
Here i checked `vmlinux` with the `CONFIG_PHYSICAL_START` is `0x1000000`. So we have the start point of the kernel `.text` - `0xffffffff80000000` and offset - `0x1000000`, the resulted virtual address will be `0xffffffff80000000 + 1000000 = 0xffffffff81000000`.
|
||||
这里我将 `CONFIG_PHYSICAL_START` 设置为 `0x1000000` 来检查 `vmlinux` 。所以我们有内核代码段的起始点 - `0xffffffff80000000` 和 偏移 - `0x1000000` ,计算出来的虚拟地址将会是 `0xffffffff80000000 + 1000000 = 0xffffffff81000000` 。
|
||||
|
||||
After the kernel `.text` region there is the virtual memory region for kernel modules, `vsyscalls` and an unused hole of 2 megabytes.
|
||||
在内核代码段之后有一个为内核模块 `vsyscalls` 准备的虚拟内存区域和 2M 无用的空洞。
|
||||
|
||||
We've seen how the kernel's virtual memory map is laid out and how a virtual address is translated into a physical one. Let's take for example following address:
|
||||
我们已经看见内核虚拟内存映射是如何布局的以及虚拟地址是如何转换位物理地址。让我们以下面的地址为例:
|
||||
|
||||
```
|
||||
0xffffffff81000000
|
||||
```
|
||||
|
||||
In binary it will be:
|
||||
|
||||
在二进制内它将是:
|
||||
|
||||
```
|
||||
1111111111111111 111111111 111111110 000001000 000000000 000000000000
|
||||
63:48 47:39 38:30 29:21 20:12 11:0
|
||||
```
|
||||
|
||||
This virtual address is split in parts as described above:
|
||||
这个虚拟地址将被分为如下描述的几部分:
|
||||
|
||||
* `63:48` - bits not used;
|
||||
* `47:39` - bits of the given linear address stores an index into the paging structure level-4;
|
||||
* `38:30` - bits stores index into the paging structure level-3;
|
||||
* `29:21` - bits stores an index into the paging structure level-2;
|
||||
* `20:12` - bits stores an index into the paging structure level-1;
|
||||
* `11:0` - bits provide the byte offset into the physical page.
|
||||
* `48-63` - 不使用的位;
|
||||
* `37-49` - 给定线性地址的这些位描述一个 4 级分页结构的索引;
|
||||
* `30-38` - 这些位存储一个 3 级分页结构的索引;
|
||||
* `21-29` - 这些位存储一个 2 级分页结构的索引;
|
||||
* `12-20` - 这些位存储一个 1 级分页结构的索引;
|
||||
* `0-11` - 这些位提供物理页的偏移;
|
||||
|
||||
That is all. Now you know a little about theory of `paging` and we can go ahead in the kernel source code and see the first initialization steps.
|
||||
|
||||
Conclusion
|
||||
就这样了。现在你知道了一些关于分页理论,而且我们可以在内核源码上更近一步,查看那些最先的初始化步骤。
|
||||
|
||||
总结
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
It's the end of this short part about paging theory. Of course this post doesn't cover every detail of paging, but soon we'll see in practice how the linux kernel builds paging structures and works with them.
|
||||
这简短的关于分页理论的部分至此已经结束了。当然,这个帖子不可能包含分页的所有细节,但是我们很快会看到在实践中 Linux 内核如何构建分页结构以及使用它们工作。
|
||||
|
||||
**Please note that English is not my first language and I am really sorry for any inconvenience. If you've found any mistakes please send me PR to [linux-internals](https://github.com/0xAX/linux-internals).**
|
||||
|
||||
|
||||
Links
|
||||
链接
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
* [Paging on Wikipedia](http://en.wikipedia.org/wiki/Paging)
|
||||
|
||||
@@ -15,3 +15,5 @@
|
||||
[@mudongliang](https://github.com/mudongliang)
|
||||
|
||||
[@oska874](https://github.com/oska874)
|
||||
|
||||
[@cloudusers](https://github.com/cloudusers)
|
||||
|
||||
Reference in New Issue
Block a user