Bootloader是BIOS启动后首先执行的磁盘程序。这个程序负责加载真正的操作系统。由于这个职能,它可以为内核传递参数,管理多个操作系统的启动,查看基本的硬件信息,识别分区、操作磁盘,还可以提供更多其他功能。目前常见的Bootloader有Grub和U-Boot,例如现在的grub2开源Bootloader程序已经是模块化的了,除了提供基本的加载操作系统的功能外,每一个模块都是单独存在的,要使用该模块所实现的命令,grub需要首先加载这个模块。
U-Boot常用在Sparc或MIPS系统,x86中Grub用的最多。要注意的是在安装grub-install之前要确认安装的grub程序是grub-bios还是grub-efi,因为两个是不同的软件包。
如果有裸片编程的经验就很容易理解Bootloader。硬件启动时首先要设置一些寄存器,在代码执行的时候,硬件就已经让你的CPU可以访问到内存了(或者需要首先设置寄存器),并且把硬件映射到了一定的内存空间。这时你只需要编写简单的REG=0x123之类的C语言代码,就可以按照Spec文档完成硬件初始化。并不是每一个系统都需要Bootloader,但是Bootloader的存在使得内核的启动与硬件解耦和,并且可以支持多个操作系统的选择启动,有的甚至还支持网络加载内核。嵌入式系统一般使用解耦和的特性,桌面系统一般看重多操作系统选择的特性。代码如下。
以上是我们手动在x86的grub shell中启动Linux的流程。我们先设置根盘,再设置对应的内核文件和initrd文件,然后启动。可以看到,直观上Grub系统的最大的用处就是选择启动哪个内核版本。
在嵌入式系统中,例如路由器,Bootloader通常存在于Flash的最开始部分单独分配的一部分空间,然后内核可以边开发边通过Bootloader所提供的网口传输功能把内核二进制传输到Flash上,从而被Bootloader找到并且正确引导。当要发布整个固件时,直接使用Bootloader上传可以运行的文件系统,然后将整个Flash用读写器读出来并形成一个BIN文件,之后这个BIN文件就会被批量用于生产时进行烧录。从这个流程可以看出,Bootloader在很大程度上是便于开发而存在的。内核完全可以不使用initrd文件和Bootloader而直接自启动,关键问题在于谁能把最早的内核启动代码加载到内存。
Linux内核的制作一般在某个发型版下完成,也就是说要制作一个Linux,首先要有一个Linux(或UNIX)。通常如果在本机运行,不必调用make menuconfig命令,而是调用make localmodconfig命令,这样内核代码的脚本会自动检测当前系统中使用的模块,或者使用当前的内核配置来配置新内核,最后自动生成.config文件。如果内核版本差异过大,接下来的make命令在执行的过程中会有很多问题需要回答,然后使用make modules_install,make install命令完成本机内核和内核模块的安装。
内核的核心文件是vmlinuz,这是个压缩后的文件,x86架构编译后位于arch/x86/boot路径下。除了内核文件外还需要模块文件,模块文件并不是单独存在的。因为各个模块之间有依赖关系或者是要记录哪些模块启动时需要挂载,哪些不需要挂载,这些相关的文件连同模块本身通常放在/lib/modules/4.1.2/(假定是4.1.2版本内核)路径下。但这并不是绝对的,而且内核代码的存放位置也不是绝对的,只要grub能够指定即可。这些模块也都是在配置内核的时候选择的,一个功能可以选择被编译进内核二进制,也可以选择编译成模块或者选择不编译。若选择了编译成模块(M选项),make命令就会生成对应的ko后缀文件,即选择编译成内核模块的二进制文件。
内核的编译都是先进入各个目录,生成built-in.o,然后在上层根据一定的规则组合生成vmlinux(例如arch/arm/kernel/vmlinux.lds),然后经过处理和压缩得到最终文件。
内核文件的生成:首先ld命令链接生成的ELF文件(vmlinux),然后strip(objcopy)命令组装自解压组件为压缩后可自解压的vmlinuz(zImage)。嵌入式版本的内核和x86架构内核的生成可能会不一样,这取决于使用的Bootloader。例如U-Boot使用uImage,这是在zImage的基础上添加一个U-Boot专用的头部。
在这个过程中还要生成System.map文件。因为vmlinux文件已经被strip了,需要一个单独的文件存放符号表,否则内核无法调试。kdress工具可以用来给vmlinux重新添加Systen.map中的符号表,使用这个生成的内核文件配合/proc/kcore文件就可以用gdb调试内核了。
内核编译完成安装模块时会同时生成modules.dep和很多map文件。这些文件也可以手动生成(内核编译无非是执行了一系列的命令,例如depmod)。这些map文件(例如modules.alias)定义的是什么样的硬件应该加载本模块,而modules.dep文件定义的是各个模块之间的依赖关系,即如果要加载本模块则需要预先加载哪些模块。
生成modules.dep文件的命令是moddep,而开机启动时一次性加载所有需要的模块的代码是modprobe。这个命令可以根据modules.dep文件的内容加载尽可能多的模块。有的发行版“认为”这是不合理的,于是它们在/etc下建立了目录结构,启动时只能使用insmod逐个加载目录结构中定义的模块。
总体来说,什么模块需要加载,什么模块不需要加载,到目前为止还没有一个很好的解决办法。