进入保护模式

简介: 进入保护模式

引言

  • 【保护模式】中,我们可以总结出,实现保护模式需要一下几个部分:
  • 段描述符
  • 段描述符表
  • 段选择符
  • 进入保护模式
  • 接下来我们一一实现上面几个部分

段描述符的实现

  • 每个段描述符的长度是 8 个字节,含有 3 个主要字段:段基地址、段界限、段属性
  • 下面用汇编来定义一个段描述符数据结构
%macro Descriptor 3     ; 有三个参数:段基址、段界限、段属性
  dw  %2 & 0xFFFF       ; 段界限 1  (2 字节)
  dw  %1 & 0xFFFF       ; 段基址 1  (2 字节)
  db  (%1 >> 16) & 0xFF ; 段基址 2  (1 字节)
  dw  ((%2 >> 8) & 0xF00) | (%3 & 0xF0FF) ; 段属性 1 + 段界限 2 + 段属性 2 (2 字节)
  db  (%1 >> 24) & 0xFF ; 段基址 3 (1 字节)
%endmacro               ; 共 8 个字节
  • 使用 Descriptor 宏来定义一个连续 8 个字节的数据块
  • 这个宏传入 3 个参数:段基址、段界限、段属性
  • 第一个参数:段基址,32位,表示物理地址
  • 第二个参数:段界限,20位,表示段的总长度 这里并不是地址,而是段的字节长度
  • 第三个参数:段属性,12位, 系统、门、数据等属性
  • dw %2 & 0xFFFF ; 将第二个参数(段界限)的 bit0-bit15 位放入数据块的第 1、2 个字节
  • dw %1 & 0xFFFF ; 将第一个参数(段基址)的 bit0-bit15 放入数据块的第 3、4 个字节
  • db (%1 >> 16) & 0xFF ; 将第一个参数(段基址)的 bit16-bit23 放入数据块的第 5 个字节
  • dw ((%2 >> 8) & 0xF00) | (%3 & 0xF0FF) ; 将第三个参数(段属性)的 bit8-bit11 清零,然后再位或上第二个参数(段界限)的 bit16-bit19 ,然后放入数据块的第 6、7 个字节
  • db (%1 >> 24) & 0xFF ; 将第一个参数(段基址)的 bit24-bit31 放入数据块的第 8 个字节
  • 使用 Descriptor 宏定义一个段描述符(实例化):DES : Descriptor Addr, Limit, Attr
  • 段描述符中段属性也是比较复杂的,我们也是可以通过宏定义的方式来定义好各种属性,方便使用
DA_DR    equ  0x90  ; 数据,只读
DA_DRA   equ  0x91  ; 数据,只读,已访问
DA_DRW   equ  0x92  ; 数据,可读/写
; ...
DA_CX    equ  0x98  ; 代码,仅执行
DA_CA    equ  0x99  ; 代码,仅执行,已访问
; ...

段描述符表的实现

  • 有了一个段描述符的定义方法,那么如何来实现段描述符表的定义呢?猜测一下,连续定义多个段描述符不就构成了一个段描述符表了嘛,机智如我
DES0  : Descriptor Addr0, Limit0, Attr0
DES1  : Descriptor Addr1, Limit1, Attr1
DES2  : Descriptor Addr2, Limit2, Attr2 
; ...
DES_LEN   equ   $ - DES0  ; DES 长度 = 当前地址 - DES0 地址
  • 使用数组时不光需要知道数组首地址,还需要知道数组长度,DES_LEN 就相当于数组长度

段选择符的实现

  • 根据段选择符的格式,定义其属性,方便使用
; RPL
SA_RPL0   equ   0
SA_RPL1   equ   1
SA_RPL1   equ   2
SA_RPL1   equ   3
; TI
SA_TIG    equ   0   ; GDT
SA_TIG    equ   4   ; LDT

进入保护模式的实现

  • 在实现进入保护模式代码之前,还有一些知识点需要注意
  • 代码中必须明确指明是 16 位代码段还是 32 位代码段
  • 同样一句汇编代码,编译成 16 位机器码和编译成 32 位机器码是不一样的。别问为啥不一样,它就是不一样,设计 CPU 的人没办法弄成一样。
  • 实模式是 16 位的,而保护模式是 32 位的,这就带来问题了,编译生成的机器码怎么区分 16 还是 32 位的呢?
  • 你会说,管它干嘛?编译器会帮我们做的,然而,实际上是编译器它就没那么智能,无法自己区分。
  • 为此,编译器提供了伪指令 bits ,由程序员显性的告知编译器
  • [bits 16] 是告诉编译器,下面的代码帮我编译成 16 位的机器码
  • [bits 32] 是告诉编译器,下面的代码帮我编译成 32 位的机器码
  • 段描述符表中的第 0 个描述符不适用(仅用于占位)
  • 必须使用 jmp 指令从 16 位代码段跳转到 32 位代码段
  • 进入保护模式步骤
  • 定义描述符表:上面已经说明,这里不再描述
  • 打开 A20 地址线:实模式下只使用 20 条地址线,如今我们是在保护模式下,我们 需要突破第 20 条地址线去访问更大的内存空间。方式极其简单,将端口 0x92 的第 1 位置 1 就可以了
