本篇主讲对应于 《Orange'S:一个操作系统的实现》第三章 a 代码 pmtest1.asm
; ==========================================
; pmtest1.asm
; 编译方法:nasm pmtest1.asm -o pmtest1.bin
; ==========================================
%include "pm.inc" ; 常量, 宏, 以及一些说明
org 07c00h
jmp LABEL_BEGIN
[SECTION .gdt]
; GDT
; 段基址, 段界限 , 属性
LABEL_GDT: Descriptor 0, 0, 0 ;空描述符
LABEL_DESC_CODE32: Descriptor 0, SegCode32Len - 1, DA_C + DA_32 ;非一致代码段
LABEL_DESC_VIDEO: Descriptor 0B8000h, 0ffffh, DA_DRW ;显存首地址
; GDT 结束
GdtLen equ $ - LABEL_GDT ; GDT长度
GdtPtr dw GdtLen - 1 ; GDT界限
dd 0 ; GDT基地址
; GDT 选择子
SelectorCode32 equ LABEL_DESC_CODE32 - LABEL_GDT
SelectorVideo equ LABEL_DESC_VIDEO - LABEL_GDT
; END of [SECTION .gdt]
[SECTION .s16]
[BITS 16]
LABEL_BEGIN:
mov ax, cs
mov ds, ax
mov es, ax
mov ss, ax
mov sp, 0100h
; 初始化 32 位代码段描述符
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
; 为加载 GDTR 作准备
xor eax, eax
mov ax, ds
shl eax, 4
add eax, LABEL_GDT ; eax <- gdt 基地址
mov dword [GdtPtr + 2], eax ; [GdtPtr + 2] <- gdt 基地址
; 加载 GDTR
lgdt [GdtPtr]
; 关中断
cli
; 打开地址线A20
in al, 92h
or al, 00000010b
out 92h, al
; 准备切换到保护模式
mov eax, cr0
or eax, 1
mov cr0, eax
; 真正进入保护模式
jmp dword SelectorCode32:0 ; 执行这一句会把 SelectorCode32 装入 cs,
; 并跳转到 Code32Selector:0 处
; END of [SECTION .s16]
[SECTION .s32]; 32 位代码段. 由实模式跳入.
[BITS 32]
LABEL_SEG_CODE32:
mov ax, SelectorVideo
mov gs, ax ; 视频段选择子(目的)
mov edi, (80 * 11 + 79) * 2 ; 屏幕第 11 行, 第 79 列。
mov ah, 0Ch ; 0000: 黑底 1100: 红字
mov al, 'P'
mov [gs:edi], ax
; 到此停止
jmp $
SegCode32Len equ $ - LABEL_SEG_CODE32
; END of [SECTION .s32]
现在来逐行讲解
org 07c00h
jmp LABEL_BEGIN
这句话告诉加载器,将这段程序加载到偏移段首地址07c00h处。 然后跳转到 LABEL_BEGIN 处开始执行
[SECTION .gdt]
; GDT
; 段基址, 段界限 , 属性
LABEL_GDT: Descriptor 0, 0, 0 ; 空描述符
LABEL_DESC_CODE32: Descriptor 0, SegCode32Len - 1, DA_C + DA_32 ; 非一致代码段 段属性(DA_C: 98h 可执行; DA_32: 4000h 32位代码段)
LABEL_DESC_VIDEO: Descriptor 0B8000h, 0ffffh, DA_DRW ; 显存首地址
; GDT 结束
GdtLen equ $ - LABEL_GDT ; GDT长度
GdtPtr dw GdtLen - 1 ; GDT界限
dd 0 ; GDT基地址
; GDT 选择子
SelectorCode32 equ LABEL_DESC_CODE32 - LABEL_GDT ;两个偏移量相减就是 LABEL_DESC_CODE32 相对于 LABEL_GDT 的偏移量
SelectorVideo equ LABEL_DESC_VIDEO - LABEL_GDT
; END of [SECTION .gdt]
这段代码对于很多初学者一头雾水,我们先看以下几个问题
- [SECTION .XXX]为何物?
- 段描述符(Descriptor)、全局描述符表(GDT)、全局描述符表寄存器(GDTR)、选择子(SelectorXXX) 为何物?有什么作用?
1、[SECTION .XXX]为何物?
SECTION和SEGMENT的作用相类似,就是代表“段”的意思。从整个程序来看,该程序分为3个模块,分别是[SECTION .gdt]、[SECITON .s16]、[SECTION .s32]三部分。我们很容易就可以看出,其中的[SECTION .gdt]应该是数据段,其他的两个是代码段。通过[SECTION .XXX]将程序分成不同模块,完成不同的功能,使得程序看起来清晰明了。
2、描述符(Descriptor)、全局描述符表(GDT)、全局描述符表寄存器(GDTR)、选择子(SelectorXXX) 是什么?用来做什么?
段(Segment),在80X86中,分段机制将内存空间分成一个或者多个线性区域,我们把这些线性区域称为段。我们需要将这些段区分开来,于是分段机制为每个段赋予3个属性,分别是:段基址(Base address):指定段在线性地址空间中的开始地址。段界限(Limit):表示了段内最大可用偏移量,也就是说它定义了段的长度。段属性(Attribute):指定了段的特性,包括:可读,可写或者可执行,特权等级等特性。
段描述符(Descriptor),在程序中,我们需要定义一个数据结构来记录段的属性,有段基址(Base),段界限(Limit),段属性(Attribute),我们称它为段描述符(Descriptor)。段是逻辑概念,而段描述符是表示段的数据结构,每个段描述符要占用8个字节的空间。
段描述符表(Descriptor Table),在一个程序中,不只存在一个段(段描述符)。所以我们需要将这些段描述符组织起来,于是定义了一个存储段描述符的数组,称为段描述符表。段描述符表有两种,一种是全局描述符表(GDT),一种是局部描述符表(LDT),系统中供所有的任务共享的是全局描述符表,而不同的任务却是使用自己的局部描述符表。
段选择子(SelectorXXX),把所有段描述符都存储在段描述符表中,当我们使用其中某一个段的时候,我们并不直接指向该段,而是通过该段描述符在段描述符表中的位置来访问的。故段选择子,就是一个16位的标识符,用来标识该段描述符在描述符表中的位置。
段描述符表寄存器,如何让系统知道段描述符表在什么地方呢?处理器提供了内存管理寄存器,分别是全局描述符表寄存器(GDTR)、局部描述符表寄存器(LDTR)。GDTR寄存器中用于存放全局描述符表GDT的32位线性基地址和16位的表的长度值。LDTR寄存器中用于存放局部描述符表LDT的32位线性基地址和16位的表的长度值。通过系统指令,lgdt将GDT的线性基址和长度值加载到GDTR寄存器中,lldt将LDT的线性基址和长度值加载到LDTR寄存器中。
继续分析上述代码:
在程序中,定义了3个段描述符
LABEL_GDT(空描述符)。 Intel 要求 遵循Intel公司规定,全部置0
LABEL_DESC_CODE32(32位代码段描述符)。32位代码段描述符,供保护模式下使用
LABEL_SESC_VIDEO(显示内存描述符)。 显存段首地址,我们知道,显存首地址是0B8000H.
每个描述符都包含了3个属性,段基址、段界限、段属性。将三个描述符组织到一起构成一个全局段描述符表(GDT)。
GdtLen是GDT的长度。
GdtPtr为一个数据结构,包含两个元素,第一个元素是2 bytes的GDT界限。第二个元素是4 bytes的GDT的基地址。该数据结构与全局描述符表寄存器(GDTR)的数据结构相同,所以在加载GDTR的时候(源代码55行),就是将该GdtPtr加载到GDTR中。
由于第一个段LABEL_GDT是空描述符,它仅仅代表该GDT的初始地址,所以该描述符为空描述符,一般情况下,不为它创建选择子。然后该程序建立了两个选择子(24、25行)SelectorCode32和SelectorVideo,分别对应着这两个段LABEL_SESC_CODE32和LABEL_DESC_VIDEO。
SelectorCode32 equ LABEL_DESC_CODE32 - LABEL_GDT
SelectorVideo equ LABEL_DESC_VIDEO - LABEL_GDT
问题: 两个段描述符相减是什么?
两个段描述符相减就是 (LABEL_DESC_CODE32 - LABEL_GDT) LABEL_DESC_CODE32 相对于 LABEL_GDT 的偏移量
两个段描述符相减就是 (LABEL_DESC_VIDEO - LABEL_GDT) LABEL_DESC_VIDEO 相对于 LABEL_GDT 的偏移量
[BITS 16]
[BITS 16]告诉编译器,这是一个16位代码段,所使用的寄存器都是16位寄存器
mov ax, cs
mov ds, ax
mov es, ax
mov ss, ax
mov sp, 0100h
mov sp, 0100h ;设置堆栈的长度为100H byte
; 初始化 32 位代码段描述符
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 eax 0, 效率高于mov
mov ax, cs
shl eax, 4 向左移动4位,就是×16。 到现在为止,eax就是代码段的物理首地址了 段值 × 16 + 偏移量 = 物理地址
add eax, LABEL_SEG_CODE32 为eax (代码段首地址)加上 LABEL_SEG_CODE32 偏移量,得到的是 LABEL_SEG_CODE32 的真正物理地址
mov word [LABEL_DESC_CODE32 + 2], ax [..] 代表一个内存单元,就是将 ax 移动到 LABEL_DESC_CODE32 的 2,3 字节处
shr eax, 16 这句代码就将eax向右移动16位,低位被抛弃,高位变成了低位
现在好办了,低16位又可以分为al,和 ah,那么现在我们就将al放到4位置,ah放到7位置吧
mov byte [LABEL_DESC_CODE32 + 4], al
mov byte [LABEL_DESC_CODE32 + 7], ah
因为历史原因 描述符的基地址所占有的字节是BYTE2,BYTE3,BYTE4,BYTE7(共32bit)
; 为加载 GDTR 作准备
xor eax, eax
mov ax, ds
shl eax, 4
add eax, LABEL_GDT ; eax <- gdt 基地址
mov dword [GdtPtr + 2], eax ; [GdtPtr + 2] <- gdt 基地址
代码与上雷同,大体分析下:
我们之前定义了Gdtptr的结构,包含两个字段,GTD界限(两个字节) 和 GDT基地址(四个字节)
GdtPtr dw GdtLen - 1 ; GDT界限
dd 0 ; GDT基地址
mov dword [GdtPtr + 2], eax 而这段代码是将eax中的gdt基地址,给Gdtptr结构赋值,从第二个字节开始。
如何让系统知道段描述符表在什么地方呢?处理器提供了内存管理寄存器,分别是全局描述符表寄存器(GDTR)、局部描述符表寄存器(LDTR)
lgdt [GdtPtr]
通过系统指令,lgdt将GDT的线性基址和长度值加载到GDTR寄存器中
; 关中断
cli
CLI将IF置0,屏蔽掉“可屏蔽中断”,当可屏蔽中断到来时CPU不响应,继续执行原指令
而STI 与之相反,STI将IF置1,允许“可屏蔽中断”,中断到来转而处理中断
; 打开地址线A20
in al, 92h ;表示从21H端口读取一字节数据到AL
or al, 00000010b ;逻辑或指令,按位进行或运算
out 92h, al
; 准备切换到保护模式
mov eax, cr0
or eax, 1
mov cr0, eax
这个简单说一下,以后再详细
CR0也是一个寄存器,其中有个PE位,如果为0,就说明为实模式,
如果置1,说明为保护模式。现在我们要进入保护模式下工作,那么就要设置PE为1。
; 真正进入保护模式
jmp dword SelectorCode32:0 ; 执行这一句会把 SelectorCode32 装入 cs,
现在已经再保护模式下了,当然要使用段选择子 + 偏移量来寻址啊,这样不就是寻址到了32位代码段中去了吗,偏移量为0不就说明从第一个代码开始执行。
不是吗 ? 呵呵,那dword了?
因为现在的代码段是16位,编译器只能将它编译位16位,但处于保护模式下,它的偏移量应该是32位,所以,要显示告诉编译器,我这里使用的是32位,把我这块给编译成32位的!!!
如果不加dword,
jmp SelectorCode32:0
这句话不会出什么问题,16位的0是0,32位的0还是0,但如果这样呢?:
jmp SelectorCode32:0x12345678
跳转到偏移0x12345678中,这时就错了
如果不将dword,编译器就将该地址截断成16位,取低位,变成了0x5678
所以我们必须这样做:
jmp dword SelectorCodde32:0x12345678
; 到此停止
jmp $
$代表当前的地址
那JMP $"就是跳转到当前的地址, 所以它是一个死循环,不继续执行下面的程序了。
他的意思在于我要求的所有任务已经完成了,后面没有任务了,那么,就原地踏步吧!
最后一点小问题
写完代码编译,自己用bximage工具重新做了个a.img,然后pmtest1.bin写入到a.img中,启动bochs后报错No bootable device
查了半天代码,没找出来代码错误,然后就看了一下书,书中说是将第二章中的a.img拷贝过来,拷贝过来一式就好了。
原因是:第二章中的代码最后有两句代码:
times 510 - ($ - $$) dw 0xaa55
所以第二章中的a.img中已经被填充为512字节并且以0xaa55结束,所以BISO就认为它是一个引导区,就去加载它。在第三章中没有这样做,所以得用第二章中的a.img,从头开始覆盖,而其代码量小于510字节,所以正好可以用。