Linux 内核|启动

本文最后更新于:22 天前

1.计算机工作模式

1.1 CPU 三大构成

  • 运算单元:做加法、做位移等等。不知道算哪些数据、结果存哪里。

  • 数据单元:包括CPU内部缓存 + 寄存器组,暂时存放数据 + 运算结果。

  • 控制单元:获取指令、执行指令。这个指令会指导运算单元从数据单元取某个数,并执行某个运算,最后存入数据单元的某个地方

1.2 控制单元工作原理

CPU 在执行程序时,控制单元完成其工作?(知道取哪些数、执行哪个运算、存到哪里)

  • 指令指针寄存器:存有下一条指令在内存中的地址。

  • 指令寄存器:控制单元会按照 指令指针寄存器 中的地址,从内存中取出指令,存入 指令寄存器

PS:指令分为两个部分:操作码(运算类型) + 操作数(运算数据的地址或运算数据本身),操作码交给运算单元,告诉运算单元做什么运算,操作数交给数据单元,让数据单元去读取运算数据。

1.3 进程切换

CPU 中有两个寄存器 CS、DS,分别保存当前进程代码段的起始位置、数据段的起始位置。

例:当进程 A 切换成进程 B 时,CS、DS 便会分别储存进程 B 的代码段起始位置、数据段起始位置。

1.4 地址总线和数据总线

CPU 和内存使用总线(Bus)来传输数据。

  • 地址总线(Address Bus):传输的是地址数据。

  • 数据总线(Data Bus):传输的是真正的操作数据。

位数:

  • 地址总线的位数:决定了能访问的地址范围到底有多广。例如只有两位,那 CPU 就只能认 00,01,10,11 四个位置,超过四个位置,就区分不出来了。位数越多,能够访问的位置就越多,能管理的内存的范围也就越广。

    • 32 位系统表示,地址总线为 32 位,地址范围为 232,约 4G 大小。32 位系统最多使用 4G 的内存。
    • 64 位系统表示,地址总线为 64 位,地址范围为 264,约 16EB 大小。
  • 数据总线的位数 = CPU位宽 = CPU 内部通用寄存器的位宽:决定了一次能拿多少个数据进来。例如只有两位,那 CPU 一次只能从内存拿两位数。要想拿八位的 int,就要拿四次。位数越多,一次拿的数据就越多,访问速度也就越快。

补充:

除了地址总线和数据总线,还有片内总线、控制总线、通信总线。

  • 片内总线:CPU 芯片内部寄存器与寄存器之间、寄存器与运算单元之间的公共连接线。

  • 控制总线:传输控制信息,包括 CPU 发送出去的控制命令、主存(或外设)返回 CPU 的反馈信号。

  • 通信总线(外部总线):不同设备之间信息传输的总线。

1.5 8086 处理器

8086 处理器如下图:

1.5.1 数据单元

由 8 个 16 位的通用寄存器(AX、BX、CX、DX、SP、BP、SI、DI)组成。

  • AX,BX,CX,DX 作为数据寄存器:

    • AX (Accumulator):累加寄存器,也称之为累加器

    • BX (Base):基地址寄存器

    • CX (Count):计数器寄存器

    • DX (Data):数据寄存器

      AX、BX、CX、DX 可以分为两个 8 位的寄存器来使用。比如 AX 可以分为 AH、AL 两个寄存器,H 表示 High 高位,L 表示 Low 低位。(优点:既可以储存长数据,也可以储存短数据。也是为了兼容以前基于 8080 等 8 位微处理器的程序。)

  • SP 和 BP 作为指针寄存器:

    • SP (Stack Pointer):堆栈指针寄存器
    • BP (Base Pointer):基指针寄存器
  • SI 和 DI 作为变址寄存器:

    • SI (Source Index):源变址寄存器
    • DI (Destination Index):目的变址寄存器

1.5.2 控制单元

由 2 个控制寄存器(IP、FLAG)和 4 个段寄存器(CS、DS、SS、ES)组成。

  • 控制寄存器:

    • IP (Instruction Pointer):指令指针寄存器,IP 寄存器存储代码段中下一个指令的地址(实际上是地址偏移量 Offset)。CPU 不断通过 IP 寄存器,将指令从内存的代码段中,加载到 CPU 指令队列缓存器中,然后通过运算单元执行。
    • FLAG:标志寄存器
  • 段寄存器:

    • CS (Code Segment):代码段寄存器,代码段在内存中的起始地址。
    • DS (Data Segment):数据段寄存器,数据段在内存中的起始地址。
    • SS (Stack Segment):堆栈段寄存器,实现函数调用时的压栈入栈。
    • ES (Extra Segment):附加段寄存器。

