linux 您所在的位置:网站首页 linux内核揭秘 linux

linux

2023-05-07 06:13| 来源: 网络整理| 查看: 265

内核引导过程. 第一部分. 从引导加载程序内核

如果看过我在这之前的文章,你就会知道我已经开始涉足底层的代码编写。我写了一些关于 Linux x86_64 汇编的文章。同时,我开始深入研究 Linux 源代码。底层是如何工作的,程序是如何在电脑上运行的,它们是如何在内存中定位的,内核是如何管理进程和内存,网络堆栈是如何在底层工作的等等,这些我都非常感兴趣。因此,我决定去写另外的一系列文章关于 x86_64 框架的 Linux 内核。

注意这不是官方文档,只是学习和分享知识

需要的基础知识

理解 C 代码 理解 汇编语言 代码 (AT&T 语法)

不管怎样,如果你才开始学一些,我会在这些文章中尝试去解释一些部分。好了,小的介绍结束,我们开始深入内核和底层。

我们的文章是基于 Linux 内核 3.18 版本进行的,如果后续的内核版本有任何改变,我将作出相应的更新。

神奇的电源按钮,接下来会发生什么?

尽管这是一系列关于 Linux 内核的文章,我们在第一章并不会从内核代码开始。电脑在你按下电源开关的时候,就开始工作。主板发送信号给电源,而电源收到信号后会给电脑供应合适的电量。一旦主板收到了电源备妥信号,它会尝试启动 CPU 。CPU 则复位寄存器的所有数据,并设置每个寄存器的预定值。

80386 以及后来的 CPUs 在电脑复位后,在 CPU 寄存器中定义了如下预定义数据:

IP 0xfff0 CS selector 0xf000 CS base 0xffff0000

处理器开始在实模式工作。我们需要退回一点去理解在这种模式下的内存分段机制。从 8086到现在的 Intel 64 位 CPU,所有 x86兼容处理器都支持实模式。8086 处理器有一个20位寻址总线,这意味着它可以对0到 2^20 位地址空间( 1MB )进行操作。不过它只有16位的寄存器,所以最大寻址空间是 2^16 即 0xffff (64 KB)。实模式使用段式内存管理 来管理整个内存空间。所有内存被分成固定的65536字节(64 KB) 大小的小块。由于我们不能用16位寄存器寻址大于 64KB 的内存,一种替代的方法被设计出来了。一个地址包括两个部分:数据段起始地址和从该数据段起的偏移量。为了得到内存中的物理地址,我们要让数据段乘16并加上偏移量:

PhysicalAddress = Segment * 16 + Offset

举个例子,如果 CS:IP 是 0x2000:0x0010, 则对应的物理地址将会是:

