总结

1、硬件阶段

  • 实模式只有 1MB 内存寻址空间(X86)
    - 加电, 重置 CS 为 0xFFFF , IP 为 0x0000, 对应 BIOS 程序
    - 0xF0000-0xFFFFF 映射到 BIOS【Basic Input Output System】,程序(存储在ROM中), JMP** 命令会跳到 ROM 中做初始化工作的代码【计算机开始执行第一条指令】**

2、 BIOS

BIOS 做以下三件事:
- 检查硬件
- 提供基本输入(中断)输出(显存映射)服务 【】
-加载bootloader

3、bootloader

bootloader十分依赖具体硬件,因不同的cpu等硬件配置各不相同。常见的如 Grub2 ,就是一种bootloader。

Grub2【bootloader 首先加载安装 boot.img -> 然后是 core.img


core.img 包含以下,并依次加载:**

  1. diskboot.img
  2. lzma_decompress.img
  3. kernel.img
  4. 其他模块

    lzma_decompress.img 决定从实模式切换到保护模式

    4、从实模式切换到保护模式


cpu

CPU 和内存是如何配合工作的

CPU 其实也不是单纯的一块,它包括三个部分,运算单元、数据单元和控制单元。
1、运算单元只管算,例如做加法、做位移等等。但是,它不知道应该算哪些数据,运算结果应该放在哪里。
2、 数据单元。数据单元包括 CPU 内部的缓存和寄存器组,空间很小,但是速度飞快,可以暂时存放数据和运算结果。
3、控制单元。控制单元是一个统一的指挥中心,它可以获得下一条指令,然后执行这条指令。这个指令会指导运算单元取出数据单元中的某几个数据,计算出个结果,然后放在数据单元的某个地方。

image.jpeg

数据单元

为了暂存数据,8086 处理器内部有 8 个 16 位的通用寄存器,也就是刚才说的 CPU 内部的数据单元,分别是 AX、BX、CX、DX、SP、BP、SI、DI。这些寄存器主要用于在计算过程中暂存数据。

这些寄存器比较灵活,其中 AX、BX、CX、DX 可以分成两个 8 位的寄存器来使用,分别是 AH、AL、BH、BL、CH、CL、DH、DL,其中 H 就是 High(高位),L 就是 Low(低位)的意思。

控制单元。

IP 寄存器就是指令指针寄存器(Instruction Pointer Register),指向代码段中下一条指令的位置。CPU 会根据它来不断地将指令从内存的代码段中,加载到 CPU 的指令队列中,然后交给运算单元去执行。

每个进程都分代码段和数据段,为了指向不同进程的地址空间,有四个 16 位的段寄存器,分别是 CS、DS、SS、ES。

其中,CS 就是代码段寄存器(Code Segment Register),通过它可以找到代码在内存中的位置;DS 是数据段的寄存器,通过它可以找到数据在内存中的位置,SS 是栈寄存器(Stack Register)。

image.jpeg


地址加法器

8086是一个16位系统,寄存器是16位的,但是他的地址总线却有20位,那么16位的寄存器如何表示20位的地址?

段地址+偏移地址,这种形式就自然而然的出现了,CS这个寄存器就是保存着代码段的段地址,IP这个寄存器就是保存着代码的偏移地址,这样,通过CS:IP所表示的地址就能寻找到执行代码的地方。

那么两个16位寄存器怎么表示20位的地址?段地址*16【即段地址左移4位】+偏移地址,这就是段地址+偏移地址的真实含义,为什么要乘以16?这就是把段地址左移4位,加上原来的16位偏移地址,就可以得到一个20位的地址。


image.jpeg
在 x86 系统中,将 1M(即 2^20) 空间最上面的 0xF0000 到 0xFFFFF 这 64K 映射给 ROM,也就是说,到这部分地址访问的时候,会访问 ROM。

当电脑刚加电的时候,会做一些重置的工作,将 CS 设置为 0xFFFF【在电气上,也就是将16位代码段寄存器都置位高电平】,将 IP 设置为 0x0000,所以第一条指令就会指向【**cs会左移4位得到**0xFFFF0】 0xFFFF0,【即:CS 左移四位变成了0xFFFF0 + IP】,正是在 ROM 的范围内。在这里,有一个** JMP 命令会跳到 ROM 中做初始化工作的代码【计算机开始执行第一条指令**】,于是,BIOS 开始进行初始化的工作。

关于 Gate A20 的理解是:8086 地址线20根 -> 可用内存 0 ~ FFFFF,寄存器却是16位,寻址模式为 segment(16位):offset(16位), 最大范围变成 0xFFFF0(左移了4位) + 0xFFFF = 0x10FFEF


  • 实模式只有 1MB 内存寻址空间(X86)
    - 加电, 重置 CS 为 0xFFFF , IP 为 0x0000, 对应 BIOS 程序
    - 0xF0000-0xFFFFF 映射到 BIOS【Basic Input Output System】 程序(存储在ROM中), BIOS 做以下三件事:
    - 检查硬件
    - 提供基本输入(中断)输出(显存映射)服务
    -加载bootloader

在 BIOS 的界面上。你会看到一个启动盘的选项。启动盘有什么特点呢?它一般在第一个扇区,占 512 字节,而且以 0xAA55【**0xAA55**二进制表示:1010 1010 0101 0101】 结束。这是一个约定,当满足这个条件的时候,就说明这是一个启动盘,在 512 字节以内会启动相关的、 -> 通常是 Grub2,全称 Grand Unified Bootloader Version 2。