1.5.3 问题

  • Q:如何加载内存的数据?

    A:真实地址 = 16 位的基地址 + 16 位的偏移地址。对于一个段,有一个起始的地址(基地址),而段内的具体位置,我们称为偏移量(Offset)。在 CS 和 DS 中都存放着一个段的起始地址。代码段的偏移量在 IP 寄存器中,数据段的偏移量会放在通用寄存器中。

  • Q:CS、DS 均为 16 位寄存器,能储存的最大地址为 216,地址总线为 20 位,寻址能力为 220,如何提高存储的最大地址?

    A:原本是16 位的基地址 + 16 位的偏移地址,现在将 CS 和 DS 的值左移 4 位,即16 位基地址 × 24 + 16位偏移量。即可满足220的寻址需求。

  • Q:一个数据段多大?

    A:偏移量是 16 位,所以一个段最大的大小是 216 = 64KB。

1.6 x86 架构

1985年,intel 推出 80386 处理器,32位处理器,如何继续使用 x86 架构呢?

为了保证向后兼容(backward),intel 在原来的 x86 架构进行扩展,如下图。

  • 通用寄存器从 8 个 16 位扩展为 8 个 32 位,同时依然保持了 16 位和 8 位的使用方式。
  • IP 寄存器扩展成 32 位,同时兼容 16 位。
  • 段寄存器依然保持 16 位,但不再是段的起始地址。
    • 段的起始地址放在内存中的段描述符表,表中含有多个段描述符(Segment Descriptor),分别对应着不同的段起始地址。
    • 段寄存器中存储的则是表格中的索引,称为选择子(Selector)。
    • 段寄存器将 段起始地址 从内存中拿到CPU的段描述符缓存器中,以便 CPU 更快的取到段起始地址。

Q:为什么段寄存器不扩展成 32 位,以便向后兼容?

A:232 已经有 4G 大小的寻址能力,是否还需要保留左移 4 位操作,所以不如重新设计。

但是这样更改,段寄存器如何向后兼容呢?

  • 实模式(Real Pattern):段寄存器储存段起始地址。

  • 保护模式(Protected Pattern):段寄存器储存段描述表的选择子。

当系统刚启动时,CPU 处于实模式,可以向后兼容。当需要更多的内存时(超过 220 的寻址能力,1M),可以切换到保护模式。

1.7 实模式与保护模式

  • 实模式(Real Mode):又名 Real Address Mode,地址访问的是真实地内存地址所在位置。在此模式下,可以使用 20位(1MB)的地址空间,软件可以不受限制的操作所有地址的空间和IO设备。

  • 保护模式(Protected Mode):又名 Protected Virtual Address Mode,采用虚拟内存、页等机制对内存进行了保护,比起实模式更为安全可靠,同时也增加了灵活性和扩展性。

2.启动流程

2.1 初始化 BIOS

开机后,按特定的组合键就能进入 BIOS 界面了。

在主板上,有个 ROM(Read Only Memory,只读存储器),ROM 是只读的,上面固化了初始化程序 BIOS(Basic Input and Output System,基本输入输出系统)。

  1. 启动电源

    当按下电源键,主板会发向电源组发出信号,接收到信号后,电源会提供合适的电压给计算机。

  2. 重置寄存器

    当主板收到电源正常启动的信号后,主板会启动CPU,CPU重置所有寄存器数据,并设置初始化数据,将 CS 设置为 0xFFFF,将 IP 设置为 0x0000。

  3. 初始化 BIOS

    根据 CS 和 IP 寄存器,计算出的第一个指令地址为 0xFFFF0,这条 JMP 指令会跳到 ROM 中做初始化工作的代码,开始 BIOS 的初始化工作。

Q:为什么要将 CS 设置为 0xFFFF,将 IP 设置为 0x0000?

A:这是初始化 BIOS 的指令地址。

  • 实模式启动最多可使用 0 - 0xFFFFF 的 1M 内存空间,由于只有16位寄存器,最大地址只能表示为 0xFFFFF(64KB),因此不得不采取将内存按段划分为 64KB 的方式来充分利用 1M 内存。

  • 在主板上,有个只读的 ROM ,上面固化了初始化程序 BIOS。将 1M 空间最上面的 0xF0000 到 0xFFFFF 这 64K 映射给 ROM。

  • 根据 CS 和 IP 寄存器的值计算出的地址便在 ROM 区中(0xFFFF × 16 + 0x0000 = 0xFFFF0)。

    Generated

2.2 构建中断向量表

如上图,1M 的具体分区可以分为,

