您的位置:首页 > 其它

保护模式下,读写大地址内存 & 从32位保护模式跳回16位实模式

2016-09-03 11:09 330 查看
一、主要功能

在本章最基本代码(P25、chapter3/a/)的基础上实现大地址(超过1M)的读写。在前面程序的基础上新建一个段,这个段以5MB为基址,远远超过1MB的界限。先读出开始处8字节的内容,然后写入一个字符串,再从中读出8字节。

如果读写成功的话,两次读出的内容应该不同,而且第二次读出的内容应该就是我们写进的字符串。字符串是保存在数据段中的,也是新增加的。

************************************************************程序流程************************************************************



************************************************************程序流程************************************************************

注意:

1. 几个段的解释

数据段(选择子为SelectorData)访问相关代码标记为红色(PMMessage将在保护模式下直接显示;StrTest则在保护模式下先被拷贝到SelectorTest对应的段,然后取出后显示);

测试段(选择子为SelectoTest)访问相关代码标记为绿色(这个段的段基址设置很大,只是为了展示保护模式访问大地址的能力。它的描述符中段基址不用在[SECTION
.s16]中精确设置,只需要设置为一个很大的值即可);

Normal(选择子为SelectorNormal)段表示为紫色,这个选择子在代码最后准备跳回实模式的段[SECTION .s16code]中有用到。

其中,我认为关键的一点是实模式和保护模式内存访问方式的区别:

同样采用[ds:esi]这样的形式——两者的esi均表示偏移量(当然,实模式中应该为si);ds含义不同。在实模式中,ds表示段基值;而在保护模式中,ds会在32位代码段开始处被设置为段对应的选择子设置方法如下。

汇编代码


[SECTION .s32]; 32 位代码段. 由实模式跳入.
[BITS 32]
LABEL_SEG_CODE32:
mov ax, SelectorData
mov ds, ax ; 数据段选择子
mov ax, SelectorTest
mov es, ax ; 测试段选择子
... ...

2. 代码段访问到>1MB内存地址,并不表示代码段中的指令本身在>1MB的地址

从这个程序可以看到,保护模式下可以访问超过1MB内存,但保护模式([SECTION .32])的代码仍然在1MB以内

3. 如何证明保护模式下可以访问超过1MB的地址:

进入32位代码段后,在保护模式下调用子例程"call TestRead --> call TestWrite --> call TestRead",在这些子例程中均出现访问大地址的代码,如下:

汇编代码


; 段基址, 段界限 , 属性
LABEL_DESC_TEST: Descriptor 0500000h, 0ffffh, DA_DRW
...

[SECTION .s32] ;进入这个32段前设置了cr0[0]=1,即进入本段就在“保护模式”下了
[BITS 32]
...
mov ax, SelectorTest ;注意到测试段(Test段)的段基址为0500000h远大于1M
mov es, ax

TestRead:
...
mov al, [es:esi]
...
TestWrite:
...
mov [es:edi], al
...

二、代码

%include "pm.inc" ; 常量, 宏, 以及一些说明

org 0100h

jmp LABEL_BEGIN

[SECTION .gdt]

; GDT

; 段基址, 段界限 , 属性

LABEL_GDT: Descriptor 0, 0, 0 ; 空描述符

LABEL_DESC_NORMAL: Descriptor 0, 0ffffh, DA_DRW ; Normal 描述符,段基址在此设置为0(后面没有再重设)

LABEL_DESC_CODE32: Descriptor 0, SegCode32Len-1, DA_C+DA_32; 非一致代码段, 32

LABEL_DESC_CODE16: Descriptor 0, 0ffffh, DA_C ; 非一致代码段, 16

LABEL_DESC_DATA: Descriptor 0, DataLen-1, DA_DRW ; Data

LABEL_DESC_STACK: Descriptor 0, TopOfStack, DA_DRWA+DA_32; Stack, 32 位

LABEL_DESC_TEST: Descriptor 0500000h, 0ffffh, DA_DRW

LABEL_DESC_VIDEO: Descriptor 0B8000h, 0ffffh, DA_DRW ; 显存首地址

; GDT 结束