in al,0x92 
or al,0000_0010B 
out 0x92,al
  • 加载描述符表
  • 前面说过我们通过一个特殊寄存器来指向描述符表,这个特殊寄存器便是 GDTR ,关于 GDTR 可以不做过多的了解,你只知道软件是通过 lgdt 指令操作 GDTR 就可以了。
  • 其格式是 lgdt [6 个字节的内存数据首地址]
  • 这 6 个字节内存数据划分为两部分,其中前 16 位是 GDT 以字节为单位的界限值,所以这 16 位相当于 GDT 的字节大小减 1。后 32 位是 GDT 的起始地址。
  • 我们只需要先定义 48 位的数据
GDT_PTR :
  dw   GDT_LEN - 1
  dd   GDT_BASE
  • 然后再使用 lgdt 就可以了
lgdt [GDT_PTR]
  • 通知 CPU 进入保护模式:就是将 CR0 寄存器的 PE(bit0) 位置 1
mov eax, cr0
or eax, 0x01 
mov cr0, eax
  • OK,最终实现的代码 loader.asm
  • 补充知识点一:为什么不直接使用标签定义描述符中的段基址?即全局描述符表定义中 CODE32_DESC 定义时段基址为什么是 0 ,而不是 CODE32_START 标签
  • NASM 将汇编文件当成一个独立的代码段编译
  • 汇编代码中的标签代表的是段内偏移地址,并不是真实的物理地址
  • 而描述符中的段基址需要的是物理地址,物理地址 = cs*16+偏移地址
  • 补充知识点二:“jmp dword CODE32_SELECTOR:0” 理解
  • 虽然到了保护模式,但访问内存还是要用 “段基址:段内偏移 地址”的形式。实模式下段寄存器中放的是偏移地址,而现在已经是选择子。段基址在段描述符中,用给出的选择子索引到描述符后,CPU 自动从段描述符中取出段基址,这样再加上段内偏移地址,便凑成了 “段基址:段内偏移地址” 的形式。
  • 偏移地址是 0 ,32 位的代码段,所以直接跳到 32 位代码段的第一条指令 mov eax, 0x09 执行
  • 流水线技术
  • 处理器为了提高效率将当前指令和后续指令预取到流水线
  • 因此可能同时出现预取的指令中既有 16 位指令,又有 32 位指令
  • 为了避免 32 位代码当 16 位执行,需要在出现第一条 32 位指令时就强制刷新流水线,保证流水线中全都是 32 位指令
  • 无条件跳转 jmp 能强制刷新流水线

如何验证

  • 由于代码还在 16 位模式下打印字符 “Loader...”,跳转到 32 位保护模式后 print 打印函数也不能使用了,那该如何验证是否成功进入 32 位保护模式了呢
  • 可以通过 bochs 断点调试的方法看看程序有没有跳转到 32 位代码中执行
  • 首先进行反汇编指令,生成 loader.txt

ndisasm -o 0x900 loader.bin  > loader.txt

  • 从 “loader.txt” 文件中,找到 “jmp dword CODE32_SELECTOR:0” 这条汇编代码对于的地址是 0x974 ,在 0x974 地址处打个断点,运行到断点处后,单步调试查看到在循环执行 32 位代码,这说明我们的代码是 OK 的
<bochs:1> b 0x974
<bochs:2> info b
Num Type           Disp Enb Address
  1 pbreakpoint    keep y   0x00000974
<bochs:3> c
(0) Breakpoint 1, 0x00000974 in ?? ()
Next at t=16762500
(0) [0x00000974] 0000:00000974 (unk. ctxt): jmp far 0008:00000000     ; 66ea000000000800
<bochs:4> s
Next at t=16762501
(0) [0x0000098d] 0008:00000000 (unk. ctxt): mov eax, 0x00000009       ; b809000000
<bochs:5> s
Next at t=16762502
(0) [0x00000992] 0008:00000005 (unk. ctxt): jmp .-7 (0x0000098d)      ; ebf9
<bochs:6> s
Next at t=16762503
(0) [0x0000098d] 0008:00000000 (unk. ctxt): mov eax, 0x00000009       ; b809000000
<bochs:7> s
Next at t=16762504
(0) [0x00000992] 0008:00000005 (unk. ctxt): jmp .-7 (0x0000098d)      ; ebf9

