之前也提到,setup.s 的作用是保存一些重要参数,将 system 模块从 0x10000 移到内存地址起始处,设置全局描述符表寄存器及中断描述符表寄存器,打开 A20 地址线,编辑 8259A 芯片,CR0 寄存器 PE 位置 1,跳转至 head.s

保存参数

其中保存的参数有:

内存地址 长度(bytes) 名称 描述
0x90000 2 光标位置 光标列号与行号
0x90002 2 扩展内存数 系统从 1MB 开始的扩展内存数值(KB)
0x90004 2 显示页面 当前显示页面
0x90006 1 显示模式 当前显示模式
0x90007 1 字符列数
0x90008 2 ??
0x9000A 1 显存大小 (0-64k; 1-128k; 2-192k; 3-256k)
0x9000B 1 显示状态 0-彩色, I/O端口=0x3dX; 1-单色, I/O端口=0x3bX
0x9000C 2 特性参数 显卡特性
0x90080 16 硬盘参数表 第一个硬盘的参数表
0x90090 16 硬盘参数表 第二个硬盘的参数表(如果没有,则清零)
0x901FC 2 根设备号 根文件系统所在的设备号(bootsect 已经设置好)

下面是对实际代码的阅读:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/* Line 17 */
INITSEG = 0x9000 /* boosect 模块起始地址,现被用于存放内存/显卡等信息 */
SYSSEG = 0x1000 /* system 模块所在的段地址 */
SETUPSEG = 0x9020 /* 当前 setup 模块段地址 */

/* Line 21 */
.globl begtext, begdata, begbss, endtext, enddata, endbss
.text
begtext:
.data
begdata:
.bss
begbss:
.text

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

与 bootsect.s 相同,本程序实际上不分段

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/* Line 30 */
entry start
start:
mov ax,#INITSEG
mov ds,ax /* 将数据段寄存器赋值 0x9000 */
/* 保存光标位置 */
mov ah,#0x03 /* 功能号为 3,表示取光标位置 */
xor bh,bh /* 第 0 页 */
int 0x10 /* 返回值为 DL=列号,DH=行号 */
mov [0],dx /* 存储在 0x90000 */
/* 保存扩展内存的大小(KB) */
mov ah,#0x88
int 0x15
mov [2],ax /* 存储在 0x90002 */

关于 BIOS 0x15 号中断:

功能号 AH = 0x88 表示取系统所含扩展内存的大小,返回值 AX = 从 0x100000 处开始的扩展内存大小,出错则将错误码保存在 AX 中

1
2
3
4
5
6
7
8
9
10
11
12
/* Line 51 */
mov ah,#0x0f
int 0x10
mov [4],bx /* 当前显示页保存在 0x90004 */
mov [6],ax /* 显示模式与字符列数保存在 0x90006 */
/* 检查显示方式(EGA/VGA),并取参数 */
mov ah,#0x12
mov bl,#0x10 /* 取 EGA 参数 */
int 0x10
mov [8],ax
mov [10],bx /* 显存大小及显示状态保存在 0x9000A */
mov [12],cx /* 显卡特性参数保存在 0x9000C */

关于 BIOS 0X10 号中断:

功能号 AH = 0x0f 表示取显卡当前显示模式,返回值:

  • AH = 显示窗口的字符列数(window width)
  • AL = 显示模式
  • BH = 当前显示页

