旅游网站建设服务对象,制作网站要花多少钱如何,什么是网络营销功能,网络营销这个专业怎么样前言
启动计算机通常不是一件难事#xff1a;按下电源键#xff0c;稍等片刻#xff0c;你就能看到一个登录界面#xff0c;再输入正确的密码#xff0c;就可以开启一天的网上冲浪之旅了。
但偶尔这件事没那么顺利#xff0c;有时候迎接你的不是熟悉的登录界面#xf…前言
启动计算机通常不是一件难事按下电源键稍等片刻你就能看到一个登录界面再输入正确的密码就可以开启一天的网上冲浪之旅了。
但偶尔这件事没那么顺利有时候迎接你的不是熟悉的登录界面而是一个令人生畏的命令提示符界面一闪一闪的提示符告诉你“你碰到麻烦了”。于是你对着错误提示查找解决方法按照网页上的步骤你对着提示符输入并执行了几条你完全不理解的命令计算机又能正常启动了但同时你发现你那存有大学舍友糗照的硬盘分区被清空了。
为了防止这样的悲剧发生了解一下计算机的启动流程是非常有必要的这能帮助你下次再碰到计算机启动问题时不至于手忙脚乱而误把硬盘分区格式化掉。
下面开始正题我会以一个Linux使用者而不是专业的UEFI开发工程师的视角来叙述一下UEFI固件计算机的启动流程。虽然BIOS看起来已经成为历史但它实际上还运行在许多存量设备上——我数年前供职的第一家公司其生产的基于Linux的设备就仍在使用BIOS固件因此也会对其进行介绍。
BIOSMBR
在BIOSMBR时代对于固件来说并没有文件的概念甚至没有分区的概念它只认识扇区。在把对CPU的控制权交给存储于硬盘MBR中的bootloader代码后固件就完成了它在启动过程中的大部分工作后续的流程由bootloader来负责。
标准MBR结构1 MBR分区表项格式2 而MBR空间非常有限最多只有446个字节这并不足以容纳全部的启动流程因此通常bootloader会把启动流程划分为多个阶段每个阶段的逻辑存储在不同的地方。
以曾经广泛使用的GRUB Legacy为例它的启动流程分为三个阶段stage1、stage1.5和stage2其中stage1.5是可选的。stage1只是一个用来加载stage1.5或者stage2的入口stage1.5提供了文件系统驱动如果存在stage1.5后面的stage2的代码就可以通过文件路径来加载否则还是要通过扇区列表来加载stage2才是最终启动内核的地方根据配置它可以加载指定路径下的内核镜像也可以加载安装在其他分区PBR中的bootloader由另外的bootloader完成系统的启动。3
UEFIGPT
UEFI和BIOS很不一样根据标准UEFI固件需实现对FAT文件系统的支持4而有了对文件系统的支持以后许多事情都变得豁然开朗了bootloader再也不用争抢MBR这块弹丸之地也不需要切割逻辑把代码见缝插针地安置在磁盘分区之间的空隙中bootloader现在完全可以作为文件系统中的合法公民不再是游离在外的幽灵。
GPT
UEFI虽然也可以以CSM模式从MBR硬盘中启动但如今大多数情况都是配合GPT硬盘使用的因此了解一下GPT分区表是很有必要的。本文后续也都基于UEFIGPT的组合并且不考虑U盘和光盘等移动存储设备。
GPT分区表的结构5 出于兼容性的考虑在GPT硬盘的第0个LBA仍然保存有一份MBR格式的分区表但这个分区表将硬盘余下的所有区域标记为一个受保护的分区6 GPT硬盘的第1个LBA才是真正的GPT分区表头其格式如下5 GPT分区表头后面跟随着分区表项每个分区表项大小为128字节其格式如下5 需要注意的是这里的“分区类型GUID”里面的“分区类型”并不是根据FAT32、NTFS和ext4等具体格式划分的而是根据用途划分的。下面是gnome-disk-utility在编辑分区时支持的部分分区类型GUID 既然GPT分区表项中并没有表示分区格式的字段那么磁盘管理工具是如何获取分区格式的呢以libblkid为例它实现了一系列的probe_*函数通过读取分区头部的superblock中的数据来探测分区格式
probe_vfat
probe_ext4
probe_ext3
probe_ext2
probe_ntfs需要注意的是在lsblk等工具的输出中有一列UUID但这个UUID并不是GPT分区表项中的分区类型GUID或者分区GUID而是文件系统UUID它不是GPT标准的一部分。文件系统的UUID通常存储于superblock中并且由于没有统一的标准其存储位置和大小也不尽相同ext系列文件系统的UUID长度为16个字节FAT系列文件系统的UUID长度为4个字节NTFS文件系统的UUID长度为8个字节7
struct ext2_super_block {// *unsigned char s_uuid[16];// *
};struct msdos_super_block {// *
/* 27*/ unsigned char ms_serno[4];// *
};struct vfat_super_block {// *
/* 43*/ unsigned char vs_serno[4];// *
};struct ntfs_super_block {// *uint64_t volume_serial;// *
};在lsblk中分区类型GUID和分区GUID字段名分别为PARTTYPE和PARTUUID
$ lsblk /dev/sda --fs -o PARTTYPE,PARTUUID
NAME FSTYPE FSVER LABEL UUID FSAVAIL FSUSE% MOUNTPOINTS PARTTYPE PARTUUID
sda
├─sda1
│ vfat FAT32 81DE-2849 504.9M 1% /boot/efi c12a7328-f81f-11d2-ba4b-00a0c93ec93b 3c1a61a2-5829-7598-8673-16494a668884
└─sda2ext4 1.0 5ae3cdb0-e1d3-372b-82d9-253144874891 395.5G 5% / 0fc63daf-8483-4772-8e79-3d69d8477de4 01c4ade9-2f93-e266-b517-858b6af37179EFI系统分区ESP
在UEFIGPT时代bootloader不再存放于MBR而是作为普通文件存放在EFI系统分区ESP。ESP实际上就是一个FAT格式的分区通常是FAT32其分区类型GUID为为c12a7328-f81f-11d2-ba4b-00a0c93ec93b其GPT分区表项中属性字段的bit1被设置为08除此之外它与普通的FAT格式分区并没有什么不同。
ESP在Windows上默认是隐藏的在Linux上可以作为普通的块设备正常挂载。在Ubuntu上ESP被默认挂载于/boot/efi目录/etc/fstab中有这样一个配置项
# /boot/efi was on /dev/sda1 during installation
UUID81DE-2849 /boot/efi vfat umask0077 0 1这个配置项的一个细节是被挂载的分区并没有使用/dev/sd1这样的设备路径而是使用了UUID81DE-2849来指定这样可以防止由于硬件配置变动导致错误的分区被挂载到/boot/efi目录。
bootloader也不是随意地放在ESP中UEFI标准还对ESP的目录结构做了规定。按照标准ESP的根目录中需包含一个名为EFI的目录所有的bootloader均需放置在EFI目录的子目录下9
\EFI\OS Vendor 1 DirectoryOS Loader Image\OS Vendor 2 DirectoryOS Loader Image…\OS Vendor N DirectoryOS Loader Image\OEM DirectoryOEM Application Image\BIOS Vendor DirectoryBIOS Vendor Application Image\Third Party Tool Vendor DirectoryThird Party Tool Vendor Application Image\BOOTBOOT{machine type short name}.EFI其中BOOT目录是一个缺省目录缺省目录中又有一个缺省bootloader当没有其他可加载的bootloader时固件就会尝试加载它。这个缺省bootloader文件名和处理器架构相关在x86_64机器上它的名字是BOOTX64.EFI9。
我本机ESP目录结构如下
/boot/efi
└── EFI├── BOOT│ ├── BOOTX64.EFI│ ├── fbx64.efi│ └── mmx64.efi└── ubuntu├── BOOTX64.CSV├── grub.cfg├── grubx64.efi├── mmx64.efi└── shimx64.efiUEFI镜像
虽然我前面一直在说bootloader但UEFI固件能够加载的不仅仅是bootloader任何符合格式的文件都可以被加载它们被统称为UEFI镜像其后缀名为efi。UEFI镜像可分为两类UEFI应用和UEFI驱动bootloader是UEFI应用的一个子类它实现了加载操作系统的功能。除了bootloader之外还有其他类型的UEFI应用例如提供了命令行交互接口的UEFI shell能够以菜单形式选择不同bootloader加载的boot manager甚至连python都被移植成了UEFI应用10。
UEFI目前使用的镜像格式为PE32这是一种和Windows上的exe可执行文件大同小异的格式感兴趣的可以自己查找相关资料。在Linux下使用file命令识别它们输出如下
$ file BOOTX64.EFI
BOOTX64.EFI: PE32 executable (EFI application) x86-64 (stripped to external PDB), for MS Windowsobjdump也认识它们
$ objdump -f BOOTX64.EFI BOOTX64.EFI: file format pei-x86-64
architecture: i386:x86-64, flags 0x00000133:
HAS_RELOC, EXEC_P, HAS_SYMS, HAS_LOCALS, D_PAGED
start address 0x0000000000023000
NVRAM变量
前面提到了UEFI标准对ESP目录结构有规定bootloader需要放置在EFI目录的子目录下但是这还不够想要能够被UEFI固件直接加载还要配合NVRAM变量。
UEFI使用NVRAM来保存配置这些配置在机器断电重启后也依然能够保持其中有一些与启动过程息息相关。
名为Boot####的NVRAM变量定义了启动项条目保存了bootloader的路径其中####是一个16进制的序号。当机器启动后我们手动选择启动项时实际上就是在选择不同的Boot####变量中保存的bootloader路径进行加载。因此想要添加一个新的启动项时不但要把bootloader放在符合UEFI标准的路径下还要创建一个指向这个bootloader的Boot####变量。
名为BootOrder的NVRAM变量定义了启动项顺序当我们没有手动选择启动项时UEFI固件就会按照这个变量中配置的顺序依次加载bootloader直到有bootloader被成功加载为止11。
在Linux上可以使用efibootmgr工具来管理它们
$ efibootmgr -v
BootCurrent: 0000
Timeout: 0 seconds
BootOrder: 0002,0004,2001,2002,2003
Boot0000* USB HDD: KingstonDataTraveler 3.0 PciRoot(0x0)/Pci(0x14,0x0)/USB(17,0)/HD(1,GPT,a3382272-59c0-2911-3af9-29919cc5c581,0x800,0x1ce77df)
Boot0002* ubuntu HD(1,GPT,3c1a61a2-5829-7598-8673-16494a668884,0x800,0x100000)/File(\EFI\ubuntu\shimx64.efi)
Boot0004* Windows Boot Manager HD(1,GPT,44b82d72-7651-803f-9a23-2cf241e4c839,0x800,0x32000)/File(\EFI\Microsoft\Boot\bootmgfw.efi)
Boot2001* EFI USB Device RC
Boot2002* EFI DVD/CDROM RC
Boot2003* EFI Network RC至于如何增删启动项可以自行查阅efibootmgr手册。
如果因为某种原因NVRAM中数据被清空了那么计算机该如何启动呢一种方式是前面提到过的缺省bootloader你可以把一个GRUB2的UEFI镜像重命名为BOOTX64.EFI放到BOOT目录下来引导机器启动也可以使用fbx64.efi这种能够重建NVRAM启动项的UEFI应用来修复启动项甚至把UEFI shell作为缺省启动项手动加载要启动的bootloader另一种方式是依赖UEFI固件的一些非标准逻辑——你机器上的UEFI固件很可能无论如何都会查看ESP中的EFI/Microsoft/Boot/bootmgfw.efi文件是否存在并尝试加载它而无视NVRAM中的配置。12
GRUB2
前面是任何使用UEFI固件的计算机启动时都会涉及的流程而当bootloader被成功加载后操作系统如何被加载就不在UEFI标准之中了。下面以GRUB2为例看一下Linux内核是如何被加载的。
GRUB2安装在ESP中的bootloader镜像在x86_64机器上名字是grubx64.efi即使在UEFI时代GRUB2也没有把全部的代码和数据统统塞进ESP中主配置和数据仍然保存在普通数据分区中。GRUB2在ESP中有一份简单的配置文件grub.cfg它的作用就是告诉grubx64.efi应当去哪里加载主配置文件下面是我机器上ESP分区中grub.cfg的内容
search.fs_uuid 5ae3cdb0-e1d3-372b-82d9-253144874891 root hd0,gpt2
set prefix($root)/boot/grub
configfile $prefix/grub.cfggrubx64.efi可以根据这份配置文件的内容首先按照文件系统UUID找到存放主配置文件的分区然后再根据路径找到主配置文件加载。
而主配置文件的内容就比较长了此处列出其中三个启动项的配置
第一个是Ubuntu启动项在以正常方式启动Ubuntu时这个启动项会被GRUB2加载。这个启动项被加载时GRUB2首先会进行必要的配置使用insmod命令加载必要的模块然后根据文件系统UUID找到存放内核镜像的分区找到内核镜像后使用linux命令向其传递启动参数并加载使用initrd命令加载initrd最后会执行隐含的boot命令启动内核13
menuentry Ubuntu --class ubuntu --class gnu-linux --class gnu --class os $menuentry_id_option gnulinux-simple-5ae3cdb0-e1d3-372b-82d9-253144874891 {recordfailload_videogfxmode $linux_gfx_modeinsmod gzioif [ x$grub_platform xxen ]; then insmod xzio; insmod lzopio; fiinsmod part_gptinsmod ext2set roothd0,gpt2if [ x$feature_platform_search_hint xy ]; thensearch --no-floppy --fs-uuid --setroot --hint-bioshd0,gpt2 --hint-efihd0,gpt2 --hint-baremetalahci0,gpt2 5ae3cdb0-e1d3-372b-82d9-253144874891elsesearch --no-floppy --fs-uuid --setroot 5ae3cdb0-e1d3-372b-82d9-253144874891filinux /boot/vmlinuz-5.19.0-50-generic rootUUID5ae3cdb0-e1d3-372b-82d9-253144874891 ro quiet splash $vt_handoffinitrd /boot/initrd.img-5.19.0-50-generic
}第二个是Windows Boot Manager启动项GRUB2并不单纯是一个bootloader它还可以使用chainloader命令加载其他UEFI镜像在这个启动项中它就加载了Windows的bootloader
menuentry Windows Boot Manager (on /dev/nvme0n1p1) --class windows --class os $menuentry_id_option osprober-efi-9493-2BA0 {insmod part_gptinsmod fatsearch --no-floppy --fs-uuid --setroot 9493-2BA0chainloader /efi/Microsoft/Boot/bootmgfw.efi
}第三个是UEFI Firmware SettingsGRUB2还可以放弃加载内核或者其他bootloader调用fwsetup重启机器并进入UEFI固件配置界面
menuentry UEFI Firmware Settings $menuentry_id_option uefi-firmware {fwsetup
}总结
总结一下使用UEFI固件的计算机从开机到Linux内核启动的典型流程如下
UEFI固件初始化UEFI固件根据NVRAM配置或者手动选择在ESP中加载bootloader。如果想要启动Linux这个bootloader大部分情况是GRUB2GRUB2加载ESP分区的配置文件找到主配置文件路径加载主配置文件展示GRUB2启动项选择菜单手动或者默认选择Linux启动项通过一系列的insmod、linux、initrd和boot命令加载并启动Linux内核
当然了UEFI固件的实现并不一定会完全按照标准来Linux也并不只是有GRUB2这一个bootloader可用所以每台计算机实际的启动流程并不一定和这里完全相符。但是万变不离其宗在正常启动的情况下CPU的控制权总还是会按照UEFI固件-UEFI应用-Linux内核的顺序流转的。 https://zh.wikipedia.org/wiki/%E4%B8%BB%E5%BC%95%E5%AF%BC%E8%AE%B0%E5%BD%95 ↩︎ https://wiki.osdev.org/Partition_Table ↩︎ https://www.system-rescue.org/disk-partitioning/Grub-boot-stages/ ↩︎ https://uefi.org/specs/UEFI/2.10/13_Protocols_Media_Access.html#file-system-format ↩︎ https://zh.wikipedia.org/wiki/GUID%E7%A3%81%E7%A2%9F%E5%88%86%E5%89%B2%E8%A1%A8 ↩︎ ↩︎ ↩︎ https://uefi.org/specs/UEFI/2.10/05_GUID_Partition_Table_Format.html#protective-mbr ↩︎ https://github.com/util-linux/util-linux/tree/master/libblkid/src/superblocks ↩︎ https://uefi.org/specs/UEFI/2.10/13_Protocols_Media_Access.html#partition-discovery ↩︎ https://uefi.org/specs/UEFI/2.10/13_Protocols_Media_Access.html#directory-structure ↩︎ ↩︎ https://github.com/tianocore/edk2-staging/tree/MicroPythonTestFramework ↩︎ https://uefi.org/specs/UEFI/2.10/03_Boot_Manager.html#globally-defined-variables ↩︎ https://www.rodsbooks.com/efi-bootloaders/fallback.html ↩︎ https://www.gnu.org/software/grub/manual/grub/grub.html#boot ↩︎