一、预备理论
1.关于堆栈
我们都知道,可以通过call和jmp实现长跳转或者短跳转(段内转移或者段间转移)。但是jmp和call毕竟是有分别的,其中call指令是会影响到堆栈的:对于短调用,call指令执行时,下一条指令的eip入栈,ret指令执行时候=,这个eip就会从堆栈中弹出。
短调用call指令执行前后堆栈的变化如下图所示(注意带有参数的ret指令):短调用返回时候的示意图:
长调用的情况与此相似,不同的是call指令执行时,被压栈的还有cs。
2.特权级变换
注意到:如果我们采用call指令进行长跳转,而且特权级别发生了变换,这个时候就发生了堆栈切换,比如从A切换到了B。但是我们的数据保存在A上面,如何从B中返回呢?原来,intel提供了一种机制,能够将A上的一些内容copy到B堆栈中。同时,A堆栈要切换到B堆栈,但是它怎么知道B堆栈在哪里呢答案:原来这个内容保存在任务的TSS结构中,TSS是任务状态段。每个任务都最多有四个堆栈段,但是只有一个esp和ss,TSS就解决了堆栈切换方面的数据保存问题。TSS的相关介绍,可以参考:保护模式及其编程——任务切换http://blog.csdn.net/trochiluses/article/details/19768579。
示意图如下:
比如,我们目前是ring 3,需要转移到ring 1,那时,堆栈将自动切换到ss1和esp1。(由于只是从外层到内层切换,才从TSS中取得堆栈信息,所以,TSS中没有ring 3相关的堆栈信息。那么从内层向外层切换,如何获得目的地的信息呢?提前压入栈中,来获得返回信息)。
切换过程中有关堆栈的处理,可以参考:保护模式及其编程——保护的详尽意义:通过调用门转移特权级http://blog.csdn.net/trochiluses/article/details/19573651
二、代码分析
这一部分代码与pmtest4中的代码比较相似,我们仅仅用加粗部分标记改变和增加的代码,没有改变而且不是特别影响逻辑的代码我们直接省略,来进行分析:
; ==========================================
%include "pm.inc"; 常量, 宏, 以及一些说明
org 0100h
jmp LABEL_BEGIN
[SECTION .gdt]
; GDT
; 段基址, 段界限 , 属性
LABEL_GDT: Descriptor 0, 0, 0 ; 空描述符
LABEL_DESC_CODE_RING3:Descriptor 0, SegCodeRing3Len - 1, DA_C + DA_32 + DA_DPL3; 非一致代码段, 32
LABEL_DESC_STACK: Descriptor 0, TopOfStack, DA_DRWA + DA_32 ; Stack, 32 位;没有标记的stack,实际上是ring 0
LABEL_DESC_STACK3: Descriptor 0, TopOfStack3, DA_DRWA + DA_32 + DA_DPL3; Stack, 32 位;ring 3
LABEL_DESC_LDT: Descriptor 0, LDTLen - 1, DA_LDT ; LDT
LABEL_DESC_TSS: Descriptor 0, TSSLen - 1, DA_386TSS ; TSS
LABEL_DESC_VIDEO: Descriptor0B8000h, 0ffffh, DA_DRW + DA_DPL3; 显存首地址,为了能在ring 3中读写显存,我们改变了显存段的特权级别
; 门 目标选择子, 偏移, DCount, 属性
LABEL_CALL_GATE_TEST: Gate SelectorCodeDest, 0, 0, DA_386CGate + DA_DPL3;门描述符的特权级别也是ring 3,不然没法访问
; GDT 结束
GdtLen equ $ - LABEL_GDT ; GDT长度
GdtPtr dw GdtLen - 1 ; GDT界限
dd 0; GDT基地址
; GDT 选择子
SelectorNormal equLABEL_DESC_NORMAL - LABEL_GDT
SelectorCodeRing3equ LABEL_DESC_CODE_RING3- LABEL_GDT + SA_RPL3
SelectorStack3equ LABEL_DESC_STACK3- LABEL_GDT + SA_RPL3
SelectorTSSequ LABEL_DESC_TSS- LABEL_GDT
SelectorCallGateTestequ LABEL_CALL_GATE_TEST- LABEL_GDT + SA_RPL3
; END of [SECTION .gdt]
;========================================================================================================
[SECTION .data1] ; 数据段
SPValueInRealMode dw 0
; 字符串
PMMessage: db "In Protect Mode now. ^-^", 0 ; 进入保护模式后显示此字符串
;*******************************
DataLen equ $ - LABEL_DATA
; END of [SECTION .data1]
===============================================================================================
; 全局堆栈段
[SECTION .gs]
LABEL_STACK:
times 512 db 0
TopOfStack equ$ - LABEL_STACK - 1
; END of [SECTION .gs]
================================================================================================
; 堆栈段ring3
[SECTION .s3]
ALIGN 32
[BITS 32]
LABEL_STACK3:
times 512 db 0
TopOfStack3 equ$ - LABEL_STACK3 - 1
; END of [SECTION .s3]
=================================================================================================
; TSS ---------------------------------------------------------------------------------------------
[SECTION .tss]
ALIGN 32
[BITS 32]
LABEL_TSS:
DD 0; Back
DD TopOfStack; 0 级堆栈
DD SelectorStack;
DD 0; 1 级堆栈
DD 0;
; TSS ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
========================================================================================================
[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
;********************
mov byte [LABEL_DESC_CODE16 + 7], ah
; 初始化 32 位代码段描述符
; 初始化测试调用门的代码段描述符
; 初始化数据段描述符
; 初始化堆栈段描述符
; 初始化堆栈段描述符(ring3)
xor eax, eax
mov ax, ds
shl eax, 4
add eax, LABEL_STACK3
mov word [LABEL_DESC_STACK3 + 2], ax
shr eax, 16
mov byte [LABEL_DESC_STACK3 + 4], al
mov byte [LABEL_DESC_STACK3 + 7], ah
; 初始化 LDT 在 GDT 中的描述符
; 初始化 LDT 中的描述符
; 初始化Ring3描述符
xor eax, eax
mov ax, ds
shl eax, 4
add eax, LABEL_CODE_RING3
mov word [LABEL_DESC_CODE_RING3 + 2], ax
shr eax, 16
mov byte [LABEL_DESC_CODE_RING3 + 4], al
mov byte [LABEL_DESC_CODE_RING3 + 7], ah
; 初始化 TSS 描述符
xoreax, eax
mov ax, ds
shl eax, 4
add eax, LABEL_TSS
mov word [LABEL_DESC_TSS + 2], ax
shr eax, 16
mov byte [LABEL_DESC_TSS + 4], al
mov byte [LABEL_DESC_TSS + 7], ah
; 为加载 GDTR 作准备
; 加载 GDTR
; 关中断
; 打开地址线A20
; 准备切换到保护模式
; 真正进入保护模式
jmp dword SelectorCode32:0; 执行这一句会把 SelectorCode32 装入 cs, 并跳转到 Code32Selector:0 处
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
LABEL_REAL_ENTRY: ; 从保护模式跳回到实模式就到了这里
mov ax, cs
关闭 A20 地址线
; 开中断
; ┛回到 DOS
; END of [SECTION .s16]
==============================================================================================
[SECTION .s32]; 32 位代码段. 由实模式跳入.
[BITS 32]
LABEL_SEG_CODE32:
movax, SelectorData
movds, ax ; 数据段选择子
; 视频段选择子
; 堆栈段选择子
movesp, TopOfStack
; 下面显示一个字符串
; 显示完毕
call DispReturn
; Load TSS
movax, SelectorTSS
ltrax ; 在任务内发生特权级变换时要切换堆栈,而内层堆栈的指针存放在当前任务的TSS中,所以要设置任务状态段寄存器 TR。
push TopOfStack3 ;sp3
push SelectorCodeRing3 ;cs3
push 0 ;Eip
retf ; Ring0 -> Ring3,历史性转移!将打印数字 '3'。
注意:这一个关键的retf指令,它弹出两个值,一个给EIP,另一个给ECS;通过这种方式,转移到ring 3;这里要特别关注retf指令,它用于长返回,而且可以用于带有特权级别变换的长返回。上面之所以要push 4个参数,是因为retf要用到这四个参数。之所以要loadTSS,是因为待会还要从ring3回到ring 0.
另外,需要特别注意的是此处的TSS;如果没有TSS,我们只能从高特权级别转向低特权级;没法从低特权级别转向高特权级别。也就是说,如果涉及到堆栈切换,那么必须要用到TSS。我们可以将ltr ax这句注释掉,然后查看结果(见最后一副图)。
; ------------------------------------------------------------------------
SegCode32Len equ$ - LABEL_SEG_CODE32
; END of [SECTION .s32]
;=======================================================================================================
[SECTION .sdest]; 调用门目标段
[BITS 32]
LABEL_SEG_CODE_DEST:
mov ax, SelectorVideo
mov gs, ax; 视频段选择子(目的)
movedi, (80 * 12 + 0) * 2 ; 屏幕第 12 行, 第 0 列。
mov ah, 0Ch; 0000: 黑底 1100: 红字
mov al, 'C'
mov [gs:edi], ax
; Load LDT
mov ax, SelectorLDT
lldt ax
jmpSelectorLDTCodeA:0 ; 跳入局部任务,将打印字母 'L'。
;retf
SegCodeDestLenequ $ - LABEL_SEG_CODE_DEST
; END of [SECTION .sdest]
======================================================================================
; 16 位代码段. 由 32 位代码段跳入, 跳出后到实模式
[SECTION .s16code]
ALIGN 32
[BITS 16]
LABEL_SEG_CODE16:
; 跳回实模式:
mov ax, SelectorNormal
;******************************************
LABEL_GO_BACK_TO_REAL:
jmp 0:LABEL_REAL_ENTRY; 段地址会在程序开始处被设置成正确的值
Code16Len equ $ - LABEL_SEG_CODE16
; END of [SECTION .s16code]
=============================================================================
; LDT
[SECTION .ldt]
ALIGN 32
LABEL_LDT:
; 段基址 段界限 , 属性
LABEL_LDT_DESC_CODEA: Descriptor 0, CodeALen - 1, DA_C + DA_32 ; Code, 32 位
LDTLen equ $ - LABEL_LDT
; LDT 选择子
SelectorLDTCodeA equLABEL_LDT_DESC_CODEA - LABEL_LDT + SA_TIL
; END of [SECTION .ldt]
==================================================================================
; CodeA (LDT, 32 位代码段)
[SECTION .la]
ALIGN 32
[BITS 32]
LABEL_CODE_A:
mov ax, SelectorVideo
mov gs, ax; 视频段选择子(目的)
movedi, (80 * 13 + 0) * 2 ; 屏幕第 13 行, 第 0 列。
mov ah, 0Ch; 0000: 黑底 1100: 红字
mov al, 'L'
mov [gs:edi], ax
; 准备经由16位代码段跳回实模式
jmp SelectorCode16:0
CodeALen equ $ - LABEL_CODE_A
; END of [SECTION .la]
=======================================================================================
; CodeRing3
[SECTION .ring3]
ALIGN 32
[BITS 32]
LABEL_CODE_RING3:
mov ax, SelectorVideo
mov gs, ax; 视频段选择子(目的)
movedi, (80 * 14 + 0) * 2 ; 屏幕第 14 行, 第 0 列。
mov ah, 0Ch; 0000: 黑底 1100: 红字
mov al, '3'
mov [gs:edi], ax
callSelectorCallGateTest:0 ; 测试调用门(有特权级变换),将打印字母 'C'。
jmp$ ;注意:程序将停在这里
; END of [SECTION .ring3]
程序的大致执行过程如下:
保护模式下进入16b的代码段,ring=0,初始化相关段描述符、门描述符、ldt描述符、数据段描述符、堆栈段描述符、ldt中的描述符、ring 3的代码段描述符TSS描述符;加载gdt,通过长jmp,进入保护模式,进入32b的代码段,ring =0:初始化数据段、堆栈段、视频段的选择子寄存器;显示一个字符串;load TSS;利用retf指令,从ring 0转移到ring 3的代码段:在代码段中显示‘3’,然后通过调用门进入ring 0的代码段:打印字母C,然后通过jmp跳转到局部代码段:打印字母‘L’;程序回到ring 3,成为循环。
查看运行结果:我们第一次进入了不同的特权级。
下图:注释TSS的结果,可以看到可以从ring 0进入ring 3;但是无法从ring 3进入ring 0.