原文:https://www.pediy.com/kssd/pediy10/62548.html
看雪的朋友门我来鸟。。
在编程区里:看给位大侠将的什么中断,内核,听得我直发毛。。
因为有很多专业词汇,内核知识。。从来没听过不知道大侠们在讲什么!!!
那么本人不甘心 。我就从头开始学吧
..现在那点笔记出来跟大家分享一下!!!
由于是笔记,也许记得有点糟糕!!
呵呵,来看雪总想发点东西,!!请见谅啊!!!
【保护模式编程、一】
【8086模式编程】
如果想更深、更亲近的了解电脑软件。
那么学习cpu是你的必选!!
386是CPU史的一大转折点,那386做基础课是最好不过了。那么我们将开始进行学习之旅!!!
大家跟我一块学习吧,呵呵!!!
一、准备工作
1、NASM 编译环境
2、虚拟机
Virtual PC(Windows平台 ,执行比较快,即模拟又虚拟硬件)、
WMWarve(WIndows平台 虚拟硬件,)、
Bochs(支持Windows平台、也支持在Linux平台上运行 有RPM版本的)
我们这些生长在Windows这棵大树下的朋友们,还是用Virtual PC吧.。
3、写虚拟启动镜像文件的程序
:不知道我观察的对不对?
用Nasm 编译一个bin 然后将它转换为img 镜像文件的时候。
只要文件大小符合软驱的标准就能启动。那么就代表a.bin 与a.img 文件的内容一模样就是文件大小不一样!
我是不太了解镜像文件格式.我用的是Virtual PC。
二、开始接触引导程序
1、Com文件
Com文件是纯二进制的文件,也是直接与Cpu交换的顺序指令文件。
Com文件的大小是有限制的,不能超过64KB.
因为8086时代的CPU地址线是20位的,20位能表达的数值也就是fffffh(1MB )
而寄存器最高也只是16位,无法用5个F的形式来表达地址,所以用CS(段基地址)*16:IP(偏移地址)来寻址!
80386后通用寄存器都得到了32位扩展! 而Cpu地址线也得到了32位的扩展。
引导程序前期是需要进入实模式的,因为这是硬件上的限制是IA32的限制。
386cpu只有两种模式: 实模式与保护模式!!!!,,
2、引导程序
引导程序也是有限制的,这个限制是靠Bios处理的,
开机后Bios经过自检后,会从软驱或者硬盘的0面0磁道1扇区搜寻一个程序文件。
该文件的数据必需是等于512Byte,并且以aa55h结尾的(高高低低)。那么bios会认为它是引导程序,
这个时候就会把该512byte 装载到内存7c00开始处。然后将主控权交给程序的第一行代码。
那么这个时候程序脱离Bios的控制。Cpu将执行程序的代码.
三、写一个引导程序
引导程序可以说是非常简单:
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 转换成软驱大小的镜像文件 载入虚拟机启动就可以.
【保护模式编程、二】
【80386保护模式编程】
8086到80386的跳转,80386与8086在硬件上的区别在这就不说了!!
那么80386与8086在软件逻辑上面的区别就是:
8086是实模式,而80386 不仅包括实模式,而且还可以进入保护模式!!!
保护模式不仅不受64KB内存寻址的限制,而且还拥有4GB的寻址空间。这是因为386扩展了20地址线,将它扩展成32位了(32位能表达的字节数就是4GB).
此时的段寄存器不再是段基地了,而被叫做是选择子 ,存放的是一个段描述符的索引值.
而我们的通用寄存器与EIP 也是32位的,可以表达4GB地址!
不过计算机开机后,CPU默认是实模式。这就需要我们编程手动转换到386.
那么我们该怎么去做呢:
一、【准备GDT (全局描述符表)】
首先我们需要准备GDT结构体,它是386保护模式必须的东西。
全局描述符寄存器GDTR 指向的是所有段描述符表的信息.
前面提到得段选择子索引,指得就是指向段描述符的索引.
段描述符是8个字节的结构体、里面存放着段的段界限、段基址、段属性等信息.
LDTR寄存器是指向局部某一个段的描述符表。
段描述符表结构体用一个宏来表示(注意段1 2表示同一个段描述内容被分开来放的):
【段界限】20位、表示段的总长度 这里并不是地址,而是段的字节长度。
【段基址】32位、表示物理地址
【段属性】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 结构体就这样成形了.
二、【编写程序跳转到保护模式】
%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
【总结】
编写一个386 程序主要用的步骤
1、准备GDT描述符集合结构体
2、用lgdt [gdtPtr] 载入 gdtPtr 这6个字节的结构体,,低字是描述符集合的界限 也就是集合总长度,高双字是描述符集合的基地址.
3、打开A20地址线
有一种方法是向键盘端口 IO,
4、置CR0的PE位 即0位为1
5、JMP [段索引]:[段基址偏移]
呵呵 接下来继续学习啊!!!
【继续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后 就可以转换成实模式寻址了!!!
接下来继续学习:
【LDT描述符&&特权级&&门】
一、LDT(局部描述符)
GDT是全局描述符,是整个系统的描述符,描述符着所有的段!!!
在前几章我们已经熟悉了GDT的一些基本功能,与运作机制。
对GDT描述符的定义与使用也就那么几项固定的步骤,接下来再了解LDT.
LDT是局部描述符。看字面LDT与GDT很相似.它们都是描述符。
只不过GDT是全局描述符、而LDT是局部描述符。
那么LDT该如何【定义】与【使用】呢?(与GDT非常类似,不过LDT是归属于GDT的):
@【定义】:
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的基本定义已经完成了!!!
@【使用】:
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 eax,eax
mov eax,ds
shl eax,4
add eax,LABEL_LDT_CODEA
mov WORD [LABEL_DESC_LDT_CODEA + 2],ax
shr eax,16
mov BYTE [LABEL_DESC_LDT_CODEA + 4],al
mov BYTE [LABEL_DESC_LDT_CODEA + 7],ah
上面几个初始化段基地址的代码相信大家已经很熟悉了!!!!!!!!
2、 先前就说过了LDT跟GDT类似都是描述符集合,那么他们在使用起来也有点类似!!!
需要用lldt指令:它的参数并不是一个结构体的指针,因为LDT是属于GDT的。不能独立的加载。必需依靠GDT这个桥梁。那么这个参数也就是LDT在GDT当中的索引值.,
在进入保护模式后我们添加如下代码进行LDT的加载:
mov ax,SelectorLdt
lldt ax ;lldt加载时是从GDT当中的ax索引位置的段做LDT段加载到LDTR寄存器的.\
jmp SelectorLdtCodeA:0 ;首先检查选择子的TI位,当是LDT选择子的时候。马上去LDTR寄存器取LDT在GDT当中的索引,找到LDT的位置。最后根据SelectorLdtCodeA在LDT局部描述符集合中查找。
------------------------------------------------------------------------------------------------------------------------
二、特权级:
我们在WIindows里使用Ring3程序也好、Ring0函数也好,在执行他们的时候或者调用他们的时候是按一定规则执行的? 当然Windows自己包装的规则不在讨论之类,.我们现在需要了解CPU最铁最硬的规则!!!
Ring0 -Ring3并不是属于WINDOWS的东西,而是属于CPU本身的!!! 那么当我们在使用程序的时候或者模块的时候cpu是如何鉴别程序特权级的呢?
1、CPL(Current Privilege Level)
CPL是指当前执行的程序或者任务的特权级,它存储在CS和SS的 0 到1位。CS、SS在这里都是段选择子了,(第2位是TI位代表LDT),这0-1位合起来可以表示Ring0-Ring3个级别.
通常情况下CPL等于代码本身所在段的特权级(DPL), 当程序跳转到不同特权级的代码段时,处理器将改变CPL, 以上指得是非一致代码段。 如果是一致代码段,一致代码段可以被同等级的或者低特权级访问,并且跳转过去后CPL也不会改变。
2、DPL(Descriptor Privilege Level)
DPL表示段或者门的特权级. 门其实也是一个段描述符来描述的。所以他们的DPL值就是段描述符属性里面的DPL位。下面来列举一些访问这些段时所需要的CPL值。
×数据段 :
一个代码段要想访问这个数据段,那么必需CPL(代码段) <= DPL(数据段)。
这样代码段才能正常访问此数据段。
×非一致代码段 :(不使用调用门的情况下)
访问非一致代码段的条件是 CPL == DPL(非一致代码段)
×调用门 :
访问调用门的条件: 跟数据一样 CPL(代码段) <= DPL(调用门段)
×一致代码段和通过调用门访问的非一致代码段:
访问一致代码段的条件: 必需CPL >= DPL
通过调用门访问非一致代码段条件: 必需CPL >= DPL
×TSS(任务状态段):
Tss是一个32位的结构体,它包含了100个字段。其中偏移量为4 -27里有3个ss和3个esp信息。它们记录着不同特权级任务切换时的堆栈信息。 由于只有特权级低到高切换任务时,新堆栈才会从TSS中取得。所以TSS里没有Ring3的SS ESP信息。
访问TSS的条件: 跟数据一样 CPL(代码段) <= DPL(TSS)
3、RPL(Request Privilege Level):
它存放在段选择子的 0-1这两位上面。 在访问目标段时CPU会做一系列检查。
其中RPL是访问目标段的关键:如果当前代码段CPL为0,要访问对方数据段的DPL值是1,而RPL值却是2。那么访问是不成功的。数据段的RPL值一定要<=DPL才能正常访问。。
------------------------------------------------------------------------------------------------------------------------
三、门(描述符 说得通俗点,门有点像函数指针):
门描述符分位4种:×调用门、×中断门、×陷阱门、×任务门。
程序从一段代码转移到另外一段代码时,目标代码的选择子会被加载到CS中,而在加载之前CPU还要对目标段描述符、界限、类型、特权级等信息做检查。检查通过后目标段选择子被加载到CS,然而EIP指向新偏移地址. 程序的转移可以由:jmp、call 、ret、sysenter、sysexit、int n、iret 这些命令来控制。
×用jmp或者Call指令进行转移的时:
直接用Call 指令转移时:
如果目标代码段是非一致代码段那么CPL必需等于目标DPL,同时要求RPL<=DPL.。
如果是一致代码段那么CPL必需>=目标DPL,RPL此时不做检查。转移过去后CPL不变。
由此看来call 、jmp 指令只适用于低特权向高特权转移。
那么怎样从高特权转移到低特权呢?继续看下面:
②、引用调用门:
门也是一种描述符,它跟Descriptor 一样大小都是8个字节。不过这个8个字节代表的意义大有不同之处。
首先看看这个Gate宏:
%macro Gate 4 ;门宏有4个参数:选择子、偏移量、调用者堆栈元素数量、属性
dw (%2 & 0FFFFh) ; 偏移 1 (2 字节)
dw %1 ; 选择子 (2 字节)
dw (%3 & 1Fh) | ((%4 << 8) & 0FF00h) ; 属性 (2 字节)
dw ((%2 >> 16) & 0FFFFh) ; 偏移 2 (2 字节)
%endmacro ; 共 8 字节
门描述符的字节意义说明:
第0、1个字节代表[偏移1]。
第2、3个字节代表段选择子。
第4、5个字节,其中第5个字节跟段描述符的第5个字节一样,TYPE(属性)、S(存储段而言,S=1代表不是系统段和门段。所以这里要变成0)、DPL(段特权级)、P(存在位)这几个位。而第4个字节是代表.调用者堆栈元素数量,这是要根据TSS完成调用者的堆栈元素复制到目标堆栈里。
第6、7个字节代表[偏移2]。与0、1[偏移1]按高高低低合并就可以得到,偏移值了。
调用门描述符的定义:
1、在GDT全局描述符里添加一个:
;门宏 、选择子 、偏移、ParamCount 、描述符属性
LABEL_CALL_GATE_TEST: Gate ,SelectorGateCode32, 0, 0 ,DA_386CGate
SelectorGateCode32只是一个普通的代码段选择子,只不过是被调用门来使用而已。
2、同样门也需要自己在GDT中的位置也就是选择子:
SelectorGateTest equ LABEL_CALL_GATE_TEST - LABEL_GDT
调用门描述符的使用:
Call SelectorGateTest:0 ;这个指令,CPU首先找到选择子在GDT的位置,然后检查选择子对应的描述符,是个什么描述符。 检查完毕以后就做对应的处理。
这里描述符是个门描述符所以CPU会进行门描述符对应的位功能检测,那么会得到:一个代码段的选择子,与偏移、等信息。 这就可以进行跳转了。将会跳转到SelectorGateCode32代码段选择子对应偏移为0的代码处。
;--------------------------------------------------------------------------------------------
四、让程序走进Ring3:
调用门的过程实际分为两部分:
1、从低特权向高特权访问, 要Call调用门指令。
2、从高特权向低特权 用Ret指令
在8086时期的Call指令短转移(段内)时,CPU会先将ip + 1 压栈。 等待ret 指令完成后才会弹出到新的ip。
而在长转移时,CPU会将 cs与ip 一起压栈.这样等待retf 指令完成后 才会弹出到对应的 ip cs。
JMP指令比较简单 只是目标操作数不同而已。
在386后不同特权级之间的堆栈是独立的,这样的话如果是远转移的话,那么就无法正确获得本身堆栈里的EIP信息了.因为都不是同一个堆栈了。那么386机制有个办法就是靠任务状态堆TSS来解决。在任务进行切换时,可以先从TSS里获得任务的重要信息。TSS是一个32位的结构体,含有100个字段。其中 4 -27字段就是3个特权级的堆栈信息。??特权级有4种,为什么确只记录3个堆栈信息呢。因为只有从低特权级到高特权级转移时,才能从TSS里获取到新堆栈信息.所以没有Ring4就无法在TSS里获Ring3的信息,
通过调用门从一个代码段跳转到另外一个代码段的整个步骤:
1、根据目标代码的DPL对应的特权级从TSS中选择SS和ESP (TSS结构体第4-27记录着3个特权级堆栈信息).如果目标代码DPL=1 那么 就选ss1 、esp1;(12 -16字段)。
2、第二步是检查所选择的ss1 esp1 或Tss是否有界限错误(ERR #TS)。
3、第三步对SS堆栈段选择子进行校验。
4、暂时性的保存当前SS和ESP的值,调用者自己的堆栈信息。
5、加载新的ss1和esp1到 ss、esp。
6、将刚才保存的当前ss、esp压栈。调用者自己的堆栈信息
7、从调用者的堆栈中将参数复制到新堆栈中,复制的数量由门描述符中的ParamCount指定。
8、将当前的cs 和下一个eip 压栈。
9、加载调用门指定的 CS EIP,开始执行新的代码段。此时的堆栈也是新的。
通过调用门方式转移时。只有低特权转高特权才能在TSS中获得新堆栈信息。如果不是低特权转高特权那么堆栈信息是不会改变的。用Call指令只能实现低特权到高特权。
通过调用门从一个代码段跳转到另外一个代码段后再返回到调用段的步骤:
1、检查保存在堆栈中的CS(也就是调用者调用时的CS值(段选择子))中的RPL ,以判断返回是否需要变换特权级,这个CS保存在新堆栈中弹出调用者eip后esp指向的地方。
2、加载新堆栈里的CS和EIP (也就是调用者的CS:EIP),此时会对加载的CS:eip进行校验。
3、如果Ret指令含有参数,则跳过Ret指定的数量。然后esp指向调用者的ss:esp信息在新堆栈中的位置。
Ret参数应该对应与门描述符的ParamCount。
4、加载堆栈里面的SS、esp到 ss:esp寄存器里,被调用者的ss esp 被覆盖。进行ss:esp校验。
5、.如果先前的Ret有参数。也就是代表是从调用者堆栈复制过去的参数,现在调用过程完成了,不需要这些参数了,同样在调用堆栈里也清理掉。 esp +参数。
6、检查ds、es、fs、gs的值,因为cs、ss都已经检验过了。要对其它的段寄存器进行检验了,如果哪一个段寄存器的段描述符DPL小于CPL(也就是段特权级大于了CPL.那么CPL不能对它进行访问了),那么一个空的描述符覆盖给此段寄存器。
Ret转移的方式跟Cal恰恰l相反,所以它适合从高特权转移到低特权。不过在转移的时候会经过一系列检查与堆栈操作。
*从Ring0 到RIng3的方法:
1、使用ret 指令(高特权到低特权):
可在32位实模式入口段( 这是个逆操作,可以看作是被Ring3 Call进来的.所以返回可以是retf):
push SelectStack3
push TpoOfStack3
push SecletorR3Code32
push 0
retf ;Retf的步骤上面有说明!
这就是从Ring0到RIng3的代码。
*从Ring3 到RIng0的方法
2、使用Call指令调用门(低特权到高特权):
从低特权到高特权需要新的堆栈段那么需要TSS数据段:首先就定义一个TSS数据段吧.
;-----------------TSS数据段-----------------
[SECTION .tss]
align 32 ;因为TSS本身是32位字段,那么就用32位对齐
[BITS 32]
LABEL_TSS:
dd 0 ;上一个任务链接
dd TopOfStack0 ;0级堆栈esp
dd SelectorStack0 ;0级堆栈ss 4-12字段
dd 0 ;1级堆栈
dd 0 ;1级堆栈
dd 0 ;2级堆栈
dd 0 ;2级堆栈 字段 r0 -r2
dd 0 ;cr3
dd 0 ;eip
dd 0 ;eflags
dd 0 ;eax
dd 0 ;ecx
dd 0 ;edx
dd 0 ;ebx
dd 0 ;esp
dd 0 ;ebp
dd 0 ;esi
dd 0 ;edi
dd 0 ;es
dd 0 ;cs
dd 0 ;ss
dd 0 ;ds
dd 0 ;fs
dd 0 ;gs
dd 0 ;LDT
dw $ - LABEL_TSS + 2 ;I/O位图基址
dw 0ffh ;I/O位图结束标志
TssLen equ $ - LABEL_TSS
;--------加载TSS------
mov ax,SelectorTss ;
ltr ax ;与加载LDT类似都是从GDT索引位置找到信息后,加载到对应的寄存器里.
下面来看看这个程序的来龙去脉吧:
1、入口段实模式:
进行GDT、等数据初始化。
然后跳转到SelectorCode32(Ring0);
2、SelectorCode32(Ring0)
首先显示一行字符串:"Hello Protect Mode".
;--------加载TSS------
mov ax,SelectorTss
ltr ax
;-------这是个逆向,这里看作是被Ring3 Call进来的.所以返回可以是retf
push SelectorStack3 ;ss
push TopOfStack3 ;esp
push SelectorR3Code32 ;cs
push 0 ;eip
retf ; Ring0 到Ring3
3、SelectorR3Code32(Ring3)
首先显示:字符"3"。
call SelectorGateTest:0 ;通过门进行低特权Ring3到高特权Ring0的转移
4、SelectorGateCode32(Ring0) 是调用门指向的代码段
首先显示:字符"0"。
mov ax,SelectorLdt
lldt ax
jmp SelectorLdtCode32:0 ;跳转到LDT代码区 ,
5、SelectorLdtCode32(Ring0)
首先显示:字符"L".
jmp SelectorCode16:0 回到实模式了。
【总结】
这个程序的整个路线就是这样了,在这里的知识点比较多。本人在学习的时候也碰到了很多问题。不过想不通的先借过,等了解到新知识后再返回来学习,效果会更好!
这里有以下几个重要知识点:
一、LDT(局部描述符)
局部描述段跟GDT的定义很相似。因为都是存放数据或者代码段的描述符集合!
LDT的加载是用 Lldt ax ;其中这个ax 是LDT描述符在GDT中的选择子。
那么LDT段的寻址方式略有不同。 jmp S:0 ;S 是LDT集合的一个选择子。其中TI位为1表示是LDT的选择子。这个时候CPU会从 LDTR寄存器指向的LDT集合中去找对应的选择子。
二、特权级(局部描述符)
特权级用在对数据访问的控制和对代码转移的控制.
对数据的访问比较简单: CPL <=目标DPL 并且 RPL<=DPL.
对代码的转移目前接触了:
1、直接用Call 、jmp指令进行转移。
当目标是非一致代码的时候,必需CPL==目标DPL,RPL<=DPL.也就是说必需是相等特权转移。
当目标是一致代码的时候,必需CPL<=目标DPL,RPL不做检查,转移后CPL不变。
所以直接用Call、Jmp 指令转移只能是低特权到高特权。如果目标是非一致那还必需等于、
2、CALL调用门:
Call 调用门 可以从低特权到高特权,并且不受非一致代码限制。 但是他要用到TSS.
3、Ret返回指令
Ret 是返回指令,Call的逆向。了解Ret的运作机制就可以 正确从高特权向底特权转移了。
特权级的标志也就是这3个: CPL、DPL、RPL。
接下来继续学习......Day Day Up