你可以通过 grub2-mkconfig -o /boot/grub2/grub.cfg 来配置系统启动的选项。你可以看到里面有类似这样的配置。

  1. 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' {
  2. load_video
  3. set gfxpayload=keep
  4. insmod gzio
  5. insmod part_msdos
  6. insmod ext2
  7. set root='hd0,msdos1'
  8. if [ x$feature_platform_search_hint = xy ]; then
  9. search --no-floppy --fs-uuid --set=root --hint='hd0,msdos1' b1aceb95-6b9e-464a-a589-bed66220ebee
  10. else
  11. search --no-floppy --fs-uuid --set=root b1aceb95-6b9e-464a-a589-bed66220ebee
  12. fi
  13. 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
  14. initrd16 /boot/initramfs-3.10.0-862.el7.x86_64.img
  15. }

grub2 第一个要安装的就是 boot.img【见下图】。它由 boot.S 编译而成,一共 512 字节,正式安装到启动盘的第一个扇区。这个扇区通常称为MBR(Master Boot Record,主引导记录 / 扇区)。

BIOS 完成任务后,会将 boot.img 从硬盘加载到内存中的 0x7c00 来运行。

由于 512 个字节实在有限,boot.img 做不了太多的事情。它能做的最重要的一个事情就是加载 grub2 的另一个镜像 core.img

core.img 就是管理处,它们知道的和能做的事情就多了一些。core.img 由 lzma_decompress.img、diskboot.img、kernel.img 和一系列的模块组成,功能比较丰富,能做很多事情。
image.jpeg

boot.img 先加载的是 core.img 的第一个扇区。如果从硬盘启动的话,这个扇区里面是 diskboot.img,对应的代码是 diskboot.S。

boot.img 将控制权交给 diskboot.img 后,diskboot.img 的任务就是将 core.img 的其他部分加载进来,先是解压缩程序 lzma_decompress.img,再往下是 kernel.img,最后是各个模块 module 对应的映像。这里需要注意,它不是 Linux 的内核,而是 grub 的内核。

lzma_decompress.img 对应的代码是 startup_raw.S,本来 kernel.img 是压缩过的,现在执行的时候,需要解压缩。【需要打开 Gate A20】

关于 Gate A20 的理解是:8086 地址线20根 -> 可用内存 0 ~ FFFFF,寄存器却是16位,寻址模式为 segment(16位):offset(16位), 最大范围变成 0xFFFF0(左移了4位) + 0xFFFF = 0x10FFEF

在这之前,我们所有遇到过的程序都非常非常小,完全可以在实模式下运行,但是随着我们加载的东西越来越大,实模式这 1M 的地址空间实在放不下了,所以在真正的解压缩之前,lzma_decompress.img 做了一个重要的决定,就是调用 real_to_prot,切换到保护模式,这样就能在更大的寻址空间里面,加载更多的东西。下面就是开始切换到保护模式了


从实模式切换到保护模式

切换到保护模式要干很多工作,大部分工作都与内存的访问方式有关。

  • 第一项是启用分段,就是在内存里面建立段描述符表,将寄存器里面的段寄存器变成段选择子【如cs,ds,ss等】指向某个段描述符,这样就能实现不同进程的切换了。
  • 第二项是启动分页。能够管理的内存变大了,就需要将内存分成相等大小的块,这些我们放到内存那一节详细再讲。

保护模式需要做一项工作,那就是打开 Gate A20,也就是第 21 根地址线的控制线。在实模式 8086 下面,一共就 20 个地址线,可访问 1M 的地址空间。如果超过了这个限度怎么办呢?当然是绕回来了。在保护模式下,第 21 根要起作用了,于是我们就需要打开 Gate A20。

切换保护模式的函数 DATA32 call real_to_prot 会打开 Gate A20,也就是第 21 根地址线的控制线。

现在好了,有的是空间了。接下来我们要对压缩过的 kernel.img 进行解压缩,然后跳转到 kernel.img 开始运行。

kernel.img 对应的代码是 startup.S 以及一堆 c 文件,在 startup.S 中会调用 grub_main,这是 grub kernel 的主函数。

在这个函数里面,grub_load_config() 开始解析,我们上面写的那个 grub.conf 文件里的配置信息。

如果是正常启动,grub_main 最后会调用 grub_command_execute (“normal”, 0, 0),最终会调用grub_normal_execute() 函数。在这个函数里面,grub_show_menu() 会显示出让你选择的那个操作系统的列表。当选择启动某个操作系统后,就要开始调用 grub_menu_execute_entry() ,开始解析并执行你选择的那一项。

例如里面的 linux16 命令,表示装载指定的内核文件,并传递内核启动参数。于是 grub_cmd_linux() 函数会被调用,它会首先读取 Linux 内核镜像头部的一些数据结构,放到内存中的数据结构来,进行检查。如果检查通过,则会读取整个 Linux 内核镜像到内存。

如果配置文件里面还有 initrd 命令,用于为即将启动的内核传递 init ramdisk 路径。于是 grub_cmd_initrd() 函数会被调用,将 initramfs 加载到内存中来。

当这些事情做完之后,grub_command_execute (“boot”, 0, 0) 才开始真正地启动内核。