Linux之系统启动流程(2)—BIOS与Bootloader

date
slug
linux-1
author
status
Public
tags
Linux
summary
type
Post
thumbnail
fSSLnMbq55M7TPHVgyoUjn-1200-80.jpeg
category
updatedAt
Mar 22, 2023 09:22 AM

什么是BIOS

 
BIOS(Basic Input and Output System,基本输入输出系统),这个相信一般的IT从业者都知道这是啥。当然了,就我认识的人来说,还是有相当一部分人是电脑白痴,他们肯定也不知道BIOS到底是个啥东西。
 
简而言之,BIOS就是我们按下开机键计算机启动的第一个程序。
 
华硕BIOS-UEFI
华硕BIOS-UEFI
从一个玩机爱好者的角度来说,BIOS能做的事情真是太多了,下面简单列举一下:
  • 设置启动盘、电源管理
  • 修改散热风扇转速
  • 设置CPU、内存频率
  • 开启/关闭CPU虚拟化
  • 修改灯光设置(申明:本人是拒绝光污染玩家)
 

BIOS与ROM

 
或许你会发现,不管我们如何去破坏操作系统,去“蹂躏”我们的电脑(前提是不破坏硬件),每次开机时BIOS总是完好无损的出现在我们面前,然后“Give your shit”,那么BIOS为什么总是在开机的时候启动呢,这个程序是存放在哪里的呢?
 
在计算机主板上,有个东西叫做ROM(Read only Memory),上面由每个主板的厂家固化了一个初始化程序,也就是BIOS。当我们通过BIOS设置完一些功能之后,我们保存的这些设置在下次开机的时候仍然是生效的,那说明我们的配置是写入进去的。
 
不是说好的Read only吗,为什么可以写呢?这就不得不提到CMOS,CMOS是计算机上另一个重要的存储器。之所以提到它,是因为BIOS程序的设置值、硬件参数侦测值就保存在CMOS中。而且,在BIOS程序启动计算机时,需要加载CMOS中的设置值。CMOS通常被集成在南桥芯片组中。
 

BIOS与CMOS的区别

 
二者的区别是,BIOS是存储在只读存储器EEPROM闪存),而CMOS为随机存储器(RAM);BIOS中存储的是程序,而CMOS中存储的是普通信息。
CMOS的内容在断电会消失。所以,把主板的电池拆出,便可重置其内容。另外,拆出电池也会重置时间
 

BIOS与UEFI

 
在X86系统中,将 1M 空间最上面的 0xF0000 到 0xFFFFF 这 64K 映射给 ROM,也就是说,到这部分地址访问的时候,会访问 ROM。
 
当电脑刚加电的时候,会做一些重置的工作,将 CS 设置为 0xFFFF,将 IP 设置为 0x0000,所以第一条指令就会指向 0xFFFF0,正是在 ROM 的范围内。在这里,有一个 JMP 命令会跳到 ROM 中做初始化工作的代码,(所有CPU都会从某个地址开始执行,这是由处理器设计决定的)。于是,BIOS 开始进行初始化的工作。
 
由于历史原因,在刚开机时只能以实模式启动,所以这里最多只能访问1M的地址空间,具体可以看上篇文章:
 
而这1M的地址空间除了分给ROM一部分,其他又需要做很多事情:
notion image
 
64K空间能实现的BIOS程序功能肯定是有限的,现在的计算机组件日新月异,BIOS已经满足不了各种硬件的初始化需求了,所以在2007年,英特尔,AMD,微软和PC制造商就新的统一可扩展固件接口(UEFI)规范达成一致。 这是一个全行业标准管理的统一扩展固件接口,并不完全由英特尔推动。 Windows Vista Service Pack 1和Windows 7引入了对UEFI的支持。现在可以购买的绝大多数计算机现在都使用UEFI而不是传统的BIOS。
 
UEFI破除了很多传统BIOS的限制:
 
  • UEFI 支持高达 9 泽字节的驱动器大小,而 BIOS 仅支持 2.2 TB。
  • UEFI 提供更快的启动时间。UEFI 具有独立的驱动程序支持,而 BIOS 具有存储在其 ROM 中的驱动程序支持,因此更新 BIOS 固件有点困难。
  • UEFI 提供诸如“安全启动”之类的安全性,可防止计算机从未经授权/未签名的应用程序启动。这有助于防止 rootkit,但也会阻碍双启动,因为它将其他操作系统视为未签名的应用程序。目前,只有 Windows 和 Ubuntu 是签名操作系统(如果我错了,请告诉我)。
  • UEFI 以 32 位或 64 位模式运行,而 BIOS 以 16 位模式运行。因此 UEFI 能够提供 GUI(使用鼠标导航),而不是 BIOS,后者只允许使用键盘进行导航。
 
虽然现在传统的BIOS基本上消身匿迹了,但是写这篇文章,基于一个学习的角度,我还是会从BIOS以及MBR那一套来描述Linux的启动过程,毕竟“经典永不过时”😎
 

Bootloader

 
BIOS前面已经干了一大堆活了:
  1. 自检及初始化,开机后BIOS最先被启动,然后它会对电脑的硬件设施进行完全彻底的检验和测试。
  1. BIOS系统设置程序,设置/读取CMOS中参数信息
  1. 设置中断,开机时,BIOS会告诉CUP各硬件设备的中断号,当用户发出使用某个设备的指令后,CPU就根据中断号使用相应的硬件来完成工作,再根据中断号跳回去执行原来的工作。
  1. 在完成POST自检后,ROM BIOS将按照系统CMOS设置中的启动顺序搜寻软、硬盘驱动器及CDROM、网络服务器等有效的启动驱动器,读入操作系统引导记录,然后将系统控制权交给引导记录由引导记录完成系统的启动。
 
