之前也提到,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 的结构再来回顾一下:
描述符结构:
详细的介绍在 保护模式 这篇文章中。
打开 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 芯片的编程方法有必要时再专门记录在另一篇文章中