前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >保护模式下的中断和异常(下) -- 软件实战篇

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

作者头像
用户3147702
发布2022-06-27 14:43:28
1.1K0
发布2022-06-27 14:43:28
举报
文章被收录于专栏:小脑斧科技博客

1. 引言

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

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

2. 门描述符

提到“门”,你一定已经不陌生了,因为在此前的文章中,我们对门描述符已经有过一定的介绍: 利用调用门实现特权级间跳转(上) — 原理篇

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

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

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

3. 中断描述符表与中断门和陷阱门

此前我们介绍过用于内存分段的全局描述符表 GDT 与局部描述符表 LDT,保护模式下还有另一个描述符表 — IDT。 GDT、LDT 中存储的是代码段描述符、数据段描述符、调用门描述符,而 IDT 中则存储的是中断门描述符、陷阱门描述符以及任务门描述符,而存储了中断门和陷阱门的 IDT 所充当的就是实地址模式下的中断向量表。 下图展示了中断门、陷阱门、任务门的存储结构:

这里我们先不介绍任务门,重点关注中断门与陷阱门的结构,可以看到,除了类型位中断门与陷阱门各自的取值不同,其他字段上两者的结构是完全相同的,而与先前的调用门结构也是相同的。 门描述符各个位的含义先前也进行过介绍了,具体可以参看图中的解释或回看之前的文章。 那么,在中断描述符表中,如何实现中断向量号与中断描述符的对应关系呢?答案很简单,中断描述符表中,中断描述符的 index 就是中断向量号,参考上篇文章中中断、陷阱、异常与中断向量号的对应关系,IDT 中第一个表项就是 0 号向量号的 DIV 除 0 错误、第4个表项,也就是 3 号描述符就是调试断点向量,从 32 ~ 255 则是用户可以自定义的中断,我们可以通过 int n 触发相应的中断。

4. 实战中断门与陷阱门 — 编写中断响应函数

接下来,我们就通过实际的代码,来看看如何在程序中应用中断门和陷阱门吧。 既然中断有两种方式触发 — 硬件随机触发和 int n 手动触发,我们就编写两个函数,分别用来响应硬件中断和我们手动 int n 触发的中断。 最方便的硬件中断就是时钟中断,因为他会以固定间隔自动触发中断。

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

在本系列第一篇文章中,我们曾经介绍过,硬件启动之初,当电源供电稳定时,会自动向级联在 CPU 上的 8284A 时钟发生器发送 PowerGood 信号,8284A 芯片会向 CPU 发送 RESET 信号。 事实上,8284A 芯片作为时钟发生器,最为常用的功能就是在加电晶振的带动下产生每秒 1024 次时钟中断信号供硬件使用,因此他一般不被我们的软件所使用。 而主 8259A 的 IR0 引脚上级联的 8254 时钟芯片则是通常我们所说的操作系统时钟中断的信号源,因此,在本次的程序中,我们也要使用 IRQ0 中断作为中断信号源。

4.2. 中断响应函数的编写

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

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

众所周知,我们使用 ret 指令完成一个函数的调用并跳转回函数调用位置继续执行,与此类似,中断响应函数则通过 iretiretd 两个指令来实现中断处理完成后的跳转工作,他们分别应用于 16 位系统与 32 位系统中。

4.3. 手动触发的中断响应函数

我们只要在手动触发的中断响应函数中实现一个字符串的显示,就可以证明中断正常的被触发:

代码语言:javascript
复制
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,他用来记录当前的段偏移,从而稍后用来初始化我们的中断门描述符。

4.4. 自动触发的时钟中断响应函数

既然是时钟中断,我们当然要做一些周期触发的事情,最简单的就是我们通过周期性的切换两个字符串展示,来模拟字符串的闪烁功能:

代码语言:javascript
复制
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

5. 创建 IDT

下面我们就来创建中断描述符表,并添加中断描述符。

5.1. 中断描述符宏

和代码段数据段描述符一样,我们通过一个宏来实现描述符的定义:

代码语言:javascript
复制
; ---------------- 门描述符宏 -------------  
; 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

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

保护模式下,中断向量号就是中断描述符表中描述符的 index,因此我们要定义我们指定向量号的中断门,就需要跳过前面 N 个描述符。 你可以通过复制粘贴的方式生成大量的代码来实现,但 NASM 提供了更为简单的预处理方式 — %rep。 下面的代码实现了 inc&nbsp;word&nbsp;[table&nbsp;+&nbsp;2*i] 语句的 100 次循环添加:

代码语言:javascript
复制
%assign i 0  
%rep 100  
    inc word [table+2*i]  
    %assign i i+1  
%endrep

这样我们就可以非常简单的定义 IDT 中的所有中断门了:

代码语言:javascript
复制
; -------------------- 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 设置为对应的中断响应函数。

6. 中段描述符表的加载

