diff --git a/Booting/README.md b/Booting/README.md index 86747e6..95956e9 100644 --- a/Booting/README.md +++ b/Booting/README.md @@ -2,9 +2,8 @@ 本章介绍了Linux内核引导过程。你将在这看到一些描述内核加载过程的整个周期的相关文章: -* [从引导加载程序内核](http://xinqiu.gitbooks.io/linux-inside-zh/content/Booting/index.html/linux-bootstrap-1.html) - 介绍了从启动计算机到内核执行第一条指令之前的所有阶段; -* [在内核安装代码的第一步](http://xinqiu.gitbooks.io/linux-inside-zh/content/Booting/linux-bootstrap-2.html) - 介绍了在内核设置代码的第一个步骤。你会看到堆的初始化,查询不同的参数,如EDD,IST和等... -* [视频模式初始化和转换到保护模式](http://xinqiu.gitbooks.io/linux-inside-zh/content/Booting/linux-bootstrap-3.html) - 介绍了视频模式初始化内核设置代码并过渡到保护模式。 -* [过渡到64位模式](http://xinqiu.gitbooks.io/linux-inside-zh/content/Booting/linux-bootstrap-4.html) - 介绍了过渡到64位模式的准备并过渡到64位。 +* [从引导加载程序内核](https://xinqiu.gitbooks.io/linux-insides-cn/content/Booting/linux-bootstrap-1.html) - 介绍了从启动计算机到内核执行第一条指令之前的所有阶段; +* [在内核安装代码的第一步](https://xinqiu.gitbooks.io/linux-insides-cn/content/Booting/linux-bootstrap-2.html) - 介绍了在内核设置代码的第一个步骤。你会看到堆的初始化,查询不同的参数,如EDD,IST和等... +* [视频模式初始化和转换到保护模式](https://xinqiu.gitbooks.io/linux-insides-cn/content/Booting/linux-bootstrap-3.html) - 介绍了视频模式初始化内核设置代码并过渡到保护模式。 +* [过渡到64位模式](https://xinqiu.gitbooks.io/linux-insides-cn/content/Booting/linux-bootstrap-4.html) - 介绍了过渡到64位模式的准备并过渡到64位。 * [内核解压缩](http://xinqiu.gitbooks.io/linux-inside-zh/content/Booting/linux-bootstrap-5.html) - 介绍了内核解压缩之前的准备然后直接解压缩。 - diff --git a/Booting/linux-bootstrap-1.md b/Booting/linux-bootstrap-1.md index 5d32381..f323173 100644 --- a/Booting/linux-bootstrap-1.md +++ b/Booting/linux-bootstrap-1.md @@ -1,12 +1,12 @@ -内核引导过程. Part 1. +内核引导过程. 第一部分. ================================================================================ 从引导加载程序内核 -------------------------------------------------------------------------------- -如果你已经看过我之前的[文章](http://0xax.blogspot.com/search/label/asm),就知道之前我开始和底层编程打交道。我写了一些关于Linux x86_64 汇编的文章。同时,我开始深入研究Linux源代码。底层是如果工作的,程序是如何在电脑上运行的,他们是如何在内存中定位的,内核是如何管理进程和内存,网络堆栈是如何在底层工作的等等,这些我都非常感兴趣。因此,我决定去写另外的一系列文章关于**x86_64**框架的Linux内核。 +如果你已经看过我之前的[文章](http://0xax.blogspot.com/search/label/asm),就知道之前我开始和底层编程打交道。我写了一些关于 Linux x86_64 汇编的文章。同时,我开始深入研究 Linux 源代码。底层是如果工作的,程序是如何在电脑上运行的,他们是如何在内存中定位的,内核是如何管理进程和内存,网络堆栈是如何在底层工作的等等,这些我都非常感兴趣。因此,我决定去写另外的一系列文章关于 **x86_64** 框架的 Linux 内核。 -值得注意的是我不是一个专业的内核黑客并且我的工作不是为内核贡献代码。这只是小兴趣。我只是喜欢底层的东西,底层是如何工作的让我产生了很大的兴趣。如果你发现任何迷惑的地方或者你有任何问题/备注,[twitter](https://twitter.com/0xAX),[email](anotherworldofworld@gmail.com)我或者提一个[issue](https://github.com/0xAX/linux-insides).(PS:翻译上的问题请mail我:xinqiu.94@gmail.com或github上@xinqiu)。我会很高兴。所有的文章也可以在[linux-insides](https://github.com/0xAX/linux-insides)上看,如果你发现哪里英文或内容错误,随意提个PR。(PS:中文版地址:https://github.com/xinqiu/linux-insides) +请注意我不是一个专业的内核黑客并且我的工作不是为内核贡献代码。这只是小兴趣。我只是喜欢底层的东西,底层是如何工作的让我产生了很大的兴趣。如果你发现任何迷惑的地方或者你有任何问题/备注,[twitter](https://twitter.com/0xAX),[email](anotherworldofworld@gmail.com)我或者提一个[issue](https://github.com/0xAX/linux-insides).(PS:翻译上的问题请mail我:xinqiu.94@gmail.com或github上@xinqiu)。我会很高兴。所有的文章也可以在[linux-insides](https://github.com/0xAX/linux-insides)上看,如果你发现哪里英文或内容错误,随意提个PR。(PS:中文版地址:https://github.com/xinqiu/linux-insides) *注意这不是官方文档,只是学习和分享知识* @@ -18,12 +18,12 @@ 不管怎样,如果你才开始学一些,我会在这些文章中尝试去解释一些部分。好了,小的介绍结束,我们开始深入内核和底层。 -所有的代码实际上是内核 - 3.18.如果有任何改变,我将会做相应的更新。 +我们的文章是基于 Linux 内核 3.18 版本进行的,如果后续的内核版本有任何改变,我将作出相应的更新。 神奇的电源按钮,接下来会发生什么? -------------------------------------------------------------------------------- -尽管这一系列文章关于 Linux 内核,我们还没有从内核代码(至少在这一章)开始。好了,当你按下你笔记本或台式机的神奇电源按钮,它开始工作。在主板发送一个信号给[电源](https://en.wikipedia.org/wiki/Power_supply),电源提供电脑适当量的电力。一旦主板收到了[电源备妥信号](https://en.wikipedia.org/wiki/Power_good_signal),它会尝试运行 CPU 。CPU 复位寄存器里的所有剩余数据,设置预定义的值给每个寄存器。 +尽管这一系列文章关于 Linux 内核,我们还没有从内核代码(至少在这一章)开始。好了,当你按下你笔记本或台式机的神奇电源按钮,它开始工作。在主板发送一个信号给[电源](https://en.wikipedia.org/wiki/Power_supply),电源提供电脑适当量的电力。一旦主板收到了[电源备妥信号](https://en.wikipedia.org/wiki/Power_good_signal),它会尝试启动 CPU 。CPU 复位寄存器里的所有剩余数据,设置预定义的值给每个寄存器。 [80386](https://en.wikipedia.org/wiki/Intel_80386) @@ -35,7 +35,7 @@ CS selector 0xf000 CS base 0xffff0000 ``` -处理器开始在[实模式](https://en.wikipedia.org/wiki/Real_mode)工作,我们需要退回一点去理解在这种模式下的内存分割。所有 x86兼容处理器都支持实模式,从[8086](https://en.wikipedia.org/wiki/Intel_8086)到现在的Intel 64位 CPU。8086处理器有一个20位寻址总线,这意味着它可以对0到2^20 位地址空间进行操作(1Mb).不过它只有16位的寄存器,通过这个16位寄存器最大寻址是2^16即 0xffff(64 Kb)。[内存分配](http://en.wikipedia.org/wiki/Memory_segmentation) 被用来充分利用所有空闲地址空间。所有内存被分成固定的65535 字节或64 KB大小的小块。由于我们不能用16位寄存器寻址小于64KB的内存,一种替代的方法被设计出来了。一个地址包括两个部分:数据段起始地址和从该数据段起的偏移量。为了得到内存中的物理地址,我们要让数据段乘16并加上偏移量: +处理器开始在[实模式](https://en.wikipedia.org/wiki/Real_mode)工作,我们需要退回一点去理解在这种模式下的内存分割。所有 x86兼容处理器都支持实模式,从 [8086](https://en.wikipedia.org/wiki/Intel_8086)到现在的 Intel 64 位 CPU。8086 处理器有一个20位寻址总线,这意味着它可以对0到 2^20 位地址空间进行操作( 1Mb ).不过它只有16位的寄存器,通过这个16位寄存器最大寻址是 2^16 即 0xffff (64 Kb)。实模式使用[段式内存管理](http://en.wikipedia.org/wiki/Memory_segmentation) 来管理整个内存空间。所有内存被分成固定的 64KB 大小的小块。由于我们不能用16位寄存器寻址大于 64KB 的内存,一种替代的方法被设计出来了。一个地址包括两个部分:数据段起始地址和从该数据段起的偏移量。为了得到内存中的物理地址,我们要让数据段乘16并加上偏移量: ``` PhysicalAddress = Segment * 16 + Offset @@ -48,31 +48,31 @@ PhysicalAddress = Segment * 16 + Offset '0x20010' ``` -不过如果我们让最大端进行偏移:`0xffff:0xffff`,将会是: +不过如果我们使用16位2进制能表示的最大值进行寻址:`0xffff:0xffff`,根据上面的公式,结果将会是: ```python >>> hex((0xffff << 4) + 0xffff) '0x10ffef' ``` -这超出1MB65519字节。既然只有1MB在实模式中可以访问,`0x10ffef` 变成有[A20](https://en.wikipedia.org/wiki/A20_line)缺陷的 `0x00ffef`。 +这超出 1MB 65519 字节。既然实模式下, CPU 只能访问 1MB 地址空间,`0x10ffef` 变成有 [A20](https://en.wikipedia.org/wiki/A20_line) 缺陷的 `0x00ffef`。 -我们知道实模式和内存地址。回到复位后的寄存器值。 +我们了解了实模式和在实模式下的内存寻址方式,让我们来回头继续来看复位后的寄存器值。 -`CS` register consists of two parts: the visible segment selector and hidden base address. We know predefined `CS` base and `IP` value, logical address will be: +`CS` 寄存器包含两个部分:可视段选择器和隐含基址。 结合之前定义的 `CS` 基址和 `IP` 值,逻辑地址应该是: ``` 0xffff0000:0xfff0 ``` -In this way starting address formed by adding the base address to the value in the EIP register: +这种形式的起始地址为EIP寄存器里的值加上基址地址: ```python >>> 0xffff0000 + 0xfff0 '0xfffffff0' ``` -We get `0xfffffff0` which is 4GB - 16 bytes. This point is the [Reset vector](http://en.wikipedia.org/wiki/Reset_vector). This is the memory location at which CPU expects to find the first instruction to execute after reset. It contains a [jump](http://en.wikipedia.org/wiki/JMP_%28x86_instruction%29) instruction which usually points to the BIOS entry point. For example, if we look in [coreboot](http://www.coreboot.org/) source code, we will see it: +得到的 `0xfffffff0` 是 4GB - 16 字节。 这个地方是 [复位向量(Reset vector)](http://en.wikipedia.org/wiki/Reset_vector) 。 这是CPU在重置后期望执行的第一条指令的内存地址。它包含一个 [jump](http://en.wikipedia.org/wiki/JMP_%28x86_instruction%29) 指令,这个指令通常指向BIOS入口点。举个例子,如果访问 [coreboot](http://www.coreboot.org/) 源代码,将看到: ```assembly .section ".reset" @@ -84,7 +84,8 @@ reset_vector: ... ``` -We can see here the jump instruction [opcode](http://ref.x86asm.net/coder32.html#xE9) - 0xe9 to the address `_start - ( . + 2)`. And we can see that `reset` section is 16 bytes and starts at `0xfffffff0`: +上面的跳转指令( [opcode](http://ref.x86asm.net/coder32.html#xE9) - 0xe9)跳转到地址 `_start - ( . + 2)` 去执行代码。 `reset` 段是16字节代码段, 起始于地址 +`0xfffffff0`,因此 CPU 复位之后,就会跳到这个地址来执行相应的代码 : ``` SECTIONS { @@ -98,7 +99,7 @@ SECTIONS { } ``` -Now the BIOS has started to work. After initializing and checking the hardware, it needs to find a bootable device. A boot order is stored in the BIOS configuration. The function of boot order is to control which devices the kernel attempts to boot. In the case of attempting to boot a hard drive, the BIOS tries to find a boot sector. On hard drives partitioned with an MBR partition layout, the boot sector is stored in the first 446 bytes of the first sector (512 bytes). The final two bytes of the first sector are `0x55` and `0xaa` which signals the BIOS that the device is bootable. For example: +现在BIOS已经开始工作了。在初始化和检查硬件之后,需要寻找到一个可引导设备。可引导设备列表存储在在 BIOS 配置中, BIOS 将根据其中配置的顺序,尝试从不同的设备上寻找引导程序。对于硬盘,BIOS 将尝试寻找引导扇区。如果在硬盘上存在一个MBR分区,那么引导扇区储存在第一个扇区(512字节)的头446字节,引导扇区的最后必须是 `0x55` 和 `0xaa` ,这2个字节称为魔术字节,如果 BIOS 看到这2个字节,就知道这个设备是一个可引导设备。举个例子: ```assembly ; @@ -122,45 +123,45 @@ db 0x55 db 0xaa ``` -Build and run it with: +构建并运行: ``` nasm -f bin boot.nasm && qemu-system-x86_64 boot ``` -This will instruct [QEMU](http://qemu.org) to use the `boot` binary we just built as a disk image. Since the binary generated by the assembly code above fulfills the requirements of the boot sector (the origin is set to `0x7c00`, and we end with the magic sequence). QEMU will treat the binary as the master boot record(MBR) of a disk image. +这让 [QEMU](http://qemu.org) 使用刚才新建的 `boot` 二进制文件作为磁盘镜像。由于这个二进制文件是由上述汇编语言产生,它满足引导扇区(起始设为 `0x7c00`, 用Magic Bytes结束)的需求。QEMU将这个二进制文件作为磁盘镜像的主引导记录(MBR)。 -We will see: +将看到: ![Simple bootloader which prints only `!`](http://oi60.tinypic.com/2qbwup0.jpg) -In this example we can see that this code will be executed in 16 bit real mode and will start at 0x7c00 in memory. After the start it calls the [0x10](http://www.ctyme.com/intr/rb-0106.htm) interrupt which just prints `!` symbol. It fills rest of 510 bytes with zeros and finish with two magic bytes `0xaa` and `0x55`. +在这个例子中,这段代码被执行在16位的实模式,起始于内存0x7c00。之后调用 [0x10](http://www.ctyme.com/intr/rb-0106.htm) 中断打印 `!` 符号。用0填充剩余的510字节并用两个Magic Bytes `0xaa` 和 `0x55` 结束。 -You can see binary dump of it with `objdump` util: +可以使用 `objdump` 工具来查看转储信息: ``` nasm -f bin boot.nasm objdump -D -b binary -mi386 -Maddr16,data16,intel boot ``` -A real-world boot sector has code for continuing the boot process and the partition table instead of a bunch of 0's and an exclamation point :) Ok so, from this point onwards BIOS hands over the control to the bootloader and we can go ahead. +一个真实的启动扇区包含了分区表,已经用来启动系统的指令,而不是像我们上面的程序,只是输出了一个感叹号就结束了。从启动扇区的代码被执行开始,BIOS 就将系统的控制权转移给了引导程序,让我们继续往下看看引导程序都做了些什么。 -**NOTE**: As you can read above the CPU is in real mode. In real mode, calculating the physical address in memory is done as following: +**NOTE**: 强调一点,上面的引导程序是运行在实模式下的,因此 CPU 是使用下面的公式进行物理地址的计算的: ``` PhysicalAddress = Segment * 16 + Offset ``` -Same as I mentioned before. But we have only 16 bit general purpose registers. The maximum value of 16 bit register is: `0xffff`; So if we take the biggest values the result will be: +而且正如我前面所说的,在实模式下,CPU 只能使用16位的通用寄存器。16位寄存器能够表达的最大数值是:`0xffff` ,所以按照上面的公式计算出的最大物理地址是: ```python >>> hex((0xffff * 16) + 0xffff) '0x10ffef' ``` -Where `0x10ffef` is equal to `1MB + 64KB - 16b`. But a [8086](https://en.wikipedia.org/wiki/Intel_8086) processor, which was the first processor with real mode. It had 20 bit address line and `2^20 = 1048576.0` is 1MB. So, it means that the actual memory available is 1MB. +这个地址在 [8086](https://en.wikipedia.org/wiki/Intel_8086) 处理器下,将被转换成地址 `0x0ffef`, 原因是因为,8086 cpu 只有20位地址线,只能表示 `2^20 = 1MB` 的地址,而上面这个地址已经超出了 1MB 地址的范围,所以 CPU 就舍弃了最高位。 -General real mode's memory map is: +实模式下的 1MB 地址空间分配表: ``` 0x00000000 - 0x000003FF - Real Mode Interrupt Vector Table @@ -176,24 +177,24 @@ General real mode's memory map is: 0x000F0000 - 0x000FFFFF - System BIOS ``` -But stop, at the beginning of post I wrote that first instruction executed by the CPU is located at the address `0xFFFFFFF0`, which is much bigger than `0xFFFFF` (1MB). How can CPU access it in real mode? As I write about it and you can read in [coreboot](http://www.coreboot.org/Developer_Manual/Memory_map) documentation: +如果你的记性不错,在看到这张表的时候,一定会跳出来一个问题。在上面的章节中,我说了 CPU 执行的第一条指令是在地址 `0xFFFFFFF0` 处,这个地址远远大于 `0xFFFFF` ( 1MB )。那么实模式下的 CPU 是如何访问到这个地址的呢?文档 [coreboot](http://www.coreboot.org/Developer_Manual/Memory_map) 给出了答案: ``` 0xFFFE_0000 - 0xFFFF_FFFF: 128 kilobyte ROM mapped into address space ``` -At the start of execution BIOS is not in RAM, it is located in the ROM. +`0xFFFFFFF0` 这个地址被映射到了 ROM,因此 CPU 执行的第一条指令来自于 ROM,而不是 RAM。 -Bootloader +引导程序 -------------------------------------------------------------------------------- -There are a number of bootloaders which can boot Linux, such as [GRUB 2](https://www.gnu.org/software/grub/) and [syslinux](http://www.syslinux.org/wiki/index.php/The_Syslinux_Project). The Linux kernel has a [Boot protocol](https://github.com/torvalds/linux/blob/master/Documentation/x86/boot.txt) which specifies the requirements for bootloaders to implement Linux support. This example will describe GRUB 2. +在现实世界中,要启动 Linux 系统,有多种引导程序可以选择。比如 [GRUB 2](https://www.gnu.org/software/grub/) 和 [syslinux](http://www.syslinux.org/wiki/index.php/The_Syslinux_Project)。Linux内核通过 [Boot protocol](https://github.com/torvalds/linux/blob/master/Documentation/x86/boot.txt) 来定义应该如何实现引导程序。在这里我们将只介绍 GRUB 2。 -Now that the BIOS has chosen a boot device and transferred control to the boot sector code, execution starts from [boot.img](http://git.savannah.gnu.org/gitweb/?p=grub.git;a=blob;f=grub-core/boot/i386/pc/boot.S;hb=HEAD). This code is very simple due to the limited amount of space available, and contains a pointer that it uses to jump to the location of GRUB 2's core image. The core image begins with [diskboot.img](http://git.savannah.gnu.org/gitweb/?p=grub.git;a=blob;f=grub-core/boot/i386/pc/diskboot.S;hb=HEAD), which is usually stored immediately after the first sector in the unused space before the first partition. The above code loads the rest of the core image into memory, which contains GRUB 2's kernel and drivers for handling filesystems. After loading the rest of the core image, it executes [grub_main](http://git.savannah.gnu.org/gitweb/?p=grub.git;a=blob;f=grub-core/kern/main.c). +现在 BIOS 已经选择了一个启动设备,并且将控制权转移给了启动扇区中的代码,在我们的例子中,启动扇区代码是 [boot.img](http://git.savannah.gnu.org/gitweb/?p=grub.git;a=blob;f=grub-core/boot/i386/pc/boot.S;hb=HEAD)。因为这段代码只能占用一个扇区,因此非常简单,只做一些必要的初始化,然后就跳转到 GRUB 2's core image 去执行。 Core image 的代码请参考 [diskboot.img](http://git.savannah.gnu.org/gitweb/?p=grub.git;a=blob;f=grub-core/boot/i386/pc/diskboot.S;hb=HEAD),一般来说 core image 在磁盘上存储在启动扇区之后到第一个可用分区之前。core image 的初始化代码会把整个 core image (包括 GRUB 2的内核代码和文件系统驱动) 引导到内存中。 引导完成之后,[grub_main](http://git.savannah.gnu.org/gitweb/?p=grub.git;a=blob;f=grub-core/kern/main.c)将被调用。 -`grub_main` initializes console, gets base address for modules, sets root device, loads/parses grub configuration file, loads modules etc. At the end of execution, `grub_main` moves grub to normal mode. `grub_normal_execute` (from `grub-core/normal/main.c`) completes last preparation and shows a menu for selecting an operating system. When we select one of grub menu entries, `grub_menu_execute_entry` begins to be executed, which executes grub `boot` command. It starts to boot the selected operating system. +`grub_main` 初始化控制台,计算模块基地址,设置 root 设备,读取 grub 配置文件,加载模块。最后,将 GRUB 置于 normal 模式,在这个模式中,`grub_normal_execute` (from `grub-core/normal/main.c`) 将被调用以完成最后的准备工作,然后显示一个菜单列出所用可用的操作系统。当某个操作系统被选择之后,`grub_menu_execute_entry` 开始执行,它将调用 GRUB 的 `boot` 命令,来引导被选中的操作系统。 -As we can read in the kernel boot protocol, the bootloader must read and fill some fields of kernel setup header which starts at `0x01f1` offset from the kernel setup code. Kernel header [arch/x86/boot/header.S](https://github.com/torvalds/linux/blob/master/arch/x86/boot/header.S) starts from: +就像 kernel boot protocol 所描述的,引导程序必须填充 kernel setup header (位于 kernel setup code 偏移 `0x01f1` 处) 的必要字段。kernel setup header的定义开始于 [arch/x86/boot/header.S](https://github.com/torvalds/linux/blob/master/arch/x86/boot/header.S): ```assembly .globl hdr @@ -207,9 +208,9 @@ hdr: boot_flag: .word 0xAA55 ``` -The bootloader must fill this and the rest of the headers (only marked as `write` in the Linux boot protocol, for example [this](https://github.com/torvalds/linux/blob/master/Documentation/x86/boot.txt#L354)) with values which it either got from command line or calculated. We will not see description and explanation of all fields of kernel setup header, we will get back to it when kernel uses it. Anyway, you can find description of any field in the [boot protocol](https://github.com/torvalds/linux/blob/master/Documentation/x86/boot.txt#L156). +bootloader必须填充在 Linux boot protocol 中标记为 `write` 的头信息,比如 [type_of_loader](https://github.com/torvalds/linux/blob/master/Documentation/x86/boot.txt#L354),这些头信息可能来自命令行,或者通过计算得到。在这里我们不会详细介绍所有的 kernel setup header,我们将在需要的时候逐个介绍。不过,你可以自己通过 [boot protocol](https://github.com/torvalds/linux/blob/master/Documentation/x86/boot.txt#L156) 来了解这些设置。 -As we can see in kernel boot protocol, the memory map will be the following after kernel loading: +通过阅读 kernel boot protocol,在内核被引导如内存后,内存使用情况将入下表所示: ```shell | Protected-mode kernel | @@ -236,34 +237,33 @@ X+08000 +------------------------+ ``` -So after the bootloader transferred control to the kernel, it starts somewhere at: +所以当 bootloader 完成任务,将执行权移交给 kernel,kernel 的代码从以下地址开始执行: ``` 0x1000 + X + sizeof(KernelBootSector) + 1 +个人以为应该是 X + sizeof(KernelBootSector) + 1 因为 X 已经是一个具体的物理地址了,不是一个偏移 ``` -where `X` is the address of kernel bootsector loaded. In my case `X` is `0x10000`, we can see it in memory dump: +上面的公式中, `X` 是 kernel bootsector 被引导如内存的位置。在我的机器上, `X` 的值是 `0x10000`,我们可以通过 memory dump 来检查这个地址: ![kernel first address](http://oi57.tinypic.com/16bkco2.jpg) -Ok, now the bootloader has loaded Linux kernel into the memory, filled header fields and jumped to it. Now we can move directly to the kernel setup code. +到这里,引导程序完成它的使命,并将控制权移交给了 Linux kernel。下面我们就来看看 kernel setup code 都做了些什么。 -Start of Kernel Setup +内核设置 -------------------------------------------------------------------------------- -Finally we are in the kernel. Technically kernel didn't run yet, first of all we need to setup kernel, memory manager, process manager etc. Kernel setup execution starts from [arch/x86/boot/header.S](https://github.com/torvalds/linux/blob/master/arch/x86/boot/header.S) at the [_start](https://github.com/torvalds/linux/blob/master/arch/x86/boot/header.S#L293). It is a little strange at the first look, there are many instructions before it. +经过上面的一系列操作,我们终于进入到内核了。不过从技术上说,内核还没有被运行起来,因为首先我们需要正确设置内核,启动内存管理,进程管理等等。内核设置代码的运行起点是 [arch/x86/boot/header.S](https://github.com/torvalds/linux/blob/master/arch/x86/boot/header.S) 中定义的 [_start](https://github.com/torvalds/linux/blob/master/arch/x86/boot/header.S#L293) 函数。 在 `_start` 函数开始之前,还有很多的代码,那这些代码是做什么的呢? -Actually Long time ago Linux kernel had its own bootloader, but now if you run for example: +实际上 `_start` 开始之前的代码是 kenerl 自带的 bootloader。在很久以前,是可以使用这个 bootloader 来启动 Linux 的。不过在新的 Linux 中,这个 bootloader 代码已经不再启动 Linux 内核,而只是输出一个错误信息。 如果你运行下面的命令,直接使用 Linux 内核来启动,你会看到下图所示的错误: ``` qemu-system-x86_64 vmlinuz-3.18-generic ``` -You will see: - ![Try vmlinuz in qemu](http://oi60.tinypic.com/r02xkz.jpg) -Actually `header.S` starts from [MZ](https://en.wikipedia.org/wiki/DOS_MZ_executable) (see image above), error message printing and following [PE](https://en.wikipedia.org/wiki/Portable_Executable) header: +为了能够作为 bootloader 来使用, `header.S` 开始处定义了 [MZ] [MZ](https://en.wikipedia.org/wiki/DOS_MZ_executable) 魔术数字, 并且定义了 [PE](https://en.wikipedia.org/wiki/Portable_Executable) 头,在 PE 头中定义了输出的字符串: ```assembly #ifdef CONFIG_EFI_STUB @@ -279,9 +279,9 @@ pe_header: .word 0 ``` -It needs this for loading the operating system with [UEFI](https://en.wikipedia.org/wiki/Unified_Extensible_Firmware_Interface). Here we will not see how it works (we will these later in the next parts). +之所以代码需要这样写,这个是因为遵从 [UEFI](https://en.wikipedia.org/wiki/Unified_Extensible_Firmware_Interface) 的硬件需要这样的结构才能正常引导操作系统。 -So the actual kernel setup entry point is: +去除这些作为 bootloader 使用的代码,真正的内核代码就从 `_start` 开始了: ``` // header.S line 292 @@ -289,7 +289,7 @@ So the actual kernel setup entry point is: _start: ``` -Bootloader (grub2 and others) knows about this point (`0x200` offset from `MZ`) and makes a jump directly to this point, despite the fact that `header.S` starts from `.bstext` section which prints error message: +其他的 bootloader (grub2 and others) 知道 _start 所在的位置( 从 `MZ` 头开始偏移 `0x200` 字节 ),所以这些 bootloader 就会忽略所有在这个位置前的代码(这些之前的代码位于 `.bstext` 段中), 直接跳转到这个位置启动内核。 ``` // @@ -300,8 +300,6 @@ Bootloader (grub2 and others) knows about this point (`0x200` offset from `MZ`) .bsdata : { *(.bsdata) } ``` -So kernel setup entry point is: - ```assembly .globl _start _start: @@ -313,37 +311,33 @@ _start: // ``` -Here we can see `jmp` instruction opcode - `0xeb` to the `start_of_setup-1f` point. `Nf` notation means following: `2f` refers to the next local `2:` label. In our case it is label `1` which goes right after jump. It contains rest of setup [header](https://github.com/torvalds/linux/blob/master/Documentation/x86/boot.txt#L156) and right after setup header we can see `.entrytext` section which starts at `start_of_setup` label. +`_start` 开始就是一个 `jmp` 语句(`jmp` 语句的 opcode 是 `0xeb` ),这个跳转语句是一个短跳转,跟在后面的是一个相对地址 ( `start_of_setup - 1f ` )。在汇编代码中 `Nf` 代表了当前代码之后第一个标号为 `N` 的代码段的地址。回到我们的代码,在 `_start` 标号之后的第一个标号为 `1` 的代码段中包含了剩下的 setup header 结构。在标号为 `1` 的代码段结束之后,紧接着就是标号为 `start_of_setup` 的代码段 (这个代码段位于 `.entrytext` 代码区,这个代码段中的第一条指令实际上是内核开始执行之后的第一条指令) 。 -Actually it's the first code which starts to execute besides previous jump instruction. After kernel setup got the control from bootloader, first `jmp` instruction is located at `0x200` (first 512 bytes) offset from the start of kernel real mode. This we can read in Linux kernel boot protocol and also see in grub2 source code: +下面让我们来看一下 GRUB2 的代码是如何跳转到 `_start` 标号处的。从 Linux 内核代码中,我们知道 `_start` 标号的代码位于偏移 `0x200` 处。在 GRUB2 的源代码中我们可以看到下面的代码: ```C state.gs = state.fs = state.es = state.ds = state.ss = segment; state.cs = segment + 0x20; ``` -It means that segment registers will have following values after kernel setup starts to work: +在我的机器上,因为我的内核代码被加载到了内存地址 `0x10000` 处,所以在上面的代码执行完成之后 `cs = 0x1020` ( 因此第一条指令的内存地址将是 `cs << 4 + 0 = 0x10200`,刚好是 `0x10000` 开始后的 `0x200` 处的指令): ``` fs = es = ds = ss = 0x1000 cs = 0x1020 ``` -for my case when kernel loaded at `0x10000`. +从 `start_of_setup` 标号开是的代码需要完成下面这些事情: -After jump to `start_of_setup`, it needs to do the following things: +* 将所有段寄存器的值设置成一样的内容 +* 设置堆栈 +* 设置 [bss](https://en.wikipedia.org/wiki/.bss) (静态变量区) +* 跳转到 [main.c](https://github.com/torvalds/linux/blob/master/arch/x86/boot/main.c) 开始执行代码 -* Be sure that all values of all segment registers are equal -* Setup correct stack if needed -* Setup [bss](https://en.wikipedia.org/wiki/.bss) -* Jump to C code at [main.c](https://github.com/torvalds/linux/blob/master/arch/x86/boot/main.c) - -Let's look at implementation. - -Segment registers align +段寄存器设置 -------------------------------------------------------------------------------- -First of all it ensures that `ds` and `es` segment registers point to the same address and enables interrupts with `sti` instruction: +在代码的一开始,就将 `ds` 和 `es` 段寄存器的内容设置成一样,并且使用指令 `sti` 来允许中断发生: ```assembly movw %ds, %ax @@ -351,15 +345,7 @@ First of all it ensures that `ds` and `es` segment registers point to the same a sti ``` -As I wrote above, grub2 loads kernel setup code at `0x10000` address and `cs` at `0x1020` because execution doesn't start from the start of file, but from: - -``` -_start: - .byte 0xeb - .byte start_of_setup-1f -``` - -`jump`, which is 512 bytes offset from the [4d 5a](https://github.com/torvalds/linux/blob/master/arch/x86/boot/header.S#L47). Also need to align `cs` from `0x10200` to `0x10000` as all other segment registers. After that we setup the stack: +就像我在上面一节中所写的, 为了能够跳转到 `_start` 标号出执行代码,grub2 将 `cs` 段寄存器的值设置成了 `0x1020`,这个值和其他段寄存器都是不一样的,因此下面的代码就是将 `cs` 段寄存器的值和其他段寄存器一致: ```assembly pushw %ds @@ -367,12 +353,12 @@ _start: lretw ``` -push `ds` value to stack, and address of [6](https://github.com/torvalds/linux/blob/master/arch/x86/boot/header.S#L494) label and execute `lretw` instruction. When we call `lretw`, it loads address of label `6` to [instruction pointer](https://en.wikipedia.org/wiki/Program_counter) register and `cs` with value of `ds`. After it we will have `ds` and `cs` with the same values. +上面的代码使用了一个小小的技巧来重置 `cs` 寄存器的内容,下面我们就来仔细分析。 这段代码首先将 `ds`寄存器的值入栈,然后将标号为 [6](https://github.com/torvalds/linux/blob/master/arch/x86/boot/header.S#L494) 的代码段地址入栈 ,接着执行 `lretw` 指令,这条指令,将把标号为 `6` 的内存地址放入 `ip` 寄存器 ([instruction pointer](https://en.wikipedia.org/wiki/Program_counter)),将 `ds` 寄存器的值放入 `cs` 寄存器。 这样一来 `ds` 和 `cs` 段寄存器就拥有了相同的值。 -Stack Setup +设置堆栈 -------------------------------------------------------------------------------- -Actually, almost all of the setup code is preparation for C language environment in the real mode. The next [step](https://github.com/torvalds/linux/blob/master/arch/x86/boot/header.S#L467) is checking of `ss` register value and making of correct stack if `ss` is wrong: +绝大部分的 setup 代码都是为 C 语言运行环境做准备。在设置了 `ds` 和 `es` 寄存器之后,接下来 [step](https://github.com/torvalds/linux/blob/master/arch/x86/boot/header.S#L467) 的代码将检查 `ss` 寄存器的内容,如果寄存器的内容不对,那么将进行更正: ```assembly movw %ss, %dx @@ -381,15 +367,15 @@ Actually, almost all of the setup code is preparation for C language environment je 2f ``` -Generally, it can be 3 different cases: +当进入这段代码的时候, `ss` 寄存器的值可能是一下三种情况之一: -* `ss` has valid value 0x10000 (as all other segment registers beside `cs`) -* `ss` is invalid and `CAN_USE_HEAP` flag is set (see below) -* `ss` is invalid and `CAN_USE_HEAP` flag is not set (see below) +* `ss` 寄存器的值是 0x10000 ( 和其他除了 `cs` 寄存器之外的所有寄存器的一样) +* `ss` 寄存器的值不是 0x10000,但是 `CAN_USE_HEAP` 标志被设置了 +* `ss` 寄存器的值不是 0x10000,同时 `CAN_USE_HEAP` 标志没有被设置 -Let's look at all of these cases: +下面我们就来分析在这三中情况下,代码都是如何工作的: -1. `ss` has a correct address (0x10000). In this case we go to label [2](https://github.com/torvalds/linux/blob/master/arch/x86/boot/header.S#L481): +* `ss` 寄存器的值是 0x10000,在这种情况下,代码将直接跳转到标号为 `2` 的代码处执行: ``` 2: andw $~3, %dx @@ -400,20 +386,11 @@ Let's look at all of these cases: sti ``` -Here we can see aligning of `dx` (contains `sp` given by bootloader) to 4 bytes and checking that it is not zero. If it is zero we put `0xfffc` (4 byte aligned address before maximum segment size - 64 KB) to `dx`. If it is not zero we continue to use `sp` given by bootloader (0xf7f4 in my case). After this we put `ax` value to `ss` which stores correct segment address `0x10000` and set up correct `sp`. After it we have correct stack: +这段代码首先将 `dx` 寄存器的值(就是当前`sp` 寄存器的值)4字节对齐,然后检查是否为0(如果是0,堆栈就不对了,因为堆栈是从大地址向小地址发展的),如果是0,那么就将 `dx` 寄存器的值设置成 `0xfffc` (64KB地址段的最后一个4字节地址)。如果不是0,那么就保持当前值不变。接下来,就将 `ax` 寄存器的值( 0x10000 )设置到 `ss` 寄存器,并根据 `dx` 寄存器的值设置正确的 `sp`。这样我们就得到了正确的堆栈设置,具体请参考下图: ![stack](http://oi58.tinypic.com/16iwcis.jpg) -2. In the second case (`ss` != `ds`), first of all put [_end](https://github.com/torvalds/linux/blob/master/arch/x86/boot/setup.ld#L52) (address of end of setup code) value in `dx`. And check `loadflags` header field with `testb` instruction too see if we can use heap or not. [loadflags](https://github.com/torvalds/linux/blob/master/arch/x86/boot/header.S#L321) is a bitmask header which is defined as: - -```C -#define LOADED_HIGH (1<<0) -#define QUIET_FLAG (1<<5) -#define KEEP_SEGMENTS (1<<6) -#define CAN_USE_HEAP (1<<7) -``` - -And as we can read in the boot protocol: +* 下面让我们来看 `ss` != `ds`的情况,首先将 setup code 的结束地址 [_end](https://github.com/torvalds/linux/blob/master/arch/x86/boot/setup.ld#L52) 写入 `dx` 寄存器。然后检查 `loadflags` 中是否设置了 `CAN_USE_HEAP` 标志。 根据 kernel boot protocol 的定义,[loadflags](https://github.com/torvalds/linux/blob/master/arch/x86/boot/header.S#L321) 是一个标志字段。这个字段的 `Bit 7` 就是 `CAN_USE_HEAP` 标志: ``` Field name: loadflags @@ -426,29 +403,36 @@ Field name: loadflags functionality will be disabled. ``` -If `CAN_USE_HEAP` bit is set, put `heap_end_ptr` to `dx` which points to `_end` and add `STACK_SIZE` (minimal stack size - 512 bytes) to it. After this if `dx` is not carry, jump to `2` (it will not be carry, dx = _end + 512) label as in previous case and make correct stack. +`loadflags` 字段其他可以设置的标志包括: + +```C +#define LOADED_HIGH (1<<0) +#define QUIET_FLAG (1<<5) +#define KEEP_SEGMENTS (1<<6) +#define CAN_USE_HEAP (1<<7) +``` + +如果 `CAN_USE_HEAP` 被置位,那么将 `heap_end_ptr` 放入 `dx` 寄存器,然后加上 `STACK_SIZE` (最小堆栈大小是 512 bytes)。在加法完成之后,如果结果没有溢出(CF flag 没有置位,如果置位那么程序就出错了),那么就跳转到标号为 `2` 的代码处继续执行(这段代码的逻辑在1中已经详细介绍了),接着我们就得到了如下图所示的堆栈: ![stack](http://oi62.tinypic.com/dr7b5w.jpg) -3. The last case when `CAN_USE_HEAP` is not set, we just use minimal stack from `_end` to `_end + STACK_SIZE`: +* 最后一种情况就是 `CAN_USE_HEAP` 没有置位, 那么我们就将 `dx` 寄存器的值加上 `STACK_SIZE`,然后跳转到标号为 `2` 的代码处继续执行,接着我们就得到了如下图所示的堆栈: ![minimal stack](http://oi60.tinypic.com/28w051y.jpg) -BSS Setup +BSS段设置 -------------------------------------------------------------------------------- -The last two steps that need to happen before we can jump to the main C code, are that we need to set up the [BSS](https://en.wikipedia.org/wiki/.bss) area, and check the "magic" signature. Firstly, signature checking: +在我们正式执行 C 代码之前,我们还有2件事情需要完成。1)设置正确的 [BSS](https://en.wikipedia.org/wiki/.bss)段 ;2)检查 `magic` 签名。接下来的代码,首先检查 `magic` 签名 [setup_sig](https://github.com/torvalds/linux/blob/master/arch/x86/boot/setup.ld#L39),如果签名不对,直接跳转到 `setup_bad` 部分执行代码: ```assembly cmpl $0x5a5aaa55, setup_sig jne setup_bad ``` -This simply consists of comparing the [setup_sig](https://github.com/torvalds/linux/blob/master/arch/x86/boot/setup.ld#L39) against the magic number `0x5a5aaa55`. If they are not equal, a fatal error is reported. +如果 `magic` 签名是对的, 那么我们只要设置好 `BSS` 段,就可以开始执行 C 代码了。 -But if the magic number matches, knowing we have a set of correct segment registers, and a stack, we need only setup the BSS section before jumping into the C code. - -The BSS section is used for storing statically allocated, uninitialized, data. Linux carefully ensures this area of memory is first blanked, using the following code: +BSS 段用来存储那些没有被初始化的静态变量。对于这个段使用的内存, Linux 首先使用下面的代码将其全部清零: ```assembly movw $__bss_start, %di @@ -459,29 +443,31 @@ The BSS section is used for storing statically allocated, uninitialized, data. L rep; stosl ``` -First of all the [__bss_start](https://github.com/torvalds/linux/blob/master/arch/x86/boot/setup.ld#L47) address is moved into `di`, and the `_end + 3` address (+3 - aligns to 4 bytes) is moved into `cx`. The `eax` register is cleared (using an `xor` instruction), and the bss section size (`cx`-`di`) is calculated and put into `cx`. Then, `cx` is divided by four (the size of a 'word'), and the `stosl` instruction is repeatedly used, storing the value of `eax` (zero) into the address pointed to by `di`, and automatically increasing `di` by four (this occurs until `cx` reaches zero). The net effect of this code, is that zeros are written through all words in memory from `__bss_start` to `_end`: +在这段代码中,首先将 [__bss_start](https://github.com/torvalds/linux/blob/master/arch/x86/boot/setup.ld#L47) 地址放入 `di` 寄存器,然后将 `_end + 3` (4字节对齐) 地址放入 `cx`,接着使用 `xor` 指令将 `ax` 寄存器清零,接着计算 BSS 段的大小 ( `cx` - `di` ),让后将大小放入 `cx` 寄存器。接下来将 `cx` 寄存器除4,最后使用 `rep; stosl` 指令将 `ax` 寄存器的值(0)写入 寄存器整个 BSS 段。 代码执行完成之后,我们将得到如下图所示的 BSS 段: ![bss](http://oi59.tinypic.com/29m2eyr.jpg) -Jump to main +跳转到 main 函数 -------------------------------------------------------------------------------- -That's all, we have the stack, BSS and now we can jump to the `main()` C function: +到目前为止,我们完成了堆栈和 BSS 的设置,现在我们可以正式跳入 `main()` 函数来执行 C 代码了: ```assembly calll main ``` -The `main()` function is located in [arch/x86/boot/main.c](https://github.com/torvalds/linux/blob/master/arch/x86/boot/main.c). What will be there? We will see it in the next part. +`main()` 函数定义在 [arch/x86/boot/main.c](https://github.com/torvalds/linux/blob/master/arch/x86/boot/main.c),我们将在下一章详细介绍这个函数做了什么事情。 -Conclusion +结束语 -------------------------------------------------------------------------------- -This is the end of the first part about Linux kernel internals. If you have questions or suggestions, ping me in twitter [0xAX](https://twitter.com/0xAX), drop me [email](anotherworldofworld@gmail.com) or just create [issue](https://github.com/0xAX/linux-internals/issues/new). In the next part we will see first C code which executes in Linux kernel setup, implementation of memory routines as `memset`, `memcpy`, `earlyprintk` implementation and early console initialization and many more. +本章到此结束了,在下一章中我们将详细介绍在 Linux 内核设置过程中调用的第一个 C 代码( `main()` ),也将介绍诸如 `memset`, `memcpy`, `earlyprintk` 这些底层函数的实现,敬请期待。 -**Please note that English is not my first language and I am really sorry for any inconvenience. If you found any mistakes please send me PR to [linux-internals](https://github.com/0xAX/linux-internals).** +如果你有任何的问题或者建议,你可以留言,也可以直接发消息给我[twitter](https://twitter.com/0xAX)。 -Links +**英文不是我的母语。如果你发现我的英文描述有任何问题,请提交一个PR到[linux-insides](https://github.com/0xAX/linux-internals).** + +相关链接 -------------------------------------------------------------------------------- * [Intel 80386 programmer's reference manual 1986](http://css.csail.mit.edu/6.858/2014/readings/i386.pdf) diff --git a/Booting/linux-bootstrap-2.md b/Booting/linux-bootstrap-2.md old mode 100755 new mode 100644 index 049e8f0..906f609 --- a/Booting/linux-bootstrap-2.md +++ b/Booting/linux-bootstrap-2.md @@ -3,7 +3,7 @@ 内核启动的第一步 -------------------------------------------------------------------------------- -在[上一节中](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))。 +在[上一节中](https://xinqiu.gitbooks.io/linux-insides-cn/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))。 在这一节中我们将继续对内核启动过程的研究,我们将 * 认识`保护模式` @@ -22,7 +22,7 @@ 淘汰[实模式](http://wiki.osdev.org/Real_Mode)的主要原因是因为在实模式下,系统能够访问的内存非常有限。如果你还记得我们在上一节说的,在实模式下,系统最多只能访问1M内存,而且在很多时候,实际能够访问的内存只有640K。 -保护模式带来了很多的改变,不过主要的改变都集中在内存管理方法。在保护模式中,实模式的20位地址线被替换成32位地址线,因此系统可以访问多大4GB的地址空间。另外,在保护模式中引入了[内存分页](http://en.wikipedia.org/wiki/Paging)功能,在后面的章节中我们将介绍这个功能。 +保护模式带来了很多的改变,不过主要的改变都集中在内存管理方法。在保护模式中,实模式的20位地址线被替换成32位地址线,因此系统可以访问多达4GB的地址空间。另外,在保护模式中引入了[内存分页](http://en.wikipedia.org/wiki/Paging)功能,在后面的章节中我们将介绍这个功能。 保护模式提供了2种完全不同的内存管理机制: diff --git a/Booting/linux-bootstrap-3.md b/Booting/linux-bootstrap-3.md new file mode 100644 index 0000000..e71aab2 --- /dev/null +++ b/Booting/linux-bootstrap-3.md @@ -0,0 +1,622 @@ +内核启动过程,第三部分 +================================================================================ + +显示模式初始化和进入保护模式 +-------------------------------------------------------------------------------- + +这一章是`内核启动过程`的第三部分,在[前一章](linux-bootstrap-2.md#kernel-booting-process-part-2)中,我们的内核启动过程之旅停在了对 `set_video` 函数的调用(这个函数定义在 [main.c](https://github.com/torvalds/linux/blob/master/arch/x86/boot/main.c#L181))。在这一章中,我们将接着上一章继续我们的内核启动之旅。在这一章你将读到下面的内容: +- 显示模式的初始化, +- 在进入保护模式之前的准备工作, +- 正式进入保护模式 + +**注意** 如果你对保护模式一无所知,你可以查看[前一章](linux-bootstrap-2.md#protected-mode) 的相关内容。另外,你也可以查看下面这些[链接](linux-bootstrap-2.md#links) 以了解更多关于保护模式的内容。 + +就像我们前面所说的,我们将从 `set_video` 函数开始我们这章的内容,你可以在 [arch/x86/boot/video.c](https://github.com/torvalds/linux/blob/master/arch/x86/boot/video.c#L315) 找到这个函数的定义。 这个函数首先从 `boot_params.hdr` 数据结构获取显示模式设置: + +```C +u16 mode = boot_params.hdr.vid_mode; +``` + +至于 `boot_params.hdr` 数据结构中的内容,是通过 `copy_boot_params` 函数实现的 (关于这个函数的实现细节请查看上一章的内容),`boot_params.hdr` 中的 `vid_mode` 是引导程序必须填入的字段。你可以在 `kernel boot protocol` 文档中找到关于 `vid_mode` 的详细信息: + +``` +Offset Proto Name Meaning +/Size +01FA/2 ALL vid_mode Video mode control +``` + +而在 `linux kernel boot protocol` 文档中定义了如何通过命令行参数的方式为 `vid_mode` 字段传入相应的值: + +``` +**** SPECIAL COMMAND LINE OPTIONS +vga= + here is either an integer (in C notation, either + decimal, octal, or hexadecimal) or one of the strings + "normal" (meaning 0xFFFF), "ext" (meaning 0xFFFE) or "ask" + (meaning 0xFFFD). This value should be entered into the + vid_mode field, as it is used by the kernel before the command + line is parsed. +``` + +根据上面的描述,我们可以通过将 `vga` 选项写入 grub 或者起到引导程序的配置文件,从而让内核命令行得到相应的显示模式设置信息。这个选项可以接受不同类型的值来表示相同的意思。比如你可以传入 0XFFFD 或者 ask,这2个值都表示需要显示一个菜单让用户选择想要的显示模式。下面的链接就给出了这个菜单: + +![video mode setup menu](http://oi59.tinypic.com/ejcz81.jpg) + +通过这个菜单,用户可以选择想要进入的显示模式。不过再我们进一步了解显示模式的设置过程之前,让我们先回头了解一些重要的概念。 + +内核数据类型 +-------------------------------------------------------------------------------- + +在前面的章节中,我们已经接触到了一个类似于 `u16` 的内核数据类型。下面列出了更多内核支持的数据类型: + + +| Type | char | short | int | long | u8 | u16 | u32 | u64 | +|------|------|-------|-----|------|----|-----|-----|-----| +| Size | 1 | 2 | 4 | 8 | 1 | 2 | 4 | 8 | + +如果你尝试阅读内核代码,最好能够牢记这些数据类型。 + +堆操作 API +-------------------------------------------------------------------------------- + +在 `set_video` 函数将 `vid_mod` 的值设置完成之后,将调用 `RESET_HEAP` 宏将 HEAP 头指向 `_end` 符号。`RESET_HEAP` 宏定义在 [boot.h](https://github.com/torvalds/linux/blob/master/arch/x86/boot/boot.h#L199): + +```C +#define RESET_HEAP() ((void *)( HEAP = _end )) +``` + +如果你阅读过第二部分,你应该还记得在第二部分中,我们通过 [`init_heap`](https://github.com/torvalds/linux/blob/master/arch/x86/boot/main.c#L116) 函数完成了 HEAP 的初始化。在 `boot.h` 中定义了一系列的方法来操作被初始化之后的 HEAP。这些操作包括: + +```C +#define RESET_HEAP() ((void *)( HEAP = _end )) +``` + +就像我们在前面看到的,这个宏只是简单的将 HEAP 头设置到 `_end` 标号。在上一章中我们已经说明了 `_end` 标号,在 `boot.h` 中通过 `extern char _end[];` 来引用(从这里可以看出,在内核初始化的时候堆和栈是共享内存空间的,详细的信息可以查看第一章的堆栈初始化和第二章的堆初始化): + +下面一个是 `GET_HEAP` 宏: + +```C +#define GET_HEAP(type, n) \ + ((type *)__get_heap(sizeof(type),__alignof__(type),(n))) +``` + +可以看出这个宏调用了 `__get_heap` 函数来进行内存的分配。`__get_heap` 需要下面3个参数来进行内存分配操作: + +* 某个数据类型所占用的字节数 +* `__alignof__(type)` 返回对于请求的数据类型需要怎样的对齐方式 ( 根据我的了解这个是 gcc 提供的一个功能 ) +* `n` 需要分配对少个对应数据类型的对象 + +下面是 `__get_heap` 函数的实现: + +```C +static inline char *__get_heap(size_t s, size_t a, size_t n) +{ + char *tmp; + + HEAP = (char *)(((size_t)HEAP+(a-1)) & ~(a-1)); + tmp = HEAP; + HEAP += s*n; + return tmp; +} +``` + +现在让我们来了解这个函数是如何工作的。 这个函数首先根据对齐方式要求(参数 `a` )调整 `HEAP` 的值,然后将 `HEAP` 值赋值给一个临时变量 `tmp`。接下来根据需要分配的对象的个数(参数 `n` ),预留出所需要的内存,然后将 `tmp` 返回给调用端。 + +最后一个关于 HEAP 的操作是: + +```C +static inline bool heap_free(size_t n) +{ + return (int)(heap_end - HEAP) >= (int)n; +} +``` + +这个函数简单做了一个减法 `heap_end - HEAP`,如果相减的结果大于请求的内存,那么就返回真,否则返回假。 + +我们已经看到了所有可以对 HEAP 进行操作,下面让我们继续显示模式设置过程。 + +设置显示模式 +-------------------------------------------------------------------------------- + +在我们分析了内核数据类型以及和 HEAP 相关的操作之后,让我们回来继续分析显示模式的初始化。在 `RESET_HEAP()` 函数被调用之后,`set_video` 函数接着调用 `store_mode_params` 函数将对应显示模式的相关参数写入 `boot_params.screen_info` 字段。这个字段的结构定义可以在 [include/uapi/linux/screen_info.h](https://github.com/0xAX/linux/blob/master/include/uapi/linux/screen_info.h) 中找到。 + +`store_mode_params` 函数将调用 `store_cursor_position` 函数将当前屏幕上光标的位置保存起来。下面让我们来看 `store_cursor_poistion` 函数是如何实现的。 + +首先函数初始化一个类型为 `biosregs` 的变量,将其中的 `AH` 寄存器内容设置成 `0x3`,然后调用 `0x10` BIOS 中断。当中断调用返回之后,`DL` 和 `DH` 寄存器分别包含了当前光标的行和列信息。接着,这2个信息将被保存到 `boot_params.screen_info` 字段的 `orig_x` 和 `orig_y`字段。 + +在 `store_cursor_position` 函数执行完毕之后,`store_mode_params` 函数将调用 `store_vide_mode` 函数将当前使用的显示模式保存到 `boot_params.screen_info.orig_video_mode`。 + +接下來 `store_mode_params` 函数将根据当前显示模式的设定,给 `video_segment` 变量设置正确的值(实际上就是设置显示内存的起始地址)。在 BIOS 将控制权转移到引导扇区的时候,显示内存地址和显示模式的对应关系如下表所示: + +``` +0xB000:0x0000 32 Kb Monochrome Text Video Memory +0xB800:0x0000 32 Kb Color Text Video Memory +``` + +根据上表,如果当前显示模式是 MDA, HGC 或者单色 VGA 模式,那么 `video_sgement` 的值将被设置成 `0xB000`;如果当前显示模式是彩色模式,那么 `video_segment` 的值将被设置成 `0xB800`。在这之后,`store_mode_params` 函数将保存字体大小信息到 `boot_params.screen_info.orig_video_points`: + +```C +//保存字体大小信息 +set_fs(0); +font_size = rdfs16(0x485); +boot_params.screen_info.orig_video_points = font_size; +``` + +这段代码首先调用 `set_fs` 函数(在 [boot.h](https://github.com/0xAX/linux/blob/master/arch/x86/boot/boot.h) 中定义了许多类似的函数进行寄存器操作)将数字 `0` 放入 `FS` 寄存器。接着从内存地址 `0x485` 处获取字体大小信息并保存到 `boot_params.screen_info.orig_video_points`。 + +``` + x = rdfs16(0x44a); + y = (adapter == ADAPTER_CGA) ? 25 : rdfs8(0x484)+1; +``` + +接下来代码将从地址 `0x44a` 处获得屏幕列信息,从地址 `0x484` 处获得屏幕行信息,并将它们保存到 `boot_params.screen_info.orig_video_cols` 和 `boot_params.screen_info.orig_video_lines`。到这里,`store_mode_params` 的执行就结束了。 + +接下来,`set_video` 函数将调用 `save_screen` 函数将当前屏幕上的所有信息保存到 HEAP 中。这个函数首先获得当前屏幕的所有信息(包括屏幕大小,当前光标位置,屏幕上的字符信息),并且保存到 `saved_screen` 结构体中。这个结构体的定义如下所示: + +```C +static struct saved_screen { + int x, y; + int curx, cury; + u16 *data; +} saved; +``` + +接下来函数将检查 HEAP 中是否有足够的空间保存这个结构体的数据: + +```C +if (!heap_free(saved.x*saved.y*sizeof(u16)+512)) + return; +``` + +如果 HEAP 有足够的空间,代码将在 HEAP 中分配相应的空间并且将 `saved_screen` 保存到 HEAP。 + +接下来 `set_video` 函数将调用 `probe_cards(0)`(这个函数定义在 [arch/x86/boot/video-mode.c](https://github.com/0xAX/linux/blob/master/arch/x86/boot/video-mode.c#L33))。 这个函数简单遍历所有的显卡,并通过调用驱动程序设置显卡所支持的显示模式: + +```C +for (card = video_cards; card < video_cards_end; card++) { + if (card->unsafe == unsafe) { + if (card->probe) + card->nmodes = card->probe(); + else + card->nmodes = 0; + } +} +``` + +如果你仔细看上面的代码,你会发现 `video_cards` 这个变量并没有被声明,那么程序怎么能够正常编译执行呢?实际上很简单,它指向了一个在 [arch/x86/boot/setup.ld](https://github.com/0xAX/linux/blob/master/arch/x86/boot/setup.ld) 中定义的叫做 `.videocards` 的内存段: +``` + .videocards : { + video_cards = .; + *(.videocards) + video_cards_end = .; + } +``` +那么这段内存里面存放的数据是什么呢,下面我们就来详细分析。在内核初始化代码中,对于每个支持的显示模式都是使用下面的代码进行定义的: + +```C +static __videocard video_vga = { + .card_name = "VGA", + .probe = vga_probe, + .set_mode = vga_set_mode, +}; +``` + +`__videocard` 是一个宏定义,如下所示: + +```C +#define __videocard struct card_info __attribute__((used,section(".videocards"))) +``` + +因此 `__videocard` 是一个 `card_info` 结构,这个结构定义如下: + +```C +struct card_info { + const char *card_name; + int (*set_mode)(struct mode_info *mode); + int (*probe)(void); + struct mode_info *modes; + int nmodes; + int unsafe; + u16 xmode_first; + u16 xmode_n; +}; +``` + +在 `.videocards` 内存段实际上存放的就是所有被内核初始化代码定义的 `card_info` 结构(可以看成是一个数组),所以 `probe_cards` 函数可以使用 `video_cards`,通过循环遍历所有的 `card_info`。 + +在 `probe_cards` 执行完成之后,我们终于进入 `set_video` 函数的主循环了。在这个循环中,如果 `vid_mode=ask`,那么将显示一个菜单让用户选择想要的显示模式,然后代码将根据用户的选择或者 `vid_mod` 的值 ,通过调用 `set_mode` 函数来设置正确的显示模式。如果设置成功,循环结束,否则显示菜单让用户选择显示模式,继续进行设置显示模式的尝试。 + +```c +for (;;) { + if (mode == ASK_VGA) + mode = mode_menu(); + + if (!set_mode(mode)) + break; + + printf("Undefined video mode number: %x\n", mode); + mode = ASK_VGA; + } +``` + +你可以在 [video-mode.c](https://github.com/0xAX/linux/blob/master/arch/x86/boot/video-mode.c#L147) 中找到 `set_mode` 函数的定义。这个函数只接受一个参数,这个参数是对应的显示模式的数字表示(这个数字来自于显示模式选择菜单,或者从内核命令行参数获得)。 + +`set_mode` 函数首先检查传入的 `mode` 参数,然后调用 `raw_set_mode` 函数。而后者将遍历内核知道的所有 `card_info` 信息,如果发现某张显卡支持传入的模式,这调用 `card_info` 结构中保存的 `set_mode` 函数地址进行显卡显示模式的设置。以 `video_vga` 这个 `card_info` 结构来说,保存在其中的 `set_mode` 函数就指向了 `vga_set_mode` 函数。下面的代码就是 `vga_set_mode` 函数的实现,这个函数根据输入的 vga 显示模式,调用不同的函数完成显示模式的设置: + +```C +static int vga_set_mode(struct mode_info *mode) +{ + vga_set_basic_mode(); + + force_x = mode->x; + force_y = mode->y; + + switch (mode->mode) { + case VIDEO_80x25: + break; + case VIDEO_8POINT: + vga_set_8font(); + break; + case VIDEO_80x43: + vga_set_80x43(); + break; + case VIDEO_80x28: + vga_set_14font(); + break; + case VIDEO_80x30: + vga_set_80x30(); + break; + case VIDEO_80x34: + vga_set_80x34(); + break; + case VIDEO_80x60: + vga_set_80x60(); + break; + } + return 0; +} +``` + +在上面的代码中,每个 `vga_set***` 函数只是简单调用 `0x10` BIOS 中断来进行显示模式的设置。 + +在显卡的显示模式被正确设置之后,这个最终的显示模式被写回 `boot_params.hdr.vid_mode`。 + +接下来 `set_video` 函数将调用 `vesa_store_edid` 函数, 这个函数只是简单的将 [EDID](https://en.wikipedia.org/wiki/Extended_Display_Identification_Data) (**E**xtended **D**isplay **I**dentification **D**ata) 写入内存,以便于内核访问。最后, `set_video` 将调用 `do_restore` 函数将前面保存的当前屏幕信息还原到屏幕上。 + +到这里为止,显示模式的设置完成,接下来我们可以切换到保护模式了。 + +在切换到保护模式之前的最后的准备工作 +-------------------------------------------------------------------------------- + +在进入保护模式之前的最后一个函数调用发生在 [main.c](https://github.com/torvalds/linux/blob/master/arch/x86/boot/main.c#L184) 中的 `go_to_protected_mode` 函数,就像这个函数的注释说的,这个函数将进行最后的准备工作然后进入保护模式,下面就让我们来具体看看最后的准备工作是什么,以及系统是如何切换如保护模式的。 + +`go_to_protected_mode` 函数本身定义在 [arch/x86/boot/pm.c](https://github.com/torvalds/linux/blob/master/arch/x86/boot/pm.c#L104)。 这个函数调用了一些其他的函数进行最后的准备工作,下面就让我们来具体看看这些函数。 + +`go_to_protected_mode` 函数首先调用的是 `realmode_switch_hook` 函数,后者如果发现 `realmode_switch` hook, 那么将调用它并禁止 [NMI](http://en.wikipedia.org/wiki/Non-maskable_interrupt) 中断,反之将直接禁止 NMI 中断。只有当 bootloader 运行在宿主环境下(比如在 DOS 下运行 ), hook 才会被使用。你可以在 [boot protocol](https://www.kernel.org/doc/Documentation/x86/boot.txt) (see **ADVANCED BOOT LOADER HOOKS**) 中详细了解 hook 函数的信息。 + +```c +/* + * Invoke the realmode switch hook if present; otherwise + * disable all interrupts. + */ +static void realmode_switch_hook(void) +{ + if (boot_params.hdr.realmode_swtch) { + asm volatile("lcallw *%0" + : : "m" (boot_params.hdr.realmode_swtch) + : "eax", "ebx", "ecx", "edx"); + } else { + asm volatile("cli"); + outb(0x80, 0x70); /* Disable NMI */ + io_delay(); + } +} +``` + +`realmode_switch` 指向了一个16 位实模式代码地址(远跳转指针),这个16位代码将禁止 NMI 中断。所以在上述代码中,如果 `realmode_swtch` hook 存在,代码是用了 `lcallw` 指令进行远函数调用。在我的环境中,因为不存在这个 hook ,所以代码是直接进入 `else` 部分进行了 NMI 的禁止: + +```assembly +asm volatile("cli"); +outb(0x80, 0x70); /* Disable NMI */ +io_delay(); +``` + +上面的代码首先调用 `cli` 汇编指令清楚了中断标志 `IF`,这条指令执行之后,外部中断就被禁止了,紧接着的下一行代码就禁止了 NMI 中断。 + +这里简单介绍一下中断。中断是由硬件或者软件产生的,当中断产生的时候, CPU 将得到通知。这个时候, CPU 将停止当前指令的执行,保存当前代码的环境,然后将控制权移交到中断处理程序。当中断处理程序完成之后,将恢复中断之前的运行环境,从而被中断的代码将继续运行。 NMI 中断是一类特殊的中断,往往预示着系统发生了不可恢复的错误,所以在正常运行的操作系统中,NMI 中断是不会被禁止的,但是在进入保护模式之前,由于特殊需求,代码禁止了这类中断。我们将在后续的章节中对中断做更多的介绍,这里就不展开了。 + +现在让我们回到上面的代码,在 NMI 中断被禁止之后(通过写 `0x80` 进 CMOS 地址寄存器 `0x70` ),函数接着调用了 `io_delay` 函数进行了短暂的延时以等待 I/O 操作完成。下面就是 `io_delay` 函数的实现: + +```C +static inline void io_delay(void) +{ + const u16 DELAY_PORT = 0x80; + asm volatile("outb %%al,%0" : : "dN" (DELAY_PORT)); +} +``` + +对 I/O 端口 `0x80` 写入任何的字节都将得到 1 ms 的延时。在上面的代码中,代码将 `al` 寄存器中的值写到了这个端口。在这个 `io_delay` 调用完成之后, `realmode_switch_hook` 函数就完成了所有工作,下面让我们进入下一个函数。 + +下一个函数调用是 `enable_a20`,这个函数使能 [A20 line](http://en.wikipedia.org/wiki/A20_line),你可以在 [arch/x86/boot/a20.c](https://github.com/torvalds/linux/blob/master/arch/x86/boot/a20.c) 找到这个函数的定义,这个函数会尝试使用不同的方式来使能 A20 地址线。首先这个函数将调用 `a20_test_short`(该函数将调用 `a20_test` 函数) 来检测 A20 地址线是否已经被激活了: + +```C +static int a20_test(int loops) +{ + int ok = 0; + int saved, ctr; + + set_fs(0x0000); + set_gs(0xffff); + + saved = ctr = rdfs32(A20_TEST_ADDR); + + while (loops--) { + wrfs32(++ctr, A20_TEST_ADDR); + io_delay(); /* Serialize and make delay constant */ + ok = rdgs32(A20_TEST_ADDR+0x10) ^ ctr; + if (ok) + break; + } + + wrfs32(saved, A20_TEST_ADDR); + return ok; +} +``` + +这个函数首先将 `0x0000` 放入 `FS` 寄存器,将 `0xffff` 放入 `GS` 寄存器。然后通过 `rdfs32` 函数调用,将 `A20_TEST_ADDR` 内存地址的内容放入 `saved` 和 `ctr` 变量。 + +接下来我们使用 `wrfs32` 函数将更新过的 `ctr` 的值写入 `fs:gs` ,接着延时 1ms, 然后从 `GS:A20_TEST_ADDR+0x10` 读取内容,如果该地址内容不为0,那么 A20 已经被激活。如果 A20 没有被激活,代码将尝试使用多种方法进行 A20 地址激活。其中的一种方法就是调用 BIOS `0X15` 中断激活 A20 地址线。 + +如果 `enabled_a20` 函数调用失败,显示一个错误消息并且调用 `die` 函数结束操作系统运行。`die` 函数定义在 [arch/x86/boot/header.S](https://github.com/torvalds/linux/blob/master/arch/x86/boot/header.S): + +```assembly +die: + hlt + jmp die + .size die, .-die +``` + +A20 地址线被激活之后,`reset_coprocessor` 函数被调用: + + ```C +outb(0, 0xf0); +outb(0, 0xf1); +``` + +这个函数非常简单,通过将 `0` 写入 I/O 端口 `0xf0` 和 `0xf1` 以复位数字协处理器。 + +接下来 `mask_all_interrupts` 函数将被调用: + +```C +outb(0xff, 0xa1); /* Mask all interrupts on the secondary PIC */ +outb(0xfb, 0x21); /* Mask all but cascade on the primary PIC */ +``` + +这个函数调用激活主和从中断控制器 (Programmable Interrupt Controller)上的中断,唯一的例外是主中断控制器上的级联中断(所有从中断控制器的中断将通过这个级联中断报告给 CPU )。 + +到这里位置,我们就完成了所有的准备工作,下面我们就将正式开始从实模式转换到保护模式。 + +设置中断描述符表 +-------------------------------------------------------------------------------- + +现在内核将调用 `setup_idt` 方法来设置中断描述符表( IDT ): + +```C +static void setup_idt(void) +{ + static const struct gdt_ptr null_idt = {0, 0}; + asm volatile("lidtl %0" : : "m" (null_idt)); +} +``` + +上面的代码使用 `lidtl` 指令将 `null_idt` 所指向的中断描述符表引入寄存器 IDT。由于 `null_idt` 没有设定中断描述符表的长度(长度为 0 ),所以这段指令执行之后,实际上没有任何中断调用被设置成功(所有中断调用都是空的),在后面的章节中我们将看到正确的设置。`null_idt` 是一个 `gdt_ptr` 机构的数据,这个结构的定义如下所示: + +```C +struct gdt_ptr { + u16 len; + u32 ptr; +} __attribute__((packed)); +``` + +在上面的定义中,我们可以看到上面这个结构包含一个 16 bit 的长度字段,和一个 32 bit 的指针字段。`__attribute__((packed))` 意味着这个结构就只包含 48 bit 信息(没有字节对齐优化)。在下面一节中,我们将看到相同的结构将被导入 `GDTR` 寄存器(如果你还记得上一章的内容,应该记得 GDTR 寄存器是 48 bit 长度的)。 + +设置全局描述符表 +-------------------------------------------------------------------------------- + +在设置完中断描述符表之后,我们将使用 `setup_gdt` 函数来设置全部描述符表(关于全局描述符表,大家可以参考[上一章](linux-bootstrap-2.md#protected-mode) 的内容)。在 `setup_gdt` 函数中,使用 `boot_gdt` 数组定义了需要引入 GDTR 寄存器的段描述符信息: + +```C + //GDT_ENTRY_BOOT_CS 定义在http://lxr.free-electrons.com/source/arch/x86/include/asm/segment.h#L19 = 2 + static const u64 boot_gdt[] __attribute__((aligned(16))) = { + [GDT_ENTRY_BOOT_CS] = GDT_ENTRY(0xc09b, 0, 0xfffff), + [GDT_ENTRY_BOOT_DS] = GDT_ENTRY(0xc093, 0, 0xfffff), + [GDT_ENTRY_BOOT_TSS] = GDT_ENTRY(0x0089, 4096, 103), + }; +``` + +在上面的 `boot_gdt` 数组中,我们定义了代码,数据和 TSS 段的段描述符,因为我们并没有设置任何的中断调用(记得上面所的 `null_idt`吗?),所以 TSS 段并不会被使用到。TSS 段存在的唯一目的就是让 Intel 处理器能够正确进入保护模式。下面让我们详细了解一下 `boot_gdt` 这个数组,首先,这个数组被 `__attribute__((aligned(16)))` 修饰,这就意味着这个数组将以 16 字节为单位对齐。让我们通过下面的例子来了解一下什么叫 16 字节对齐: + +```C +#include + +struct aligned { + int a; +}__attribute__((aligned(16))); + +struct nonaligned { + int b; +}; + +int main(void) +{ + struct aligned a; + struct nonaligned na; + + printf("Not aligned - %zu \n", sizeof(na)); + printf("Aligned - %zu \n", sizeof(a)); + + return 0; +} +``` + +上面的代码可以看出,一旦指定了 16 字节对齐,即使结构中只有一个 `int` 类型的字段,整个结构也将占用 16 个字节: + +``` +$ gcc test.c -o test && test +Not aligned - 4 +Aligned - 16 +``` + +因为在 `boot_gdt` 的定义中, `GDT_ENTRY_BOOT_CS = 2`,所以在数组中有2个空项,第一项是一个空的描述符,第二项在代码中没有使用。在没有 `align 16` 之前,整个结构占用了(8*5=40)个字节,加了 `align 16` 之后,结构就占用了 48 字节 。 + +上面代码中出现的 `GDT_ENTRY` 是一个宏定义,这个宏接受 3 个参数(标志,基地址,段长度)来产生段描述符结构。让我们来具体分析上面数组中的代码段描述符( `GDT_ENTRY_BOOT_CS` )来看看这个宏是如何工作的,对于这个段,`GDT_ENTRY` 接受了下面 3 个参数: + +* 基地址 - 0 +* 段长度 - 0xfffff +* 标志 - 0xc09b + +上面这些数字表明,这个段的基地址是 0, 段长度是 `0xfffff` ( 1 MB ),而标志字段展开之后是下面的二进制数据: + +``` +1100 0000 1001 1011 +``` + +这些二进制数据的具体含义如下: + +* 1 - (G) 这里为 1,表示段的实际长度是 `0xfffff * 4kb ` = `4GB` +* 1 - (D) 表示这个段是一个32位段 +* 0 - (L) 这个代码段没有运行在 long mode +* 0 - (AVL) Linux 没有使用 +* 0000 - 段长度的4个位 +* 1 - (P) 段已经位于内存中 +* 00 - (DPL) - 段优先级为0 +* 1 - (S) 说明这个段是一个代码或者数据段 +* 101 - 段类型为可执行/可读 +* 1 - 段可访问 + +关于段描述符的更详细的信息你可以从上一章中获得 [上一章](linux-bootstrap-2.md),你也可以阅读 [Intel® 64 and IA-32 Architectures Software Developer's Manuals 3A](http://www.intel.com/content/www/us/en/processors/architectures-software-developer-manuals.html)获取全部信息。 + +在定义了数组之后,代码将获取 GDT 的长度: + +```C +gdt.len = sizeof(boot_gdt)-1; +``` + +接下来是将 GDT 的地址放入 gdt.ptr 中: + +```C +gdt.ptr = (u32)&boot_gdt + (ds() << 4); +``` + +这里的地址计算很简单,因为我们还在实模式,所以就是 ( ds << 4 + 数组起始地址)。 + +最后通过执行 `lgdtl` 指令将 GDT 信息写入 GDTR 寄存器: + +```C +asm volatile("lgdtl %0" : : "m" (gdt)); +``` + +切换进入保护模式 +-------------------------------------------------------------------------------- + +`go_to_protected_mode` 函数在完成 IDT, GDT 初始化,并禁止了 NMI 中断之后,将调用 `protected_mode_jump` 函数完成从实模式到保护模式的跳转: + +```C +protected_mode_jump(boot_params.hdr.code32_start, (u32)&boot_params + (ds() << 4)); +``` + +`protected_mode_jump` 函数定义在 [arch/x86/boot/pmjump.S](https://github.com/torvalds/linux/blob/master/arch/x86/boot/pmjump.S#L26),它接受下面2个参数: + +* 保护模式代码的入口 +* `boot_params` 结构的地址 + +第一个参数保存在 `eax` 寄存器,而第二个参数保存在 `edx` 寄存器。 + +代码首先在 `boot_params` 地址放入 `esi` 寄存器,然后将 `cs` 寄存器内容放入 `bx` 寄存器,接着执行 `bx << 4 + 标号为2的代码的地址`,这样一来 `bx` 寄存器就包含了标号为2的代码的地址。接下来代码将把数据段索引放入 `cx` 寄存器,将 TSS 段索引放入 `di` 寄存器: + +```assembly +movw $__BOOT_DS, %cx +movw $__BOOT_TSS, %di +``` + +就像前面我们看到的 `GDT_ENTRY_BOOT_CS` 的值为2,每个段描述符都是 8 字节,所以 `cx` 寄存器的值将是 `2*8 = 16`,`di` 寄存器的值将是 `4*8 =32`。 + +接下来,我们通过设置 `CR0` 寄存器相应的位使 CPU 进入保护模式: + +```assembly +movl %cr0, %edx +orb $X86_CR0_PE, %dl +movl %edx, %cr0 +``` + +在进入保护模式之后,通过一个长跳转进入 32 位代码: + +```assembly + .byte 0x66, 0xea +2: .long in_pm32 + .word __BOOT_CS ;(GDT_ENTRY_BOOT_CS*8) = 16,段描述符表索引 +``` + +这段代码中 +* `0x66` 操作符前缀允许我们混合执行 16 位和 32 位代码 +* `0xea` - 跳转指令的操作符 +* `in_pm32` 跳转地址偏移 +* `__BOOT_CS` 代码段描述符索引 + +在执行了这个跳转命令之后,我们就在保护模式下执行代码了: + +```assembly +.code32 +.section ".text32","ax" +``` + +保护模式代码的第一步就是重置所有的段寄存器(除了 `CS` 寄存器): + +```assembly +GLOBAL(in_pm32) +movl %ecx, %ds +movl %ecx, %es +movl %ecx, %fs +movl %ecx, %gs +movl %ecx, %ss +``` + +还记得我们在实模式代码中将 `$__BOOT_DS` (数据段描述符索引)放入了 `cx` 寄存器,所以上面的代码设置所有段寄存器(除了 `CS` 寄存器)指向数据段。接下来代码将所有的通用寄存器清 0 : + +```assembly +xorl %ecx, %ecx +xorl %edx, %edx +xorl %ebx, %ebx +xorl %ebp, %ebp +xorl %edi, %edi +``` + +最后使用长跳转跳入正在的 32 位代码(通过参数传入的地址) + +``` +jmpl *%eax ;?jmpl cs:eax? +``` + +到这里,我们就进入了保护模式开始执行代码了,下一章我们将分析这段 32 位代码到底做了些什么。 + +结论 +-------------------------------------------------------------------------------- + +这章到这里就结束了,在下一章中我们将具体介绍这章最后跳转到的 32 位代码,并且了解系统是如何进入 [long mode](http://en.wikipedia.org/wiki/Long_mode)的。 + +如果你有任何的问题或者建议,你可以留言,也可以直接发消息给我[twitter](https://twitter.com/0xAX). + +**英文不是我的母语。如果你发现我的英文描述有任何问题,请提交一个PR到[linux-insides](https://github.com/0xAX/linux-internals).** + +链接 +-------------------------------------------------------------------------------- + +* [VGA](http://en.wikipedia.org/wiki/Video_Graphics_Array) +* [VESA BIOS Extensions](http://en.wikipedia.org/wiki/VESA_BIOS_Extensions) +* [Data structure alignment](http://en.wikipedia.org/wiki/Data_structure_alignment) +* [Non-maskable interrupt](http://en.wikipedia.org/wiki/Non-maskable_interrupt) +* [A20](http://en.wikipedia.org/wiki/A20_line) +* [GCC designated inits](https://gcc.gnu.org/onlinedocs/gcc-4.1.2/gcc/Designated-Inits.html) +* [GCC type attributes](https://gcc.gnu.org/onlinedocs/gcc/Type-Attributes.html) +* [Previous part](linux-bootstrap-2.md) + diff --git a/Booting/linux-bootstrap-4.md b/Booting/linux-bootstrap-4.md new file mode 100644 index 0000000..10f7ced --- /dev/null +++ b/Booting/linux-bootstrap-4.md @@ -0,0 +1,597 @@ +内核引导过程. Part 4. +================================================================================ + +切换到64位模式 +-------------------------------------------------------------------------------- + +这是 `内核引导过程` 的第四部分,我们将会看到在[保护模式](https://zh.wikipedia.org/wiki/%E4%BF%9D%E8%AD%B7%E6%A8%A1%E5%BC%8F)中的最初几步,比如确认CPU是否支持[长模式](https://zh.wikipedia.org/wiki/%E9%95%BF%E6%A8%A1%E5%BC%8F),[SSE](https://zh.wikipedia.org/wiki/SSE)和[分页](https://zh.wikipedia.org/wiki/%E5%88%86%E9%A0%81)以及页表的初始化,在这部分的最后我们还将讨论如何切换到[长模式](https://zh.wikipedia.org/wiki/%E9%95%BF%E6%A8%A1%E5%BC%8F)。 + +**注意:这部分将会有大量的汇编代码,如果你不熟悉汇编,建议你找本书参考一下。** + +在[前一章节](https://github.com/MintCN/linux-insides-zh/blob/master/Booting/linux-bootstrap-3.md),我们停在了跳转到位于 [arch/x86/boot/pmjump.S](https://github.com/torvalds/linux/blob/master/arch/x86/boot/pmjump.S) 的 32 位入口点这一步: + +```assembly +jmpl *%eax +``` + +回忆一下, `eax` 寄存器包含了 32 位入口点的地址。我们可以在 [x86 linux 内核引导协议](https://www.kernel.org/doc/Documentation/x86/boot.txt) 中找到相关内容: + +``` +When using bzImage, the protected-mode kernel was relocated to 0x100000 +``` + +``` +当使用 bzImage 时,保护模式下的内核被重定位至 0x100000 +``` + + +让我们检查一下 32 位入口点的寄存器值来确保这是对的: + +``` +eax 0x100000 1048576 +ecx 0x0 0 +edx 0x0 0 +ebx 0x0 0 +esp 0x1ff5c 0x1ff5c +ebp 0x0 0x0 +esi 0x14470 83056 +edi 0x0 0 +eip 0x100000 0x100000 +eflags 0x46 [ PF ZF ] +cs 0x10 16 +ss 0x18 24 +ds 0x18 24 +es 0x18 24 +fs 0x18 24 +gs 0x18 24 +``` + +我们在这里可以看到 `cs` 寄存器包含了 - `0x10` (回忆前一章节,这代表了全局描述符表中的第二个索引项), `eip` 寄存器的值是 `0x100000`,并且包括代码段在内的所有内存段的基地址都为0。所以我们可以得到物理地址: `0:0x100000` 或者 `0x100000`,这和协议规定的一样。现在让我们从 32 位入口点开始。 + +32 位入口点 +-------------------------------------------------------------------------------- + +我们可以在汇编源码 [arch/x86/boot/compressed/head_64.S](https://github.com/torvalds/linux/blob/master/arch/x86/boot/compressed/head_64.S) 中找到 32 位入口点的定义。 + +```assembly + __HEAD + .code32 +ENTRY(startup_32) +.... +.... +.... +ENDPROC(startup_32) +``` + +首先,为什么目录名叫做 `被压缩的 (compressed)` ?实际上 `bzimage` 是由 `vmlinux + 头文件 + 内核启动代码` 被 gzip 压缩之后获得的。我们在前几个章节已经看到了启动内核的代码。所以, `head_64.S` 的主要目的就是做好进入长模式的准备之后进入长模式,进入以后再解压内核。在这一章节,我们将会看到直到内核解压缩之前的所有步骤。 + +在 `arch/x86/boot/compressed` 目录下有两个文件: + +* [head_32.S](https://github.com/torvalds/linux/blob/master/arch/x86/boot/compressed/head_32.S) +* [head_64.S](https://github.com/torvalds/linux/blob/master/arch/x86/boot/compressed/head_64.S) + +但是,你可能还记得我们这本书只和 `x86_64` 有关,所以我们只会关注 `head_64.S` ;在我们这里 `head_32.S` 没有被用到。让我们看一下 [arch/x86/boot/compressed/Makefile](https://github.com/torvalds/linux/blob/master/arch/x86/boot/compressed/Makefile)。在那里我们可以看到以下目标: + +```Makefile +vmlinux-objs-y := $(obj)/vmlinux.lds $(obj)/head_$(BITS).o $(obj)/misc.o \ + $(obj)/string.o $(obj)/cmdline.o \ + $(obj)/piggy.o $(obj)/cpuflags.o +``` + +注意 `$(obj)/head_$(BITS).o` 。这意味着我们将会选择基于 `$(BITS)` 所设置的文件执行链接操作,即 head_32.o 或者 head_64.o。`$(BITS)` 在 [arch/x86/kernel/Makefile](https://github.com/torvalds/linux/blob/master/arch/x86/kernel/Makefile) 之中根据 .config 文件另外定义: + +```Makefile +ifeq ($(CONFIG_X86_32),y) + BITS := 32 + ... + ... +else + ... + ... + BITS := 64 +endif +``` + +现在我们知道从哪里开始了,那就来吧。 + +必要时重新加载内存段寄存器 +-------------------------------------------------------------------------------- + +正如上面阐述的,我们先从 [arch/x86/boot/compressed/head_64.S](https://github.com/torvalds/linux/blob/master/arch/x86/boot/compressed/head_64.S) 这个汇编文件开始。首先我们看到了在 `startup_32` 之前的特殊段属性定义: + +```assembly + __HEAD + .code32 +ENTRY(startup_32) +``` + +这个 `__HEAD` 是一个定义在头文件 [include/linux/init.h](https://github.com/torvalds/linux/blob/master/include/linux/init.h) 中的宏,展开后就是下面这个段的定义: + +```C +#define __HEAD .section ".head.text","ax" +``` + +其拥有 `.head.text` 的命名和 `ax` 标记。在这里,这些标记告诉我们这个段是[可执行的](https://en.wikipedia.org/wiki/Executable)或者换种说法,包含了代码。我们可以在 [arch/x86/boot/compressed/vmlinux.lds.S](https://github.com/torvalds/linux/blob/master/arch/x86/boot/compressed/vmlinux.lds.S) 这个链接脚本里找到这个段的定义: + +``` +SECTIONS +{ + . = 0; + .head.text : { + _head = . ; + HEAD_TEXT + _ehead = . ; + } +``` + +如果你不熟悉 `GNU LD` 这个链接脚本语言的语法,你可以在[这个文档](https://sourceware.org/binutils/docs/ld/Scripts.html#Scripts)中找到更多信息。简单来说,这个 `.` 符号是一个链接器的特殊变量 - 位置计数器。其被赋值为相对于该段的偏移。在这里,我们将位置计数器赋值为0,这意味着我们的代码被链接到内存的 `0` 偏移处。此外,我们可以从注释里找到更多信息: + +``` +Be careful parts of head_64.S assume startup_32 is at address 0. +``` + +``` +要小心, head_64.S 中一些部分假设 startup_32 位于地址 0。 +``` + +好了,现在我们知道我们在哪里了,接下来就是深入 `startup_32` 函数的最佳时机。 + +在 `startup_32` 函数的开始,我们可以看到 `cld` 指令将[标志寄存器](http://baike.baidu.com/view/1845107.htm)的 `DF` (方向标志)位清空。当方向标志被清空,所有的串操作指令像[stos](http://x86.renejeschke.de/html/file_module_x86_id_306.html), [scas](http://x86.renejeschke.de/html/file_module_x86_id_287.html)等等将会增加索引寄存器 `esi` 或者 `edi` 的值。我们需要清空方向标志是因为接下来我们会使用汇编的串操作指令来做为页表腾出空间等工作。 + +在我们清空 `DF` 标志后,下一步就是从内核加载头中的 `loadflags` 字段来检查 `KEEP_SEGMENTS` 标志。你是否还记得在本书的[最初一节](https://github.com/MintCN/linux-insides-zh/blob/master/Booting/linux-bootstrap-1.md),我们已经看到过 `loadflags` 。在那里我们检查了 `CAN_USE_HEAP` 标记以使用堆。现在我们需要检查 `KEEP_SEGMENTS` 标记。这些标记在 linux 的[引导协议](https://www.kernel.org/doc/Documentation/x86/boot.txt)文档中有描述: + +``` +Bit 6 (write): KEEP_SEGMENTS + Protocol: 2.07+ + - If 0, reload the segment registers in the 32bit entry point. + - If 1, do not reload the segment registers in the 32bit entry point. + Assume that %cs %ds %ss %es are all set to flat segments with + a base of 0 (or the equivalent for their environment). +``` + +``` +第 6 位 (写): KEEP_SEGMENTS + 协议版本: 2.07+ + - 为0,在32位入口点重载段寄存器 + - 为1,不在32位入口点重载段寄存器。假设 %cs %ds %ss %es 都被设到基地址为0的普通段中(或者在他们的环境中等价的位置)。 +``` + +所以,如果 `KEEP_SEGMENTS` 位在 `loadflags` 中没有被设置,我们需要重置 `ds` , `ss` 和 `es` 段寄存器到一个基地址为 `0` 的普通段中。如下: + +```C + testb $(1 << 6), BP_loadflags(%esi) + jnz 1f + + cli + movl $(__BOOT_DS), %eax + movl %eax, %ds + movl %eax, %es + movl %eax, %ss +``` + +记住 `__BOOT_DS` 是 `0x18` (位于[全局描述符表](https://en.wikipedia.org/wiki/Global_Descriptor_Table)中数据段的索引)。如果设置了 `KEEP_SEGMENTS` ,我们就跳转到最近的 `1f` 标签,或者当没有 `1f` 标签,则用 `__BOOT_DS` 更新段寄存器。这非常简单,但是这是一个有趣的操作。如果你已经读了[前一章节](https://github.com/MintCN/linux-insides-zh/blob/master/Booting/linux-bootstrap-3.md),你或许还记得我们在 [arch/x86/boot/pmjump.S](https://github.com/torvalds/linux/blob/master/arch/x86/boot/pmjump.S) 中切换到[保护模式](https://zh.wikipedia.org/wiki/%E4%BF%9D%E8%AD%B7%E6%A8%A1%E5%BC%8F)的时候已经更新了这些段寄存器。那么为什么我们还要去关心这些段寄存器的值呢?答案很简单,Linux 内核也有32位的引导协议,如果一个引导程序之前使用32位协议引导内核,那么在 `startup_32` 之前的代码就会被忽略。在这种情况下 `startup_32` 将会变成引导程序之后的第一个入口点,不保证段寄存器会不会处于未知状态。 + +在我们检查了 `KEEP_SEGMENTS` 标记并且给段寄存器设置了正确的值之后,下一步就是计算我们代码的加载和编译运行之间的位置偏差了。记住 `setup.ld.S` 包含了以下定义:在 `.head.text` 段的开始 `. = 0` 。这意味着这一段代码被编译成从 `0` 地址运行。我们可以在 `objdump` 工具的输出中看到: + +``` +arch/x86/boot/compressed/vmlinux: file format elf64-x86-64 + + +Disassembly of section .head.text: + +0000000000000000 : + 0: fc cld + 1: f6 86 11 02 00 00 40 testb $0x40,0x211(%rsi) +``` + + `objdump` 工具告诉我们 `startup_32` 的地址是 `0` 。但实际上并不是。我们当前的目标是获知我们实际上在哪里。在[长模式](https://zh.wikipedia.org/wiki/%E9%95%BF%E6%A8%A1%E5%BC%8F)下,这非常简单,因为其支持 `rip` 相对寻址,但是我们当前处于[保护模式](https://zh.wikipedia.org/wiki/%E4%BF%9D%E8%AD%B7%E6%A8%A1%E5%BC%8F)下。我们将会使用一个常用的方法来确定 `startup_32` 的地址。我们需要定义一个标签并且跳转到它,然后把栈顶抛出到一个寄存器中: + +```assembly +call label +label: pop %reg +``` + +在这之后,那个寄存器将会包含标签的地址,让我们看看在 Linux 内核中类似的寻找 `startup_32` 地址的代码: + +```assembly + leal (BP_scratch+4)(%esi), %esp + call 1f +1: popl %ebp + subl $1b, %ebp +``` + +回忆前一节, `esi` 寄存器包含了 [boot_params](https://github.com/torvalds/linux/blob/master/arch/x86/include/uapi/asm/bootparam.h#L113) 结构的地址,这个结构在我们切换到保护模式之前已经被填充了。`bootparams` 这个结构体包含了一个特殊的字段 `scratch` ,其偏移量为 `0x1e4` 。这个 4 字节的区域将会成为 `call` 指令的临时栈。我们把 `scratch` 的地址加 4 存入 `esp` 寄存器。我们之所以在 `BP_scratch` 基础上加 `4` 是因为,如之前所说的,这将成为一个临时的栈,而在 `x86_64` 架构下,栈是自顶向下生长的。所以我们的栈指针就会指向栈顶。接下来我们就可以看到我上面描述的过程。我们跳转到 `1f` 标签并且把该标签的地址放入 `ebp` 寄存器,因为在执行 `call` 指令之后我们把返回地址放到了栈顶。那么,目前我们拥有 `1f` 标签的地址,也能够很容易得到 `startup_32` 的地址。我们只需要把我们从栈里得到的地址减去标签的地址: + +``` +startup_32 (0x0) +-----------------------+ + | | + | | + | | + | | + | | + | | + | | + | | +1f (0x0 + 1f offset) +-----------------------+ %ebp - 实际物理地址 + | | + | | + +-----------------------+ +``` + + `startup_32` 被链接为在 `0x0` 地址运行,这意味着 `1f` 的地址为 `0x0 + 1f 的偏移量` 。实际上偏移量大概是 `0x22` 字节。 `ebp` 寄存器包含了 `1f` 标签的实际物理地址。所以如果我们从 `ebp` 中减去 `1f` ,我们就会得到 `startup_32` 的实际物理地址。Linux 内核的[引导协议](https://www.kernel.org/doc/Documentation/x86/boot.txt)描述了保护模式下的内核基地址是 `0x100000` 。我们可以用 [gdb](https://zh.wikipedia.org/wiki/GNU%E4%BE%A6%E9%94%99%E5%99%A8) 来验证。让我们启动调试器并且在 `1f` 的地址 `0x100022` 添加断点。如果这是正确的,我们将会看到在 `ebp` 寄存器中值为 `0x100022` : + +``` +$ gdb +(gdb)$ target remote :1234 +Remote debugging using :1234 +0x0000fff0 in ?? () +(gdb)$ br *0x100022 +Breakpoint 1 at 0x100022 +(gdb)$ c +Continuing. + +Breakpoint 1, 0x00100022 in ?? () +(gdb)$ i r +eax 0x18 0x18 +ecx 0x0 0x0 +edx 0x0 0x0 +ebx 0x0 0x0 +esp 0x144a8 0x144a8 +ebp 0x100021 0x100021 +esi 0x142c0 0x142c0 +edi 0x0 0x0 +eip 0x100022 0x100022 +eflags 0x46 [ PF ZF ] +cs 0x10 0x10 +ss 0x18 0x18 +ds 0x18 0x18 +es 0x18 0x18 +fs 0x18 0x18 +gs 0x18 0x18 +``` + +如果我们执行下一条指令 `subl $1b, %ebp` ,我们将会看到: + +``` +nexti +... +ebp 0x100000 0x100000 +... +``` + +好了,那是对的。`startup_32` 的地址是 `0x100000` 。在我们知道了 `startup_32` 的地址之后,我们可以开始准备切换到[长模式](https://zh.wikipedia.org/wiki/%E9%95%BF%E6%A8%A1%E5%BC%8F)了。我们的下一个目标是建立栈并且确认 CPU 对长模式和 [SSE](https://zh.wikipedia.org/wiki/SSE) 的支持。 + +栈的建立和 CPU 的确认 +-------------------------------------------------------------------------------- + +如果不知道 `startup_32` 标签的地址,我们就无法建立栈。我们可以把栈看作是一个数组,并且栈指针寄存器 `esp` 必须指向数组的底部。当然我们可以在自己的代码里定义一个数组,但是我们需要知道其真实地址来正确配置栈指针。让我们看一下代码: + +```assembly + movl $boot_stack_end, %eax + addl %ebp, %eax + movl %eax, %esp +``` + + `boots_stack_end` 被定义在同一个汇编文件 [arch/x86/boot/compressed/head_64.S](https://github.com/torvalds/linux/blob/master/arch/x86/boot/compressed/head_64.S) 中,位于 [.bss](https://en.wikipedia.org/wiki/.bss) 段: + +```assembly + .bss + .balign 4 +boot_heap: + .fill BOOT_HEAP_SIZE, 1, 0 +boot_stack: + .fill BOOT_STACK_SIZE, 1, 0 +boot_stack_end: +``` + +首先,我们把 `boot_stack_end` 放到 `eax` 寄存器中。现在 `eax` 寄存器将包含 `boot_stack_end` 链接后的地址或者说 `0x0 + boot_stack_end` 。为了得到 `boot_stack_end` 的实际地址,我们需要加上 `startup_32` 的实际地址。回忆一下,前面我们找到了这个地址并且把它存到了 `ebp` 寄存器中。最后,`eax` 寄存器将会包含 `boot_stack_end` 的实际地址,我们只需要将其加到栈指针上。 + +在外面建立了栈之后,下一步是 CPU 的确认。既然我们将要切换到 `长模式` ,我们需要检查 CPU 是否支持 `长模式` 和 `SSE`。我们将会在跳转到 `verify_cpu` 函数之后执行: + +```assembly + call verify_cpu + testl %eax, %eax + jnz no_longmode +``` + +这个函数定义在 [arch/x86/kernel/verify_cpu.S](https://github.com/torvalds/linux/blob/master/arch/x86/kernel/verify_cpu.S) 中,只是包含了几个对 [cpuid](https://en.wikipedia.org/wiki/CPUID) 指令的调用。该指令用于获取处理器的信息。在我们的情况下,它检查了对 `长模式` 和 `SSE` 的支持,通过 `eax` 寄存器返回0表示成功,1表示失败。 + +如果 `eax` 的值不是 0 ,我们就跳转到 `no_longmode` 标签,用 `hlt` 指令停止 CPU ,期间不会发生硬件中断: + +```assembly +no_longmode: +1: + hlt + jmp 1b +``` + +如果 `eax` 的值为0,万事大吉,我们可以继续。 + +计算重定位地址 +-------------------------------------------------------------------------------- + +下一步是在必要的时候计算解压缩之后的地址。首先,我们需要知道内核重定位的意义。我们已经知道 Linux 内核的32位入口点地址位于 `0x100000` 。但是那是一个32位的入口。默认的内核基地址由内核配置项 `CONFIG_PHYSICAL_START` 的值所确定,其默认值为 `0x100000` 或 `1 MB` 。这里的主要问题是如果内核崩溃了,内核开发者需要一个配置于不同地址加载的 `救援内核` 来进行 [kdump](https://www.kernel.org/doc/Documentation/kdump/kdump.txt)。Linux 内核提供了特殊的配置选项以解决此问题 - `CONFIG_RELOCATABLE` 。我们可以在内核文档中找到: + +``` +This builds a kernel image that retains relocation information +so it can be loaded someplace besides the default 1MB. + +Note: If CONFIG_RELOCATABLE=y, then the kernel runs from the address +it has been loaded at and the compile time physical address +(CONFIG_PHYSICAL_START) is used as the minimum location. +``` + +``` +这建立了一个保留了重定向信息的内核镜像,这样就可以在默认的 1MB 位置之外加载了。 + +注意:如果 CONFIG_RELOCATABLE=y, 那么 内核将会从其被加载的位置运行,编译时的物理地址 (CONFIG_PHYSICAL_START) 将会被作为最低地址位置的限制。 +``` + +简单来说,这意味着相同配置下的 Linux 内核可以从不同地址被启动。这是通过将程序以 [位置无关代码](https://zh.wikipedia.org/wiki/%E5%9C%B0%E5%9D%80%E6%97%A0%E5%85%B3%E4%BB%A3%E7%A0%81) 的形式编译来达到的。如果我们参考 [/arch/x86/boot/compressed/Makefile](https://github.com/torvalds/linux/blob/master/arch/x86/boot/compressed/Makefile),我们将会看到解压器的确是用 `-fPIC` 标记编译的: + +```Makefile +KBUILD_CFLAGS += -fno-strict-aliasing -fPIC +``` + +当我们使用位置无关代码时,一段代码的地址是由一个控制地址加上程序计数器计算得到的。我们可以从任意一个地址加载使用这种方式寻址的代码。这就是为什么我们需要获得 `startup_32` 的实际地址。现在让我们回到 Linux 内核代码。我们目前的目标是计算出内核解压的地址。这个地址的计算取决于内核配置项 `CONFIG_RELOCATABLE` 。让我们看代码: + +```assembly +#ifdef CONFIG_RELOCATABLE + movl %ebp, %ebx + movl BP_kernel_alignment(%esi), %eax + decl %eax + addl %eax, %ebx + notl %eax + andl %eax, %ebx + cmpl $LOAD_PHYSICAL_ADDR, %ebx + jge 1f +#endif + movl $LOAD_PHYSICAL_ADDR, %ebx +1: + addl $z_extract_offset, %ebx +``` + +记住 `ebp` 寄存器的值就是 `startup_32` 标签的物理地址。如果在内核配置中 `CONFIG_RELOCATABLE` 内核配置项开启,我们就把这个地址放到 `ebx` 寄存器中,对齐到 `2M` ,然后和 `LOAD_PHYSICAL_ADDR` 的值比较。 `LOAD_PHYSICAL_ADDR` 宏定义在头文件 [arch/x86/include/asm/boot.h](https://github.com/torvalds/linux/blob/master/arch/x86/include/asm/boot.h) 中,如下: + +```C +#define LOAD_PHYSICAL_ADDR ((CONFIG_PHYSICAL_START \ + + (CONFIG_PHYSICAL_ALIGN - 1)) \ + & ~(CONFIG_PHYSICAL_ALIGN - 1)) +``` + +我们可以看到该宏只是展开成对齐的 `CONFIG_PHYSICAL_ALIGN` 值,其表示了内核加载位置的物理地址。在比较了 `LOAD_PHYSICAL_ADDR` 和 `ebx` 的值之后,我们给 `startup_32` 加上偏移来获得解压内核镜像的地址。如果 `CONFIG_RELOCATABLE` 选项在内核配置时没有开启,我们就直接将默认的地址加上 `z_extract_offset` 。 + +在前面的操作之后,`ebp` 包含了我们加载时的地址,`ebx` 被设为内核解压缩的目标地址。 + +进入长模式前的准备工作 +-------------------------------------------------------------------------------- + +在我们得到了重定位内核镜像的基地址之后,我们需要做切换到64位模式之前的最后准备。首先,我们需要更新[全局描述符表](https://en.wikipedia.org/wiki/Global_Descriptor_Table): + +```assembly + leal gdt(%ebp), %eax + movl %eax, gdt+2(%ebp) + lgdt gdt(%ebp) +``` + +在这里我们把 `ebp` 寄存器加上 `gdt` 的偏移存到 `eax` 寄存器。接下来我们把这个地址放到 `ebp` 加上 `gdt+2` 偏移的位置上,并且用 `lgdt` 指令载入 `全局描述符表` 。为了理解这个神奇的 `gdt` 偏移量,我们需要关注 `全局描述符表` 的定义。我们可以在同一个[源文件](https://github.com/torvalds/linux/blob/master/arch/x86/boot/compressed/head_64.S)中找到其定义: + +```assembly + .data +gdt: + .word gdt_end - gdt + .long gdt + .word 0 + .quad 0x0000000000000000 /* NULL descriptor */ + .quad 0x00af9a000000ffff /* __KERNEL_CS */ + .quad 0x00cf92000000ffff /* __KERNEL_DS */ + .quad 0x0080890000000000 /* TS descriptor */ + .quad 0x0000000000000000 /* TS continued */ +gdt_end: +``` + +我们可以看到其位于 `.data` 段,并且包含了5个描述符: `null` 、内核代码段、内核数据段和其他两个任务描述符。我们已经在[上一章节](https://github.com/MintCN/linux-insides-zh/blob/master/Booting/linux-bootstrap-3.md)载入了 `全局描述符表` ,和我们现在做的差不多,但是将描述符改为 `CS.L = 1` `CS.D = 0` 从而在 `64` 位模式下执行。我们可以看到, `gdt` 的定义从两个字节开始: `gdt_end - gdt` ,代表了 `gdt` 表的最后一个字节,或者说表的范围。接下来的4个字节包含了 `gdt` 的基地址。记住 `全局描述符表` 保存在 `48位 GDTR-全局描述符表寄存器` 中,由两个部分组成: + +* 全局描述符表的大小 (16位) +* 全局描述符表的基址 (32位) + +所以,我们把 `gdt` 的地址放到 `eax` 寄存器,然后存到 `.long gdt` 或者 `gdt+2`。现在我们已经建立了 `GDTR` 寄存器的结构,并且可以用 `lgdt` 指令载入 `全局描述符表` 了。 + +在我们载入 `全局描述符表` 之后,我们必须启用 [PAE](http://en.wikipedia.org/wiki/Physical_Address_Extension) 模式。方法是将 `cr4` 寄存器的值传入 `eax` ,将第5位置1,然后再写回 `cr4` 。 + +```assembly + movl %cr4, %eax + orl $X86_CR4_PAE, %eax + movl %eax, %cr4 +``` + +现在我们已经接近完成进入64位模式前的所有准备工作了。最后一步是建立页表,但是在此之前,这里有一些关于长模式的知识。 + +长模式 +-------------------------------------------------------------------------------- + +[长模式](https://zh.wikipedia.org/wiki/%E9%95%BF%E6%A8%A1%E5%BC%8F)是 [x86_64](https://en.wikipedia.org/wiki/X86-64) 系列处理器的原生模式。首先让我们看一看 `x86_64` 和 `x86` 的一些区别。 + + `64位` 模式提供了一些新特性,比如: + +* 从 `r8` 到 `r15` 8个新的通用寄存器,并且所有通用寄存器都是64位的了。 +* 64位指令指针 - `RIP` ; +* 新的操作模式 - 长模式; +* 64位地址和操作数; +* RIP 相对寻址 (我们将会在接下来的章节看到一个例子). + +长模式是一个传统保护模式的扩展,其由两个子模式构成: + +* 64位模式 +* 兼容模式 + +为了切换到 `64位` 模式,我们需要完成以下操作: + +* 启用 [PAE](https://en.wikipedia.org/wiki/Physical_Address_Extension); +* 建立页表并且将顶级页表的地址放入 `cr3` 寄存器; +* 启用 `EFER.LME` ; +* 启用分页; + + +我们已经通过设置 `cr4` 控制寄存器中的 `PAE` 位启动 `PAE` 了。在下一个段落,我们就要建立[页表](https://zh.wikipedia.org/wiki/%E5%88%86%E9%A0%81)的结构了。 + +初期页表初始化 +-------------------------------------------------------------------------------- + +现在,我们已经知道了在进入 `64位` 模式之前,我们需要先建立页表,那么就让我们看看如何建立初期的 `4G` 启动页表。 + +**注意:我不会在这里解释虚拟内存的理论,如果你想知道更多,查看本节最后的链接** + +Linux 内核使用 `4级` 页表,通常我们会建立6个页表: + +* 1 个 `PML4` 或称为 `4级页映射` 表,包含 1 个项; +* 1 个 `PDP` 或称为 `页目录指针` 表,包含 4 个项; +* 4 个 页目录表,包含 `2048` 个项; + +让我们看看其实现方式。首先我们在内存中为页表清理一块缓存。每个表都是 `4096` 字节,所以我们需要 `24` KB 的空间: + +```assembly + leal pgtable(%ebx), %edi + xorl %eax, %eax + movl $((4096*6)/4), %ecx + rep stosl +``` + +我们把和 `ebx` 相关的 `pgtable` 的地址放到 `edi` 寄存器中,清空 `eax` 寄存器,并将 `ecx` 赋值为 `6144` 。 `rep stosl` 指令将会把 `eax` 的值写到 `edi` 指向的地址,然后给 `edi` 加 4 , `ecx` 减 4 ,重复直到 `ecx` 小于等于 0 。所以我们才把 `6144` 赋值给 `ecx` 。 + + `pgtable` 定义在 [arch/x86/boot/compressed/head_64.S](https://github.com/torvalds/linux/blob/master/arch/x86/boot/compressed/head_64.S) 的最后: + +```assembly + .section ".pgtable","a",@nobits + .balign 4096 +pgtable: + .fill 6*4096, 1, 0 +``` + +我们可以看到,其位于 `.pgtable` 段,大小为 `24KB` 。 + +在我们为 `pgtable` 分配了空间之后,我们可以开始构建顶级页表 - `PML4` : + +```assembly + leal pgtable + 0(%ebx), %edi + leal 0x1007 (%edi), %eax + movl %eax, 0(%edi) +``` + +还是在这里,我们把和 `ebx` 相关的,或者说和 `startup_32` 相关的 `pgtable` 的地址放到 `ebi` 寄存器。接下来我们把相对此地址偏移 `0x1007` 的地址放到 `eax` 寄存器中。 `0x1007` 是 `PML4` 的大小 `4096` 加上 `7` 。这里的 `7` 代表了 `PML4` 的项标记。在我们这里,这些标记是 `PRESENT+RW+USER` 。在最后我们把第一个 `PDP(页目录指针)` 项的地址写到 `PML4` 中。 + +在接下来的一步,我们将会在 `页目录指针(PDP)` 表(3级页表)建立 4 个带有 `PRESENT+RW+USE` 标记的 `Page Directory (2级页表)` 项: + +```assembly + leal pgtable + 0x1000(%ebx), %edi + leal 0x1007(%edi), %eax + movl $4, %ecx +1: movl %eax, 0x00(%edi) + addl $0x00001000, %eax + addl $8, %edi + decl %ecx + jnz 1b +``` + +我们把 3 级页目录指针表的基地址(从 `pgtable` 表偏移 `4096` 或者 `0x1000` )放到 `edi` ,把第一个 2 级页目录指针表的首项的地址放到 `eax` 寄存器。把 `4` 赋值给 `ecx` 寄存器,其将会作为接下来循环的计数器,然后将第一个页目录指针项写到 `edi` 指向的地址。之后, `edi` 将会包含带有标记 `0x7` 的第一个页目录指针项的地址。接下来我们就计算后面的几个页目录指针项的地址,每个占 8 字节,把地址赋值给 `eax` ,然后回到循环开头将其写入 `edi` 所在地址。建立页表结构的最后一步就是建立 `2048` 个 `2MB` 页的页表项。 + +```assembly + leal pgtable + 0x2000(%ebx), %edi + movl $0x00000183, %eax + movl $2048, %ecx +1: movl %eax, 0(%edi) + addl $0x00200000, %eax + addl $8, %edi + decl %ecx + jnz 1b +``` + +在这里我们做的几乎和上面一样,所有的表项都带着标记 - `$0x00000183` - `PRESENT + WRITE + MBZ` 。最后我们将会拥有 `2048` 个 `2MB` 大的页,或者说: + +```python +>>> 2048 * 0x00200000 +4294967296 +``` + +一个 `4G` 页表。我们刚刚完成我们的初期页表结构,其映射了 `4G` 大小的内存,现在我们可以把高级页表 `PML4` 的地址放到 `cr3` 寄存器中了: + +```assembly + leal pgtable(%ebx), %eax + movl %eax, %cr3 +``` + +这样就全部结束了。所有的准备工作都已经完成,我们可以开始看如何切换到长模式了。 + +切换到长模式 +-------------------------------------------------------------------------------- + +首先我们需要设置 [MSR](http://en.wikipedia.org/wiki/Model-specific_register) 中的 `EFER.LME` 标记为 `0xC0000080` : + +```assembly + movl $MSR_EFER, %ecx + rdmsr + btsl $_EFER_LME, %eax + wrmsr +``` + +在这里我们把 `MSR_EFER` 标记(在 [arch/x86/include/uapi/asm/msr-index.h](https://github.com/torvalds/linux/blob/master/arch/x86/include/uapi/asm/msr-index.h#L7) 中定义)放到 `ecx` 寄存器中,然后调用 `rdmsr` 指令读取 [MSR](http://en.wikipedia.org/wiki/Model-specific_register) 寄存器。在 `rdmsr` 执行之后,我们将会获得 `edx:eax` 中的结果值,其取决于 `ecx` 的值。我们通过 `btsl` 指令检查 `EFER_LME` 位,并且通过 `wrmsr` 指令将 `eax` 的数据写入 `MSR` 寄存器。 + +下一步我们将内核段代码地址入栈(我们在 GDT 中定义了),然后将 `startup_64` 的地址导入 `eax` 。 + +```assembly + pushl $__KERNEL_CS + leal startup_64(%ebp), %eax +``` + +在这之后我们把这个地址入栈然后通过设置 `cr0` 寄存器中的 `PG` 和 `PE` 启用分页: + +```assembly + movl $(X86_CR0_PG | X86_CR0_PE), %eax + movl %eax, %cr0 +``` + +然后执行: + +```assembly +lret +``` + +指令。记住前一步我们已经将 `startup_64` 函数的地址入栈,在 `lret` 指令之后,CPU 取出了其地址跳转到那里。 + +这些步骤之后我们最后来到了64位模式: + +```assembly + .code64 + .org 0x200 +ENTRY(startup_64) +.... +.... +.... +``` + + +就是这样! + +总结 +-------------------------------------------------------------------------------- + +这是 linux 内核启动流程的第4部分。如果你有任何的问题或者建议,你可以留言,也可以直接发消息给我 [twitter](https://twitter.com/0xAX) 或者创建一个 [issue](https://github.com/0xAX/linux-insides/issues/new)。 + +下一节我们将会看到内核解压缩流程和其他更多。 + +**英文不是我的母语。如果你发现我的英文描述有任何问题,请提交一个PR到 [linux-insides](https://github.com/0xAX/linux-internals)。** + +相关链接 +-------------------------------------------------------------------------------- + +* [Protected mode](http://en.wikipedia.org/wiki/Protected_mode) +* [Intel® 64 and IA-32 Architectures Software Developer’s Manual 3A](http://www.intel.com/content/www/us/en/processors/architectures-software-developer-manuals.html) +* [GNU linker](http://www.eecs.umich.edu/courses/eecs373/readings/Linker.pdf) +* [SSE](http://en.wikipedia.org/wiki/Streaming_SIMD_Extensions) +* [Paging](http://en.wikipedia.org/wiki/Paging) +* [Model specific register](http://en.wikipedia.org/wiki/Model-specific_register) +* [.fill instruction](http://www.chemie.fu-berlin.de/chemnet/use/info/gas/gas_7.html) +* [Previous part](https://github.com/0xAX/linux-insides/blob/master/Booting/linux-bootstrap-3.md) +* [Paging on osdev.org](http://wiki.osdev.org/Paging) +* [Paging Systems](https://www.cs.rutgers.edu/~pxk/416/notes/09a-paging.html) +* [x86 Paging Tutorial](http://www.cirosantilli.com/x86-paging/) diff --git a/DataStructures/README.md b/DataStructures/README.md index be40530..87af433 100644 --- a/DataStructures/README.md +++ b/DataStructures/README.md @@ -5,5 +5,5 @@ Linux内核对很多数据结构提供不同的实现方法,比如,双向链 这部分考虑这些数据结构和算法。 - * [双向链表](https://github.com/MintCN/linux-insides/blob/master/DataStructures/dlist.md) - * [基数树](https://github.com/MintCN/linux-insides/blob/master/DataStructures/radix-tree.md) + * [双向链表](https://xinqiu.gitbooks.io/linux-insides-cn/content/DataStructures/dlist.html) + * [基数树](https://xinqiu.gitbooks.io/linux-insides-cn/content/DataStructures/radix-tree.html) diff --git a/DataStructures/dlist.md b/DataStructures/dlist.md index 4b4a408..46168f1 100644 --- a/DataStructures/dlist.md +++ b/DataStructures/dlist.md @@ -1,12 +1,12 @@ -Linux内核中的数据结构 +Linux 内核里的数据结构——双向链表 ================================================================================ 双向链表 -------------------------------------------------------------------------------- -Linux kernel provides its own doubly linked list implementation which you can find in the [include/linux/list.h](https://github.com/torvalds/linux/blob/master/include/linux/list.h). We will start `Data Structures in the Linux kernel` from the doubly linked list data structure. Why? Because it is very popular in the kernel, just try to [search](http://lxr.free-electrons.com/ident?i=list_head) +Linux 内核自己实现了双向链表,可以在 [include/linux/list.h](https://github.com/torvalds/linux/blob/master/include/linux/list.h) 找到定义。我们将会从双向链表数据结构开始`内核的数据结构`。为什么?因为它在内核里使用的很广泛,你只需要在 [free-electrons.com](http://lxr.free-electrons.com/ident?i=list_head) 检索一下就知道了。 -First of all let's look on the main structure: +首先让我们看一下在 [include/linux/types.h](https://github.com/torvalds/linux/blob/master/include/linux/types.h) 里的主结构体: ```C struct list_head { @@ -14,7 +14,7 @@ struct list_head { }; ``` -You can note that it is different from many lists implementations which you have seen. For example this doubly linked list structure from the [glib](http://www.gnu.org/software/libc/): +你可能注意到这和你以前见过的双向链表的实现方法是不同的。举个例子来说,在 [glib](http://www.gnu.org/software/libc/) 库里是这样实现的: ```C struct GList { @@ -24,9 +24,9 @@ struct GList { }; ``` -Usually a linked list structure contains a pointer to the item. Linux kernel implementation of the list does not. So the main question is - `where does the list store the data?`. The actual implementation of lists in the kernel is - `Intrusive list`. An intrusive linked list does not contain data in its nodes - A node just contains pointers to the next and previous node and list nodes part of the data that are added to the list. This makes the data structure generic, so it does not care about entry data type anymore. +通常来说一个链表会包含一个指向某个项目的指针。但是内核的实现并没有这样做。所以问题来了:`链表在哪里保存数据呢?`。实际上内核里实现的链表实际上是`侵入式链表`。侵入式链表并不在节点内保存数据-节点仅仅包含指向前后节点的指针,然后把数据是附加到链表的。这就使得这个数据结构是通用的,使用起来就不需要考虑节点数据的类型了。 -For example: +比如: ```C struct nmi_desc { @@ -35,13 +35,13 @@ struct nmi_desc { }; ``` -Let's look at some examples to understand how `list_head` is used in the kernel. As I already wrote about, there are many, really many different places where lists are used in the kernel. Let's look for example in miscellaneous character drivers. Misc character drivers API from the [drivers/char/misc.c](https://github.com/torvalds/linux/blob/master/drivers/char/misc.c) is used for writing small drivers for handling simple hardware or virtual devices. This drivers share major number: +让我们看几个例子来理解一下在内核里是如何使用 `list_head` 的。如上所述,在内核里有实在很多不同的地方用到了链表。我们以杂项字符驱动为例来说明双向链表的使用。在 [drivers/char/misc.c](https://github.com/torvalds/linux/blob/master/drivers/char/misc.c) 的杂项字符驱动API 被用来编写处理小型硬件和虚拟设备的小驱动。这些驱动共享相同的主设备号: ```C #define MISC_MAJOR 10 ``` -but have their own minor number. For example you can see it with: +但是都有各自不同的次设备号。比如: ``` ls -l /dev | grep 10 @@ -67,7 +67,7 @@ crw------- 1 root root 10, 63 Mar 21 12:01 vga_arbiter crw------- 1 root root 10, 137 Mar 21 12:01 vhci ``` -Now let's have a close look at how lists are used in the misc device drivers. First of all let's look on `miscdevice` structure: +现在让我们看看它是如何使用链表的。首先看一下结构体 `miscdevice` : ```C struct miscdevice @@ -83,32 +83,32 @@ struct miscdevice }; ``` -We can see the fourth field in the `miscdevice` structure - `list` which is a list of registered devices. In the beginning of the source code file we can see the definition of misc_list: +我们可以看到结构体的第四个变量 `list` 是所有注册过的设备的链表。在源代码文件的开始可以看到这个链表的定义: ```C static LIST_HEAD(misc_list); ``` -which expands to definition of the variables with `list_head` type: +它扩展开来实际上就是定义了一个 `list_head` 类型的变量: ```C #define LIST_HEAD(name) \ struct list_head name = LIST_HEAD_INIT(name) ``` -and initializes it with the `LIST_HEAD_INIT` macro which set previous and next entries: +然后使用宏 `LIST_HEAD_INIT` 进行初始化,这会使用变量 `name` 的地址来填充 `prev` 和 `next` 结构体的两个变量。 ```C #define LIST_HEAD_INIT(name) { &(name), &(name) } ``` -Now let's look on the `misc_register` function which registers a miscellaneous device. At the start it initializes `miscdevice->list` with the `INIT_LIST_HEAD` function: +现在来看看注册杂项设备的函数 `misc_register` 。它在开始就用 `INIT_LIST_HEAD` 初始化了`miscdevice->list`。 ```C INIT_LIST_HEAD(&misc->list); ``` -which does the same as the `LIST_HEAD_INIT` macro: +作用和宏 `LIST_HEAD_INIT`一样。 ```C static inline void INIT_LIST_HEAD(struct list_head *list) @@ -118,13 +118,14 @@ static inline void INIT_LIST_HEAD(struct list_head *list) } ``` -In the next step after device created with the `device_create` function we add it to the miscellaneous devices list with: +下一步在函数 `device_create` 创建了设备后我们就用下面的语句将设备添加到设备链表: ``` list_add(&misc->list, &misc_list); ``` -Kernel `list.h` provides this API for the addition of new entry to the list. Let's look on it's implementation: +内核文件 `list.h` 提供了向链表添加新项的接口函数。我们来看看它的实现: + ```C static inline void list_add(struct list_head *new, struct list_head *head) @@ -133,13 +134,13 @@ static inline void list_add(struct list_head *new, struct list_head *head) } ``` -It just calls internal function `__list_add` with the 3 given parameters: +实际上就是使用3个指定的参数来调用了内部函数 `__list_add`: -* new - new entry; -* head - list head after which the new item will be inserted -* head->next - next item after list head. +* new - 新项。 +* head - 新项将会被添加到`head` 之后. +* head->next - `head` 之后的项。 -Implementation of the `__list_add` is pretty simple: +`__list_add`的实现非常简单: ```C static inline void __list_add(struct list_head *new, @@ -153,35 +154,35 @@ static inline void __list_add(struct list_head *new, } ``` -Here we set new item between `prev` and `next`. So `misc` list which we defined at the start with the `LIST_HEAD_INIT` macro will contain previous and next pointers to the `miscdevice->list`. +我们会在 `prev` 和 `next` 之间添加一个新项。所以我们用宏 `LIST_HEAD_INIT` 定义的 `misc` 链表会包含指向 `miscdevice->list` 的向前指针和向后指针。 -There is still one question: how to get list's entry. There is a special macro: +这里仍有一个问题:如何得到列表的内容呢?这里有一个特殊的宏: ```C #define list_entry(ptr, type, member) \ container_of(ptr, type, member) ``` -which gets three parameters: +使用了三个参数: -* ptr - the structure list_head pointer; -* type - structure type; -* member - the name of the list_head within the structure; +* ptr - 指向链表头的指针; +* type - 结构体类型; +* member - 在结构体内类型为 `list_head` 的变量的名字; -For example: +比如说: ```C const struct miscdevice *p = list_entry(v, struct miscdevice, list) ``` -After this we can access to any `miscdevice` field with `p->minor` or `p->name` and etc... Let's look on the `list_entry` implementation: - +然后我们就可以使用 `p->minor` 或者 `p->name`来访问 `miscdevice`。让我们来看看 `list_entry` 的实现: + ```C #define list_entry(ptr, type, member) \ container_of(ptr, type, member) ``` -As we can see it just calls `container_of` macro with the same arguments. At first sight, the `container_of` looks strange: +如我们所见,它仅仅使用相同的参数调用了宏 `container_of`。初看这个宏挺奇怪的: ```C #define container_of(ptr, type, member) ({ \ @@ -189,9 +190,9 @@ As we can see it just calls `container_of` macro with the same arguments. At fir (type *)( (char *)__mptr - offsetof(type,member) );}) ``` -First of all you can note that it consists of two expressions in curly brackets. Compiler will evaluate the whole block in the curly braces and use the value of the last expression. +首先你可以注意到花括号内包含两个表达式。编译器会执行花括号内的全部语句,然后返回最后的表达式的值。 -For example: +举个例子来说: ``` #include @@ -203,9 +204,9 @@ int main() { } ``` -will print `2`. +最终会打印 `2` -The next point is `typeof`, it's simple. As you can understand from its name, it just returns the type of the given variable. When I first saw the implementation of the `container_of` macro, the strangest thing for me was the zero in the `((type *)0)` expression. Actually this pointer magic calculates the offset of the given field from the address of the structure, but as we have `0` here, it will be just a zero offset alongwith the field width. Let's look at a simple example: +下一点就是 `typeof`,它也很简单。就如你从名字所理解的,它仅仅返回了给定变量的类型。当我第一次看到宏 `container_of` 的实现时,让我觉得最奇怪的就是 `container_of` 中的 0 。实际上这个指针巧妙的计算了从结构体特定变量的偏移,这里的 `0` 刚好就是位宽里的零偏移。让我们看一个简单的例子: ```C #include @@ -213,7 +214,7 @@ The next point is `typeof`, it's simple. As you can understand from its name, it struct s { int field1; char field2; - char field3; + char field3; }; int main() { @@ -222,17 +223,17 @@ int main() { } ``` -will print `0x5`. +结果显示 `0x5`。 -The next offsetof macro calculates offset from the beginning of the structure to the given structure's field. Its implementation is very similar to the previous code: +下一个宏 `offsetof` 会计算从结构体的某个变量的相对于结构体起始地址的偏移。它的实现和上面类似: ```C #define offsetof(TYPE, MEMBER) ((size_t) &((TYPE *)0)->MEMBER) ``` -Let's summarize all about `container_of` macro. `container_of` macro returns address of the structure by the given address of the structure's field with `list_head` type, the name of the structure field with `list_head` type and type of the container structure. At the first line this macro declares the `__mptr` pointer which points to the field of the structure that `ptr` points to and assigns `ptr` to it. Now `ptr` and `__mptr` point to the same address. Technically we don't need this line but its useful for type checking. First line ensures that that given structure (`type` parameter) has a member called `member`. In the second line it calculates offset of the field from the structure with the `offsetof` macro and subtracts it from the structure address. That's all. +现在我们来总结一下宏 `container_of`。只需要知道结构体里面类型为 `list_head` 的变量的名字和结构体容器的类型,它可以通过结构体的变量 `list_head` 获得结构体的起始地址。在宏定义的第一行,声明了一个指向结构体成员变量 `ptr` 的指针 `__mptr` ,并且把 `ptr` 的地址赋给它。现在 `ptr` 和 `__mptr` 指向了同一个地址。从技术上讲我们并不需要这一行,但是它可以方便的进行类型检查。第一行保证了特定的结构体(参数 `type`)包含成员变量 `member`。第二行代码会用宏 `offsetof` 计算成员变量相对于结构体起始地址的偏移,然后从结构体的地址减去这个偏移,最后就得到了结构体的起始地址。 -Of course `list_add` and `list_entry` is not the only functions which `` provides. Implementation of the doubly linked list provides the following API: +当然了 `list_add` 和 `list_entry` 不是 `` 提供的唯一函数。双向链表的实现还提供了如下API: * list_add * list_add_tail @@ -243,5 +244,7 @@ Of course `list_add` and `list_entry` is not the only functions which ` 分页机制提供一种机制,为了实现一种常见的页需求,虚拟内存系统,其中一个程序执行环境中的段将要按照需求被映射到物理地址。 +> 分页机制提供一种机制,为了实现常见的按需分页,比如虚拟内存系统就是将一个程序执行环境中的段按照需求被映射到物理地址。 所以... 在这个帖子中我将尝试解释分页背后的理论。当然它将与64位版本的 Linux 内核关系密切,但是我们将不会深入太多细节(至少在这个帖子里面)。 @@ -52,7 +52,7 @@ wrmsr 分页数据结构 -------------------------------------------------------------------------------- -分页将线性地址分为固定尺寸的页。页会被映射进入物理地址空间或外部存储设备。这个固定尺寸在 `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) 这个文件中已经看到了。 +分页将线性地址分为固定尺寸的页。页会被映射进入物理地址空间或外部存储设备。这个固定尺寸在 `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 @@ -86,7 +86,7 @@ movl %eax, %cr3 线性地址转换过程如下所示: -* 一个给定的线性地址到达 [MMU](http://en.wikipedia.org/wiki/Memory_management_unit) 而不是存储器总线; +* 一个给定的线性地址传递给 [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 位提供物理页的字节偏移; @@ -133,12 +133,12 @@ Linux 内核中的分页结构 就如我们已经看到的那样, `x86_64`Linux 内核使用4级页表。它们的名字是: -* 页全局目录 -* 页上层目录 -* 页中间目录 +* 全局页目录 +* 上层页目录 +* 中间页目录 * 页表项 -在你已经编译和安装 Linux 内核,你可以看到存储内核中使用的函数的虚拟地址的 `System.map` 文件。例如: +在你已经编译和安装 Linux 内核之后,你可以看到保存了内核函数的虚拟地址的文件 `System.map`。例如: ``` $ grep "start_kernel" System.map @@ -193,21 +193,21 @@ ffffffffff600000 - ffffffffffdfffff (=8 MB) vsyscalls ffffffffffe00000 - ffffffffffffffff (=2 MB) unused hole ``` -这里我们可以看到用户空间,内核空间和非标准空间的内存映射。用户空间的内存映射很简单。让我们来更近地查看内核空间。我们可以看到它始于为管理程序 (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) 这个文件中看到防卫空洞的概念! +这里我们可以看到用户空间,内核空间和非标准空间的内存映射。用户空间的内存映射很简单。让我们来更近地查看内核空间。我们可以看到它始于为管理程序 (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) ``` -以前防卫空洞和 `__PAGE_OFFSET` 是从 `0xffff800000000000` 到 `0xffff80ffffffffff` ,来防止对非标准区域的访问,但是后来为了管理程序扩展了 3 位。 +以前防御空洞和 `__PAGE_OFFSET` 是从 `0xffff800000000000` 到 `0xffff80ffffffffff` ,用来防止对非标准区域的访问,但是后来为了管理程序扩展了 3 位。 -紧接着是内核空间中最低的可用空间 - `ffff880000000000` 。这个虚拟地址空间是为了所有的物理内存的直接映射。在这块空间之后,还是防卫空洞。它位于所有物理内存的直接映射地址和被 vmalloc 分配的地址之间。在第一个 1TB 的虚拟内存映射和无用的空洞之后,我们可以看到 `ksan` 影子内存 (shadow memory) 。它是通过 [commit](https://github.com/torvalds/linux/commit/ef7f0d6a6ca8c9e4b27d78895af86c2fbfaeedb2) 提交到内核中,并且保持内核空间无害。在紧接着的无用空洞之后,我们可以看到 `esp` 固定栈(我们会在本书其他部分讨论它)。内核代码段的开始从物理地址 - `0` 映射。我们可以在相同的文件中找到将这个地址定义为 `__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) ``` -通常内核的 `.text` 段开始于 `CONFIG_PHYSICAL_START` 偏移。我们已经在 [ELF64](https://github.com/0xAX/linux-insides/blob/master/Theory/ELF.md) 相关帖子中看见。 +通常内核的 `.text` 段开始于 `CONFIG_PHYSICAL_START` 偏移。我们已经在 [ELF64](https://github.com/MintCN/linux-insides-zh/blob/master/Theory/ELF.md) 相关帖子中看见。 ``` readelf -s vmlinux | grep ffffffff81000000 diff --git a/Theory/README.md b/Theory/README.md index d45e744..aae7171 100644 --- a/Theory/README.md +++ b/Theory/README.md @@ -2,5 +2,5 @@ 这一章描述各种理论性概念和那些不直接涉及实践,但是知道了会很有用的概念。 -* [分页](http://xinqiu.gitbooks.io/linux-insides/content/Theory/Paging.html) -* [Elf64 格式](http://xinqiu.gitbooks.io/linux-insides/content/Theory/ELF.html) +* [分页](https://xinqiu.gitbooks.io/linux-insides-cn/content/Theory/Paging.html) +* [Elf64 格式](https://xinqiu.gitbooks.io/linux-insides-cn/content/Theory/ELF.html) diff --git a/contributors.md b/contributors.md index f6e32ba..28f44f8 100644 --- a/contributors.md +++ b/contributors.md @@ -17,3 +17,7 @@ [@oska874](https://github.com/oska874) [@cloudusers](https://github.com/cloudusers) + +[@hailincai](https://github.com/hailincai) + +[@zmj1316](https://github.com/zmj1316)