剩下的就交给Bootloader大兄弟了,Bootloader是在操作系统运行之前执行的一段小程序。通过这段小程序,我们可以初始化硬件设备、建立内存空间的映射表,从而建立适当的系统软硬件环境,为最终调用操作系统内核做好准备。
 
开源的Bootloader还真不少:
Bootloader
Monitor
描    述
x86
ARM
PowerPC
LILO
Linux磁盘引导程序
GRUB
GNU的LILO替代程序
Loadlin
从DOS引导Linux
ROLO
从ROM引导Linux而不需要BIOS
Etherboot
通过以太网卡启动Linux系统的固件
LinuxBIOS
完全替代BUIS的Linux引导程序
BLOB
LART等硬件平台的引导程序
U-boot
通用引导程序
RedBoot
基于eCos的引导程序
以GRUB为例,上面我们说到bootloader的第四步是从存储设备中读取引导记录。我们一般把这个存储设备叫做“启动盘”。启动盘有什么特点呢?它一般在第一个扇区,占 512 字节,而且以 0xAA55 结束。这是一个约定,当满足这个条件的时候,就说明这是一个启动盘,在 512 字节以内会启动相关的代码。这个就是Grub啦。
 
我们可以通过/boot/grub2/grub.cfg文件可以看到类似如下的配置
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
}
这里面的选项会在系统启动的时候,成为一个列表,让你选择从哪个系统启动。最终显示出来的结果就是下面这张图。至于上面选项的具体意思,我们后面再说。
notion image
使用 grub2-install /dev/sda,可以将启动程序安装到相应的位置。grub2 第一个要安装的就是 boot.img。它由 boot.S 编译而成,一共 512 字节,正式安装到启动盘的第一个扇区。这个扇区通常称为 MBR(Master Boot Record,主引导记录 / 扇区)。
 
现在进行到了BIOS→MBR,BIOS 完成任务后,会将 boot.img 从硬盘加载到内存中的 0x7c00 来运行。
 
由于 512 个字节实在有限,boot.img 做不了太多的事情。它能做的最重要的一个事情就是加载 grub2 的另一个镜像 core.img。
 
core.img 由 lzma_decompress.img、diskboot.img、kernel.img 和一系列的模块组成,功能比较丰富,能做很多事情。
 
notion image
boot.img 先加载的是 core.img 的第一个扇区。如果从硬盘启动的话,这个扇区里面是 diskboot.img,对应的代码是 diskboot.S。
 
boot.img 将控制权交给 diskboot.img 后,diskboot.img 的任务就是将 core.img 的其他部分加载进来,先是解压缩程序 lzma_decompress.img,再往下是 kernel.img,最后是各个模块 module 对应的映像。这里需要注意,它不是 Linux 的内核,而是 grub 的内核。
 
等等,现在可还是在实模式下运行啊,1M的寻址空间已经捉襟见肘了,所以需要切换到保护模式了。
 

从实模式切换到保护模式

 
切换到保护模式要干很多工作,大部分工作都与内存的访问方式有关。前两个操作便是跟内存相关的:
  • 第一项是启用分段,就是在内存里面建立段描述符表,将寄存器里面的段寄存器变成段选择子,指向某个段描述符,这样就能实现不同进程的切换了。
  • 第二项是启动分页。能够管理的内存变大了,就需要将内存分成相等大小的块,这些我们放到内存那一节详细再讲。
 
下一步就是是打开 Gate A20,也就是第 21 根地址线的控制线。在实模式 8086 下面,一共就 20 个地址线,可访问 1M 的地址空间。如果超过了这个限度怎么办呢?当然是绕回来了。在保护模式下,第 21 根要起作用了,于是我们就需要打开 Gate A20。
 
进入到保护模式之后,大致会运行如下步骤:
  1. 解压并运行kernel.img(一堆startup.S和C文件)
  1. startup.S 中调用grub_main(grub kernel的主函数)
  1. grub_main→grub_command_execute (“normal”, 0, 0)→grub_normal_execute()。在这个函数里面,grub_show_menu() 会显示出让你选择的那个操作系统的列表。
  1. 选择启动某个操作系统之后,调用grub_menu_execute_entry() ,开始解析并执行你选择的那一项例如里面的 linux16 命令,表示装载指定的内核文件,并传递内核启动参数。于是 grub_cmd_linux() 函数会被调用,它会首先读取 Linux 内核镜像头部的一些数据结构,放到内存中的数据结构来,进行检查。如果检查通过,则会读取整个 Linux 内核镜像到内存。
  1. 如果配置文件里面还有 initrd 命令,用于为即将启动的内核传递 init ramdisk 路径。grub_cmd_initrd() 函数会被调用,将 initramfs 加载到内存中来。
  1. 最后调用grub_command_execute (“boot”, 0, 0)真正的启动内核。
 

总结

 
纸上得来终觉浅,我觉得要理解启动流程,最好的办法就是自己去制作一个mini版的linux,想当年我也牛逼哄哄的按照网上的教程做了一个(花了我三天时间)~