; 注意:

; 16位实模式段基址是cs<<4+LABEL_SEG_CODE16

; 32位保护模式段基址是cs<<4+LABEL_SEG_CODE32

; 数据段基址是ds<<4+LABEL_DATA

; 堆栈段基址是ds<<4+LABEL_STACK

; 它们都将在16位实模式段[SECTION .s16]开始被设置

GdtLen equ $ - LABEL_GDT ; GDT长度

GdtPtr dw GdtLen - 1 ; GDT界限

dd 0 ; GDT基地址

; GDT 选择子

SelectorNormal equ LABEL_DESC_NORMAL - LABEL_GDT

SelectorCode32 equ LABEL_DESC_CODE32 - LABEL_GDT

SelectorCode16 equ LABEL_DESC_CODE16 - LABEL_GDT

SelectorData equ LABEL_DESC_DATA - LABEL_GDT

SelectorStack equ LABEL_DESC_STACK - LABEL_GDT

SelectorTest equ LABEL_DESC_TEST - LABEL_GDT

SelectorVideo equ LABEL_DESC_VIDEO - LABEL_GDT

; END of [SECTION .gdt]

[SECTION .data1] ; 数据段

ALIGN 32

[BITS 32]

LABEL_DATA:

SPValueInRealMode dw 0

; 字符串

PMMessage: db "In Protect Mode now. ^-^", 0 ; 在保护模式中显示

OffsetPMMessage equ PMMessage - $$

StrTest: db "ABCDEFGHIJKLMNOPQRSTUVWXYZ", 0

OffsetStrTest equ StrTest - $$

DataLen equ $ - LABEL_DATA

; END of [SECTION .data1]

; 全局堆栈段

[SECTION .gs]

ALIGN 32

[BITS 32]

LABEL_STACK:

times 512 db 0

TopOfStack equ $ - LABEL_STACK - 1

; END of [SECTION .gs]

[SECTION .s16]

[BITS 16]

LABEL_BEGIN:

mov ax, cs

mov ds, ax

mov es, ax

mov ss, ax

mov sp, 0100h

mov [LABEL_GO_BACK_TO_REAL+3], ax

mov [SPValueInRealMode], sp

; 初始化 16 位代码段描述符

mov ax, cs

movzx eax, ax

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

; 初始化 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位计算物理地址的方法(16位段基值左移4位,再加偏移量),但以前是加法器帮我们做的。

; 现在我们自己来计算这个物理地址(20bit),就需要用到32bit寄存器eax

xor eax, eax

mov ax, ds

shl eax, 4

add eax, LABEL_DATA

; 将计算出来的物理地址设置到数据段描述符对应位置

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

; 为加载 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 ; P35讲解为何要用dword

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

LABEL_REAL_ENTRY: ; 从保护模式跳回到实模式就到了这里

mov ax, cs

mov ds, ax

mov es, ax

mov ss, ax

mov sp, [SPValueInRealMode] //SPValueInRealMode到底有什么用呢,实模式下的sp为什么要这么精心的保存起来??????

in al, 92h ; `.

and al, 11111101b ; | 关闭 A20 地址线

out 92h, al ; /

sti ; 开中断

mov ax, 4c00h ; `.

int 21h ; / 回到 DOS

; END of [SECTION .s16]

