上一篇文章中,我们详细介绍了保护模式下的中断和异常以及他们的硬件基础结构 — 可编程中断控制器 8259A,以及他的初始化和中断的屏蔽与打开: 保护模式下的中断和异常(上) — 硬件原理篇
现在,硬件已经完成初始化与设定,进入操作状态,一切就绪,只欠东风,我们如何在保护模式中通过程序实现中断与陷阱的设计和响应呢?本文我们就来详细介绍。
提到“门”,你一定已经不陌生了,因为在此前的文章中,我们对门描述符已经有过一定的介绍: 利用调用门实现特权级间跳转(上) — 原理篇
在文中我们提到,门描述符分为以下四类:
可以看到,门描述符都是实现特殊的程序跳转的手段,所以,你也可以将中断门、陷阱门、任务门看成是特殊的调用门,他们在实现自身特性的程序跳转基础上,也都具有和调用门用法一样的不同特权级间调用的功能,这部分的具体实现参考上面的文章,本文就不再赘述了。
此前我们介绍过用于内存分段的全局描述符表 GDT 与局部描述符表 LDT,保护模式下还有另一个描述符表 — IDT。 GDT、LDT 中存储的是代码段描述符、数据段描述符、调用门描述符,而 IDT 中则存储的是中断门描述符、陷阱门描述符以及任务门描述符,而存储了中断门和陷阱门的 IDT 所充当的就是实地址模式下的中断向量表。 下图展示了中断门、陷阱门、任务门的存储结构:
这里我们先不介绍任务门,重点关注中断门与陷阱门的结构,可以看到,除了类型位中断门与陷阱门各自的取值不同,其他字段上两者的结构是完全相同的,而与先前的调用门结构也是相同的。
门描述符各个位的含义先前也进行过介绍了,具体可以参看图中的解释或回看之前的文章。
那么,在中断描述符表中,如何实现中断向量号与中断描述符的对应关系呢?答案很简单,中断描述符表中,中断描述符的 index 就是中断向量号,参考上篇文章中中断、陷阱、异常与中断向量号的对应关系,IDT 中第一个表项就是 0 号向量号的 DIV 除 0 错误、第4个表项,也就是 3 号描述符就是调试断点向量,从 32 ~ 255 则是用户可以自定义的中断,我们可以通过 int n
触发相应的中断。
接下来,我们就通过实际的代码,来看看如何在程序中应用中断门和陷阱门吧。 既然中断有两种方式触发 — 硬件随机触发和 int n 手动触发,我们就编写两个函数,分别用来响应硬件中断和我们手动 int n 触发的中断。 最方便的硬件中断就是时钟中断,因为他会以固定间隔自动触发中断。
在本系列第一篇文章中,我们曾经介绍过,硬件启动之初,当电源供电稳定时,会自动向级联在 CPU 上的 8284A 时钟发生器发送 PowerGood 信号,8284A 芯片会向 CPU 发送 RESET 信号。 事实上,8284A 芯片作为时钟发生器,最为常用的功能就是在加电晶振的带动下产生每秒 1024 次时钟中断信号供硬件使用,因此他一般不被我们的软件所使用。 而主 8259A 的 IR0 引脚上级联的 8254 时钟芯片则是通常我们所说的操作系统时钟中断的信号源,因此,在本次的程序中,我们也要使用 IRQ0 中断作为中断信号源。
中断响应函数与普通的函数在编写上并没有很大的区别,他通常包含两部分逻辑:
众所周知,我们使用 ret
指令完成一个函数的调用并跳转回函数调用位置继续执行,与此类似,中断响应函数则通过 iret
与 iretd
两个指令来实现中断处理完成后的跳转工作,他们分别应用于 16 位系统与 32 位系统中。
我们只要在手动触发的中断响应函数中实现一个字符串的显示,就可以证明中断正常的被触发:
DefaultMessage: db "Default Interrupt Handler", 0
OffsetDefaultMessage equ DefaultMessage - $$
_PrintDefaultText:
PrintDefaultText equ _PrintDefaultText - $$
mov eax, 0Ch ; 黑底红字,不闪烁
push eax
mov eax, 80 * 3 * 2
push eax
push OffsetDefaultMessage
call DisplayString
add esp, 12
; 发送 EOI
mov al, 20h
out 20h, al
iretd
这里我们定义了一个 dword 标签 PrintDefaultText,他用来记录当前的段偏移,从而稍后用来初始化我们的中断门描述符。
既然是时钟中断,我们当然要做一些周期触发的事情,最简单的就是我们通过周期性的切换两个字符串展示,来模拟字符串的闪烁功能:
BootMessage: db "Hello World my OS, techlog.cn!", 0
OffsetBootMessage equ BootMessage - $$
DispeareMessage: db " ", 0
OffsetDispeareMessage equ DispeareMessage - $$
_clockNum dd 0
clockNum equ _clockNum - $$
_PrintText:
PrintText equ _PrintText - $$
mov eax, 0Ch ; 黑底红字,不闪烁
push eax
mov eax, 80 * 2 * 2
push eax
mov eax, dword [clockNum]
mov ebx, 2
xor edx, edx
div ebx
cmp edx, 0
jne print_dispear
push OffsetBootMessage
jmp print_text
print_dispear:
push OffsetDispeareMessage
print_text:
call DisplayString
add esp, 12
add dword [clockNum], 1
; 发送 EOI
mov al, 20h
out 20h, al
iretd
下面我们就来创建中断描述符表,并添加中断描述符。
和代码段数据段描述符一样,我们通过一个宏来实现描述符的定义:
; ---------------- 门描述符宏 -------------
; usage: Gate Selector, Offset, DCount, Attr
; Selector: dw
; Offset: dd
; DCount: db
; Attr: db
%macro Gate 4
dw (%2 & 0FFFFh) ; 偏移 1 (2 字节)
dw %1 ; 选择子 (2 字节)
dw (%3 & 1Fh) | ((%4 << 8) & 0FF00h) ; 属性 (2 字节)
dw ((%2 >> 16) & 0FFFFh) ; 偏移 2 (2 字节)
%endmacro
保护模式下,中断向量号就是中断描述符表中描述符的 index,因此我们要定义我们指定向量号的中断门,就需要跳过前面 N 个描述符。
你可以通过复制粘贴的方式生成大量的代码来实现,但 NASM 提供了更为简单的预处理方式 — %rep。
下面的代码实现了 inc word [table + 2*i]
语句的 100 次循环添加:
%assign i 0
%rep 100
inc word [table+2*i]
%assign i i+1
%endrep
这样我们就可以非常简单的定义 IDT 中的所有中断门了:
; -------------------- IDT ---------------------
[SECTION .idt]
ALIGN 32
[BITS 32]
LABEL_IDT:
; 目标选择子, 偏移, DCount, 属性
%rep 32
Gate SelectorCode32, PrintDefaultText, 0, 8Eh
%endrep
.020h: Gate SelectorCode32, PrintText, 0, 8Eh ; 时钟中断描述符
%rep 95
Gate SelectorCode32, PrintDefaultText, 0, 8Eh
%endrep
.080h: Gate SelectorCode32, PrintDefaultText, 0, 8Eh
; ------------------ END OF IDT ----------------
IdtLen equ $ - LABEL_IDT
IdtPtr dw IdtLen - 1 ; 段界限
dd 0 ; 段基址
上述代码中,首先通过循环 32 次,用默认的响应函数初始化了 32 个系统保留的中断向量,然后,我们将 PrintText 函数注册为了中断向量号为 20h 的中断响应函数(这里 .020h 标签的声明实际上并没有什么作用,只是为了便于理解) 虽然,通过同样的方式跳过 95 个中断门,便来到了 080h 向量号,于是我们将 PrintDefaultText 设置为对应的中断响应函数。
正如 CPU 中存在一个 gdtr 寄存器用来存储 gdt 的首地址,ldtr 寄存器用来存储 ldt 的首地址,CPU 中也同样存在一个 idtr 寄存器,用来存储 idt 的首地址。
而指令 lidt
则用来加载 idt 首地址到 idtr 寄存器中。
同时,为了跳转回实地址模式后,能够还原 idtr 寄存器的值,以及中断屏蔽信息,我们还需要先将这些信息进行保存:
_SavedIMREG: db 0 ; 中断屏蔽寄存器值
_SavedIDTR: dd 0 ; 用于保存修改前的 IDTR
dd 0
; 准备加载 IDTR
xor eax, eax
mov ax, ds
shl eax, 4
add eax, LABEL_IDT ; eax <- idt 基地址
mov dword [IdtPtr + 2], eax ; [IdtPtr + 2] <- idt 基地址
; 保存 IDTR 与中断屏蔽寄存器原值
sidt [_SavedIDTR]
in al, 21h
mov [_SavedIMREG], al
; 加载 IDTR
lidt [IdtPtr]
上一篇文章中,我们已经详细介绍了可编程中断控制器 8259A 的初始化和使用方法,这里我们就可以直接使用了:
; ------------------ 初始化可编程中断控制器 ----------------------
Init8259A:
mov al, 011h
out 020h, al ; 主8259, ICW1
call io_delay
out 0A0h, al ; 从8259, ICW1
call io_delay
mov al, 020h ; IRQ0 对应中断向量 0x20
out 021h, al ; 主8259, ICW2
call io_delay
mov al, 028h ; IRQ8 对应中断向量 0x28
out 0A1h, al ; 从8259, ICW2
call io_delay
mov al, 004h ; IR2 对应从8259
out 021h, al ; 主8259, ICW3
call io_delay
mov al, 002h ; 对应主8259的 IR2
out 0A1h, al ; 从8259, ICW3
call io_delay
mov al, 001h
out 021h, al ; 主8259, ICW4
call io_delay
out 0A1h, al ; 从8259, ICW4
call io_delay
mov al, 11111110b ; 仅开启定时器中断
out 021h, al ; 主8259, OCW1
call io_delay
mov al, 11111111b ; 屏蔽从8259所有中断
out 0A1h, al ; 从8259, OCW1
call io_delay
; 发送 EOI
mov al, 20h
out 20h, al
ret
io_delay:
nop
nop
nop
nop
ret
可以看到,我们把 IRQ0 中断向量指向了 020h,并且屏蔽了除此之外的其他中断。
手动触发中断很简单:
int 80h
但如果想要让可屏蔽中断能够触发,光是通过 OCW1 设置屏蔽信息还不够,还需要将 eflags 寄存器的中断开关打开,因此我们需要执行:
sti
接下来,我们只要等待程序触发就可以了。
当我们的程序运行完成,我们当然是希望他能够正常的返回到实地址模式下了,因此我们有几件事要做。
我们要观察时钟中断的效果,就要等待一段时间,上面的文章中,为了能够实现周期变换,我们定义了一个触发次数字段 clockNum,他正好可以作为等待的循环依据:
wait_loop:
cmp dword [clockNum], 4096
jb wait_loop
cli
; 恢复 IDTR 与中断屏蔽寄存器 IMREG
lidt [_SavedIDTR]
mov al, [_SavedIMREG]
out 21h, al
; ------------------ 恢复可编程中断控制器 ----------------------
SetRealmode8259A:
mov ax, SelectorData
mov fs, ax
mov al, 015h ; 4 字节中断向量
out 020h, al ; 主8259, ICW1.
call io_delay
out 0A0h, al ; 从8259, ICW1
call io_delay
mov al, 008h ; IRQ0 对应中断向量 0x8
out 021h, al ; 主8259, ICW2.
call io_delay
mov al, 0F0h ; IRQ8 对应中断向量 0x28
out 0A1h, al ; 从8259, ICW2
call io_delay
mov al, 004h ; IR2 对应从8259
out 021h, al ; 主8259, ICW3
call io_delay
mov al, 002h ; 对应主8259的 IR2
out 0A1h, al ; 从8259, ICW3
call io_delay
mov al, 001h
out 021h, al ; 主8259, ICW4.
call io_delay
out 0A1h, al ; 从8259, ICW4
call io_delay
mov al, 0 ; 开启主 8259A 所有中断
out 021h, al ; 主8259, OCW1
call io_delay
mov al, 0 ; 开启从 8259A 所有中断
out 0A1h, al ; 从8259, OCW1
call io_delay
; 发送 EOI
mov al, 20h
out 20h, al
; 恢复中断屏蔽寄存器(IMREG)
mov al, [fs:SavedIMREG]
out 021h, al
call io_delay
ret
这里的代码与 8259A 的初始化代码略有不同,区别在于,16位实地址模式下,中断向量长度是4字节。 另外,虽然在实地址模式的默认情况下,程序只使用主 8259A 芯片,但仍然必须设置为级联模式,而不能将级联位设置为 1,虽然我在一些书中看到,在回跳时,主 ICW1 设置为了 17h,即 single 模式,然后省去了 ICW3 以及所有从 8259A 的设置,但如果这么做,系统会触发异常:
master: ICW1: single mode not supported
执行我们的系统,可以看到下面的效果:
上一篇文章中的 CPU 预设的异常列表中,中断向量号为 8、17 的中断会在中断响应函数执行前先将 32 位的 Error Code 压入栈顶,但此后即便是中断响应函数返回,Error Code 也并不会自动出栈,这意味着你需要自己手动进行处理,如下图所示:
到此为止,似乎看上去中断门与陷阱门并没有什么区别,但实际上二者还是有一点点细微的差别的。 通过中断门注册的中断响应函数在返回时,会自动复位 eflags 寄存器的 IF 位,而陷阱门则不会改变。
计算机是如何启动的?如何制作自己的操作系统 如何调试操作系统
操作系统的内存管理 — 分段与分页、虚拟地址、逻辑地址、线性地址、物理地址
详解 32 位保护模式与内存分段机制 进军保护模式 保护模式进阶 — 再回实模式 实战局部描述符表 LDT 利用调用门实现特权级间跳转(上) — 原理篇 利用调用门实现特权级间跳转(下) — 实战篇
详解操作系统分页机制与实战 实战分页机制实现 — 通过实际内存大小动态调整页表个数
; ------------------------------------------------------------------------
; 显示一个字节中的数字
;
; params:
; db property
; dw display_index
; db number
;
; return:
; eax display_index
; ------------------------------------------------------------------------
DisplayByteNumber:
push ebp
mov ebp, esp
push edx
push edi
push esi
mov ah, [ebp + 16]
mov edi, [ebp + 12]
mov al, [ebp + 8]
mov dl, al
shr al, 4
mov ecx, 2
.begin:
and al, 01111b
cmp al, 9
ja .letter
add al, '0'
jmp .number
.letter:
sub al, 0Ah
add al, 'A'
.number:
mov [gs:edi], ax
add edi, 2
mov al, dl
loop .begin
mov eax, edi
pop esi
pop edi
pop edx
pop ebp
ret
; ------------------------------------------------------------------------
; 显示一个整形数
;
; params:
; db property
; dw display_index
; db number
;
; return:
; eax display_index
; ------------------------------------------------------------------------
DisplayInt:
push ebp
mov ebp, esp
push edx
push edi
push esi
push ebx
push ecx
mov ah, [ebp + 16]
mov edi, [ebp + 12]
mov ecx, 4
mov ebx, 24
push eax
.loop:
mov eax, [ebp + 8]
shr eax, 4
push edi
push eax
call DisplayByteNumber
add esp, 8
add edi, 2
sub ebx, 8
loop .loop
mov al, 'h'
push edi
mov [gs:edi], ax
add edi, 4
mov eax, edi
pop ecx
pop ebx
pop esi
pop edi
pop edx
pop ebp
ret
; ------------------------------------------------------------------------
; 显示一个字符串
;
; params:
; db property
; dw display_index
; dw string_offset
; ------------------------------------------------------------------------
DisplayString:
push ebp
mov ebp, esp
push eax
push edi
push esi
mov ah, [ebp + 16]
mov edi, [ebp + 12]
mov esi, [ebp + 8]
cld
.loop_label:
lodsb
test al, al
jz .over_print
mov [gs:edi], ax
add edi, 2
jmp .loop_label
.over_print:
pop esi
pop edi
pop eax
pop ebp
ret
; ------------------------------------------------------------------------
; 显示 AL 中的数字
; ------------------------------------------------------------------------
DispAL:
push ecx
push edx
push edi
mov edi, [dwDispPos]
mov ah, 0Fh ; 0000b: 黑底 1111b: 白字
mov dl, al
shr al, 4
mov ecx, 2
.begin:
and al, 01111b
cmp al, 9
ja .1
add al, '0'
jmp .2
.1:
sub al, 0Ah
add al, 'A'
.2:
mov [gs:edi], ax
add edi, 2
mov al, dl
loop .begin
;add edi, 2
mov [dwDispPos], edi
pop edi
pop edx
pop ecx
ret
; DispAL 结束-------------------------------------------------------------
; ------------------------------------------------------------------------
; 显示一个整形数
; ------------------------------------------------------------------------
DispInt:
mov eax, [esp + 4]
shr eax, 24
call DispAL
mov eax, [esp + 4]
shr eax, 16
call DispAL
mov eax, [esp + 4]
shr eax, 8
call DispAL
mov eax, [esp + 4]
call DispAL
mov ah, 07h ; 0000b: 黑底 0111b: 灰字
mov al, 'h'
push edi
mov edi, [dwDispPos]
mov [gs:edi], ax
add edi, 4
mov [dwDispPos], edi
pop edi
ret
; DispInt 结束------------------------------------------------------------
; ------------------------------------------------------------------------
; 显示一个字符串
; ------------------------------------------------------------------------
DispStr:
push ebp
mov ebp, esp
push ebx
push esi
push edi
mov esi, [ebp + 8] ; pszInfo
mov edi, [dwDispPos]
mov ah, 0Fh
.1:
lodsb
test al, al
jz .2
cmp al, 0Ah ; 是回车吗?
jnz .3
push eax
mov eax, edi
mov bl, 160
div bl
and eax, 0FFh
inc eax
mov bl, 160
mul bl
mov edi, eax
pop eax
jmp .1
.3:
mov [gs:edi], ax
add edi, 2
jmp .1
.2:
mov [dwDispPos], edi
pop edi
pop esi
pop ebx
pop ebp
ret
; DispStr 结束------------------------------------------------------------
; ------------------------------------------------------------------------
; 换行
; ------------------------------------------------------------------------
DispReturn:
push szReturn
call DispStr ;printf("\n");
add esp, 4
ret
; DispReturn 结束---------------------------------------------------------
; ---------------- 内存段描述符宏 -------------
; usage: Descriptor Base, Limit, Attr
; Base: dd
; Limit: dd (low 20 bits available)
; Attr: dw (lower 4 bits of higher byte are always 0)
%macro Descriptor 3
dw %2 & 0FFFFh ; 段界限1
dw %1 & 0FFFFh ; 段基址1
db (%1 >> 16) & 0FFh ; 段基址2
dw ((%2 >> 8) & 0F00h) | (%3 & 0F0FFh) ; 属性1 + 段界限2 + 属性2
db (%1 >> 24) & 0FFh ; 段基址3
%endmacro
; ---------------- 门描述符宏 -------------
; usage: Gate Selector, Offset, DCount, Attr
; Selector: dw
; Offset: dd
; DCount: db
; Attr: db
%macro Gate 4
dw (%2 & 0FFFFh) ; 偏移 1 (2 字节)
dw %1 ; 选择子 (2 字节)
dw (%3 & 1Fh) | ((%4 << 8) & 0FF00h) ; 属性 (2 字节)
dw ((%2 >> 16) & 0FFFFh) ; 偏移 2 (2 字节)
%endmacro
PageDirBase equ 200000h ; 页目录开始地址: 2M
PageTblBase equ 201000h ; 页表开始地址: 2M+4K
; ------------ DOS 加载初始内存地址 -----------
org 0100h
jmp LABEL_BEGIN
; ------------------- GDT ---------------------
[SECTION .gdt]
ALIGN 32
[BITS 32]
; GDT
; 段基址, 段界限, 属性
LABEL_GDT: Descriptor 0, 0, 0 ; 空描述符
LABEL_DESC_NORMAL: Descriptor 0, 0ffffh, 92h ; Normal 描述符
LABEL_DESC_FLAT_RW: Descriptor 0, 0fffffh, 8092h ; 段界限为 1023 * 4096 字节
LABEL_DESC_PAGE_DIR: Descriptor PageDirBase, 4095, 92h ; Page Directory,可读写
LABEL_DESC_PAGE_TBL: Descriptor PageTblBase, 1023, 8092h ; Page Tables,段界限为 1023 * 4096 字节
LABEL_DESC_CODE32: Descriptor 0, SegCode32Len - 1, 4098h ; 非一致代码段
LABEL_DESC_CODE16: Descriptor 0, 0ffffh, 98h ; 非一致代码段, 用于跳回 16 BITS 模式
LABEL_DESC_DATA: Descriptor 0, DataLen-1, 92h ; 可读写数据段,界限 64KB
LABEL_DESC_STACK: Descriptor 0, TopOfStack, 4093h ; 32 位全局堆栈段,可读写数据段,且栈指针默认使用 esp 寄存器
LABEL_DESC_VIDEO: Descriptor 0B8000h, 0ffffh, 92h ; 显存首地址
; ------------------ END OF GDT ----------------
GdtLen equ $ - LABEL_GDT ; GDT长度
GdtPtr dw GdtLen - 1 ; GDT界限
dd 0 ; GDT基地址
; ------------------ GDT 选择子 -----------------
SelectorNormal equ LABEL_DESC_NORMAL - LABEL_GDT
SelectorPageDir equ LABEL_DESC_PAGE_DIR - LABEL_GDT
SelectorPageTbl equ LABEL_DESC_PAGE_TBL - LABEL_GDT
SelectorCode32 equ LABEL_DESC_CODE32 - LABEL_GDT
SelectorCode16 equ LABEL_DESC_CODE16 - LABEL_GDT
SelectorFlatRW equ LABEL_DESC_FLAT_RW - LABEL_GDT
SelectorData equ LABEL_DESC_DATA - LABEL_GDT
SelectorStack equ LABEL_DESC_STACK - LABEL_GDT
SelectorVideo equ LABEL_DESC_VIDEO - LABEL_GDT
; --------------- END OF 段选择子 ----------------
; -------------------- IDT ---------------------
[SECTION .idt]
ALIGN 32
[BITS 32]
LABEL_IDT:
; 目标选择子, 偏移, DCount, 属性
%rep 32
Gate SelectorCode32, PrintDefaultText, 0, 8Eh
%endrep
.020h: Gate SelectorCode32, PrintText, 0, 8Eh ; 时钟中断描述符
%rep 95
Gate SelectorCode32, PrintDefaultText, 0, 8Eh
%endrep
.080h: Gate SelectorCode32, PrintDefaultText, 0, 8Eh
; ------------------ END OF IDT ----------------
IdtLen equ $ - LABEL_IDT
IdtPtr dw IdtLen - 1 ; 段界限
dd 0 ; 段基址
[SECTION .data1] ; 数据段
ALIGN 32
[BITS 32]
LABEL_DATA:
SPValueInRealMode dw 0
BootMessage: db "Hello World my OS, techlog.cn!", 0
OffsetBootMessage equ BootMessage - $$
DispeareMessage: db " ", 0
OffsetDispeareMessage equ DispeareMessage - $$
DefaultMessage: db "Default Interrupt Handler", 0
OffsetDefaultMessage equ DefaultMessage - $$
_szReturn db 0Ah, 0
szReturn equ _szReturn - $$
_clockNum dd 0
clockNum equ _clockNum - $$
_MemChkBuf: times 256 db 0 ; ARDS 缓冲区
_dwMCRNumber: dd 0 ; ARDS 个数
_dwDispPos: dd (80 * 6 + 0) * 2 ; 屏幕第 6 行, 第 0 列。_PageTableNumber dd 0 ; 页表个数
_dwMemSize: dd 0 ; 最大连续内存大小
_SavedIMREG: db 0 ; 中断屏蔽寄存器值
_SavedIDTR: dd 0 ; 用于保存修改前的 IDTR
dd 0
_ARDStruct: ; Address Range Descriptor Structure
_dwBaseAddrLow: dd 0
_dwBaseAddrHigh: dd 0
_dwLengthLow: dd 0
_dwLengthHigh: dd 0
_dwType: dd 0
MemChkBuf equ _MemChkBuf - $$
dwMCRNumber equ _dwMCRNumber - $$
PageTableNumber equ _PageTableNumber - $$
dwMemSize equ _dwMemSize - $$
dwDispPos equ _dwDispPos - $$
SavedIMREG equ _SavedIMREG - $$
ARDStruct equ _ARDStruct - $$
dwBaseAddrLow equ _dwBaseAddrLow - $$
dwBaseAddrHigh equ _dwBaseAddrHigh - $$
dwLengthLow equ _dwLengthLow - $$
dwLengthHigh equ _dwLengthHigh - $$
dwType equ _dwType - $$
DataLen equ $ - LABEL_DATA
; 全局堆栈段
[SECTION .gs]
ALIGN 32
[BITS 32]
LABEL_STACK:
times 512 db 0
TopOfStack equ $ - LABEL_STACK - 1
[SECTION .s16]
[BITS 16]
LABEL_BEGIN:
; 初始化段基址寄存器
mov ax, cs
mov ds, ax
mov es, ax
mov ss, ax
mov sp, 0100h
mov [LABEL_GO_BACK_TO_REAL+3], ax
mov [SPValueInRealMode], sp
; 循环获取 ARDS
xor ebx, ebx ; 清零 EBX
mov di, _MemChkBuf ; DI 寄存器中保存写入地址偏移
.loop:
mov eax, 0E820h ; 初始化 EAX 为固定值
mov ecx, 20 ; ARDS 字节数
mov edx, 0534D4150h ; 初始化 EDX 为固定值
int 15h ; 触发 15H 中断
jc LABEL_MEM_CHK_FAIL ; EFLAGS 寄存器 CF 位为 1 则跳转,表示失败
add di, 20 ; DI 寄存器指向下一个待写入位置偏移
inc dword [_dwMCRNumber] ; 计数变量 + 1
cmp ebx, 0 ; 比较 EBX 判断是否完成 ARDS 获取
jne .loop
jmp LABEL_MEM_CHK_OK
LABEL_MEM_CHK_FAIL:
mov dword [_dwMCRNumber], 0
LABEL_MEM_CHK_OK:
; 初始化 16 位代码段描述符
mov ax, cs
movzx eax, ax
shl eax, 4
add eax, LABEL_SEG_CODE16
mov word [LABEL_DESC_CODE16 + 2], ax
shr eax, 16
mov byte [LABEL_DESC_CODE16 + 4], al
mov byte [LABEL_DESC_CODE16 + 7], ah
; 初始化非一致代码段描述符
xor eax, eax
mov ax, cs
shl eax, 4
add eax, LABEL_SEG_CODE32 ; 计算非一致代码段基地址物理地址
mov word [LABEL_DESC_CODE32 + 2], ax
shr eax, 16
mov byte [LABEL_DESC_CODE32 + 4], al
mov byte [LABEL_DESC_CODE32 + 7], ah
; 初始化数据段描述符
xor eax, eax
mov ax, ds
shl eax, 4
add eax, LABEL_DATA
mov word [LABEL_DESC_DATA + 2], ax
shr eax, 16
mov byte [LABEL_DESC_DATA + 4], al
mov byte [LABEL_DESC_DATA + 7], ah
; 初始化堆栈段描述符
xor eax, eax
mov ax, ds
shl eax, 4
add eax, LABEL_STACK
mov word [LABEL_DESC_STACK + 2], ax
shr eax, 16
mov byte [LABEL_DESC_STACK + 4], al
mov byte [LABEL_DESC_STACK + 7], ah
; 准备加载 GDTR
xor eax, eax ; 清空 eax 寄存器
mov ax, ds
shl eax, 4
add eax, LABEL_GDT ; 计算出 GDT 基地址的物理地址
mov dword [GdtPtr + 2], eax ; [GdtPtr + 2] <- gdt 基地址
; 准备加载 IDTR
xor eax, eax
mov ax, ds
shl eax, 4
add eax, LABEL_IDT ; eax <- idt 基地址
mov dword [IdtPtr + 2], eax ; [IdtPtr + 2] <- idt 基地址
; 加载 GDTR
lgdt [GdtPtr]
; 关闭硬件中断
cli
; 保存 IDTR 与中断屏蔽寄存器原值
sidt [_SavedIDTR]
in al, 21h
mov [_SavedIMREG], al
; 加载 IDTR
lidt [IdtPtr]
; 打开 A20 地址总线
in al, 92h
or al, 00000010b
out 92h, al
; 置位 PE 标志位,打开保护模式
mov eax, cr0
or eax, 1
mov cr0, eax
; 跳转进入保护模式
jmp dword SelectorCode32:0 ; 执行这一句会把 SelectorCode32 装入 cs,
; 并跳转到 Code32Selector:0 处
; 从保护模式跳回到实模式
LABEL_REAL_ENTRY:
mov ax, cs
mov ds, ax
mov es, ax
mov ss, ax
mov sp, [SPValueInRealMode]
; 恢复 IDTR 与中断屏蔽寄存器 IMREG
lidt [_SavedIDTR]
mov al, [_SavedIMREG]
out 21h, al
; 关闭 A20 地址线
in al, 92h
and al, 0fdh
out 92h, al
; 打开硬件中断
sti
; 触发 BIOS int 21h 中断,回到实地址模式
mov ax, 4c00h
int 21h
[SECTION .s32] ; 32 位代码段. 由实模式跳入.
[BITS 32]
LABEL_SEG_CODE32:
mov ax, SelectorData
mov ds, ax ; 数据段选择子
mov ax, SelectorData
mov es, ax
mov ax, SelectorVideo
mov gs, ax ; 赋值视频段选择子
mov ax, SelectorStack
mov ss, ax ; 堆栈段选择子
mov esp, TopOfStack
call SetupPaging ; 启动分页机制
call Init8259A
int 080h
sti
wait_loop:
cmp dword [clockNum], 4096
jb wait_loop
cli
call SetRealmode8259A
jmp SelectorCode16:0
; ------------------ 初始化可编程中断控制器 ----------------------
Init8259A:
mov al, 011h
out 020h, al ; 主8259, ICW1
call io_delay
out 0A0h, al ; 从8259, ICW1
call io_delay
mov al, 020h ; IRQ0 对应中断向量 0x20
out 021h, al ; 主8259, ICW2
call io_delay
mov al, 028h ; IRQ8 对应中断向量 0x28
out 0A1h, al ; 从8259, ICW2
call io_delay
mov al, 004h ; IR2 对应从8259
out 021h, al ; 主8259, ICW3
call io_delay
mov al, 002h ; 对应主8259的 IR2
out 0A1h, al ; 从8259, ICW3
call io_delay
mov al, 001h
out 021h, al ; 主8259, ICW4
call io_delay
out 0A1h, al ; 从8259, ICW4
call io_delay
mov al, 0FEh ; 仅开启定时器中断
out 021h, al ; 主8259, OCW1
call io_delay
mov al, 0FFh ; 屏蔽从8259所有中断
out 0A1h, al ; 从8259, OCW1
call io_delay
; 发送 EOI
mov al, 20h
out 20h, al
ret
; ------------------ 恢复可编程中断控制器 ----------------------
SetRealmode8259A:
mov ax, SelectorData
mov fs, ax
mov al, 015h ; 4 字节中断向量
out 020h, al ; 主8259, ICW1.
call io_delay
out 0A0h, al ; 从8259, ICW1
call io_delay
mov al, 008h ; IRQ0 对应中断向量 0x8
out 021h, al ; 主8259, ICW2.
call io_delay
mov al, 0F0h ; IRQ8 对应中断向量 0x28
out 0A1h, al ; 从8259, ICW2
call io_delay
mov al, 004h ; IR2 对应从8259
out 021h, al ; 主8259, ICW3
call io_delay
mov al, 002h ; 对应主8259的 IR2
out 0A1h, al ; 从8259, ICW3
call io_delay
mov al, 001h
out 021h, al ; 主8259, ICW4.
call io_delay
out 0A1h, al ; 从8259, ICW4
call io_delay
mov al, 0 ; 开启主 8259A 所有中断
out 021h, al ; 主8259, OCW1
call io_delay
mov al, 0 ; 开启从 8259A 所有中断
out 0A1h, al ; 从8259, OCW1
call io_delay
; 发送 EOI
mov al, 20h
out 20h, al
; 恢复中断屏蔽寄存器(IMREG)
mov al, [fs:SavedIMREG]
out 021h, al
call io_delay
ret
io_delay:
nop
nop
nop
nop
ret
; ---------------------- 获取内存信息 ---------------------------
GetMemInfos:
push esi
push edi
push ecx
; 循环获取 ARDS 4 个成员
mov esi, MemChkBuf ; 寻址缓存区
mov ecx, [dwMCRNumber] ; 获取循环次数 ARDS 个数
.loop:
mov edx, 5 ; 循环遍历 ARDS 的 4 个成员
mov edi, ARDStruct
.1:
; 将缓冲区中成员赋值给 ARDStruct
mov eax, dword [esi]
stosd
add esi, 4
dec edx
cmp edx, 0
jnz .1
; Type 是 AddressRangeMemory 赋值 dwMemSize
cmp dword [dwType], 1
jne .2
mov eax, [dwBaseAddrLow]
add eax, [dwLengthLow]
cmp eax, [dwMemSize]
jb .2
mov [dwMemSize], eax
.2:
loop .loop
pop ecx
pop edi
pop esi
ret
; ---------------------- 分页机制启动 ---------------------------
SetupPaging:
; 获取内存信息
call GetMemInfos
; 根据内存大小计算应初始化多少PDE以及多少页表
xor edx, edx
mov eax, [dwMemSize]
mov ebx, 400000h ; 一个页表对应的内存 4MB
div ebx
mov ecx, eax ; ecx 保存页表个数
test edx, edx
jz .no_remainder
inc ecx ; 余数不为 0 则增加一个页表
.no_remainder:
mov [PageTableNumber], ecx ; 暂存页表个数
; 为简化处理, 所有线性地址对应相等的物理地址. 并且不考虑内存空洞.
; 初始化页目录
mov ax, SelectorFlatRW
mov es, ax
mov edi, PageDirBase ; 此段首地址为 PageDirBase
xor eax, eax
mov eax, 201000h | 7 ; 用户级,存在于内存,可读写
.filter_pde:
stosd
add eax, 4096 ; 为了简化, 所有页表在内存中是连续的.
loop .filter_pde
; 初始化所有页表
mov eax, [PageTableNumber] ; 页表个数
mov ebx, 1024 ; 每个页表 1024 个 PTE
mul ebx
mov ecx, eax ; PTE 个数 = 页表个数 * 1024
mov edi, 201000h
xor eax, eax
mov eax, 7 ; 用户级、存在于内存、可读写
.filter_pte:
stosd
add eax, 4096 ; 每一页指向 4K 的空间
loop .filter_pte
; 设置页目录表起始地址
mov eax, PageDirBase
mov cr3, eax
; 开启分页机制
mov eax, cr0
or eax, 80000000h
mov cr0, eax
ret
_PrintText:
PrintText equ _PrintText - $$
mov eax, 0Ch ; 黑底红字,不闪烁
push eax
mov eax, 80 * 2 * 2
push eax
mov eax, dword [clockNum]
mov ebx, 2
xor edx, edx
div ebx
cmp edx, 0
jne print_dispear
push OffsetBootMessage
jmp print_text
print_dispear:
push OffsetDispeareMessage
print_text:
call DisplayString
add esp, 12
add dword [clockNum], 1
; 发送 EOI
mov al, 20h
out 20h, al
iretd
_PrintDefaultText:
PrintDefaultText equ _PrintDefaultText - $$
mov eax, 0Ch ; 黑底红字,不闪烁
push eax
mov eax, 80 * 3 * 2
push eax
push OffsetDefaultMessage
call DisplayString
add esp, 12
; 发送 EOI
mov al, 20h
out 20h, al
iretd
%include "lib.asm" ; 库函数
SegCode32Len equ $ - LABEL_SEG_CODE32
; 16 位代码段. 由 32 位代码段跳入, 跳出后到实模式
[SECTION .s16code]
ALIGN 32
[BITS 16]
LABEL_SEG_CODE16:
; 跳回实模式:
mov ax, SelectorNormal
mov ds, ax
mov es, ax
mov fs, ax
mov gs, ax
mov ss, ax
mov eax, cr0
and eax, 7FFFFFFEh ; PE=0, PG=0
mov cr0, eax
LABEL_GO_BACK_TO_REAL:
jmp word 0:LABEL_REAL_ENTRY ; 段地址会在程序开始处被设置成正确的值
Code16Len equ $ - LABEL_SEG_CODE16
; END of [SECTION .s16code]