第16课 - 保护模式中的特权级(中)

Posted dua677

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了第16课 - 保护模式中的特权级(中)相关的知识,希望对你有一定的参考价值。

一种新的描述符:门描述符(Gate Descriptor)

    通过门描述符在不同特权级的代码间进行跳转
    根据应用场景的不同,门描述符分为:
        调用门(Call Gates)
        中断门(Interrupt Gates)
        陷阱门(Trap Gate)
        任务门(Task Gate)

门描述符的内存结构

    每一个门描述符占用8字节内存
    不同类项门描述的内存含义不同
    技术分享图片技术分享图片

调用门描述符(Call Gates)的定义

    技术分享图片技术分享图片

调用门描述符的工作原理

    技术分享图片技术分享图片

调用门描述符的使用

    技术分享图片技术分享图片
    技术分享图片技术分享图片

汇编小贴士

    汇编语言中的跳转方式
        段内跳转:call,jmp
            参数为响度地址,函数调用时只需要保存当前偏移地址
        段间跳转:call far, jmp far
            参数为选择子和偏移地址
            函数调用时需要同时保存段基地址和偏移地址

实验结论

    门描述符是一种特殊的描述符需要注册于段描述符表
    调用门可以看作一个函数指针保存具体函数的入口地址
    通过调用门选择子对相应的函数进行远调用call far)
    可以直接使用选择子:偏移地址的方式调用其它段的函数
    使用调用门时偏移地址无意义,仅仅是语法需要

如何复用14课的PrintString函数?

    将不同代码段需要复用的函数定义到独立的段中(retf)
    计算每一个可复用函数的偏移量(FuncName - $$)
    通过段选择子:偏移地址的方式对目标函数进行远调用

小结

    门描述符是一种特殊的描述符,需要注册于段描述符表
    门描述符分为:调用门,中断门,陷阱门,任务门
    调用门可以看作一个函数指针保存具体函数的入口地址
    调用门选择子对应的函数调用方式为远调用(call far)

代码

// inc.asm
; Segment Attribute     ; 一致性是指高特权不能访问低特权,但是低特权能访问高特权,不过特权级别不变
DA_32    equ    0x4000  ; 保护模式32位段
DA_DR    equ    0x90    ; 只读数据段
DA_DRW   equ    0x92    ; 可读写数据段
DA_DRWA  equ    0x93    ; 已访问可读写数据段
DA_C     equ    0x98    ; 只执行代码段
DA_CR    equ    0x9A    ; 可执行可读代码段
DA_CCO   equ    0x9C    ; 只执行一致代码段
DA_CCOR  equ    0x9E    ; 可执行可读一致代码段
 
; Segment Privilege
DA_DPL0        equ        0x00    ; DPL = 0
DA_DPL1        equ        0x20    ; DPL = 1
DA_DPL2        equ        0x40    ; DPL = 2
DA_DPL3        equ        0x60    ; DPL = 3
 
; Special Attribute
DA_LDT        equ 0x82
DA_TaskGate    equ 0x85    ; 任务门类型值
DA_386TSS      equ 0x89    ; 可用 386 任务状态段类型值
DA_386CGate    equ 0x8C    ; 386 调用门类型值
DA_386IGate    equ 0x8E    ; 386 中断门类型值
DA_386TGate    equ 0x8F    ; 386 陷阱门类型值
 
; Selector Attribute
SA_RPL0    equ    0        ; 高特权,内核级
SA_RPL1    equ    1        ; 中高特权,服务级
SA_RPL2    equ    2        ; 中低特权,服务级
SA_RPL3    equ    3        ; 低特权,用户级
 
SA_TIG    equ    0         ; GDT
SA_TIL    equ    4         ; LDT,本质是1,因为后面还有2bit的RPL标志位
 
; 描述符
; 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 & 0xFFFF                          ; 段界限1
    dw    %1 & 0xFFFF                          ; 段基址1
    db    (%1 >> 16) & 0xFF                    ; 段基址2
    dw    ((%2 >> 8) & 0xF00) | (%3 & 0xF0FF)  ; 属性1 + 段界限2 + 属性2
    db    (%1 >> 24) & 0xFF                    ; 段基址3
%endmacro                                      ; 共8字节
 