>>> hex((0x2000 >>> hex((0xffff >>> 0xffff0000 + 0xfff0 '0xfffffff0'

得到的 0xfffffff0 是 4GB - 16 字节。 这个地方是 复位向量(Reset vector) 。 这是CPU在重置后期望执行的第一条指令的内存地址。它包含一个 jump 指令,这个指令通常指向BIOS入口点。举个例子,如果访问 coreboot 源代码,将看到:

.section ".reset", "ax", %progbits .code16 .globl _start _start: .byte 0xe9 .int _start16bit - ( . + 2 ) ...

上面的跳转指令( opcode - 0xe9)跳转到地址  _start16bit - ( . + 2) 去执行代码。 reset 段是 16 字节代码段, 起始于地址 0xfffffff0(src/cpu/x86/16bit/reset16.ld),因此 CPU 复位之后,就会跳到这个地址来执行相应的代码 :

SECTIONS { /* Trigger an error if I have an unuseable start address */ _bogus = ASSERT(_start16bit >= 0xffff0000, "_start16bit too low. Please report."); _ROMTOP = 0xfffffff0; . = _ROMTOP; .reset . : { *(.reset); . = 15; BYTE(0x00); } }

现在BIOS已经开始工作了。在初始化和检查硬件之后,需要寻找到一个可引导设备。可引导设备列表存储在在 BIOS 配置中, BIOS 将根据其中配置的顺序,尝试从不同的设备上寻找引导程序。对于硬盘,BIOS 将尝试寻找引导扇区。如果在硬盘上存在一个MBR分区,那么引导扇区储存在第一个扇区(512字节)的头446字节,引导扇区的最后必须是 0x55 和 0xaa ,这2个字节称为魔术字节(Magic Bytes),如果 BIOS 看到这2个字节,就知道这个设备是一个可引导设备。举个例子:

; ; Note: this example is written in Intel Assembly syntax ; [BITS 16] [ORG 0x7c00] boot: mov al, '!' mov ah, 0x0e mov bh, 0x00 mov bl, 0x07 int 0x10 jmp $ times 510-($-$$) db 0 db 0x55 db 0xaa

构建并运行:

nasm -f bin boot.nasm && qemu-system-x86_64 boot

这让 QEMU 使用刚才新建的 boot 二进制文件作为磁盘镜像。由于这个二进制文件是由上述汇编语言产生,它满足引导扇区(起始设为 0x7c00, 用Magic Bytes结束)的需求。QEMU将这个二进制文件作为磁盘镜像的主引导记录(MBR)。

将看到:

Simple bootloader which prints only !

在这个例子中,这段代码被执行在16位的实模式,起始于内存0x7c00。之后调用 0x10 中断打印 ! 符号。用0填充剩余的510字节并用两个Magic Bytes 0xaa 和 0x55 结束。

可以使用 objdump 工具来查看转储信息:

nasm -f bin boot.nasm objdump -D -b binary -mi386 -Maddr16,data16,intel boot

一个真实的启动扇区包含了分区表,以及用来启动系统的指令,而不是像我们上面的程序,只是输出了一个感叹号就结束了。从启动扇区的代码被执行开始,BIOS 就将系统的控制权转移给了引导程序,让我们继续往下看看引导程序都做了些什么。

NOTE: 强调一点,上面的引导程序是运行在实模式下的,因此 CPU 是使用下面的公式进行物理地址的计算的:

PhysicalAddress = Segment * 16 + Offset

而且正如我前面所说的,在实模式下,CPU 只能使用16位的通用寄存器。16位寄存器能够表达的最大数值是:0xffff ,所以按照上面的公式计算出的最大物理地址是:

>>> hex((0xffff * 16) + 0xffff) '0x10ffef'

这个地址在 8086 处理器下,将被转换成地址 0x0ffef, 原因是因为,8086 cpu 只有20位地址线,只能表示 2^20 = 1MB 的地址,而上面这个地址已经超出了 1MB 地址的范围,所以 CPU 就舍弃了最高位。

实模式下的 1MB 地址空间分配表:

0x00000000 - 0x000003FF - Real Mode Interrupt Vector Table 0x00000400 - 0x000004FF - BIOS Data Area 0x00000500 - 0x00007BFF - Unused 0x00007C00 - 0x00007DFF - Our Bootloader 0x00007E00 - 0x0009FFFF - Unused 0x000A0000 - 0x000BFFFF - Video RAM (VRAM) Memory 0x000B0000 - 0x000B7777 - Monochrome Video Memory 0x000B8000 - 0x000BFFFF - Color Video Memory 0x000C0000 - 0x000C7FFF - Video ROM BIOS 0x000C8000 - 0x000EFFFF - BIOS Shadow Area 0x000F0000 - 0x000FFFFF - System BIOS

如果你的记性不错,在看到这张表的时候,一定会跳出来一个问题。在上面的章节中,我说了 CPU 执行的第一条指令是在地址 0xFFFFFFF0 处,这个地址远远大于 0xFFFFF ( 1MB )。那么实模式下的 CPU 是如何访问到这个地址的呢?文档 coreboot 给出了答案:

0xFFFE_0000 - 0xFFFF_FFFF: 128 kilobyte ROM mapped into address space

0xFFFFFFF0 这个地址被映射到了 ROM,因此 CPU 执行的第一条指令来自于 ROM,而不是 RAM。

引导程序

在现实世界中,要启动 Linux 系统,有多种引导程序可以选择。比如 GRUB 2 和 syslinux。Linux内核通过 Boot protocol 来定义应该如何实现引导程序。在这里我们将只介绍 GRUB 2。

现在 BIOS 已经选择了一个启动设备,并且将控制权转移给了启动扇区中的代码,在我们的例子中,启动扇区代码是 boot.img。因为这段代码只能占用一个扇区,因此非常简单,只做一些必要的初始化,然后就跳转到 GRUB 2's core image 去执行。 Core image 的代码请参考 diskboot.img,一般来说 core image 在磁盘上存储在启动扇区之后到第一个可用分区之前。core image 的初始化代码会把整个 core image (包括 GRUB 2的内核代码和文件系统驱动) 引导到内存中。 引导完成之后,grub_main将被调用。

grub_main 初始化控制台,计算模块基地址,设置 root 设备,读取 grub 配置文件,加载模块。最后,将 GRUB 置于 normal 模式,在这个模式中,grub_normal_execute (from grub-core/normal/main.c) 将被调用以完成最后的准备工作,然后显示一个菜单列出所用可用的操作系统。当某个操作系统被选择之后,grub_menu_execute_entry 开始执行,它将调用 GRUB 的 boot 命令,来引导被选中的操作系统。

就像 kernel boot protocol 所描述的,引导程序必须填充 kernel setup header (位于 kernel setup code 偏移 0x01f1 处) 的必要字段。kernel setup header的定义开始于 arch/x86/boot/header.S:

.globl hdr hdr: setup_sects: .byte 0 root_flags: .word ROOT_RDONLY syssize: .long 0 ram_size: .word 0 vid_mode: .word SVGA_MODE root_dev: .word 0 boot_flag: .word 0xAA55

bootloader必须填充在 Linux boot protocol 中标记为 write 的头信息,比如 type_of_loader,这些头信息可能来自命令行,或者通过计算得到。在这里我们不会详细介绍所有的 kernel setup header,我们将在需要的时候逐个介绍。不过,你可以自己通过 boot protocol 来了解这些设置。

通过阅读 kernel boot protocol,在内核被引导入内存后,内存使用情况将如下表所示:

| Protected-mode kernel | 100000 +------------------------+ | I/O memory hole | 0A0000 +------------------------+ | Reserved for BIOS | Leave as much as possible unused ~ ~ | Command line | (Can also be below the X+10000 mark) X+10000 +------------------------+ | Stack/heap | For use by the kernel real-mode code. X+08000 +------------------------+ | Kernel setup | The kernel real-mode code. | Kernel boot sector | The kernel legacy boot sector. X +------------------------+ | Boot loader |


【本文地址】

公司简介

联系我们

今日新闻

    推荐新闻

    专题文章
      CopyRight 2018-2019 实验室设备网 版权所有