实战

  • 实战内容:保护模式下的显存操作
  • 前面在 完善MBR 中在做过实验,通过显存的方式来打印字符,只不过那个是实模式下的,我们现在来做个保护模式下的显存操作
  • 接下来先显示一个字符 'P'
  • 准备工作就两点:
  • 显存段描述符:VIDEO_DESC
  • 显存段选择符:VIDEO_SELECTOR
; 改动处 1
VIDEO_DESC   : Descriptor     0xB8000,      0xBFFFF - 0xB8000,      DA_DRWA + DA_32
; 改动处 2
VIDEO_SELECTOR     equ   (0x0002 << 3) + SA_RPL0 + SA_TIG
; 改动处 3
mov ax, VIDEO_SELECTOR
mov gs, ax
mov al, 'P'
mov ah, 0x0F
mov [gs:320], ax    ; 320 = (80*2 +0)*2 第三行第一列
                    ; 每行最多显示 80 个字符,
                    ; 每个字符显示都需要两个字节
  • 我们的任务结束了吗?肯定不会那么简单啦,实战当然要有一丢丢深度吧
  • 首先我们就要来实现一个 print_str_32 函数,用来打印指定内存中的字符串
  • 既然是函数,那么就需要设置栈顶用于函数的入栈和出栈

mov esp, 0x900

  • 其实不设置也没关系,因为 16 位实模式下设置过 sp 寄存器了,进入保护模式后 esp 的值依是 sp 中的值
  • 我们又可以定义一个数据段用来存放要打印的数据。
  • 同上,我们需要先定义一个数据段,然后设置数据段描述符和段选择符
; 定义一个数据段
DATA_SEGMENT:
  msg2 db "Enter protection", 0               ; 以 0 为字符串结束标志
  msg2Offset equ  msg2 - DATA_SEGMENT         ; msg2 在数据段 DATA_SEGMENT 中的偏移量
DATA_SEG_LEN equ    $ - DATA_SEGMENT
; 数据段描述符
DATA_DESC : Descriptor  0,  DATA_SEG_LEN - 1, DA_DR + DA_32
; 数据段选择符
DATA_SELECTOR   equ   (0x0004 << 3) + SA_RPL0 + SA_TIG
; 初始化数据段描述符中的段基址
mov esi, DATA_SEGMENT
mov edi, DATA_DESC
call InitDescItem
  • DATA_DESC 这个描述符在定义的时候跟 CODE32_DESC 一样,他们的段基址在都为 0 ,后面需要再做赋值操作,重复的操作我们当然要封装成函数了
  • 源代码:
mov eax, 0
mov ax, cs
shl eax, 4
add eax, CODE32_START
mov word [CODE32_DESC + 2], ax
shr eax, 16
mov byte [CODE32_DESC + 4], al
mov byte [CODE32_DESC + 7], ah
• CODE32_START 和 CODE32_DESC 可以看成函数参数。用 esi 替代 CODE32_START ,用 edi 替代 CODE32_DESC ,封装后:
; 参数: esi --> 代码段标签
; 参数: edi --> 段描述符标签
InitDescItem:
    push eax
    mov eax, 0
    mov ax, cs
    shl eax, 4
    add eax, esi
    mov word [edi + 2], ax
    shr eax, 16
    mov byte [edi + 4], al
    mov byte [edi + 7], ah
    pop eax
    ret
  • 封装好了 InitDescItem 函数后,初始化段描述符中段基址操作如下:
mov esi, CODE32_START
mov edi, CODE32_DESC
call InitDescItem
mov esi, DATA_SEGMENT
mov edi, DATA_DESC
call InitDescItem
  • 最后一点点了,32 位模式下,我们再封装一个使用显存方式打印字符串函数(16 位的打印函数不能在 32 位中使用了)
  • 其原理就是将要打印的数据一个一个的搬到显存中,遇到 0 则终止搬运