功能号 AH = 0x12,BL = 0x10 表示取 EGA 配置信息,返回值:

  • BH = 显示状态(0-彩色模式, I/O端口=0x3dX; 1-单色模式, I/O端口=0x3bX)
  • BL = 安装的显存大小(0-64k; 1-128k; 2-192k; 3-256k)
  • CX = 显卡特性参数
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
/* Line 67 */
/* 第一个硬盘的参数 */
mov ax,#0x0000
mov ds,ax /* ds 指向中断向量表 */
/* LDS reg,mem 意味把 mem 所指的 32 位地址指针的段地址送入 DS,偏移地址送入 reg */
/* 第一个硬盘参数表的地址指针放在 0x41 号中断向量处 */
lds si,[4*0x41] /* 现在 ds:si 指向 hd0 的参数表 */
mov ax,#INITSEG
mov es,ax
mov di,#0x0080
mov cx,#0x10
rep
movsb /* 将 hd0 的参数表拷贝至 0x90080,表长为 16 个字节 */
/* 同理,以下部分在将 hd1 的参数表拷贝至 0x90090 */
mov ax,#0x0000
mov ds,ax
lds si,[4*0x46] /* 第二个硬盘参数表的地址指针为 0x46 号中断向量 */
mov ax,#INITSEG
mov es,ax
mov di,#0x0090
mov cx,#0x10
rep
movsb
/* 现在来检查是否有 hd1,没有就将 0x90090 处 16 字节清空 */
mov ax,#0x01500 /* 功能号 0x15,表示取磁盘类型 */
mov dl,#0x81 /* 要查询第二个硬盘 */
int 0x13
jc no_disk1 /* 没有这个盘就跳去 no_disk1 */
cmp ah,#3 /* 如果该磁盘类型为硬盘 */
je is_disk1 /* 就不作处理跳到 is_disk1,否则仍将刚才读入的第二硬盘参数表清空 */
no_disk1: /* 将 0x90090 处 16 字节清空 */
mov ax,#INITSEG
mov es,ax
mov di,#0x0090
mov cx,#0x10
mov ax,#0x00
rep
stosb
is_disk1:
cli /* 屏蔽所有外中断,准备进行 system 模块的移位 */

至此,重要参数读完,


移动 system & 设置 GDTR/IDTR

接下来将 system 移到内存起始处,并设置 IDTR 与 GDTR

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
/* Line 113 */
mov ax,#0x0000
cld /* 将 DF 位置 0,表示后面 movsw 时,si 与 di 向高地址递增 */
do_move: /* 进入 move 循环 */
mov es,ax /* ax 为上次复制操作的目标段 */
add ax,#0x1000 /* 加上 0x1000 */
cmp ax,#0x9000 /* 判断是否等于 0x9000 */
jz end_move /* 相等表示移动完毕,跳出循环 */
mov ds,ax /* 否则将下个目标段放在 ds */
sub di,di /* 清空 di,si */
sub si,si
mov cx,#0x8000 /* 循环拷贝 0x8000 次 */
rep
movsw /* 每次拷贝 2 字节,所以一次拷贝 64KB */
jmp do_move

! then we load the segment descriptors

end_move:
mov ax,#SETUPSEG
mov ds,ax /* ds 指向 setup 模块 */
lidt idt_48 /* 将 6 字节的 0 载入 IDTR,指向一张空的中断表 */
lgdt gdt_48 /* 将 GDT 信息载入 GDTR */

/* Line 205 */
gdt: /* 定义了 3 个全局描述符 */
.word 0,0,0,0 /* 第一个为空描述符 */

.word 0x07FF /* 段界限为 8M */
.word 0x0000 /* 段基址为 0 */
.word 0x9A00 /* P=1, DPL=00, S=1, 代码段,只读,可执行 */
.word 0x00C0 /* G 位为 1,表示颗粒度为 4K */

.word 0x07FF /* 段界限为 8M */
.word 0x0000 /* 段基址为 0 */
.word 0x9200 /* P=1, DPL=00, S=1, 数据段,可读可写 */
.word 0x00C0 /* G 位为 1,表示颗粒度为 4K */
idt_48:
.word 0 /* idt 界限为 0 */
.word 0,0 /* idt 基址为 0 */
gdt_48:
.word 0x800 /* gdt 段限长暂时定为 2KB,能放 256 个描述符 */
.word 512+gdt,0x9 /* gdt 基址为0x90200 + gdt */

GDTR 与 IDTR 的结构再来回顾一下:

GDTR与IDTR

描述符结构:

描述符

详细的介绍在 保护模式 这篇文章中。


打开 A20 地址线

此时 system 已经移到内存开始,下面打开 A20 地址线,需要打开 A20 地址线的原因是实模式下 A20 地址线默认关闭,只能访问 1M 的存储空间,打开后可以访问所有 4G 空间

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/* Line 138 */
call empty_8042 /* 反复测试 8042 状态寄存器,判断输入缓冲器是否为空 */
mov al,#0xD1 /* 0xD1 命令码表示写数据到 8042 的 P2 端口 */
out #0x64,al /* P2 端口位 1 用于 A20 地址线的选通 */
call empty_8042 /* 等待输入缓冲器为空,查看命令是否被接受 */
mov al,#0xDF /* 0xD1 命令码表示打开 A20 地址线 */
out #0x60,al /* 数据写到 0x60 口 */
call empty_8042 /* 若此时输入缓冲器为空,表示 A20 已经打开 */

