现在的规则变成这样:
一个跨代码段的函数调用例子如下:
; caller代码段
code1 segment
....
call someProc
....
code1 ends
;callee 代码段
code2 segment
....
someProc PROC
...
RET
someProc ENDP
code2 ends
[SECTION .gdt] ;定义GDT
; GDT
; 段基址, 段界限 , 属性
LABEL_GDT: Descriptor 0, 0, 0 ; 空描述符
LABEL_DESC_NORMAL: Descriptor 0, 0ffffh, DA_DRW ; Normal 描述符
LABEL_DESC_CODE32: Descriptor 0, SegCode32Len-1, DA_C+DA_32; 非一致代码段,32
LABEL_DESC_CODE16: Descriptor 0, 0ffffh, DA_C ; 非一致代码段,16
; **目标代码段的段描述符**
LABEL_DESC_CODE_DEST: Descriptor 0,SegCodeDestLen-1, DA_C+DA_32; 非一致代码段,32
LABEL_DESC_DATA: Descriptor 0, DataLen-1, DA_DRW ; Data
LABEL_DESC_STACK: Descriptor 0, TopOfStack, DA_DRWA+DA_32;Stack, 32 位
LABEL_DESC_LDT: Descriptor 0, LDTLen-1, DA_LDT ; LDT
LABEL_DESC_VIDEO: Descriptor 0B8000h, 0ffffh, DA_DRW ; 显存首地址
; **调用门描述符**
LABEL_CALL_GATE_TEST: Gate SelectorCodeDest, 0, 0, DA_386CGate+DA_DPL0
......
;**目标代码段选择子**
SelectorCodeDest equ LABEL_DESC_CODE_DEST - LABEL_GDT
;**门描述符选择子**
SelectorCallGateTest equ LABEL_CALL_GATE_TEST - LABEL_GDT
Caller代码段:
[SECTION .s32]
[BITS 32]
LABEL_SEG_CODE32:
......
call SelectorCallGateTest:0
; END of [SECTION .s32]
Callee代码段:
[SECTION .sdest]
[BITS 32]
LABEL_SEG_CODE_DEST:
;jmp $
mov ax, SelectorVideo
mov gs, ax ; 视频段选择子(目的)
mov edi, (80 * 12 + 0) * 2 ; 屏幕第 12 行, 第 0 列。
mov ah, 0Ch ; 0000: 黑底 1100: 红字
mov al, 'C'
mov [gs:edi], ax
retf
SegCodeDestLen equ $ - LABEL_SEG_CODE_DEST
; END of [SECTION .sdest]
JMP或Call后跟着48位全指针(16位段选择子+32位地址偏移),且其中的段选择子指向代码段描述符,这样的跳转称为直接(普通)跳转。普通跳转不能使特权级发生跃迁,即不会引起CPL的变化,看下面的详细描述:
要求:CPL >= DPL ,RPL不检查。
转跳后程序的CPL = 转跳前程序的CPL
要求:CPL = DPL AND RPL<= DPL
转跳后程序的CPL = 转跳前程序的CPL
当段间转移指令JMP和段间转移指令CALL后跟着的目标段选择子指向一个调用门描述符时,该跳转就是利用调用门的跳转。这时如果选择子后跟着32位的地址偏移,也不会被cpu使用,因为调用门描述符已经记录了目标代码的偏移。使用调门进行的跳转比普通跳转多一个步骤,即在访问调用门描述符时要将描述符当作一个数据段来检查访问权限,要求指示调用门的选择子的 RPL≤门描述符DPL,同时当前代码段CPL≤门描述符DPL,就如同访问数据段一样,要求访问数据段的程序的CPL≤待访问的数据段的DPL,同时选择子的RPL≤待访问的数据段或堆栈段的DPL。只有满足了以上条件,CPU才会进一步从调用门描述符中读取目标代码段的选择子和地址偏移,进行下一步的操作。
从调用门中读取到目标代码的段选择子和地址偏移后,我们当前掌握的信息又回到了先前,和普通跳转站在了同一条起跑线上(普通跳转一开始就得到了目标代码的段选择子和地址偏移),有所不同的是,此时,CPU会将读到的目标代码段选择子中的RPL清0,即忽略了调用门中代码段选择子的RPL的作用。完成这一步后,CPU开始对当前程序的CPL,目标代码段选择子的RPL(事实上它被清0后总能满足要求)以及由目标代码选择子指示的目标代码段描述符中的DPL进行特权级检查,并根据情况进行跳转,具体情况如下:
要求:CPL >= DPL ,RPL不检查,因为RPL被清0,所以事实上永远满足RPL <= DPL,这一点与普通跳转一致,适用于JMP和CALL。
转跳后程序的CPL = 转跳前程序的CPL,因此特权级没有发生跃迁。
要求:CPL = DPL (RPL被清0,不检查),若不满足要求则程序引起异常。
转跳后程序的CPL = DPL
因为前提是CPL=DPL,所以转跳后程序的CPL = DPL不会改变CPL的值,特权级也没有发生变化。如果访问时不满足前提CPL=DPL,则引发异常。
要求:CPL >= DPL(RPL被清0,不检查),若不满足要求则程序引起异常。
转跳后程序的CPL = DPL
当条件CPL=DPL时,程序跳转后CPL=DPL,特权级不发生跃迁;当CPL>DPL时,程序跳转后CPL=DPL,特权级发生跃迁,这是我们当目前位置唯一见到的使程序当前执行优先级(CPL)发生变化的跳转方法,即用CALL指令+调用门方式跳转,且目标代码段是非一致代码段。
typedef struct _CALL_GATE
{
USHORT OffsetLow;
USHORT Selector;
UCHAR NumberOfArguments:5;
UCHAR Reserved:3;
UCHAR Type:5;
UCHAR Dpl:2;
UCHAR Present:1;
USHORT OffsetHigh;
}CALL_GATE,*PCALL_GATE;
BASE23~16 BASE15~0 : 描述子所描述的那个段的段基地址。
Limit (段限): 该段的最后一个字节的偏移量,指明了该段的大小。
A: 该段是否被访问,该段已被访问过,则 A←1;该段未被访问过,则 A←0。该位与操作系统的时钟相结合,可进行段淘汰算法。
S: 描述子类型,1 代表数据代码段描述符;0 代表系统描述符(如门描述符/任务状态段描述符)
DPL: 共两位,规定可以访问该描述子所描述的那个段的任务的最低特权级。
P: 0 表示该描述子所描述的段不在物理空间;1 表示该描述子所描述的段在物理空间。
TYPE:由三位构成,即数据段(E, ED, W) 或代码段(E, C, R)。这个TYPE对所描述的存储段的具体属性有着极其重要的意义。
若该段为数据段,则 E=0,需要配合TYPE的ED和W。ED为0则段向上生长,所以要求偏移量小于Limit(段限);ED为1则段向下生长,所以要求偏移量大于Limit(段限);W为0则该数据段只能读,不能写;如果W为1则该数据段可读、可写。
若该段为代码段,则E=1,需要配合TYPE的C和R(ED变成了C,W变成了R)。C为0则非一致性代码段访问和被访问代码段特权级相同;C为1则一致性代码段访问和被访问代码段特权级可以不同;R为0则代码段只能执行,不能读;R为1代码段可以执行,也可以读。