; 使用显存方式打印字符串
; ds:ebp --> 打印的数据起始地址(相对于段基址的偏移地址)
; bl     --> 打印属性
; dx     --> 打印起始坐标 (dl, dh)
print_str_32:
    push ebp
    push eax
    push edi
    push cx
    push dx
  ; 循环  
  s:
    mov cl, [ds:ebp]    ; 取地址 ds:ebp 中的数据存到 cl 寄存器
    cmp cl, 0           ; 比较 cl 是否为 0 
    je end              ; 若为 0 ,就结束打印
    ; 根据坐标 (dl, dh) 计算出偏移量,存入 eax 中
    ; (80 * dh + dl)*2 ; 每行最多显示 80 个字符
    mov eax, 80
    mul dh
    add al, dl
    shl eax, 1          ; eax = eax*2
    mov edi, eax        ; edi :显存中的偏移量
    mov ah, bl          ; 显示属性
    mov al, cl          ; 要打印的字符
    mov [gs:edi], ax    ; 显示数据放入显存
    inc ebp             ; 自增
    inc dl              ; 自增
    jmp s               ; 循环
; 打印结束
end:
    pop dx
    pop cx
    pop edi
    pop eax
    pop ebp
    ret
  • 至此,32 位保护模式下打印 "Enter protection" 程序:loader.asm
  • 让我们看一下最终效果吧

  • 为了进一步理解 32 位保护模式下的段,我们给保护模式下专门设置一个栈段
  • 同上,我们需要先定义一个栈段,然后设置栈段描述符和段选择符
; 定义一个栈段
STACK32_SEGMENT:
  times 1024 * 4 db 0   ; 开辟 4K 的内存空间当做栈                  
STACK32_SEG_LEN equ    $ - STACK32_SEGMENT
TOP_OF_STACK32  equ    STACK32_SEG_LEN - 1  ; 栈顶 
; 栈段描述符
STACK32_DESC : Descriptor 0, TOP_OF_STACK32, DA_DRW + DA_32
; 栈段选择符
STACK32_SELECTOR equ (0x0004 << 3) + SA_RPL0 + SA_TIG
; 初始化栈段描述符中的段基址
mov esi, STACK32_SEGMENT
mov edi, STACK32_DESC
call InitDescItem
  • 对于栈,初始化时还需要设置 ss 和 esp 寄存器
mov ax, STACK32_SELECTOR
mov ss, ax
mov eax, TOP_OF_STACK32
mov esp, eax
  • 嗯~,这个没啥现象!就不验证了。最终程序:loader.asm
  • 注意了,原先我们在将 loader.bin 写入硬盘 a.img 的时候只写了一个扇区,由于给栈段分配了 4K 的内存,那么 loader.bin 的大小已经超过 1 个扇区大小(512 字节)。我们的 Makefile 和 boot.asm 文件中扇区数相关处也需要改动
# 将 loader.bin 写入硬盘 a.img 的第 2 个扇区开始的连续 20 个扇区(10K)
dd if=$(LOADER_BIN) of=$(IMG) bs=512 count=20 seek=2 conv=notrunc
mov eax, 0x02
mov bx, 0x900
; mov cx, 0x01
mov cx, 20
call rd_disk_to_mem
目录
相关文章
|
3月前
|
Linux
保护模式中的特权级
保护模式中的特权级
46 0
|
3月前
|
存储 程序员 数据安全/隐私保护
保护模式
保护模式
92 0
|
3月前
|
存储 NoSQL 安全
保护模式下的中断
保护模式下的中断
79 0
|
3月前
|
安全 Linux Windows
中断处理与特权级转移
中断处理与特权级转移
82 0
|
10月前
|
C语言 Windows
[笔记] Windows内核课程:保护模式《一》保护模式
[笔记] Windows内核课程:保护模式《一》保护模式
|
编解码 编译器
4.1保护模式
4.1保护模式
123 0
|
存储 Linux 算法
《操作系统真象还原》——0.20 BIOS中断、DOS中断、Linux中断的区别
BIOS够底层吧?难道它还要依赖别人?是啊,BIOS也是软件,也要有求于别人。首先硬件厂商为了让自己生产的产品易用,肯定事先写好了一组调用接口,必然是越简单越好,直接给接口函数传一个参数,硬件就能返回一个输出,如果不易用的话,厂商肯定倒闭了。
4645 0
|
Linux
自制操作系统Antz day02——进入保护模式 (上) jmp到保护模式
最近在看操作系统底层方面的东西,最开始的为什么是07c00h这个问题就让我对操作系统有了很大的兴趣。所以准备在看书之余顺便写一个操作系统(Anz)。至于为什么这个系统会被叫做Antz,可以参考Antz Uhl Kone, 日语为アインズ·ウール·ゴウン , 与之对应的还有接下来准备写的自制脚本语言A.
1483 0