; 门
; usage: Gate Selector, Offset, DCount, Attr
;            Selector:    dw
;            Offset:      dd
;            DCount:      db
;            Attr:        db
%macro Gate 4
    dw (%2 & 0xFFFF)                         ; 偏移地址1
    dw %1                                    ; 选择子
    dw (%3 & 0x1F) | ((%4 << 8) & 0xFF00)    ; 属性
    dw ((%2 >> 16) & 0xFFFF)                 ; 偏移地址2
%endmacro
 
 
 
// loader.asm
%include "inc.asm"
 
org 0x9000
 
jmp ENTRY_SEGMENT
 
[section .gdt]                                ; 全局描述符表,部分段基址暂未知地址需使用时再调节
; GDT definition
;                        "函数名"    段基址        段界限                    段属性
GDT_ENTRY       :        Descriptor    0,            0,                        0                ; 全局段描述符表第0项不使用
CODE32_DESC     :        Descriptor    0,            Code32SegLen - 1,        DA_C    + DA_32
VIDEO_DESC      :        Descriptor    0xB8000,      0x07FFF,                 DA_DRWA + DA_32
STACK32_DESC    :        Descriptor    0,            TopOfStack32,            DA_DRW  + DA_32
FUNCTION_DESC   :        Descriptor    0,            FunctionSegLen - 1,      DA_C + DA_32
; Gate Descriptor
; Call Gate                            选择子            偏移    参数个数    属性
FUNC_CG_ADD_DESC        Gate    FunctionSelector,    CG_Add,        0,        DA_386CGate
FUNC_CG_SUB_DESC        Gate    FunctionSelector,    CG_Sub,        0,        DA_386CGate
; GDT end
 
GdtLen    equ    $ - GDT_ENTRY
 
GdtPtr:                                        ; 全局描述符表指针
        dw    GdtLen - 1    ; 偏移,记录描述符数量
        dd    0             ; 全局描述符起始地址,先初始化为0
 
 
; GDT Selector
;                                TI:全局、局部    RPL:请求权限级别
Code32Selector       equ (0x0001 << 3) + SA_TIG + SA_RPL0    ; 0x0001==第二个选择子
VideoSelector        equ (0x0002 << 3) + SA_TIG + SA_RPL0
Stack32Selector      equ (0x0003 << 3) + SA_TIG + SA_RPL0
FunctionSelector     equ (0x0004 << 3) + SA_TIG + SA_RPL0
FuncCGAddSelector    equ (0x0005 << 3) + SA_TIG + SA_RPL0
FuncCGSubSelector    equ (0x0006 << 3) + SA_TIG + SA_RPL0
 
; end of [section .gdt]
 
TopOfStack16 equ 0x7c00
 
[section .s16]    ; 实模式代码段(16bit)
[bits 16]         ; 使用16位编译
ENTRY_SEGMENT:                                ; 16位保护模式入口段
    mov ax, cs                    ; 初始化相关寄存器
    mov ds, ax
    mov es, ax
    mov ss, ax
    mov sp, TopOfStack16
 
            ; initialize GDT for 32 bits code segment
    mov esi, CODE32_SEGMENT        ; 初始化32位保护模式32位代码段描述符
    mov edi, CODE32_DESC
 
    call InitDescItem
        
    mov esi, STACK32_SEGMENT       ; 初始化32位保护模式32位栈段描述符
    mov edi, STACK32_DESC
 
    call InitDescItem
 
    mov esi, FUNCTION_SEGMENT     ; 初始化函数段描述符
    mov edi, FUNCTION_DESC
 
    call InitDescItem
 
            ; initialize GDT pointer struct
    mov eax, 0                     ; 代码段地址左移4位
    mov ax, ds
    shl eax, 4
    add eax, GDT_ENTRY             ; 代码段偏移地址==> 左移过后的代码段+全局描述符表入口地址偏移量
    mov dword [GdtPtr + 2], eax    ; 写入全局描述符表指针
 
            ; 1. load GDT
    lgdt [GdtPtr]                  ; 加载全局描述符表
 
            ; 2. close interrupt
    cli                            ; 关闭中断
 
            ; 3. open A20
    in al, 0x92                    ; 通过0x92端口开启A20地址线开关
    or al, 00000010b
    out 0x92, al
 
            ; 4. enter protect mode
    mov eax, cr0                   ; 设置cr0寄存器,进入保护模式
    or eax, 0x01
    mov cr0, eax
 
            ; 5. jump to 32 bits code
    jmp dword Code32Selector : 0  ; 使用jmp跳转到32位代码段选择子的0偏移处
 
 