1
2
3
4
5
6
7
8
9
10
11
0x00000000 - 0x000003FF - Real Mode Interrupt Vector Table    # 中断向量表
0x00000400 - 0x000004FF - BIOS Data Area # BIOS 数据区
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
  • BIOS 程序在内存最开始的位置(0x00000)用1KB 的内存空间(0x00000~0x003FF)构建中断向量表
  • 在此位置之后,用 256 字节的内存空间构建 BIOS 数据区(0x00400~0x004FF)。
  • 并在大约 57KB 以后的位置(0x0E05B)加载了8KB 左右的与中断向量表相应的若干中断服务程序。
  • 中断向量表中有 256 个中断向量,每个中断向量占 4 字节,其中两个字节是 CS 的值,两个字节是 IP 的值。每个中断向量都指向一个具体的中断服务程序。

2.3 加载 BootLoader

要想启动操作系统,BIOS 需要找到引导盘的第一个扇区——主引导扇区 MBR ,并执行 MBR 的一段代码 BootLoader(引导程序)。

2.3.1 主引导扇区

  • 引导盘(Bootable Disk)第一个扇区是主引导扇区(MBR,Master Boot Record),占 512 个字节,通常是在 /dev/sda 或者 /dev/hda。
  • 由 446 字节的 MBR引导代码、64 字节的分区表和 2 字节的结束标志组成。最后两个字节为 0x55、0xAA(小端方式存储,实际值为 0xAA55)。
  • 主引导扇区中最后两个字节,是检验主引导记录是否有效的标志。

2.3.2 BootLoader 的工作

