-
-
[原创][保护模式编程、三]
-
发表于: 2008-4-7 23:28 7918
-
【继续80386编程】
在一、到二、我们了解386基本寻址机制,没错就是这么简单!!!
接下来我们谈谈 对上一个386进行扩展:
大家在第二节已经知道了进入386的基本步骤了,那么我们来具体设计吧.
编程首先当然是【声明】与【定义】:
一、【声明】:
在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
二、【定义】:
;在定义模块之前我们首先要有个概念,那就是整体的雏形。
可以简单划分出来也就是2个主要步骤:
1、定义GDT数据段:
{
1、定义Descriptor即段描述符: (通常是以一个全为零的Descriptor开始)。
2、定义GdtPtr 信息结构体(再加载gdtr时候要用到)。
3、定义每个段描述符对应的索引位置(即定义选择子)。
}
2、定义如上描述的具体段:
{
1、实模式入口段 (这是个入口段:用于跳转到386的段,并不属于386段所以没有描述符)
2、剩下的段就全是386模式的段。
}
三、【具体编码】
还是要拿实实在在的能运行的代码来讲:
先说一下代码的主要功能:
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 "This is 5MB", 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,11
mov ah, 0Ch ; 0000: 黑底 1100: 红字
.loop:
mov al,[es:esi] ;这个是SelectorTest的段,也就是我们测试的大地址段
call DispAL ;显示从test段取出来的字符
inc esi
loop .loop ;cx--
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后 就可以转换成实模式寻址了!!!
接下来继续学习:
[招生]科锐逆向工程师培训(2024年11月15日实地,远程教学同时开班, 第51期)
赞赏
- [原创][保护模式编程、四] 7118
- [原创][保护模式编程、三] 7919
- [原创]386保护模式[学习一、二] 16165
- [原创]浅谈U盘防御文件夹!!! 13292
- [原创]Kmd(kernel mode Driver)简单入门之谈!! 13898