; esi    --> code segment label
; edi    --> descriptor label
InitDescItem:                                ; 初始化描述符项目
    push eax
    
    mov eax, 0                        ; 代码段地址左移4位
    mov ax, cs
    shl eax, 4                        ; 实地址=段寄存器地址左移4位+偏移地址
    add eax, esi
    mov word [edi + 2], ax            ; 将段基址写入描述符2个字节(16位寄存器),低32位的16-31bit(偏移2字节)
    shr eax, 16                       ; 移除eax实地址中已经写入段基址的2字节数据
    mov byte [edi + 4], al            ; 将段基址写入描述符1个字节(8位寄存器),高32位的0-7bit(偏移4+0=4字节)
    mov byte [edi + 7], ah            ; 将段基址写入描述符1个字节(8位寄存器),高32位的24-31bit(偏移4+3=7字节)
 
    pop eax
    
    ret
 
[section .s32]    ; 32位代码段
[bits 32]        ; 使用32位编译
CODE32_SEGMENT:                                ; 32位代码段数据
    mov ax, VideoSelector            ; 把视频段选择子放到gs全局段寄存器
    mov gs, ax
 
    mov ax, Stack32Selector
    mov ss, ax
 
    mov eax, TopOfStack32
    mov esp, eax
 
    mov ax, 2
    mov bx, 1
 
    call FuncCGAddSelector : 0        ; call FunctionSelector : CG_Add    ;选择子:偏移量
    call FuncCGSubSelector : 0        ; call FunctionSelector : CG_Sub
 
    jmp $
 
Code32SegLen    equ    $ - CODE32_SEGMENT
 
[section .func]
[bits 32]
FUNCTION_SEGMENT:
 
; ax --> a
; bx --> b
;
; return:
;        cx --> a + b
AddFunc:
    mov cx, ax
    add cx, bx
    retf
 
CG_Add equ AddFunc - $$
 
; ax --> a
; bx --> b
;
; return:
;        cx --> a - b
SubFunc:
    mov cx, ax
    sub cx, bx
    retf
 
CG_Sub equ SubFunc - $$
 
FunctionSegLen    equ $ - FUNCTION_SEGMENT
 
[section .gs]
[bits 32]
STACK32_SEGMENT:                            ; 32位栈段定义
    times 1024 * 4 db 0
 
Stack32SegLen equ $ - STACK32_SEGMENT
TopOfStack32 equ Stack32SegLen - 1
 
 
 
// loader.asm    修改复用的PrintString(loader.asm - Lesson 14)
%include "inc.asm"
 
org 0x9000
 
jmp ENTRY_SEGMENT
 
[section .gdt]                                ; 全局描述符表,部分段基址暂未知地址需使用时再调节
; GDT definition
;                        "函数名"    段基址        段界限                    段属性
GDT_ENTRY         :        Descriptor    0,            0,                        0                ; 全局段描述符表第0项不使用
CODE32_DESC       :        Descriptor    0,            Code32SegLen - 1,       DA_C    + DA_32
VIDEO_DESC        :        Descriptor    0xB8000,      0x07FFF,                DA_DRWA + DA_32
DATA32_DESC       :        Descriptor    0,            Data32SegLen - 1,       DA_DR   + DA_32
STACK32_DESC      :        Descriptor    0,            TopOfStack32,           DA_DRW  + DA_32
CODE16_DESC       :        Descriptor    0,            0xFFFF,                 DA_C
UPDATE_DESC       :        Descriptor    0,            0xFFFF,                 DA_DRW            ; 回到保护模式
TASK_A_LDT_DESC   :        Descriptor    0,            TaskALdtLen - 1,        DA_LDT            ; 全局描述符表中的局部描述符
FUNCTION_DESC     :        Descriptor    0,            FunctionSegLen - 1,     DA_C    + DA_32   ; 选择门描述符
; GDT end
 
GdtLen    equ    $ - GDT_ENTRY
 