/* Line 198 */
empty_8042:
.word 0x00eb,0x00eb /* 效果类似 nop */
in al,#0x64 /* 读 AT 键盘控制器状态寄存器 */
test al,#2 /* 测试位 1,判断输入缓冲器是否为空 */
jnz empty_8042 /* 若非空,则跳回去循环判断 */
ret /* 否则返回 */

0xeb 是近地址跳转的操作码,可以携带一个字节的操作数,故跳转范围在 -127 ~ 127 之间,0x00eb 表示跳转到相对下一条指令偏移值为 0 的指令,相当于继续执行下一条指令,花费的 CPU 时钟周期数是 7~ 10 个,两条就可以造成 14 ~ 20 个时钟周期的延迟,一个 nop 指令只花费 3 个时钟周期数。因为 as86 中没有相应的指令助记符,所以 Linus 直接使用了机器码


编辑 8259A 芯片

接下来该对 8259A 芯片重新编程,将硬件中断号设置成从 0x20 开始。PC 机使用两片 8259A,主片的操作端口是 0x20 - 0x21,从片的端口是 0xA0 - 0xA1

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 154 */
mov al,#0x11
out #0x20,al /* 往主片端口发送 0x11 ICW1命令字,表示初始化命令开始 */
.word 0x00eb,0x00eb /* nop */
out #0xA0,al /* 再将初始化信号发送给从片 */
.word 0x00eb,0x00eb
mov al,#0x20
out #0x21,al /* 发送主片 ICW2 命令字,设置主片起始中断号为 0x20,要发给奇端口 */
.word 0x00eb,0x00eb
mov al,#0x28
out #0xA1,al /* 发送从片 ICW2 命令字,设置从片起始中断号为 0x28 */
.word 0x00eb,0x00eb
mov al,#0x04
out #0x21,al /* 发送主片 ICW3 命令字,表示主片 IR2 连接从片 INT */
.word 0x00eb,0x00eb
mov al,#0x02
out #0xA1,al /* 发送从片 ICW3 命令字,表示从片 INT 连接主片 IR2 */
.word 0x00eb,0x00eb
mov al,#0x01
out #0x21,al /* 发送主片 ICW4 命令字,8086 模式 */
.word 0x00eb,0x00eb
out #0xA1,al /* 发送从片 ICW4 命令字,8086 模式,初始化结束,芯片就绪 */
.word 0x00eb,0x00eb
mov al,#0xFF
out #0x21,al /* 屏蔽主片所有中断请求 */
.word 0x00eb,0x00eb
out #0xA1,al /* 屏蔽从片所有中断请求 */

进入保护模式

然后是 setup.s 最后部分,PE 置位,进入保护模式,结束连 Linus 都觉得无聊的与 BIOS、硬件等打交道的过程

1
2
3
4
/* Line 191 */
mov ax,#0x0001
lmsw ax /* 将 CR0 PE 位置 1 */
jmpi 0,8 /* 跳转至 head.s 执行 */

lmsw 指令意为置处理器状态字,只有操作数的低 4 位被存入 CR0,该指令仅兼容 286 的 CPU,386 以上的 CPU 应该使用 “mov cr0, ax” 切换到保护模式。同时在设置 PE 位后,随后的一条指令必须是段间跳转指令,以刷新 CPU 提前从内存取出并解码的实模式指令队列

执行最后一条跳转指令时,已处于保护模式,操作数 8 为段选择子,16 位二进制形式为 0b0000 0000 0000 1000,最低两位为 RPL,0 表示内核级,第三位为 TI 位,0 表示从 GDT 中选择描述符,其余高位为索引,这里是 1,表示选择第 2 项。另一个操作数 0,表示段内偏移量为 0。GDT 中第二项偏移 0,指向物理地址起始位置,该处为 head 模块。还记得段选择子结构吗?

选择子

关于键盘控制器 8042 与 8259A 芯片的编程方法有必要时再专门记录在另一篇文章中