# 在内核安装代码的第一步 #https://0xax.gitbooks.io/linux-insides/content/Booting/linux-bootstrap-2.html 内核启动的第一步 -------------------------------------------------------------------------------- 在[上一节中](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`寄存器中。`GRTD`是一个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*可访问)。 *A*ccessible) or CRA(*C*onforming *R*eadable *A*ccessible)。 * 如果`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`就是内存段的长度。如果你知道一个内存地址的`偏移`,那么在没有开启分页机制的情况下,这个内存的物理地址就是`基地址+偏移` ![linear address](http://oi62.tinypic.com/2yo369v.jpg)