GdtPtr:                                        ; 全局描述符表指针
        dw    GdtLen - 1    ; 偏移,记录描述符数量
        dd    0             ; 全局描述符起始地址,先初始化为0
 
 
; GDT Selector
;                            TI:全局、局部    RPL:请求权限级别
Code32Selector    equ (0x0001 << 3) + SA_TIG + SA_RPL0    ; 0x0001==第二个选择子
VideoSelector     equ (0x0002 << 3) + SA_TIG + SA_RPL0
Data32Selector    equ (0x0003 << 3) + SA_TIG + SA_RPL0
Stack32Selector   equ (0x0004 << 3) + SA_TIG + SA_RPL0
Code16Selector    equ (0x0005 << 3) + SA_TIG + SA_RPL0
UpdateSelector    equ (0x0006 << 3) + SA_TIG + SA_RPL0    ; 回到保护模式
TaskALdtSelector  equ (0x0007 << 3) + SA_TIG + SA_RPL0    ; 局部描述符表选择子A
FunctionSelector  equ (0x0008 << 3) + SA_TIG + SA_RPL0    ; 选择门
; end of [section .gdt]
 
TopOfStack16 equ 0x7c00
 
[section .dat]                                ; 32位数据段
[bits 32]
DATA32_SEGMENT:
    DTOS                db "D.T.OS!", 0    ; 注意添加字符串结束标记0
    DTOS_OFFSET            equ DTOS - $$
    HELLO_WORLD            db "Hello World!", 0
    HELLO_WORLD_OFFSET    equ HELLO_WORLD - $$
 
Data32SegLen equ $ - DATA32_SEGMENT
 
[section .s16]    ; 实模式代码段(16bit)
[bits 16]         ; 使用16位编译
ENTRY_SEGMENT:                                ; 16位保护模式入口段
    mov ax, cs                    ; 初始化相关寄存器
    mov ds, ax
    mov es, ax
    mov ss, ax
    mov sp, TopOfStack16
 
    mov [BACK_TO_REAL_MODE + 3], ax
 
            ; initialize GDT for 32 bits code segment
    mov esi, CODE32_SEGMENT        ; 初始化32位保护模式32位代码段描述符
    mov edi, CODE32_DESC
 
    call InitDescItem
        
    mov esi, DATA32_SEGMENT        ; 初始化32位保护模式32位数据段描述符
    mov edi, DATA32_DESC
 
    call InitDescItem
 
    mov esi, STACK32_SEGMENT       ; 初始化32位保护模式32位栈段描述符
    mov edi, STACK32_DESC
 
    call InitDescItem
 
    mov esi, CODE16_SEGMENT        ; 初始化32位保护模式16位代码段描述符
    mov edi, CODE16_DESC
 
    call InitDescItem
 
    mov esi, TASK_A_LDT_ENTRY      ; 初始化局部描述符表
    mov edi, TASK_A_LDT_DESC
 
    call InitDescItem
 
    mov esi, TASK_A_CODE32_SEGMENT
    mov edi, TASK_A_CODE32_DESC
 
    call InitDescItem
 
    mov esi, TASK_A_DATA32_SEGMENT
    mov edi, TASK_A_DATA32_DESC
 
    call InitDescItem
 
    mov esi, TASK_A_STACK32_SEGMENT
    mov edi, TASK_A_STACK32_DESC
 
    call InitDescItem
 
    mov esi, FUNCTION_SEGMENT
    mov edi, FUNCTION_DESC
 
    call InitDescItem
 
            ; initialize GDT pointer struct
    mov eax, 0                    ; 代码段地址左移4位
    mov ax, ds
    shl eax, 4
    add eax, GDT_ENTRY             ; 代码段偏移地址==> 左移过后的代码段+全局描述符表入口地址偏移量
    mov dword [GdtPtr + 2], eax    ; 写入全局描述符表指针
 
            ; 1. load GDT
    lgdt [GdtPtr]                 ; 加载全局描述符表
 
            ; 2. close interrupt
    cli                            ; 关闭中断
 
            ; 3. open A20
    in al, 0x92                    ; 通过0x92端口开启A20地址线开关
    or al, 00000010b
    out 0x92, al
 
            ; 4. enter protect mode
    mov eax, cr0                   ; 设置cr0寄存器,进入保护模式
    or eax, 0x01
    mov cr0, eax
 
            ; 5. jump to 32 bits code
    jmp dword Code32Selector : 0  ; 使用jmp跳转到32位代码段选择子的0偏移处
 