正如 CPU 中存在一个 gdtr 寄存器用来存储 gdt 的首地址,ldtr 寄存器用来存储 ldt 的首地址,CPU 中也同样存在一个 idtr 寄存器,用来存储 idt 的首地址。 而指令 lidt 则用来加载 idt 首地址到 idtr 寄存器中。 同时,为了跳转回实地址模式后,能够还原 idtr 寄存器的值,以及中断屏蔽信息,我们还需要先将这些信息进行保存:

代码语言:javascript
复制
_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]

7. 初始化 8259A 中断控制器

上一篇文章中,我们已经详细介绍了可编程中断控制器 8259A 的初始化和使用方法,这里我们就可以直接使用了:

代码语言:javascript
复制
; ------------------ 初始化可编程中断控制器 ----------------------  
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,并且屏蔽了除此之外的其他中断。

8. 触发中断

手动触发中断很简单:

代码语言:javascript
复制
int 80h

但如果想要让可屏蔽中断能够触发,光是通过 OCW1 设置屏蔽信息还不够,还需要将 eflags 寄存器的中断开关打开,因此我们需要执行:

代码语言:javascript
复制
sti

接下来,我们只要等待程序触发就可以了。

9. 跳转回实地址模式

当我们的程序运行完成,我们当然是希望他能够正常的返回到实地址模式下了,因此我们有几件事要做。

9.1. 等待时钟中断的周期性触发

我们要观察时钟中断的效果,就要等待一段时间,上面的文章中,为了能够实现周期变换,我们定义了一个触发次数字段 clockNum,他正好可以作为等待的循环依据:

代码语言:javascript
复制
wait_loop:  
    cmp dword [clockNum], 4096  
    jb wait_loop   
    cli

9.2. 恢复 IDTR 寄存器与 IMREG 寄存器

代码语言:javascript
复制
; 恢复 IDTR 与中断屏蔽寄存器 IMREG  
lidt    [_SavedIDTR]  
mov    al, [_SavedIMREG]  
out    21h, al

9.3. 恢复 8259A 设置

代码语言:javascript
复制
; ------------------ 恢复可编程中断控制器 ----------------------  
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

10. 程序运行效果

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

11. 本文未涉及的要点

11.1. Error Code 与恢复

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

11.2. 中断门与陷阱门的区别

到此为止,似乎看上去中断门与陷阱门并没有什么区别,但实际上二者还是有一点点细微的差别的。 通过中断门注册的中断响应函数在返回时,会自动复位 eflags 寄存器的 IF 位,而陷阱门则不会改变。

12. 附录 1 — 系列历史文章

13.1. 准备工作

计算机是如何启动的?如何制作自己的操作系统 如何调试操作系统

13.2. 保护模式

操作系统的内存管理 — 分段与分页、虚拟地址、逻辑地址、线性地址、物理地址

13.2.1. 分段

详解 32 位保护模式与内存分段机制 进军保护模式 保护模式进阶 — 再回实模式 实战局部描述符表 LDT 利用调用门实现特权级间跳转(上) — 原理篇 利用调用门实现特权级间跳转(下) — 实战篇

13.2.2. 分页

详解操作系统分页机制与实战 实战分页机制实现 — 通过实际内存大小动态调整页表个数

13.2.3. 中断与异常

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

14. 附录 2 — 完整代码

14.1. lib.asm

代码语言:javascript
复制
; ------------------------------------------------------------------------  
; 显示一个字节中的数字  
;  
; 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 结束---------------------------------------------------------

14.2. main.asm

代码语言:javascript
复制
; ---------------- 内存段描述符宏 -------------  
; 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]
本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2020-06-27,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 小脑斧科技博客 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 1. 引言
  • 2. 门描述符
  • 3. 中断描述符表与中断门和陷阱门
  • 4. 实战中断门与陷阱门 — 编写中断响应函数
    • 4.1. 8254 时钟中断与 8284A 实时时钟中断的区别
      • 4.2. 中断响应函数的编写
        • 4.3. 手动触发的中断响应函数
          • 4.4. 自动触发的时钟中断响应函数
          • 5. 创建 IDT
            • 5.1. 中断描述符宏
              • 5.2. 在 IDT 中周期填充中断描述符
              • 6. 中段描述符表的加载
              • 7. 初始化 8259A 中断控制器
              • 8. 触发中断
              • 9. 跳转回实地址模式
                • 9.1. 等待时钟中断的周期性触发
                  • 9.2. 恢复 IDTR 寄存器与 IMREG 寄存器
                    • 9.3. 恢复 8259A 设置
                    • 10. 程序运行效果
                    • 11. 本文未涉及的要点
                      • 11.1. Error Code 与恢复
                        • 11.2. 中断门与陷阱门的区别
                        • 12. 附录 1 — 系列历史文章
                          • 13.1. 准备工作
                            • 13.2. 保护模式
                              • 13.2.1. 分段
                              • 13.2.2. 分页
                              • 13.2.3. 中断与异常
                          • 14. 附录 2 — 完整代码
                            • 14.1. lib.asm
                              • 14.2. main.asm
                              领券
                              问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档