第1章 保护模式编程一
如果想更深、更亲近的了解电脑软件。那么学习 cpu是你的必选!! 386是 CPU史的一大
转折点,那 386做基础课是最好不过了。那么我们将开始进行学习之旅!!!大家跟我一块学
习吧,呵呵!!!
1.1 准备工作
l 1、NASM 编译环境(当然 Masm 也可以 但是用它来写 COM程序比较
麻烦)
l 2、虚拟机
Virtual PC(Windows平台 ,执行比较快,即模拟又虚拟硬件)、
WMWarve(WIndows平台 虚拟硬件,)、
Bochs(支持Windows平台、也支持在 Linux平台上运行 有 RPM版本的)
我们这些生长在Windows这棵大树下的朋友们,还是用 Virtual PC吧.。
l 3、写虚拟启动镜像文件的程序
:不知道我观察的对不对?用Nasm 编译一个 bin 然后将它转换为 img 镜像文件的时候。
只要文件大小符合软驱的
就能启动。那么就代表 a.bin 与 a.img 文件的内容一模样就是
文件大小不一样!我是不太了解镜像文件格式.我用的是 Virtual PC。
1.2 开始接触引导程序
1.2.1 Com文件
Com 文件是纯二进制的文件,也是直接与 Cpu 交换的顺序指令文件。Com 文件的大小
是有限制的,不能超过 64KB.因为 8086时代的 CPU地址线是 20位的,20位能表达的数值也
就是 fffffh(1MB )。而寄存器最高也只是 16位,无法用 5个 F的形式来表达地址,所以用 CS(段
基地址)*16:IP(偏移地址)来寻址!80386后通用寄存器都得到了 32位扩展! 而 Cpu地址线也得
到了 32位的扩展。引导程序前期是需要进入实模式的,因为这是硬件上的限制是 IA32的限
制。386cpu只有两种模式: 实模式与保护模式!!!!,,
1.2.2 引导程序
引导程序也是有限制的,这个限制是靠 Bios处理的,开机后 Bios经过自检后,会从软驱或
者硬盘的 0 面 0 磁道 1 扇区搜寻一个程序文件。该文件的数据必需是等于 512Byte,并且以
aa55h结尾的(高高低低)。那么 bios会认为它是引导程序,这个时候就会把该 512byte 装载到
内存 7c00开始处。然后将主控权交给程序的第一行代码。那么这个时候程序脱离 Bios的控
制。Cpu将执行程序的代码.
1.2.3 写一个引导程序
引导程序可以说是非常简单:
1、boot.asm(nasm 的源文件如下)
;-----------------------欲编译,这里改成 100h就是 com程序 -------------------------------------------
%define _BOOT_DEBUG_ ;做调试的时候用 100h
%ifdef _BOOT_DEBUG_
org 0100h
%else
org 07c00h ; 告诉编译器 以下代码段将从 07c00h内存地址处开始
%endif
mov ax,cs ;让数据段与附加段寄存器跟代码段一样,因为 COM代码数据是混合.
mov ds,ax
mov es,ax
call _HelloWorld ;让程序显示一个 HelloWorld
jmp $ ;$表示当前地址 无限循环
_HelloWorld:
mov ax,strHello ;取得字符串的地址
mov bp,ax ;给堆栈基寄存器
mov cx,strLen
mov ax,1301H ;ah代表功能号
mov bx, 000ch ; 页号为 0(BH = 0) 黑底红字(BL = 0Ch,高亮
mov dx,0001h ;显示的行与列
int 10h ;bios 10h显示中断
ret
strHello: db "Hello World"
strLen equ $ - strHello
times 510-($-$$) db 0 ; times重复定义 510-($-$$)个 $$ 表示段的起始地址
dw 0xAA55
那么引导程序完成了,用
nasm boot.asm -o a.com
就可以运行看效果.,如改成引导程序只需把%define _DEBUG_BOOT_注释 然后 nasm
boot.asm -o a.bin然后用工具将 a.bin 转换成软驱大小的镜像文件 载入虚拟机启动就可以.
第2章 保护模式编程二
8086到 80386的跳转,80386与 8086在硬件上的区别在这就不说了!!那么 80386与 8086
在软件逻辑上面的区别就是:8086 是实模式,而 80386 不仅包括实模式,而且还可以进入保
护模式!!!
保护模式不仅不受 64KB 内存寻址的限制,而且还拥有 4GB 的寻址空间。这是因为 386
扩展了 20地址线,将它扩展成 32位了(32位能表达的字节数就是 4GB).此时的段寄存器不再
是段基地了,而被叫做是选择子 ,存放的是一个段描述符的索引值.而我们的通用寄存器与 EIP
也是 32位的,可以表达 4GB地址!不过计算机开机后,CPU默认是实模式。这就需要我们编
程手动转换到 386.那么我们该怎么去做呢:
2.1 【准备 GDT (全局描述符表)】
首先我们需要准备GDT结构体,它是 386保护模式必须的东西。全局描述符寄存器GDTR
指向的是所有段描述符表的信息.前面提到得段选择子索引,指得就是指向段描述符的索引.
段描述符是 8个字节的结构体、里面存放着段的段界限、段基址、段属性等信息.LDTR寄存
器是指向局部某一个段的描述符表。
段描述符表结构体用一个宏来表示(注意段 1 2表示同一个段描述内容被分开来放的):
【段基址】32位、表示物理地址
【段界限】20位、表示段的总长度 这里并不是地址,而是段的字节长度。
【段属性】12位. 系统、门、数据等属性
%macro Descriptor 3 ; 有三个
:【段基址】、【段界限】、【段属性】
dw %2 & 0FFFFh ; 段界限 1 (2 字节)
dw %1 & 0FFFFh ; 段基址 1 (2 字节)
db (%1 >> 16) & 0FFh ; 段基址 1 (1 字节)
dw ((%2 >> 8) & 0F00h) | (%3 & 0F0FFh) ; 属性 1 + 段界限 2 + 属性 2 (2 字节)
db (%1 >> 24) & 0FFh ; 段基址 2 (1 字节)
%endmacro ; 共 8 字节
看似很简单的结构体 理解起来可不是那么简单!
【Descriptor结构体】有 8个字节。
1、【第 1、2字节】组合(word) 表示该段的[段界限①], dw %2 & 0FFFFh ;引用第二个参
数去掉高 16位
2、【第 3、4、5字节】组合表示该段的[段基址①],dw %1 & 0FFFFh ;先得到第一个参数
(段基址)低WORD。
3、接着把第 5个字节赋值,db (%1 >> 16) & 0FFh 去掉第 3第 4个字节的内容.再把剩下
的字节赋值
4、【第 6个字节】是与【第 7个字节】组合的内容可就更复杂了:
【第 6个字节】的内容:
【7(p) 6(DPL) 5(DPL) 4(S) 3(Type) 2(Type) 1(Type)
0(Type)】
0-3位表示:[段属性]、说明存储段描述符所描述的存储段的具体属性。
4位表示:说明描述符的类型, 对于存储段描述符而言,S=1表示是系统段描述符。
5-6位表示:DPL 该段的特权级别也就是 Ring 0-3;
7位表示:P: 存在(Present)位。
; P=1 表示描述符对地址转换是有效的,即描述的段在内存当中.
; P=0 表示描述符对地址转换无效,即该段不存在。使用该描述符进行内存访问时会引起异
常
【第 7个字节】的内容:
【7(G) 6(D) 5(0 ) 4(AVL) 3(段界限) 2(段界限) 1(段界限) 0(段界
限)】
0-3位表示:[段界限②]
4 位表示:软件可利用位。80386 对该位的使用未做规定,Intel 公司也保证今后开发生
产的处理器只要与 80386兼容,就不会对该位的使用做任何定义或规定。
5位表示:0 ;Intel资料也没表示
6位表示:是一个很特殊的位,在描述可执行段、向下扩展数据段或由 SS寄存器寻址的
段(通常是堆栈段)的三种描述符中的意义各不相同,通常置 1
7位表示: 段界限粒度(Granularity)位。
G=0 表示界限粒度为字节;
G=1 表示界限粒度为 4K 字节。
注意,界限粒度只对段界限有效,对段基地址无效,段基地址总是以字节为单位。
那么这段宏 dw ((%2 >> 8) & 0F00h) | (%3 & 0F0FFh)表示:
取[段界限]参数除去低 16位取 高 4位,得到【段界限②】
取[段属性]参数的低 8位 12-15位(AVL属性等)
属性 1 + 段界限 2 + 属性 2
【第 8个字节】的内容:
[段基址②] 、db (%1 >> 24) & 0FFh 取基地址参数的最高 8位
那么一个 Descriptor 结构体就这样成形了.
2.2 【编写程序跳转到保护模式】
%include "386.inc" ;是 Descriptor结构体宏
;%define _DEBUG_BOOT_
%ifdef _DEBUG_BOOT_
org 0100h
%else
org 07c00h
%endif
jmp LABEL_BEGIN
[SECTION .gdt] ;全局描述符数据段
; 段基址, 段界限 , 属性
LABEL_GDT: Descriptor 0, 0, 0 ;空描述符
LABEL_DESC_CODE32: Descriptor 0, SegCode32Len - 1, DA_CR | DA_32 ;代码段描述符
LABEL_DESC_DATA: Descriptor 0,SegDataLen - 1,DA_DRW ;数据段
LABEL_DESC_VIDEO: Descriptor 0B8000h,0FFFFh,DA_DRW ;显示器内存段 由于 DOS中断不能随
意使用了,,只能输出到显示缓冲区
; GDT 结束
GdtLen equ $ - 1
GdtPtr dw GdtLen - 1 ;GDT 的段界限,
dd 0 ;GDT基地址
; GDT 选择子
SelectorCode32 equ LABEL_DESC_CODE32 - LABEL_GDT ;代码相对全局描述符起始地址的 EA值
SelectorData equ LABEL_DESC_DATA - LABEL_GDT ;数据段
SelectorVideo equ LABEL_DESC_VIDEO - LABEL_GDT ;显示数据段
[SECTION .s16] ;16位代码段
[BITS 16] ;BITS指出处理器的模式 是 16位
LABEL_BEGIN:
mov ax, cs
mov ds, ax
mov es, ax
mov ss, ax ;初始化段寄存器
;初始化数据
mov eax,strHello
mov word[LABEL_DESC_DATA + 2],ax
mov byte[LABEL_DESC_DATA + 4],al
shr eax,16
mov byte[LABEL_DESC_DATA + 7],ah
; 初始化并把 32位段代码的基地址分配给段描述符
mov eax, LABEL_CODE32 ;
mov word [LABEL_DESC_CODE32 + 2], ax ;ax
shr eax, 16
mov byte [LABEL_DESC_CODE32 + 4], al
mov byte [LABEL_DESC_CODE32 + 7], ah
; 为加载 GDTR 作准备
mov eax, LABEL_GDT
mov dword [GdtPtr + 2], eax ;得到 GDT基地址
; 加载 全局描述符的信息结构体 到 GDTR
lgdt [GdtPtr]
CA20 ;利用键盘端口打开 A20地址线
; 将 CRO的 PE位 也就是 0位 置 1 那么就进入 386模式了
mov eax, cr0
or eax, 1
mov cr0, eax
jmp dword SelectorCode32:0 ;
;执行这一句会把 SelectorCode32 装入 cs, 并跳转到 Code32Selector:0 处
;这个描述符集合是以一个空描述符开始得,现在 LABEL_DESC_CODE32描述符的索引值因该是 8,
;所以 SelectorCode32的值应该就是 LABEL_DESC_CODE32的索引值,Code32Selector:0当中的 0是指
LABEL_DESC_CODE32 的段基址+ 0
;那么在打开 cr0的 PE位后,这个 JMP指令不再是直接跳到段地址去了;
;而是去 GDTR全局描述符寄存器当中去找这个当前 CS的索引,当前段基址+偏移 的内存地址了。
[SECTION .s32]; 32 位代码段. 由实模式跳入.
[BITS 32]
LABEL_CODE32:
;保护模式的死循环
mov ax, SelectorVideo
mov gs, ax ; 视频段选择子(目的)
mov edi, (80 * 10 + 9) * 2 ; 屏幕第 10 行, 第 0 列。
mov ah, 1Ch ; 0000: 黑底 1100: 红字
mov esi,0
mov ds,SelectData
mov ecx,11
vi:
lodsb
mov [gs:edi], ax
inc edi
LOOPNZ vi
; 到此停止
jmp $
SegCode32Len equ $ - LABEL_CODE32
[SECTION .data] ;数据段
strHello: db "Hello World"
SegDataLen equ $- strHello
2.3 【总结】
编写一个 386 程序主要用的步骤:
1、准备 GDT描述符集合结构体
2、用 lgdt [gdtPtr] 载入 gdtPtr 这 6个字节的结构体,,低字是描述符集合的界限 也就
是集合总长度,高双字是描述符集合的基地址.
3、打开 A20地址线。有一种方法是向键盘端口 IO,
4、置 CR0的 PE位 即 0位为 1
5、JMP [段索引]:[段基址偏移]
呵呵 接下来继续学习啊!!!
第3章 保护模式编程三
在一、到二、我们了解 386基本寻址机制,没错就是这么简单!!!接下来我们谈谈 对上一
个 386进行扩展:
大家在第二节已经知道了进入 386的基本步骤了,那么我们来具体设计吧.
编程首先当然是【声明】与【定义】:
3.1 【声明】:
在 386.inc 头文件里定义好需要的宏信息(好东西直接拿来用了呵呵)
;---------------------------------386.inc-----------------------------------------------------
DA_32 EQU 4000h ;32位段
DA_DPL0 EQU 00h ; DPL = 0
DA_DPL1 EQU 20h ; DPL = 1 (表示描述符特权级 Ring0-Ring3级)
DA_DPL2 EQU 40h ; DPL = 2
DA_DPL3 EQU 60h ; DPL = 3
;----------------------------------------------------------------------------
; 存储段描述符类型值说明
;----------------------------------------------------------------------------
DA_DR EQU 90h ; 存在的只读数据段类型值
DA_DRW EQU 92h ; 存在的可读写数据段属性值
DA_DRWA EQU 93h ; 存在的已访问可读写数据段类型值
DA_C EQU 98h ; 存在的只执行代码段属性值
DA_CR EQU 9Ah ; 存在的可执行可读代码段属性值
DA_CCO EQU 9Ch ; 存在的只执行一致代码段属性值
DA_CCOR EQU 9Eh ; 存在的可执行可读一致代码段属性值
;----------------------------------------------------------------------------
%macro Descriptor 3 ;3表示宏的参数有 3个 %1表示是第一个参数的标识 >>右移位
dw %2 & 0FFFFh ; 段界限 1 (2 字节)
dw %1 & 0FFFFh ; 段基址 1 (2 字节)
db (%1 >> 16) & 0FFh ; 段基址 2 (1 字节)
dw ((%2 >> 8) & 0F00h) | (%3 & 0F0FFh) ; 属性 1 + 段界限 2 + 属性 2 (2 字节)
db (%1 >> 24) & 0FFh ; 段基址 3 (1 字节)
%endmacro ; 共 8 字节
%macro CA20 0 ; 打开地址线 A20
in al, 92h
or al, 00000010b
out 92h, al
%endmacro
%macro DA20 0 ;关地址线
in al,92h
and al,11111101b
out 92h,al
%endmacro
3.2 【定义】:
;在定义模块之前我们首先要有个概念,那就是整体的雏形。可以简单划分出来也就是 2
个主要步骤:
1、定义 GDT数据段:
{
1、定义 Descriptor即段描述符: (通常是以一个全为零的 Descriptor开始)。
2、定义 GdtPtr 信息结构体(再加载 gdtr时候要用到)。
3、定义每个段描述符对应的索引位置(即定义选择子)。
}
2、定义如上描述的具体段:
{
1、实模式入口段 (这是个入口段:用于跳转到 386的段,并不属于 386段所以没有
描述符)
2、剩下的段就全是 386模式的段。
}
3.3 【具体编码】
还是要拿实实在在的能运行的代码来讲:
先说一下代码的主要功能:
1、从 8086跳到 386模式(Protect Mode)
2、在 386模式对大地址的寻址测试(超过 1MB)
3、测试完毕后回到 8086模式(Real Mode)
具体细节上,就看代码吧!!!在 386.asm 文件里实行具体模块的编写(代码比较多,刚开始
阅读有点复杂,因为是汇编可读性不是很好不过这是照上面的方法定义的,可以先从宏观入
手!!!):
;========================386.asm===============================
%include "386.inc" ; 常量, 宏, 以及一些说明
%define _DEBUG_B0OT_
%ifdef _DEBUG_B0OT_
org 0100h
%else
org 07c00h
%endif
jmp LABEL_BEGIN
;===========================;GDT全局描述符数据段==============================
[SECTION .gdt]
LABEL_GDT Descriptor 0,0,0 ;以空开头
;这个段描述符描述的段有点特殊,因为在下面并没有实际的定义它。它的作用是从保护模式跳转到 8086时
要用到的,是用来初始化段寄存器的。本人估计是(保护模式与实模式段界限与段偏移是不同的, 而这个段描
述符的段基址是 0、段界限是 0FFFFH ,段属性是读加写,与 8086 的标准是一样,所以在回到 8086模式之前,
CPU在对所有段寄存器进行实模式的转换就能正确安排界限与属性了,!!! ),
LABEL_DESC_NORMAL Descriptor 0,0ffffh,DA_DRW
LABEL_DESC_DATA Descriptor 0,SegDataLen - 1 ,DA_DRW | DA_32 ;段属性是非一致的 32位读写
数据段
LABEL_DESC_STACK Descriptor 0,TopOfStack - 1,DA_DRWA | DA_32 ;存在的已访问可读写的 32位
stack段 在保护模式下的 Call命令需要堆栈
LABEL_DESC_CODE32 Descriptor 0,SegCode32Len -1 ,DA_C | DA_32 ;Protect mode的 32位代码区
;这个段是保护模式的段,但是它是以 16位形式存放的,它是用来从保护模式跳回到 8086模式。,。。。。
因为直接用 32位代码段跳转到 8086模式是不行,必需从 16位保护模式段跳转到 16位 8086模式。就像先
前说的Normal 描述符,它也是一个具有 8086属性的描述符。,所以CS段寄存器的状态也需要先转换成与 8086
模式相同段界限与段属性。这样才能正确的转换到 8086模式,由此可见:全部的段寄存器都需要对应 8086模
式的描述符状态。才能正常的进入 8086模式。
LABEL_DESC_CODE16 Descriptor 0,0ffffh ,DA_C ;
;以下两个段描述符是内存中的段不需要自己定义
LABEL_DESC_TEST Descriptor 0500000h,0ffffh,DA_DRW ;用于测试线性空间
LABEL_DESC_VIDEO Descriptor 0B8000h,0ffffh,DA_DRW ;显示器内存
GdtLen equ $ - LABEL_GDT
GdtPtr dw GdtLen - 1 ;段界限与段基地址
dd 0
;----------------选择子---------------------
SelectorNormal equ LABEL_DESC_NORMAL - LABEL_GDT
SelectorData equ LABEL_DESC_DATA - LABEL_GDT
SelectorStack equ LABEL_DESC_STACK - LABEL_GDT
SelectorCode32 equ LABEL_DESC_CODE32 - LABEL_GDT
SelectorCode16 equ LABEL_DESC_CODE16 - LABEL_GDT
SelectorTest equ LABEL_DESC_TEST - LABEL_GDT
SelectorVideo equ LABEL_DESC_VIDEO - LABEL_GDT
;为了便于区分实模式代码与保护模式代码,
我就就把 16位的实模式段先写前面:(一般的编程
下 数据段是写前面的)
;=============================-8086的 16位实模式起始段============================
[SECTION .s16] ;16位代码段
[BITS 16] ;BITS指出处理器的模式 是 16位
LABEL_BEGIN: ;从 100h处跳进来的
mov ax,cs
mov ds,ax
mov es,ax
mov ss,ax
mov sp,100h ;在实模式下并没有用到 sp,这个 100h 只是形象表示需要保存实模式的 SP值.
mov [LABEL_GO_BACK_TO_REAL+3], ax ; 请看[LABEL_GO...]+3标号处的注释。是一个指令参数
mov [SPValueInRealMode],sp ;在这里保存实模式的 sp值
;-----------全局数据段描述符初始化-----------
xor eax,eax
mov ax,ds
shl eax,4 ;ds * 16 代表这 DS原来的基地址
add eax,LABEL_DATA1 ;得到物理地址
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;此时堆栈段描述符已经有基址了也就是可以访问了
;-----------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 ;进入保护模式后开始执行的代码段
;-----------16位代码段描述符初始化-----------
xor eax,eax
mov ax,cs
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 ;用来跳转到实模式的 386代码段
;----------GDTR Ready -----------
xor eax,eax
mov ax,ds
shl eax,4
add eax,LABEL_GDT
mov [GdtPtr + 2],eax
lgdt [GdtPtr] ;loader gdtr
;----------打开 A20--------
CA20
cli
;--------置 CR0 PE位-----
mov eax,cr0
or eax,1
mov cr0,eax
;---------------跳到 386-----------
jmp dword SelectorCode32:0 ;以上 3个步骤无需多讲了
;这个是迎接保护模式跳回来的时候,执行的代码,,欢迎 386回来啊!!!
LABEL_REAL_ENTRY:
mov ax,cs
mov ds,ax
mov es,ax
mov ss,ax
mov sp,[SPValueInRealMode] ;恢复到 Real原来的堆栈 注意这里要用[标号]
DA20 ;关闭 20地址线
sti
mov ax,4c00h
int 21h
;===================================386保护模式段==================================
;-------------------数据段--------------------------
[SECTION .data1]
align 32
[BITS 32]
LABEL_DATA1:
SPValueInRealMode dw 0 ; 这个变量用来保存实模式跳入到保护模式前的 SP值
;---------字符串-------------
PMMessage: db "welcome to Protect Mode ", 0 ; 进入保护模式后显示此字符串
OffsetPMMessage equ PMMessage - $$ ;保护模式寻址方式是按段偏移
StrTest: db "ABCDEFGHIJKLMNOPQRSTUVWXYZ", 0
OffsetStrTest equ StrTest - $$ ;测试 5MB空间所用的字符串
SegDataLen equ $ - LABEL_DATA1 ;数据段长
;-----------------------------------386全局堆栈段------------------------------------
[SECTION .stack32]
align 32
[BITS 32]
LABEL_STACK:
times 512 db 0 ;堆栈大小是 512byte
TopOfStack equ $ - LABEL_STACK - 1 ;栈顶的值
;-----------------------------------进入保护模式后的起始代码段------------------------------------
[SECTION .s32]; 32 位代码段. 由实模式跳入.
[BITS 32]
LABEL_SEG_CODE32:
mov ax,SelectorData
mov ds,ax
mov ax,SelectorTest
mov es,ax
mov ax,SelectorVideo
mov gs,ax ;以上 3个也不用多说了吧,段的选择子也就是 GDT的索引
;---堆栈--
mov ax,SelectorStack
mov ss,ax
mov esp,TopOfStack ;当然堆栈段也是段的选择子咯
;-----------显示缓冲--------
mov ah, 0Ch ; 0000: 黑底 1100: 红字
mov esi,OffsetPMMessage ;这个是字符串相对于它的段偏移值
mov edi,(80*10+0)*2 ;第 10行
cld
.1:
lodsb
test al,al
jz .2
mov [gs:edi],ax ;要用 ax做为参数传进缓冲区
add edi,2
jmp .1
.2: ;OffsetPMMessage 字符串显示完成:
;------------------------测试 5MB空间的读------------------
call DispReturn ;显示回车,也就是改变 edi的位置(edi 是显缓冲区段的偏移值)
call ReadTest
call WriteTest
call ReadTest ;当在执行 Call命令的时候 会将 eip + 1压栈,然后跳转
jmp SelectorCode16:0 ;跳到最后那个 16位代码段去了 前面有 16位段描述符的讲解。。
;--------------显示回车---------------
DispReturn:
push ebx ;临时用 ebx eax 所以先保存一下
push eax
mov bl,160
mov eax,edi
div bl
inc eax ;得到回车后的行数
mov bl,160
mul bl
mov edi,eax ;取得当前的位置
pop eax
pop ebx
ret ;ret 指令会恢复 eip + 1
;--------------读取我们定义的大地址段---------------
ReadTest:
xor esi, esi
mov ecx, 8
. loop:
mov al, [es:esi]
call DispAL
inc esi
loop .loop
call DispReturn
ret
;--------------写入我们定义的大地址段---------------
WriteTest:
push esi ;借这两个寄存器来传字符串
push edi
xor esi, esi
xor edi, edi
mov esi, OffsetStrTest ; 数据段的字符串
cld
.1:
lodsb
test al, al
jz .2
mov [es:edi], al ;把数据段的字符串传给测试段
inc edi
jmp .1
.2:
pop edi
pop esi
ret
;--------------显示 AL的内容---------------
DispAL: ;对传来的 Al字符进行处理
test al,al
jnz .next
mov al,'0'
.next:
mov ah, 0Ch ; 0000: 黑底 1100: 红字
mov [gs:edi],ax
add edi,2
ret
SegCode32Len equ $ - LABEL_SEG_CODE32
;-----------------------------------准备 8086模式的 16位段------------------------------------
[SECTION .code16]
align 32
[BITS 16]
LABEL_SEG_CODE16:
mov ax,SelectorNormal ; 保护 8086标准段属性的描述符选择子
mov ds, ax
mov es, ax
mov fs, ax
mov gs, ax
mov ss, ax ;使所有的段选择子都达到 8086标准
mov eax,cr0
and al,0feh
mov cr0,eax ;CR0 PE位为 0 回到 Real mode
LABEL_GO_BACK_TO_REAL:
jmp 0:LABEL_REAL_ENTRY ;还记得上面的[LABEL_GO..]+3吗,指的就是 0这个段值
Code16Len equ $ - LABEL_SEG_CODE16
呵呵、那么就完工了代码虽然比较繁杂,但是还不是特别复杂。有几个要注意的地方:
1、堆栈(新加了堆栈这个段,其寻址方式也是选择子。)在入口的时候是实模式,应该把当
前的 sp 值保存下来,因为在返回系统时我们需要基本恢复原来的样子。就在保护模式的全局
数据段定义一个变量用来保存 SP。在返回 DOS时候要记得恢复。
2、保护模式到实模式。实模式到保护模式 比较简单就几个固定步骤不需要考虑太多,
而从保护模式到实模式就比较复杂:首先需要定义两个 8086 标准属性的段描述符.它们分别
是 Normal数据段与 16位代码段描述符。在 16位代码段中 cs的属性跟 8086模式的 CS段界
限与段属性一致,并且把 Normal 的段属性分别付给全部的段寄存器.这样就让所有保护模式
的段寄存器全部跟 8086段界限与段属性一致!!!在 cr0 的 PE位为 0后 就可以用实模式
寻址了!!!接下来继续学习:
第4章 保护模式编程四--LDT描述符&&特权级&&门
4.1 LDT(局部描述符)
GDT是全局描述符,是整个系统的描述符,描述符着所有的段!!!在前几章我们已经
熟悉了 GDT的一些基本功能,与运作机制。对 GDT描述符的定义与使用也就那么几项固定
的步骤,接下来再了解 LDT。
LDT是局部描述符。看字面 LDT与 GDT很相似.它们都是描述符。只不过 GDT是全局
描述符、而 LDT是局部描述符。那么 LDT该如何【定义】与【使用】呢?(与 GDT非常类
似,不过 LDT是归属于 GDT的):
4.1.1 【定义】:
1、在 GDT的描述集合中插入一条 LABEL_DESC_LDT Descriptor 0,LdtLen - 1,DA_LDT
(这个段描述符描述的 LDT数据段:是一个局部描述符段的集合,结构类似 GDT.)
2、在 GDT 选择子集合中照样也插入一条 SelectorLdt equ ;LABEL_DESC_LDT -
LABEL_GDT(看似跟普通的段描述符没啥两样哦,不过接下来就有点小区别了!)
3、建一个 LDT数据段,这个数据段里的结构与 GDT描述符段类似!
{
LABEL_LDT: ;这里与 GDT不同,LDT的段描述符一开始就是实际的段
LABEL_DESC_LDT_CODEA: Descriptor 0,LdtCodeALen - 1,DA_C | DA_32
LABEL_DESC_LDT_DATA: Descriptor 0,LdtDataLen - 1,DA_DRW
LdtLen equ $ - LABEL_LDT;
;在加载 LDT的时候并不是靠一个绝对结构体,而是通过 GDT这个桥梁索引加载的。
;LDT内部段的选择子
SelectorLdtCodeA equ LABEL_DESC_LDT_CODEA - LABEL_LDT | SA_TIL
SelectorLdtData equ LABEL_DESC_LDT_DATA - LABEL_LDT | SA_TIL
;LDT内部的选择子多了一个 SA_TIL属性,他是段选择的 TI位也就是第 2位 如果是 1代表当前选择子
代表的段是 LDT的内部段。 因为选择子的 0-2位并不参与索引。所以选择子的索引都是 8的倍数或 0,
}
4、接着完成 LDT 里面段描述符描述的段。这里只定义了一个代码段 :
LABEL_DESC_LDT_CODEA(这个跟普通的代码段一样,有执行、32位等属性. 为了测试LDT
的效果那么我们还是加一个 Data段吧)那么 LDT的基本定义已经完成了!!!
4.1.2 使用
1、LDT 描述符与其内部的局部段描述符信息、,我们在进入保护模式前初始化!!!那么
我们的进入保护模式之前的段在哪还知道吗?当然就是最最开始了!在 16位段实模式入口那
里!!!我们在这里做完 GDT描述符的初始化工作后:
;--------------初始化 LDT描述符---------
xor eax,eax
mov eax,ds
shl eax,4
add eax,LABEL_LDT
mov WORD [LABEL_DESC_LDT + 2],ax
shr eax,16
mov BYTE [LABEL_DESC_LDT + 4],al
mov BYTE [LABEL_DESC_LDT + 7],ah
;---------------初始化 LDT内部的数据段描述符----------
xor eax,eax
mov eax,ds
shl eax,4
add eax,LABEL_LDT_DATA
mov WORD [LABEL_DESC_LDT_DATA + 2],ax
shr eax,16
mov BYTE [LABEL_DESC_LDT_DATA + 4],al
mov BYTE [LABEL_DESC_LDT_DATA + 7],ah
;---------------初始化 LDT内部的代码段描述符----------
xor e