保护模式下的中断和异常(下) -- 软件实战篇

2020-06-27 19:00:24   最后更新: 2020-06-27 19:00:24   访问数量:178




上一篇文章中,我们详细介绍了保护模式下的中断和异常以及他们的硬件基础结构 -- 可编程中断控制器 8259A,以及他的初始化和中断的屏蔽与打开:

保护模式下的中断和异常(上) -- 硬件原理篇

 

现在,硬件已经完成初始化与设定,进入操作状态,一切就绪,只欠东风,我们如何在保护模式中通过程序实现中断与陷阱的设计和响应呢?本文我们就来详细介绍

 

 

提到“门”,你一定已经不陌生了,因为在此前的文章中,我们对门描述符已经有过一定的介绍:

利用调用门实现特权级间跳转(上) -- 原理篇

 

在文中我们提到,门描述符分为以下四类:

  1. 调用门 -- 用来实现不同特权级之间的调用
  2. 中断门 -- 描述中断响应程序,实现正常逻辑与中断响应逻辑之间的跳转
  3. 陷阱门 -- 描述陷阱响应程序,实现正常逻辑与陷阱响应逻辑之间的跳转
  4. 任务门 -- 在抢占式多任务系统中,实现任务间切换的跳转

 

可以看到,门描述符都是实现特殊的程序跳转的手段,所以,你也可以将中断门、陷阱门、任务门看成是特殊的调用门,他们在实现自身特性的程序跳转基础上,也都具有和调用门用法一样的不同特权级间调用的功能,这部分的具体实现参考上面的文章,本文就不再赘述了

 

此前我们介绍过用于内存分段的全局描述符表 GDT 与局部描述符表 LDT,保护模式下还有另一个描述符表 -- IDT

GDT、LDT 中存储的是代码段描述符、数据段描述符、调用门描述符,而 IDT 中则存储的是中断门描述符、陷阱门描述符以及任务门描述符,而存储了中断门和陷阱门的 IDT 所充当的就是实地址模式下的中断向量表

下图展示了中断门、陷阱门、任务门的存储结构:

 

 

这里我们先不介绍任务门,重点关注中断门与陷阱门的结构,可以看到,除了类型位中断门与陷阱门各自的取值不同,其他字段上两者的结构是完全相同的,而与先前的调用门结构也是相同的

门描述符各个位的含义先前也进行过介绍了,具体可以参看图中的解释或回看之前的文章

那么,在中断描述符表中,如何实现中断向量号与中断描述符的对应关系呢?答案很简单,中断描述符表中,中断描述符的 index 就是中断向量号,参考上篇文章中中断、陷阱、异常与中断向量号的对应关系,IDT 中第一个表项就是 0 号向量号的 DIV 除 0 错误、第4个表项,也就是 3 号描述符就是调试断点向量,从 32 ~ 255 则是用户可以自定义的中断,我们可以通过 int n 触发相应的中断

 

接下来,我们就通过实际的代码,来看看如何在程序中应用中断门和陷阱门吧

既然中断有两种方式触发 -- 硬件随机触发和 int n 手动触发,我们就编写两个函数,分别用来响应硬件中断和我们手动 int n 触发的中断

最方便的硬件中断就是时钟中断,因为他会以固定间隔自动触发中断

 

8254 时钟中断与 8284A 实时时钟中断的区别

在本系列第一篇文章中,我们曾经介绍过,硬件启动之初,当电源供电稳定时,会自动向级联在 CPU 上的 8284A 时钟发生器发送 PowerGood 信号,8284A 芯片会向 CPU 发送 RESET 信号

事实上,8284A 芯片作为时钟发生器,最为常用的功能就是在加电晶振的带动下产生每秒 1024 次时钟中断信号供硬件使用,因此他一般不被我们的软件所使用

而主 8259A 的 IR0 引脚上级联的 8254 时钟芯片则是通常我们所说的操作系统时钟中断的信号源,因此,在本次的程序中,我们也要使用 IRQ0 中断作为中断信号源

 

中断响应函数的编写

中断响应函数与普通的函数在编写上并没有很大的区别,他通常包含两部分逻辑:

  1. 函数处理逻辑
  2. 发送 EOI 信号表示中断处理完成并返回

 

众所周知,我们使用 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

 

 

在 IDT 中周期填充中断描述符

保护模式下,中断向量号就是中断描述符表中描述符的 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 寄存器

; 恢复 IDTR 与中断屏蔽寄存器 IMREG lidt [_SavedIDTR] mov al, [_SavedIMREG] out 21h, al

 

 

恢复 8259A 设置

; ------------------ 恢复可编程中断控制器 ---------------------- 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

 

执行我们的系统,可以看到下面的效果:

 

 

Error Code 与恢复

上一篇文章中的 CPU 预设的异常列表中,中断向量号为 8、17 的中断会在中断响应函数执行前先将 32 位的 Error Code 压入栈顶,但此后即便是中断响应函数返回,Error Code 也并不会自动出栈,这意味着你需要自己手动进行处理,如下图所示:

 

 

中断门与陷阱门的区别

到此为止,似乎看上去中断门与陷阱门并没有什么区别,但实际上二者还是有一点点细微的差别的

通过中断门注册的中断响应函数在返回时,会自动复位 eflags 寄存器的 IF 位,而陷阱门则不会改变

 

欢迎关注微信公众号,以技术为主,涉及历史、人文等多领域的学习与感悟,每周三到七篇推文,只有全部原创,只有干货没有鸡汤

 

 

lib.asm

; ------------------------------------------------------------------------ ; 显示一个字节中的数字 ; ; 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 结束---------------------------------------------------------

 

 

main.asm

; ---------------- 内存段描述符宏 ------------- ; 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]

 

 






cpu      interrupt      8086      中断      时钟      idt      idtr      描述符      门描述符      中断门     


京ICP备15018585号