bootsect.s 预备——Linux-0.11 剖析笔记(一)
阅读原文时间:2021年04月20日阅读:1

文章目录

boot 目录下文件介绍

boot 目录中一共有三个文件,都是用汇编语言写的,如下图(图来自赵炯的书)

我是在 https://mirrors.edge.kernel.org/pub/linux/kernel/Historic/old-versions/ 下载的 Linux-0.11 的源码。

再提供一个下载地址:http://oldlinux.org/Linux.old/

打开 boot 文件夹,和上图对比一下。


修改日期差了 8 个小时,应该是不同时区导致的。

这 3 个文件虽然都是汇编程序,但却使用了两种语法格式。 bootsect.s 和 setup.s 是实模式下运行的 16 位代码程序,使用类似于 Intel 汇编语法,并需要使用 Intel 8086 汇编编译器(as86)和连接器(ld86),这一点在 Makefile 里面有体现:

...

AS86    =as86 -0 -a
LD86    =ld86 -0

...

boot/setup: boot/setup.s
    $(AS86) -o boot/setup.o boot/setup.s
    $(LD86) -s -o boot/setup boot/setup.o

boot/bootsect:    boot/bootsect.s
    $(AS86) -o boot/bootsect.o boot/bootsect.s
    $(LD86) -s -o boot/bootsect boot/bootsect.o

而 head.s,则使用 AT&T 汇编格式, 运行在保护模式下,需要用 GNU 的 gas 进行编译、gld 进行链接。Makefile 里面有体现:

AS    =gas
LD    =gld

Linus 当时使用两种汇编编译器的主要原因在于对于 Intel x86 处理器系列来讲,那时的 GNU 编译器仅支持 i386 及以后的 CPU,如果不用特殊方法,就无法编译出在实模式下运行的程序。

直到 1994 年以后发布的 GNU as 汇编器才开始支持编译 16 位代码(使用 .code16 伪指令)。

16 位代码是什么意思

有人会问,这里的 16 位代码到底是什么意思?实模式和保护模式有何区别?

最早的 8086 处理器,寄存器和数据线都是 16 位,地址线是 20 位,可以寻址 1MB 的内存。16 位的寄存器如何表示 20 位的物理地址呢?

答案是 :物理地址 = 段基址<<4 + 段内偏移

段基址和段内偏移都是 16 位的,16 位的段基址左移 4 位,再加上段内偏移,就构成了 20 位的物理地址。

后来,有了 80286,它的寄存器和数据线也是 16 位,地址线变成了 24 位。在寻址上,开创了新方法。寻址时,段寄存器保存的值不再是段基址,而是段选择子(selector),其中高 13 位指向描述符表(descriptor table)的条目;低 2 位则定义了请求权限,从 0 到 3;剩下的 1 位表示是使用全局描述符表(GDT)还是局部描述符表(LDT)。描述符表的条目为 8 字节长,其中包括 24 位的段起始物理地址,这个 24 位段起始物理地址再加上 16 位的段内偏移,就构成了访问内存的物理地址。为了和 8086 那种寻址模式区别开,就把原先的称为“实模式”,现在的这种称作“保护模式”。当然,为了兼容,80286 及以后的处理器是支持实模式的。

我认为,16 位代码是指在实模式下工作的代码,在这种模式下,寄存器和数据线都是 16 位,于是称为 16 位代码。

你有没有想过这样一个问题——计算机是怎么开始执行我们写的指令的?这就需要了解计算机的启动过程。

计算机启动过程

对于 32 位的 x86 处理器,加电后,处于实模式,段寄存器 CS 的内容是 0xF000,IP 寄存器的内容为 0xFFF0,按照实模式地址的合成方法,得到的地址就是 0xFFFF0;另外,在刚启动的时候,处理器会将地址线 A20~A31 强制为高电平,所以,32 根地址线发出的物理地址就是 0xFFFF_FFF0,这个地址存放的就是 BIOS 的第一条指令。

在 Bochs 上调试的时候,可以看到第一条指令是:

jmpf 0xf000:e05b  

上电后,BIOS 需要做的工作有很多,比如
1)初始化各种主板芯片组
2)初始化键盘控制器
3)初始化中断向量 ,中断服务例程.
4)初始化 VGA BIOS 控制器
5)显示 BIOS 的版本和公司名称
6)扫描软驱和各种介质容量
7)读取 CMOS 的启动顺序配置,并检测启动装置是否正常
8)调用 INT 19h

对于汇编语言的学习,需要了解最后两个步骤,(7)和(8)做的工作是:

BIOS 按照启动顺序,选择排在第一位的储存设备,读取该设备的第一个扇区(大小是 512B)到内存 0x7c00(物理地址)处,然后检查这 512 个字节的最后两个字节是不是 0x55 和 0xAA,如果是则表明这个设备可以用于启动,这个扇区就是主引导扇区;如果不是,则继续尝试启动顺序中的下一个设备。

如果确实是可以启动的设备,则用一个华丽的 JMP 指令跳到 0x0000:0x7c00 处执行。

jmp 0x0000:0x7c00

Linux 0.11 启动过程

如图所示,可以分为几个阶段:

  1. 如上文所述,PC 上电后,BIOS 做了很多工作,最后选择排在第一位的储存设备,读取该设备的第一个扇区(大小是 512B,对应的代码就是 bootsect.s)到内存 0x7c00(物理地址)处,并跳转到这个地方;
  2. 当 bootsect.s 被执行时,就会把自己移动到内存绝对地址 0x90000 处;
  3. bootsect.s 把启动设备中接下来的 4 个扇区(对应代码 setup.s)读入到内存 0x90200 处,而内核的其他部分( system 模块)则被 读入到从内存地址 0x10000 处,然后跳转到 setup.s 去执行;
  4. setup.s 程序把 system 模块移动到物理内存起始位置处,这样 system 模块中代码的地址也即等于实际的物理地址,便于对内核代码和数据进行操作

另外,网上还有一张图,也很好。

因为当时 system 模块的长度不会超过 512KB,所以 bootsect.s 把 system 模块读入到从内存地址 0x10000 处并不会覆盖自己。

有人会问,bootsect.s 为什么不把 system 模块直接加载到物理地址 0x0000 处,而是先加载到 0x10000,再通过 setup.s 把 system 模块移动到物理内存起始位置处,这是何苦呢?

这是因为 setup.s 代码执行的时候,需要用到 ROM BIOS 中的中断调用来获取机器的一些参数,例如显卡模式、硬盘参数表等,而 BIOS 中断向量表存放在物理内存开始处,大小为 1KB。必须等使用完 BIOS 中断调用后,才能“过河拆桥”。


参考资料

【1】《Linux 内核完剖析》,赵炯,械工业出版社(ISBN:9787111180326)

【2】维基百科「 Intel 80286 」词条

手机扫一扫

移动阅读更方便

阿里云服务器
腾讯云服务器
七牛云服务器

你可能感兴趣的文章