boot.img 就是 Linux 的 BootLoader 引导程序,位于 MBR 中;由 boot.img 来加载其他 img 文件。

  1. 加载 boot.img

    • boot.img 由 boot.S 编译而成,512字节,在 MBR 中。
    • BIOS 将 boot.img 从 MRB 中加载到内存的 0x7c00 的位置,并执行 boot.img 的代码。至此,BIOS 将控制权交给 boot.img
  2. 加载 core.img

    • core.img 由 lzma_decompress.img、diskboot.img、kernel.img 和一系列的模块组成。

    • boot.img 先加载 core.img 的第一个扇区,即 diskboot.img,对应代码为 diskboot.S。至此,boot.img 将控制权交给 diskboot.img 。

    • diskboot.img 的任务就是将 core.img 的其他部分加载进来。

      • diskboot.img 先加载 lzma_decompress.img,它是解压缩程序,用于解压缩 kernel.img,应的代码是 startup_raw.S。

        因为实模式 1M 的地址空间太小,在解压缩 kernel.img 之前,需要将实模式切换成保护模式(下文具体描述如何切换)。

      • 切换成保护模式后,对压缩过的 kernel.img 进行解压缩,对应的代码是 startup.S 以及一堆 c 文件。(kernel.img 不是 Linux 的内核,而是 grub 的内核。)

      • 最后加载各个模块 module 对应的映像。

  3. 启动 grub_main 函数

    • 在 startup.S 中会调用 grub_main,这是 grub kernel 的主函数。

    • grub_main 函数初始化控制台,计算模块基地址,设置 root 设备。

    • grub_main 函数调用 grub_load_config() 函数,对 grub.conf 文件里的配置信息进行解析。

    • 如果是正常启动,grub_main 函数调用 grub_command_execute (“normal”, 0, 0) 将 GRUB 置于 normal 模式。

    • 在 normal 模式中, grub-core/normal/main.c 中的 grub_normal_execute 函数将被调用,以完成最后的准备工作,并调用 grub_show_menu() 显示一个菜单列出所用可用的操作系统。

    • 当某个操作系统被选择之后,``grub_menu_execute_entry开始执行grub_command_execute (“boot”, 0, 0)`,它将调用 GRUB 的 boot 命令,来引导被选中的操作系统。

至此,系统终于启动了。

3.切换保护模式

lzma_decompress.img 调用 real_to_prot 函数,从实模式切换到保护模式。

两大主要事件:

  • 启用分段:在内存里面建立段描述符表,将寄存器里面的段寄存器变成段选择子,指向某个段描述符,这样就能实现不同进程的切换了。
  • 启动分页:将内存分成相等大小的块,详见内存管理部分。

3.1 新旧中断的交替

  1. 屏蔽中断
    • 在 16 位实模式下的中断由 BIOS 处理,进入保护模式后,中断将交给中断描述符表 IDT 里规定的函数处理。
    • 在刚进入保护模式时,中断描述符表寄存器(IDTR)的初始值为 0,一旦发生中断(例如BIOS的时钟中断)就将导致CPU发生异常,所以需要首先屏蔽中断。
    • 屏蔽中断可以使用 cli 指令。

全局描述符表寄存器 GDTR、中断描述符表寄存器 IDTR、局部描述符表寄存器 LDTR、任务寄存器 TR。

  1. 初始化全局描述符表 GDT
  • GDT(Global Descriptor Table,全局描述符表),在系统中唯一的存放段寄存器内容(段描述符)的数组,配合程序进行保护模式下的段寻址。它在操作系统的进程切换中具有重要意义,可理解为所有进程的总目录表,其中存放每一个任务(task)局部描述符表(LDT, Local Descriptor Table)地址和任务状态段(TSS, Task Structure Segment)地址,完成进程中各段的寻址、现场保护与现场恢复。GDTR 是 GDT 基地址寄存器,当程序通过段寄存器引用一个段描述符时,需要取得 GDT 的入口,GDTR 标识的即为此入口。在操作系统对 GDT 的初始化完成后,可以用 LGDT(Load GDT)指令将 GDT 基地址加载至 GDTR。

  • IDT(Interrupt Descriptor Table,中断描述符表),保存保护模式下所有中断服务程序的入口地址,类似于实模式下的中断向量表。IDTR(IDT 基地址寄存器),保存 IDT 的起始地址。

Q:为什么要建立全局描述符表 GDT?

A:16 位空间无法存储 64 位的段描述符。

  • 在保护模式中,段与段之间是互相隔离的,当访问的地址超出段的界限时处理器就会阻止这种访问。
  • 因此每个段都需要有起始地址、范围、访问权限以及其他属性四个部分,这四个部分合在一起叫做段描述符(Segment Descriptor),总共需要 8 个字节来描述。
  • 但 Intel 为了保持向后兼容,将段寄存器仍然规定为16 位,我们无法用16 位的段寄存器来直接存储 64 位的段描述符。
  • 解决的办法是将所有 64 位的段描述符放到一个数组中,将 16 位段寄存器的值作为下标来访问这个数组(以字节为单位),获取 64位 的段描述符,这个数组就叫做全局描述符表(Global Descriptor Table, GDT)。

3.2 启用 Gate A20

16 位实模式,20 根总线最多访问 1M 的内存空间,现在切换 32 位保护模式,需要开启第 21 根地址线的控制线。

lzma_decompress.img 调用 real_to_prot() 函数启动。打开A20,意味着CPU可以进行 32 位寻址,最大寻址空间为 4 GB。

  • 实模式下,当程序寻址超过 0xFFFFF 时,CPU将“回滚”至内存地址起始处寻址(注意,在只有20根地址线的条

件下,0xFFFFF+1=0x00000,最高位溢出)

  • 此处对A20地址线的启用相当于关闭CPU在实模式下寻址的“回滚”机制。

4.GRUB2

Grub2(Grand Unified Bootloader Version 2)

澄清:

  • boot.img、core.img 等 img 文件,属于 Grub2 这个工具的,
  • grub2 将 boot.img 转换后的内容安装到 MBR 中,将 core.img 等文件安装在硬盘的其他指定位置。

Linux 内核通过 Boot Protocol 定义如何实现 BootLoader ,有如 Grub2 和 syslinux 等具体实现方式。

  • 作用:Grub2 将 boot.img 安装在 MRB 中,作为 BootLoader(引导程序)。
1
grub2-install /dev/sda  # /dev/sda 为引导盘。
  • 查看系统启动的配置信息:
1
Shell   grub2-mkconfig -o /boot/grub2/grub.cfg
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
menuentry 'CentOS Linux (3.10.0-862.el7.x86_64) 7 (Core)' --class centos --class gnu-linux --class gnu --class os --unrestricted $menuentry_id_option 'gnulinux-3.10.0-862.el7.x86_64-advanced-b1aceb95-6b9e-464a-a589-bed66220ebee' {
load_video
set gfxpayload=keep
insmod gzio
insmod part_msdos
insmod ext2
set root='hd0,msdos1'
if [ x$feature_platform_search_hint = xy ]; then
search --no-floppy --fs-uuid --set=root --hint='hd0,msdos1' b1aceb95-6b9e-464a-a589-bed66220ebee
else
search --no-floppy --fs-uuid --set=root b1aceb95-6b9e-464a-a589-bed66220ebee
fi
linux16 /boot/vmlinuz-3.10.0-862.el7.x86_64 root=UUID=b1aceb95-6b9e-464a-a589-bed66220ebee ro console=tty0 console=ttyS0,115200 crashkernel=auto net.ifnames=0 biosdevname=0 rhgb quiet
initrd16 /boot/initramfs-3.10.0-862.el7.x86_64.img
}

以上配置在系统启动时,形成一个列表,选择从哪个系统启动。显示效果如下图。

参考


本博客所有文章均个人原创,除特别声明外均采用 CC BY-SA 4.0协议,转载请注明出处!