boot 文件夹目录结构

该文件夹下有三个文件,分别是:

  • bootsect.s:Intel 语法格式
  • setup.s:Intel 语法格式
  • head.s:AT&T 语法格式

从 linux 目录下的 Makefile 文件中,可以得知 bootsect.s 与 setup.s 是通过 as86 与 ld86 汇编链接的

1
2
3
4
5
6
7
8
# linux/Makefile  Line 89
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 是通过 as 与 ld 来汇编链接的,对于这三个文件使用两种套件的原因主要在于:

  • bootsect.s 与 setup.s 是在实模式下运行的 16 位代码程序,head.s 则运行在 32 位保护模式下
  • 1991 年的 GNU 的 as 汇编器仅支持 i386 及以后的 32位 CPU 代码指令,所以 16 位程序的代码需要另寻工具

boot 文件夹功能

各文件大致的功能在文件开头的作者注释中都简要给出了,这里还是先介绍一下 Linux 镜像 Image 的构成:

Image构成

在 Makefile 文件第 40 行有所体现,Image 按 bootsect,setup,system 模块的顺序由 tools目录下的 build 工具拼接而成,其中 system 模块又按 head,main,kernel,mm,fs,lib 的顺序组成

PC 机加电启动时执行的顺序如下(main.c 在 init 目录下):

执行顺序

加电后,处理器自动进入实模式,从 ROM-BIOS 的地址 0xFFFF0 处自动执行系统自检代码(该过程称为 POST,Power On Self Test),并在物理地址 0 处初始化 BIOS 的中断向量。

之后,将可启动设备的第一个扇区(磁盘引导扇区)读入内存地址 0x7C00 处,并跳转到该处执行,对于 Linux 而言,引导扇区装入的是 bootsect 模块的代码,标志着内核初始化工作开始

bootsect 先将自己拷贝到物理地址 0x90000 处,跳过去接着执行,将可启动设备之后的 4 个扇区(2KB)读入物理地址 0x90200 处,这 2KB 正是 setup 模块的内容。再将 system 模块的内容读入物理地址 0x10000 处,因为当时 system 模块长度不超过 0x80000 bytes,因此不会覆盖到 0x90000 处的 bootsect 与 0x90200 处的 setup。至此,bootsect 模块的使命完成,跳转至 0x90200,将控制权交给 setup 模块

setup 首先“过河拆桥”地将一些硬件信息保存在地址 0x90000(覆盖 bootsect 模块),然后将在 0x10000 处的 system 模块搬运到物理地址 0 处(内存开头),配置 IDTR 与 GDTR,打开 A20 地址线,设置好 8259A 芯片,将 CR0 的 PE 位置位,进入保护模式。setup 的使命也完成了,跳到地址 0 处,将控制权交给 system 模块中的 head.s 程序

以上流程均在下图中体现(横坐标为阶段 1 ~ 6):

功能


bootsect.s

代码中的 ! 与 /**/ 均为注释标记,篇幅有限不进行翻译,把握住程序的主干逻辑即可,下面正式开始

1
2
/* Line 6 */
SYSSIZE = 0x3000

声明了一个常量 SYSSIZE,单位是节(16 bytes),也就是说当前 system 模块的长度为 196 KB,因为 0x10000 与 0x90000 之间相隔 0x80000,所以这个值最大为 0x8000

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/* Line 25 */
.globl begtext, begdata, begbss, endtext, enddata, endbss
.text
begtext:
.data
begdata:
.bss
begbss:
.text

/* Line 255 */
.text
endtext:
.data
enddata:
.bss
endbss:

伪指令 .globl 用于定义随后的标识符是外部或全局的。.text,.data,.bss 分别定义当前代码段,数据段及未初始化数据段。ld86 在链接时会将各个目标模块中相应的段合并在一起,这里三个段都定义在同一重叠地址范围中,因此本程序实际上不分段