BACK_ENTRY_SEGMENT:               ; 返回入口段(保护模式返回实模式)
    mov ax, cs
    mov ds, ax
    mov es, ax
    mov ss, ax
    mov sp, TopOfStack16          ; 设置16位栈顶指针
 
    in al, 0x92                                ; 关闭A20地址线
    and al, 11111101b
    out 0x92, al
 
    sti                                        ; 开启中断
 
    mov bp, HELLO_WORLD                        ; 使用实模式打印提示语句,注意bp指向的是字符串而不是段起始地址
    mov cx, 12                    ; 字符串长度
    mov dx, 0                     ; 打印于第0行
    mov ax, 0x1301                ; 在电传打字机模式输出,字符串只含字符,启用BL属性
    mov bx, 0x0007                ; 打印第0页,输出白色前景色
    int 0x10
 
    jmp $
 
; esi    --> code segment label
; edi    --> descriptor label
InitDescItem:                                ; 初始化描述符项目
    push eax
    
    mov eax, 0                        ; 代码段地址左移4位
    mov ax, cs
    shl eax, 4                        ; 实地址=段寄存器地址左移4位+偏移地址
    add eax, esi
    mov word [edi + 2], ax            ; 将段基址写入描述符2个字节(16位寄存器),低32位的16-31bit(偏移2字节)
    shr eax, 16                       ; 移除eax实地址中已经写入段基址的2字节数据
    mov byte [edi + 4], al            ; 将段基址写入描述符1个字节(8位寄存器),高32位的0-7bit(偏移4+0=4字节)
    mov byte [edi + 7], ah            ; 将段基址写入描述符1个字节(8位寄存器),高32位的24-31bit(偏移4+3=7字节)
 
    pop eax
    
    ret
 
 
[section .s16]
[bits 16]
CODE16_SEGMENT:                                ; 保护模式返回实模式
    mov ax, UpdateSelector            ; 注意不要操作CS寄存器,因为当前还是保护模式
    mov ds, ax
    mov es, ax
    mov fs, ax
    mov gs, ax
    mov ss, ax
 
    mov eax, cr0                      ; 进入保护模式
    and al, 11111110b
    mov cr0, eax
 
BACK_TO_REAL_MODE:
    jmp 0 : BACK_ENTRY_SEGMENT        ; 注意使用段基地址 + 偏移量的方式
 
Code16SegLen equ $ - CODE16_SEGMENT
 
[section .func]
[bits 32]
FUNCTION_SEGMENT:
 
; ds:ebp    --> string address
; bx        --> attribute
; dx        --> dh : row, dl : col
PrintStringFunc:                            ; 打印字符串函数
    push ebp
    push eax
    push edi
    push cx
    push dx
 
print:
    mov cl, [ds:ebp]                  ; cl记录要打印的字符
    cmp cl, 0                         ; 对比字符串结束符号
    je end
    mov eax, 80                       ; 每行字符数
    mul dh                            ; 乘以行数
    add al, dl                        ; 加上列数,最终计算出要显示的位置
    shl eax, 1                        ; 左移乘以2,计算字节偏移
    mov edi, eax                      ; 写入显示的偏移地址到edi
    mov ah, bl                        ; 字符属性写入高位
    mov al, cl                        ; 字符写入低位
    mov [gs:edi], ax                  ; 写入显存对应地址
    inc ebp                           ; 指向下一个字符
    inc dl                            ; 指向屏幕的下一列
    jmp print
 
end:
    pop dx
    pop cx
    pop edi
    pop eax
    pop ebp
 
    retf
 
PrintString        equ        PrintStringFunc - $$
 
FunctionSegLen    equ        $ - FUNCTION_SEGMENT
 
