Linux启动程序(二)

前言

  上一篇文章”Linux启动程序(一)“从main函数进行了简单的分析,在进入main函数之前会用汇编对整个程序进行引导(建立堆栈信息,初始化中断等),并举了一个简单的STM32处理器的例子。好吧,上面的例子主要是在没有操作系统的裸奔状态下,下面切入到我们常用的Linux系统中。
  最近看了下新闻,“Linux Torvalds宣布了一个庞大的Linux内核5.8,与Linux4.9内核相比,这个版本改动的地方有超过14000个文件”。嗯,感觉现在Linux系统变得越来越庞大了,变得越来越好了,但是也给我们理解这个系统造成了一定的困难。带着这个疑问,我打算站在一个系统设计者的角度上,从早期的Linux系统设计出发,理解这个系统如何成长的,毕竟Linux内核的发展不是跳跃式的,现在Linux内核中仍然有早期的小内核版本的影子(当然,中间会试图穿插一些高版本内核的例子)。
  对于早期Linux的参考资料主要是以赵炯老师《Linux内核完全剖析:基于0.12内核》这本书的观后感为主,当然建议结合看看MIT 6.828 / Fall 2017这个课程中介绍的xv6系统源码,当然,英文阅读困难也可以看看清华大学的ucore系统源码(课程介绍),对于理解早期的Linux操作系统有所帮助。

早期系统设计

  站在系统设计者的角度上,一个简易的系统需要实现什么?当然,这里我们不讨论是x86架构、ARM架构还是RISC-V架构,也不管作为操作系统的硬件载体由哪些部分组成(在早期的系统设计中,统一将这些外设分成两类,一类是字符设备,一类是块设备)。
   一个简易的系统实现需要有以下内容:
  (1)引导启动部分(有部分硬件初始化,需要用到汇编)
  (2)内存管理(先实现大的页管理,再实现页以下的小内存申请管理)
  (3)中断管理(包含有系统调用和中断处理,一般是用混合编程的方式)
  (4)驱动管理(上面说的字符设备驱动和块设备驱动的管理)
  (5)进程管理(系统调度实现)
  (6)内核库的实现(一些函数的封装,如printk、memset等函数库的实现)
  当然,进一步的还有文件系统实现、线程的管理、堆管理等,但是这些内容的实现均建立在以上实现的基础上。另外,显示输入的部分在早期的系统中是在引导启动部分进行的初始化操作(会在内存中进行地址映射,显示过程就是对这些内存内容操作的过程,如0xB8000~0xBFFFF 对应的是文本模式下的显存

引导启动程序

1. 历史问题

  早期Linux系统在设计的时候,系统文件的载体主要是以软盘(floppy)为主,也就是说系统的引导是建立在软盘的基础上,当然,现在的系统文件的媒介有U盘、光盘和硬盘(虚拟光驱ISO文件)等多种方式。
  同时,早期的Linux系统还是自己在维护bootloader,现在可选的方案有很多,如GRUB(X86下用的比较多),Uboot(ARM中用的较多)等,因此,在现在的Linux内核代码中,基本不会调用自己实现的bootloader(如果不慎调用,会在屏幕上显示一个错误提示)。

2. Linux-0.12引导程序组成

  Linux-0.12的引导程序主要由3个文件组成,分别是bootsector.S,setup.S和head.s(注意文件后缀不一样,也就是汇编的格式有区别)。其实,bootsector.S和setup.S中的内容是严格意义上的boot程序,与现在的Linux内核比较,这两个文件的功能已经被GRUB或Uboot这些bootloader程序代替了,所以在现在的Linux内核中,二进制文件的开始部分是在head.s中。
  对于bootsector.S,这部分程序会有较多与读软盘扇区相关的内容(一个扇区占用512B内存,bootsector.S在内存中占用512B,也就是1个扇区的大小)。这段程序的主要功能是将bootsector.S后面的内容(紧接着是setup.S的内容,然后是head.s,之后是内核中其他部分编译的二进制文件内容)依次复制到不同的内存地址中。至于这里为什么要进行内容复制移动的操作,我认为主要是X86(早期的Linux内核貌似只有X86版本的)芯片本身的固件(ROM BIOS)在部分内存中有地址映射关系(如0地址位置有中断表项,至于为什么有分散的地址映射,或许是为了兼容考虑),setup.s中会要用到这些地址映射关系进行部分硬件的初始化操作。
  对于setup.S,这部分程序会用到ROM BIOS中的部分映射关系(如中断获取内存大小,初始化显存、探知硬盘和中断控制器8259A等),整个程序占用2K的内存(共计4个扇区)。在setup.S执行完毕了,ROM BIOS中内容,我们已经用不到了,也就是说除了后面新映射的地址(如显存地址映射),固件本身自带的地址映射部分的内存,我们可以自由使用。所以,setup.S最后还有个操作便是将setup.S之后的内容(head.s和内核中其他部分编译的二进制文件内容)移动到物理地址为0的部分。
  对于head.s,此程序是在物理内存地址为0的部分开始执行的。head.s在现在的系统内核中仍然存在(当然内容有较大的变化,如高版本的head.s的执行地址不是0,而是与ROM BIOS中提供的入口地址类似,0x7c00(32KB)处,执行内容的位置是在0x10000(1M)位置处),在早期的内核中,这部分程序的内容是初始化全局段表、初始化中断向量表,初始化栈信息,并开启分页机制(ARM中成为MMU机制)。看到这里,是不是有些熟悉,是的,这部分的内容功能有些像上一篇文章“Linux启动程序(一)”中SMT32例子中的启动汇编程序的功能。

Linux-0.12启动过程

3. 高版本内核(以Linux4.4为例)对于bootloader的处理

  上面的内容已经提到,在高版本内核中,Linux自带的boot程序(bootsector.S和setup.S)均被GRUB和Uboot等程序代替,因此基本不会对这部分的内容进行维护,那么这部分的衔接处理是怎么做的呢?
  在高版本内核中,部分系统必要硬件的初始化工作是在bootloader中完成的,在ARM中,一般在bootloader(如Uboot)执行完成后会跳转到Kernel Image位置(head.s其实是编译进了kernel中,从名字中可以看出,其是kernel的头部)处,在X86中,会跳转到活动分区对应的引导程序中,也就是说,bootloader之后便被head.s接管了(一般来说,kernel的位置是相对固定的,压缩的是0x1000,解压后是0x100000)。
  好了,回到上面的问题上,其实从arch/x86/boot/setup.ld源文件中可以看出,启动程序段位于head.S生成文件的前512K.bootload会跳转到加载位置的512偏移处开始执行。

arch/x86/boot/setup.ld源文件

  然后看arch/x86/boot/head.s源码,可以看到_start正好是偏移到了512处,此处正好是bootloader提供的出口地址,便完成了bootloader到head.S的衔接。

head.s中的偏移

4. 高版本内核启动流程

  天糊土分享了高版本Linux kernel启动过程的思维导图(原图地址,感谢他的分享),便于对比理解Linux的启动过程。

5. 下一篇

  下一篇会以X86处理器为例,详细分析下Linux启动程序中的各个文件的内容以及分页机制(ARM中称为MMU机制)。

参考资料

[1] Unix环境高级编程
[2] Linux内核完全剖析:基于0.12内核
[3] kernel启动过程总结的思维导图

posted @ 2020-06-20 18:28  临摹摆渡  阅读(156)  评论(0编辑  收藏  举报