1
2
3
4
5
6
7
8
/* Line 34 */
SETUPLEN = 4 /* setup 模块所占的扇区个数*/
BOOTSEG = 0x07c0 /* boot 模块对应的物理段地址 */
INITSEG = 0x9000 /* 要将 boot 模块移动到的目的段地址 */
SETUPSEG = 0x9020 /* setup 模块读入的段地址 */
SYSSEG = 0x1000 /* system 模块读入的段地址 */
ENDSEG = SYSSEG + SYSSIZE /* system 模块结束地址 */
ROOT_DEV = 0x306 /* 根设备号 */

之前提到 bootsect 模块被读入 0x7C000,这里的 BOOTSEG 只为 0x7c00,原因是这个数据会被装入段寄存器中,寻址时会自动乘 16,再加偏移。INITSEG,SETUPSEG,SYSSEG,ENDSEG 同理

对于根设备号而言,0x301 表示第一个硬盘第一个分区,一个硬盘最多可以分 4 个区,于是 0x301 ~ 0x304 表示第一个硬盘的 1 ~ 4 号分区,0x306 ~ 0x309 表示第二个硬盘的 1 ~ 4 号分区,0x300 及 0x305 分别表示整个第一硬盘与整个第二硬盘

计算根设备号的方法是:主设备号 * 256 + 从设备号,其中主设备号有:1-内存,2-磁盘,3-硬盘,4-ttyx,5-tty,6-并行口,7-非命名管道

此处使用的根设备号为 0x306,Linus 当年在写代码时,根文件系统放在了第二个硬盘的第一个分区,所以根设备号就为 0x306

1
2
3
4
5
6
7
8
9
10
11
12
13
/* Line 45 */
entry start /* 表示程序入口在表示符 start 处 */
start:
mov ax,#BOOTSEG
mov ds,ax
mov ax,#INITSEG
mov es,ax
mov cx,#256
sub si,si
sub di,di
rep
movw
jmpi go,INITSEG

rep; movw 就是从 ds:[si] 拷贝数据至 es:[di],每次拷贝 2 字节,重复 256 次,也就是将 bootsect 模块自身拷贝至 0x90000,之后 jmpi 跳转至 INITSEG:[go] 处继续执行,同时 0x9000 自动载入 cs 段寄存器。这里的 go 是代码中紧接着的一个标识符,所以拷贝完成后会在 0x90000 代码段中接着执行后面的代码

1
2
3
4
5
6
7
/* Line 57 */
go: mov ax,cs
mov ds,ax
mov es,ax
/* 将栈放置在 0x9ff00 */
mov ss,ax
mov sp,#0xFF00

此时 cs 已经为 0x9000,将 ds,es,ss 都设置为 0x9000

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
/* Line 67 */
load_setup:
mov dx,#0x0000 /* 驱动器A 磁头0 */
mov cx,#0x0002 /* 磁道0 扇区2 */
mov bx,#0x0200 /* 读入的地址为 es:[0x200] */
mov ax,#0x0200+SETUPLEN /* 读盘 要读入的扇区数为 4 */
int 0x13 /* 调用 bios 中断功能,中断号为 0x13 */
jnc ok_load_setup /* 如果成功读取,则跳转至标号 ok_load_setup */
mov dx,#0x0000
mov ax,#0x0000
int 0x13 /* 否则复位磁盘驱动器 */
j load_setup /* 跳转至 load_setup 重新读取 */

ok_load_setup:
mov dl,#0x00 /* 驱动器A */
mov ax,#0x0800 /* 功能号为8,表示获取磁盘参数 */
int 0x13
mov ch,#0x00 /* ch 清零 */
seg cs /* 表示下一条指令的操作数在 cs 寄存器所指的段中 */
mov sectors,cx /* 将每磁道最大扇区数保存在 cs:[sectors] 处*/
mov ax,#INITSEG
mov es,ax /* 取磁盘参数时修改了 es,现在将其改回来 */

/* Line 241 */
sectors:
.word 0

读取成功磁盘成功时,CF 位为 0,故 jnc 会跳转,关于 INT 0x13 参数传递及存放返回值的寄存器在 上一篇文章 中已经给出,这里不再赘述。此时 setup 模块已经被加载到 0x90200 处

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/* Line 94 */
mov ah,#0x03 /* 取光标位置 */
xor bh,bh /* 显存第 0 页 */
int 0x10

mov cx,#24 /* 字符串长度为 24 */
mov bx,#0x0007 /* 第 0 页,属性为 7(normal) */
mov bp,#msg1 /* 要显示的字符串在 es:[bp] */
mov ax,#0x1301 /* 在屏幕上显示 msg1,并挪动光标 */
int 0x10

/* Line 244 */
msg1:
.byte 13,10 /* 13-回车 10-换行 */
.ascii "Loading system ..."
.byte 13,10,13,10

这一段代码先获取光标的位置,保存在 DX 中,再将其作为参数调用显示字符串的功能,意为在光标位置处显示 “Loading system” 字样

关于 BIOS 0x10 中断:

  • AH = 0x03 表示读取光标位置,参数用 BH 传递,表示页号返回值如下:
    • CH = 扫描开始线
    • CL = 扫描结束线
    • DH = 行号(0x00 表示最顶端)
    • DL = 列号(0x00 表示最左边)
  • AH = 0x13 表示显示字符串至屏幕,参数为:
    • AL = 放置光标的方式及属性,0x01 表示使用 BL 中的属性值,光标停在字符串结尾
    • BH = 显示页面号
    • BL = 字符属性
    • DH = 行号
    • DL = 列号
    • CX = 显示的字符数
    • ES:BP 指向要显示的字符串起始位置
1
2
3
4
5
/* Line 107 */
mov ax,#SYSSEG /* 通过 es 给 read_it 传参 */
mov es,ax
call read_it /* 将 system 读入内存 */
call kill_motor /* 关闭驱动器马达 */

现在开始将 system 模块加载至 0x10000 处,其调用了两个函数 read_it 与 kill_motor。read_it 将 system 模块读入内存地址为 0x10000 的地方,kill_motor 将驱动器马达关闭,以便得知其状态

先来看 read_it:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
/* Line 147 */
sread: .word 1+SETUPLEN /* 当前磁道已读的扇区数,setup + bootsect 已经读入了5个扇区 */
head: .word 0 /* 当前磁头 */
track: .word 0 /* 当前磁道 */

read_it:
mov ax,es
test ax,#0x0fff /* 检查传入参数低 12 位是否都为 0,如果为都 0 表示段值为 64KB 边界 */
die: jne die /* es 必须为 64 KB(0x10000) 边界开始处,否则陷入死循环 */
xor bx,bx /* bx 用于存放段内偏移地址 */
rp_read: /* 进入循环 */
mov ax,es /* es 为当前所读的段 */
cmp ax,#ENDSEG /* 判断是否读到 system 模块末尾 */
jb ok1_read /* 如果不就是就跳到 ok1_read 继续读入数据 */
ret /* 如果是,表明 system 已经全部载入内存中,返回 */
ok1_read: /* 判断磁道中未读的扇区的空间是否大于 64 KB */
seg cs
mov ax,sectors /* sectors 为之前取磁盘参数时保存的每磁道最大扇区数 */
sub ax,sread /* 减去已经读了的扇区数 */
mov cx,ax /* cx = 还未读的扇区数 */
shl cx,#9 /* cx *= 512 bytes */
add cx,bx /* cx += bx 表示此次读操作后,段内读入的字节数 */
jnc ok2_read
je ok2_read /* 如果没有超出 64 KB,则跳到 ok2_read 执行 */
xor ax,ax /* 否则计算此时最多能读入的字节数 */
sub ax,bx /* ax(值为0)减去某数表示取这个数 64KB 的补值(ax 寄存器 16 位) */
shr ax,#9 /* ax /= 512,转换成需要读取的扇区数,放在 al 中 */
ok2_read:
call read_track /* 读当前磁道上指定起始扇区和指定扇区长度的数据,先接着往下看 */
mov cx,ax /* cx 为此次读操作已读入的扇区数 */
add ax,sread /* 加上这次操作之前已经读入的扇区数 */
seg cs
cmp ax,sectors /* 与单磁道最大扇区数比较 */
jne ok3_read /* 若当前磁道还有未读扇区,则跳到 ok3_read */
mov ax,#1 /* 否则当前磁道读完,继续读该磁道下一磁头面上的数据 */
sub ax,head /* 如果此时 head 为 0,则读 head 为 1 的所有扇区 */
jne ok4_read
inc track /* 否则当前磁道正反两面都读完,去读下一个磁道 */
ok4_read: /* 改变当前磁头,之前为 1 现在为 0;之前为 0 现在为 1 */
mov head,ax
xor ax,ax
ok3_read:
mov sread,ax /* 如果当前磁道还有未读取扇区,保存当前磁道已读扇区 */
shl cx,#9 /* 上次已读扇区数 * 512 */
add bx,cx /* bx += cx 调整段内偏移,为下次读入做准备 */
jnc rp_read /* 没有超过 64KB 则跳回上面的 rp_read 继续读数据 */
mov ax,es /* 否则当前段已经读完 */
add ax,#0x1000 /* 将 es 的值加 0x1000 */
mov es,ax /* es 指向下一个段 */
xor bx,bx /* 段内偏移清零 */
jmp rp_read /* 跳回 rp_read 继续读数据 */