[section .s32]    ; 32位代码段
[bits 32]        ; 使用32位编译
CODE32_SEGMENT:                                ; 32位代码段数据
    mov ax, VideoSelector            ; 把视频段选择子放到gs全局段寄存器
    mov gs, ax
 
    mov ax, Stack32Selector
    mov ss, ax
 
    mov eax, TopOfStack32
    mov esp, eax
 
    mov ax, Data32Selector
    mov ds, ax
 
    mov ebp, DTOS_OFFSET              ; 在保护模式输出字符串,地址为段偏移
    mov bx, 0x0C                      ; 黑底淡红字
    mov dh, 12                        ; 指定行地址,注意行列都是从0开始
    mov dl, 33                        ; 指定列地址
 
    call FunctionSelector : PrintString
 
    mov ebp, HELLO_WORLD_OFFSET       ; 打印另一个字符串
    mov bx, 0x0C
    mov dh, 13
    mov dl, 31
 
    call FunctionSelector : PrintString
 
    mov ax, TaskALdtSelector           ; 使用局部描述符表
    
    lldt ax
 
    jmp TaskACode32Selector : 0
 
    ; jmp Code16Selector : 0             ; 返回到16位实模式
 
Code32SegLen    equ    $ - CODE32_SEGMENT
 
[section .gs]
[bits 32]
STACK32_SEGMENT:                          ; 32位栈段定义
    times 1024 * 4 db 0
 
Stack32SegLen equ $ - STACK32_SEGMENT
TopOfStack32 equ Stack32SegLen - 1
 
 
; =======================================
;
;            Task A code Segment
;
; =======================================
 
[section .task-a-ldt]
; Task A LDT definition
;                                段基址,            段界限,                段属性
TASK_A_LDT_ENTRY:
TASK_A_CODE32_DESC        :    Descriptor    0,        TaskACode32SegLen - 1,        DA_C   + DA_32
TASK_A_DATA32_DESC        :    Descriptor    0,        TaskAData32SegLen - 1,        DA_DR  + DA_32
TASK_A_STACK32_DESC       :    Descriptor    0,        TaskAStack32SegLen - 1,       DA_DRW + DA_32
 
TaskALdtLen    equ $ - TASK_A_LDT_ENTRY
 
; Task A LDT Selector
TaskACode32Selector     equ (0x0000 << 3) + SA_TIL + SA_RPL0
TaskAData32Selector     equ (0x0001 << 3) + SA_TIL + SA_RPL0
TaskAStack32Selector    equ (0x0002 << 3) + SA_TIL + SA_RPL0
 
[section .task-a-dat]
[bits 32]
TASK_A_DATA32_SEGMENT:
    TASK_A_STRING            db "This is Task A!", 0
    TASK_A_STRING_OFFSET    equ TASK_A_STRING - $$
 
TaskAData32SegLen equ $ - TASK_A_DATA32_SEGMENT
 
[section .task-a-gs]
[bits 32]
TASK_A_STACK32_SEGMENT:
    times 1024 db 0
 
TaskAStack32SegLen equ $ - TASK_A_STACK32_SEGMENT
TaskATopOfStack32 equ TaskAStack32SegLen - 1
 
[section .task-a-s32]
[bits 32]
TASK_A_CODE32_SEGMENT:
    mov ax, VideoSelector
    mov gs, ax
    
    mov ax, TaskAStack32Selector
    mov ss, ax
 
    mov eax, TaskATopOfStack32
    mov esp, eax
 
    mov ax, TaskAData32Selector
    mov ds, ax
 
    mov ebp, TASK_A_STRING_OFFSET
    mov bx, 0x0C
    mov dh, 14
    mov dl, 29
 
    call FunctionSelector : PrintString
 
    jmp Code16Selector : 0                        ; 返回16位保护模式
; 删除原有TaskAPrintString函数
TaskACode32SegLen    equ    $ - TASK_A_CODE32_SEGMENT
 

输出

    ndisasm -o 0x9000 loader > loader.txt反汇编查看两个函数的调用情况,可从16位实模式跳转到32位保护模式的地方开始(包含jmp指令),下图是采用调用门调用两个函数的结果
    技术分享图片
 
    采用段选择子:偏移地址的方式调用PrintString函数
    技术分享图片技术分享图片

以上是关于第16课 - 保护模式中的特权级(中)的主要内容,如果未能解决你的问题,请参考以下文章

操作系统-保护模式中的特权级下

第十七课 保护模式中的特权级(下)

任务和特权级保护——《x86汇编语言:从实模式到保护模式》读书笔记32

保护模式特权级概述

第12课 - 实模式到保护模式(下)

《80X86汇编语言程序设计教程》十九 操作系统类指令与输入输出保护