了解一个运转良好的系统是应对不可避免的故障的最佳准备。关于开源软件最古老的笑话是:“代码是自文档化的”。经验表明,阅读源代码就像听天气预报:有理智的人还是会出门看看外面的天气。本文介绍如何使用调试工具观察分析Linux系统的启动情况。分析功能系统的启动过程有助于用户和开发人员为不可避免的故障做好准备。在某些方面,引导过程非常简单。内核在单核上以单线程和同步状态启动似乎可以理解。但是内核本身是如何启动的呢?initrd(初始ramdisk)和bootloader的作用是什么?还有,为什么网口的LED灯一直亮着?请仔细阅读,找出答案。GitHub上还提供了介绍性演示和练习的代码。开机开始:OFF状态Wake-on-LANOFF状态就是系统没有开机吧?表面很简单,其实不然。例如,如果系统启用了局域网唤醒(WOL),则以太网LED将亮起。使用以下命令检查是否属于这种情况:#sudoethtool其中是网络接口的名称,例如eth0。(ethtool在同名的Linux软件包中可用。)如果输出中的Wake-on显示g,则远程主机可以通过发送MagicPacket来启动系统。如果您无意远程唤醒系统,也不希望其他人这样做,请在系统BIOS菜单中关闭WOL,或使用:#sudoethtool-swold响应的处理器魔法数据包可能是网络接口的一部分,也可能是底板管理控制器(BMC)。英特尔管理引擎、平台控制器单元和MinixBMC并不是唯一在系统关闭时进行监听的微控制器(MCU)。x86_64系统还包括用于系统远程管理的Intel管理引擎(IME)软件套件。从服务器到笔记本电脑的各种设备都包含这项技术,支持KVM远程控制和英特尔功能许可服务等功能。根据英特尔自己的检测工具,IME存在未修补的漏洞。坏消息是禁用IME很困难。TrammellHudson启动了一个me_cleaner项目,该项目清理一些相对讨厌的IME组件,例如嵌入式Web服务器,但也会影响其运行的系统。IME固件和系统管理模式(SMM)软件基于Minix操作系统,运行在独立的平台控制器单元PlatformControllerHub(LCTT译注:南桥芯片)上,而不是主CPU。然后,SMM在主机处理器上启动通用可扩展固件接口(UEFI)软件,这一点已被多次提及。谷歌的Coreboot团队已经启动了一个雄心勃勃的非可扩展精简固件(NERF)项目,该项目旨在不仅取代UEFI,而且取代早期的Linux用户空间组件,例如systemd。在我们等待这些新结果的同时,Linux用户现在可以从Purism、System76或Dell购买禁用IME的笔记本电脑,而配备ARM64位处理器的笔记本电脑仍然值得期待。引导加载程序早期引导固件除了启动那个有问题的间谍软件之外还能做什么?引导加载程序的作用是为新上电的处理器提供通用操作系统(如Linux)所需的资源。在启动时,不仅没有虚拟内存,在控制器启动之前也没有DRAM。然后引导加载程序启动并扫描总线和接口以定位内核映像和根文件系统。U-Boot和GRUB等常见引导加载程序支持USB、PCI和NFS等接口,以及更多特定于嵌入式的设备,如NOR闪存和NAND闪存。引导加载程序还与可信平台模块(TPM)等硬件安全设备交互,以在引导一开始就建立信任链。在构建主机上的沙箱中运行u-boot引导加载程序。包括RaspberryPis、Nintendo设备、汽车主板和Chromebook在内的系统支持广泛使用的开源引导加载程序U-Boot。它没有系统日志,甚至在出现问题时也没有任何控制台输出。为了便于调试,U-Boot团队提供了一个沙箱来测试构建主机甚至夜间持续集成(CI)系统上的补丁。如果系统安装了Git、GNUCompilerCollection(GCC)等常用开发工具,使用U-Boot沙箱会相对简单:#gitclonegit://git.denx.de/u-boot;cdu-boot#makeARCH=sandboxdefconfig#make;./u-boot=>printenv=>helpRunningU-Bootonx86_64,可以测试一些tricky的功能,比如模拟存储设备重新分区,基于TPM的按键操作,USB设备的热插拔等。U-BootSandbox可以在GDB调试器下甚至单步执行。使用沙箱进行开发比将引导加载程序闪存到板上进行测试快10倍,并且可以使用Ctrl+C恢复“变砖”的沙箱。引导内核配置引导内核一旦引导加载程序完成其任务,它就会跳转到加载到主内存中的内核代码并开始执行,并传递用户指定的任何命令行选项。内核是什么样的程序?使用命令文件/boot/vmlinuz可以看到它是一个“bzImage”,意思是一个大的压缩图像。Linux源代码树包含一个可以提取此文件的工具-extract-vmlinux:#scripts/extract-vmlinux/boot/vmlinuz-$(uname-r)>vmlinux#filevmlinuxvmlinux:ELF64-bitLSBexecutable,thex86-64,版本1(SYSV),静态链接,剥离内核是一种可执行和可链接格式的可执行和链接格式(ELF)二进制文件,就像Linux用户空间程序一样。这意味着我们可以使用binutils包中的命令(例如readelf)来检查它。比较输出,例如:#readelf-S/bin/date#readelf-Svmlinux两个二进制文件中的部分大致相同。所以内核必须像任何其他LinuxELF文件一样启动,但是用户空间程序如何启动?在main()函数中?不完全是。在main()函数运行之前,程序需要一个执行上下文,包括堆栈内存和stdio、stdout和stderr的文件描述符。用户空间程序从标准库(大多数Linux系统上使用的“glibc”)获取这些资源。参考以下输出:#file/bin/date/bin/date:ELF64-bitLSBsharedobject,x86-64,version1(SYSV),dynamicallylinked,interpreter/lib64/ld-linux-x86-64.so.2,对于GNU/Linux2.6.32,BuildID[sha1]=14e8563676febeb06d701dbee35d225c5a8e565a,strippedELF二进制文件有一个解释器,就像Bash和Python脚本一样,但是解释器不需要用#!像脚本,因为ELF是Linux原生格式。ELF解释器通过调用_start()函数将所需资源配置为二进制文件,该函数可以在glibc源码包中找到,可以用GDB查看。内核显然没有解释器,必须自行配置,这怎么可能?用GDB检查内核的启动给出了答案。首先安装内核调试包,里面包含一个未剥离的未剥离的vmlinux,比如apt-getinstalllinux-image-amd64-dbg,或者从源码编译安装自己的内核,可以参考DebianKernelHandbook说明。在gdbvmlinux之后添加信息文件以显示ELF部分init.text。在init.text中用l*(address)列出程序执行的开始,其中address是init.text的十六进制开始。使用GDB可以看到x86_64内核是从内核文件arch/x86/kernel/head_64.S开始的。在这个文件中,我们找到了汇编函数start_cpu0(),以及调用x86_64start_kernel()函数之前创建的清晰代码。堆叠并解压缩zImage。ARM32位内核也有一个类似的文件arch/arm/kernel/head.S。start_kernel()不是特定于体系结构的,因此该函数驻留在内核的init/main.c中。start_kernel()可以说是Linux真正的main()函数。从start_kernel()到PID1内核的硬件清单:设备树和ACPI表在启动时,内核需要硬件信息,而不仅仅是编译它所针对的处理器类型。代码中的指令由单独存储的配置数据扩充。数据存储有两种主要方法:设备树和高级配置和电源接口(ACPI)表。内核读取这些文件以了解每次启动时需要运行哪些硬件。对于嵌入式设备,设备树是已安装硬件的清单。设备树只是一个与内核源代码同时编译的文件,一般和vmlinux一样在/boot目录下。要在ARM设备上查看设备树的内容,只需在名称与/boot/*.dtb匹配的文件上执行binutils包中的strings命令,其中dtb指的是设备树二进制文件。显然,可以通过编辑组成它的类似JSON的文件并重新运行内核源代码提供的特殊dtc编译器来简单地修改设备树。虽然设备树是一个静态文件,其文件路径通常由命令行bootloader传递给内核,但近年来增加了设备树覆盖功能,内核可以动态加载可热插拔的附加设备后启动。x86系列和许多企业ARM64设备使用ACPI机制。与设备树不同,ACPI信息存储在内核在启动时通过访问板载ROM创建的/sys/firmware/acpi/tables虚拟文件系统中。读取ACPI表的一种简单方法是使用acpica-tools包中的acpidump命令。示例:Lenovo笔记本电脑的ACPI表都是为Windows2001设置的。是的,您的Linux系统已准备好用于Windows2001,您是否应该考虑安装它?ACPI有方法和数据,不像设备树,它更像是一种硬件描述语言。ACPI方法在启动后仍然有效。例如,运行acpi_listen命令(在apcid包中)然后打开和关闭笔记本电脑的盖子将显示ACPI功能始终在运行。可以临时动态地覆盖ACPI表,而永久更改它需要与BIOS菜单交互或在启动时刷新ROM。如果你有那么多麻烦,也许你应该安装coreboot,一个开源固件替代品。从start_kernel()到用户空间init/main.c的代码证明是可读的,有趣的是,它仍然使用LinusTorvalds从1991-1992的原始版权。运行dmesg|在一个刚启动的系统上,它的输出主要来自这个文件。第一个CPU在系统中注册,全局数据结构被初始化,调度程序、中断处理程序(IRQ)、定时器和控制台按照严格的顺序依次启动。在timekeeping_init()函数运行之前,所有时间戳均为零。内核初始化的这一部分是同步的,这意味着只在一个线程中执行,直到最后一个线程完成并返回之前不会执行任何函数。因此,dmesg的输出即使在两个系统之间也是完全可重复的,只要它们具有相同的设备树或ACPI表。Linux的行为类似于RTOS(实时操作系统),例如在MCU上运行的QNX或VxWorks。这种情况在函数rest_init()中持续存在,它在终止时由start_kernel()调用。早期内核引导流程。函数rest_init()生成一个新进程来运行kernel_init()并调用do_initcalls()。用户可以通过在内核命令行追加initcall_debug来监控initcalls,这样每运行一个initcall函数,就会产生一个dmesg入口。initcalls经历七个连续的级别:early、core、postcore、arch、subsys、fs、device和late。initcalls中最用户可见的部分是检测和设置所有处理器外围设备:总线、网络、存储、显示器等,同时加载其内核模块。rest_init()还在引导处理器上生成第二个线程,它首先运行cpu_idle()然后等待调度程序分配工作。kernel_init()还可以设置对称多处理(SMP)结构。在较新的内核上,如果dmesg输出显示“正在启动辅助CPU...”之类的内容,则系统正在使用SMP。SMP通过“热插拔”CPU来工作,这意味着它使用概念上类似于热插拔USB驱动器的状态机来管理其生命周期。内核的电源管理系统通常会使内核脱机,然后根据需要将其唤醒,以在不忙的机器上重复调用同一段CPU热插拔代码。监视电源管理系统调用CPU热插拔代码的BCC工具称为offcputime.py。请注意,init/main.c中的代码在smp_init()运行时几乎已完成:大部分一次性初始化已由引导处理器完成,无需为其他内核重复。尽管如此,跨CPU的线程仍会在每个内核上生成,以管理每个内核的中断(IRQ)、工作队列、计时器和电源事件。例如,ps-opsr命令可以查看为每个CPU上的线程提供服务的软中断和工作队列。#ps-opid,psr,comm$(pgrepksoftirqd)PIDPSR命令70ksoftirqd/0161ksoftirqd/1222ksoftirqd/2283ksoftirqd/3#ps-opid,psr,comm$(pgrepkworker)PIDPSR命令40kworker/0:0H181kworker/1:0H242kworker/2:0H303kworker/3:0H[...]其中,PSR字段代表“处理器”。每个内核还必须有自己的计时器和cpuhp热插拔处理程序。那么用户空间是如何启动的呢?在***处,kernel_init()寻找可以代表它执行init进程的initrd。如果没有找到,内核直接自己执行init。那么为什么需要initrd?早期用户空间:谁授权了initrd?除了设备树之外,另一个可以在引导时提供给内核的文件路径是initrd。initrd通常位于/boot目录中,x86系统上的bzImage文件vmlinuz或ARM系统上的uImage和设备树也是如此。使用initramfs-tools-core包中的lsinitramfs工具列出initrd的内容。该发行版的initrd方案由最小的/bin、/sbin和/etc目录和内核模块以及/scripts中的一些文件组成。所有这些看起来都很熟悉,因为initrd大致是一个简单的、最小的Linux根文件系统。看似相似,其实不然,因为虚拟内存盘中/bin和/sbin目录下的几乎所有可执行文件都是指向BusyBox二进制文件的符号链接,这使得/bin和/sbin目录比glibc小了10倍.如果您只想加载一些模块并在普通根文件系统上启动init,为什么还要创建initrd?考虑到加密的rootfs,解密可能取决于加载位于rootfs的/lib/modules中的内核模块,当然还有initrd中的内核模块。密码模块可以静态编译到内核中而不是从文件中加载,但出于多种原因这是不受欢迎的。例如,使用模块静态编译内核可能会使其太大而无法放入内存,或者静态编译可能会违反软件许可条款。不出所料,存储、网络和人工输入设备(HID)驱动程序也可能存在于initrd中。initrd基本上包含挂载根文件系统所需的任何非内核代码。initrd也是用户放置自定义ACPI表代码的地方。救援模式shell和自定义initrds仍然很有趣。initrd对于测试文件系统和数据存储设备也很有用。将这些测试工具放在initrd中,并从内存中运行测试,而不是从被测对象中运行。***,当init开始运行时,系统就启动了!随着第二个处理器的运行,机器已经成为我们所熟知和喜爱的异步、可抢占、不可预测和高性能的生物。实际上,ps-opid,psr,comm-p1很容易表明用户空间init进程不再在引导处理器上运行。总结Linux启动过程可能听起来令人望而生畏,即使是简单的嵌入式设备上的软件数量也是如此。但从另一个角度来看,启动过程还是相当简单的,因为启动中没有抢占、RCU、竞争条件等复杂的功能。仅关注内核和PID1会忽略引导加载程序和辅助处理器为运行内核所做的大量准备工作。虽然内核是Linux程序中最重要的,但一些检查ELF文件的工具也可以了解其结构。了解正常的启动流程,可以帮助运维人员应对启动失败。