[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 ; 视频段选择子

mov ax, SelectorStack

mov ss, ax ; 堆栈段选择子

mov esp, TopOfStack

; 下面显示一个字符串

mov ah, 0Ch ; 0000: 黑底 1100: 红字

xor esi, esi

xor edi, edi

mov esi, OffsetPMMessage ; 源数据偏移

mov edi, (80 * 10 + 0) * 2 ; 目的数据偏移。屏幕第 10 行, 第 0 列。(屏幕为25行80列,一个位置对应两个字节)

cld

.1:

lodsb

test al, al

jz .2

mov [gs:edi], ax

add edi, 2

jmp .1

.2: ; 显示完毕

call DispReturn

call TestRead

call TestWrite

call TestRead

; 到此停止,下面跳到的是最后一个段[SECTION .s16code],这个段准备跳回实模式

jmp SelectorCode16:0

; ------------------------------------------------------------------------

TestRead:

xor esi, esi

mov ecx, 8

.loop:

mov al, [es:esi]

call DispAL

inc esi

loop .loop

call DispReturn

ret

; TestRead 结束-----------------------------------------------------------

; ------------------------------------------------------------------------

TestWrite:

push esi

push edi

xor esi, esi

xor edi, edi

mov esi, OffsetStrTest ; 源数据偏移

cld

; 下面.1和.2的效果实际上就是将[ds:esi]若干字符放到[es:edi]若干位置中

; 注意到ds在32位代码段开始时被置为SelectorData;es在32位代码段开始时被置为SelectorTest

.1:

lodsb

; lodsb 指令:从esi 指向的源地址中逐一读取一个字符(默认数据段用SelectorData作选择子),送入AL 中; (见本博客《汇编

; (NASM)》部分)。注意到前面已将SelectorData放入ds中了。所以实际上lodsb就是“ mov al, [ds:esi] ”

;mov ax, SelectorData

;mov ds, ax ; 数据段选择子

test al, al ; 检测到最后一个字符00h,zf=0,就会跳转到.2

jz .2

mov [es:edi], al

inc edi

jmp .1

.2:

pop edi

pop esi

ret

; TestWrite 结束----------------------------------------------------------

; ------------------------------------------------------------------------

; 显示 AL 中的数字

; 默认地:

; 数字已经存在 AL 中

; edi 始终指向要显示的下一个字符的位置

; 被改变的寄存器:

; ax, edi

; ------------------------------------------------------------------------

DispAL:

push ecx

push edx

mov ah, 0Ch ; 0000: 黑底 1100: 红字

mov dl, al

shr al, 4

mov ecx, 2

.begin:

and al, 01111b

cmp al, 9

ja .1

add al, '0'

jmp .2

.1:

sub al, 0Ah

add al, 'A'

.2:

mov [gs:edi], ax

add edi, 2

mov al, dl

loop .begin

add edi, 2

pop edx

pop ecx

ret

; DispAL 结束-------------------------------------------------------------

; ------------------------------------------------------------------------

DispReturn:

push eax

push ebx

mov eax, edi

mov bl, 160

div bl

and eax, 0FFh

inc eax

mov bl, 160

mul bl

mov edi, eax

pop ebx

pop eax

ret

; DispReturn 结束---------------------------------------------------------

SegCode32Len equ $ - LABEL_SEG_CODE32

; END of [SECTION .s32]

; 16 位代码段. 由 32 位代码段跳入, 跳出后到实模式

[SECTION .s16code]

ALIGN 32

[BITS 16]

LABEL_SEG_CODE16:

; 回忆其描述符的定义如下,

; LABEL_DESC_NORMAL: Descriptor 0, 0ffffh, DA_DRW ; Normal 描述符,段基址在此设置为0(后面没有再重设)

; 书上P43最上面:在准备结束保护模式回到实模式之前,需要加载一个合适的描述符选择子(SelectorNormal==>(1)段基址:0 (2)段界限:0ffffh (3)属性:DA_DRW即可读写数据段)到有关段寄存器(ds,es,fs,gs,ss)。

mov ax, SelectorNormal

mov ds, ax

mov es, ax

mov fs, ax

mov gs, ax

mov ss, ax

mov eax, cr0

and al, 11111110b

mov cr0, eax

LABEL_GO_BACK_TO_REAL:

jmp 0:LABEL_REAL_ENTRY ;段间直接转移指令(far jump),跳回到16位实模式段[SECTION .s16]

; 这里使用“段间直接转移指令”,也称为“远转移far jump”。

; A. “直接”:目标地址像立即数一样,“直接”在指令的机器代码中就是直接寻址方式;

; B. “段间”:从当前代码段跳转到另一个代码段,此时需要更改CS段地址和IP偏移地址。

; LABEL_GO_BACK_TO_REAL:

; jmp 0:LABEL_REAL_ENTRY

; 第二行代码在这里指定了IP偏移是LABEL_REAL_ENTRY(这是跳回16位实模式段[SECTION .s16]的一个偏移);

; 第二行代码并没有指定CS段地址(直接复0),但是在前面的16位实模式段[SECTION .s16]中实际上直接操作了机器码,设置其CS段地址为cs的值(如下所示)

; (1) 实模式下长跳转指令:
; BYTE1 | BYTE2 BYTE3 | BYTE4 BYTE5
; OEAh | Offset | Segment

; (2) 在跳入32位代码段前的那个16位代码段对这里远跳转的段基址进行设置
; [SECTION .s16]
; [BITS16]
; LABEL_BEGIN:
; movax, cs
; movds, ax
; moves, ax
; movss, ax
; movsp, 0100h
; mov[LABEL_GO_BACK_TO_REAL+3], ax

Code16Len equ $ - LABEL_SEG_CODE16

; END of [SECTION .s16code]

三、关于保护模式和实模式的跳转和段描述符高速缓冲寄存器(转载)

其实从实模式跳转到保护模式还是很好懂得,主要注意就是跳转指令

汇编代码


jmp dword SelectorCode32:0
//而不能是
jmp SelectorCode32:0

因为这时编译出来的是16位代码。如果目标地址的偏移不是0,而是一个较大的值,比如

汇编代码


jmp SelectorCode32:0x12345678

则编译后偏移会被截断,只剩下0x5678。

所以,这个特殊的跳转需要特殊的处理。在Linux内核中(Linux使用的是AT&T汇编,不是一般常见的IBMPC汇编),这个跳转是用DB指令直接写二进制代码的方法实现的,而NASM显然提供了更好的解决方法,就是加一dword,本来dword应该加在偏移之前,但NASM允许加在整个地址之前,就像我们之前做的那样,这也是NASM的优点吧。

总之,一个程序中可以包含多个不同位的段,32位或者16位,他们之间也可以互相跳转,只是32位段用的是32位寄存器,16位代码段用的是16位寄存器,如果要在16位段下使用32位寄存器,必须象高级语言中强制类型转换一样,显式的定义 dword。

而从保护模式跳转回实模式则不是那么好理解了,因为在准备结束保护模式,回到实模式之前,需要加载一个合适的描述符选择子到有关的段寄存器,以使对应段描述符高速缓冲寄存器 (见后面的解释)中含有合适的段界限和属性(这里正确的段界限显然是64K,即0ffffh,属性应该是DA_DRW,即92h在内存中存在的可读写/数据段),而且,不能从32位代码段返回实模式,只能从16位代码段中返回。这是因为无法实现从32位代码段返回时cs高速缓冲寄存器中的属性符合实模式的要求(实模式不能改变段属性)。

**********************************************************************************************************************************

DA_DRW——在内存中存在的可读写代码段/数据段

汇编代码


DA_DRW EQU 92h ; 存在的可读写数据段属性值

92h=0000,0000,1001,0010,也就是将

TYPE设置为0010,表示可读写。见P36

S设置为1,表示是数据段/代码段描述符(如果S=0,表示是系统段/门描述符)。见P36

P设置为1,表示在内存中存在。见P35

**********************************************************************************************************************************

在实模式下,段寄存器含有段值,为访问存储器形成物理地址时,处理器引用相应的某个段寄存器并将其值乘以16,形成20位的段基地址。在保护模式下,段寄 存器含有段选择子,如上所述,为了访问存储器形成线性地址时,处理器要使用选择子所指定的描述符中的基地址等信息。为了避免在每次存储器访问时,都要访问描述符表而获得对应的段描述符,从80286开始每个段寄存器都配有一个高速缓冲寄存器,称之为段描述符高速缓冲寄存器 或描述符投影寄存器,对程序员而言它是不可见的。每当把一个选择子装入到某个段寄存器时,处理器自动从描述符表中取出相应的描述符,把描述符中的信息保存到对应的高速缓冲寄存器中。此后访问该段时,处理器都使用对应高速缓冲寄存器中的描述符信息,而不用再从描述符表中取描述符。

新增的Normal描述符,段界限64K,属性DA_DRW,在返回实模式之前把对应选择子SelectorNormal加载到ds、es和ss正好合适。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: