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
| 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 的构成:
在 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 /* 必须位于引导扇区的最后两个字节中 */
|