read_track 函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
/* Line 198 */
read_track:
push ax
push bx
push cx
push dx /* 通用寄存器入栈 */
mov dx,track /* 取当前磁道号 */
mov cx,sread
inc cx /* cl 放置起始扇区号,sread 为已读扇区数,加一表示下一个未读扇区号 */
mov ch,dl /* ch 存放磁道号 */
mov dx,head /* 取当前磁头号 */
mov dh,dl /* 放置在高位 */
mov dl,#0 /* 驱动器A */
and dx,#0x0100 /* 磁头号不大于 1 */
mov ah,#2 /* 功能号2,表示要读盘 */
int 0x13
jc bad_rt /* 如果读取出错,跳到 bad_rt */
pop dx /* 否则将保存的寄存器弹出并返回 */
pop cx
pop bx
pop ax
ret
bad_rt: mov ax,#0
mov dx,#0
int 0x13 /* 复位驱动器 */
pop dx
pop cx
pop bx
pop ax /* 将保存的寄存器恢复 */
jmp read_track /* 跳回 read_track 重新读取数据 */

当 system 模块加载至内存中后,程序返回到 call kill_motor,涉及软驱控制卡编程的知识

1
2
3
4
5
6
7
8
/* Line 233 */
kill_motor:
push dx
mov dx,#0x3f2 /* 软驱控制卡的数字输出寄存器端口,只写 */
mov al,#0 /* A 驱动器,关闭 FDC,禁止 DMA 与中断请求,关闭马达 */
outb /* 将 al 中的内容输出到 dx 指定的端口去 */
pop dx
ret

kill_motor 返回后,来到 bootsect.s 的最后部分:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
/* Line 117 */
seg cs
mov ax,root_dev /* 取 508 字节处的一个字(根设备号) */
cmp ax,#0 /* 判断是否被定义 */
jne root_defined /* 如果被定义了就跳去 root_defined */
seg cs /* 否则现在来判断根设备号 */
mov bx,sectors /* 取每磁道最大扇区数 */
mov ax,#0x0208
cmp bx,#15 /* 与 15 比较 */
je root_defined /* 相等则说明这是 1.2MB 的软驱,根设备号就是 ax 中的 0x208 */
mov ax,#0x021c
cmp bx,#18 /* 否则拿来与 18 比较 */
je root_defined /* 相等则说明这是 1.44MB 的软驱,根设备号为 0x21c */
undef_root:
jmp undef_root /* 如果两种都不是,就进入死循环 */
root_defined:
seg cs
mov root_dev,ax /* 保存根设备号 */
/* Line 117 */
jmpi 0,SETUPSEG /* bootsect 任务完成,跳到 setup 模块执行 */

/* Line 249 */
.org 508 /* .org 伪指令指定绝对地址 */
root_dev: /* 根设备号保存在启动引导扇区的地址 508 字节处 */
.word ROOT_DEV
boot_flag: /* 0xAA55 是启动盘具有有效引导扇区的标志,供 BIOS 加载引导扇区时识别所用*/
.word 0xAA55 /* 必须位于引导扇区的最后两个字节中 */