0x01 短调用与长调用
在会汇编中,JMP指令可以实现EIP的跳转,同样通过JMP FAR可以实现段间的跳转。但是在通常情况下,用跳转的情况还是比较少的,往往都是用CALL进行调用为主。如果要实现跨段的调用就必须要学习CALL FAR,也就是长调用。CALL FAR 比 JMP FAR 要复杂,但同样的,JMP并不影响堆栈,但CALL指令会影响。
短调用
指令格式:
CALL 立即数 / 寄存器 / 内存
# 发生改变的寄存器:ESP EIP
短调用也是我们平常最常见的call,比如在某函数中调用本程序定义的另一个函数,一般都是用这种短调用实现的
长调用(跨段不提权)
指令格式:
CALL CS:EIP(EIP是废弃的)
# 发生改变的寄存器:ESP EIP CS,返回时使用retf恢复状态
长调用(跨段提权)
指令格式:
CALL CS:EIP(EIP是废弃的)
# 发生改变的寄存器:ESP EIP CS SS
注意:长调用执行后,堆栈已经不是原来的堆栈,而是0环的堆栈(ESP0),当返回时,需要用retf指令恢复状态
总结
-
CS的权限一旦改变,SS的权限也要随着改变,CS与SS的等级必须一样。所以跨段调用时,一旦有权限切换,就会切换堆栈
-
对于非一致性代码的成功转移,CPL被目的代码的DPL刷新,会引起堆栈切换;对于一致性代码,虽然允许低特权级向高特权级进行跳转,但因为CPL不会刷新(用户态仍为用户态),所以SS也不会切换。
-
JMP FAR 只能跳转到同级非一致代码段,但CALL FAR可以通过调用门提权,提升CPL的权限
那么SS与ESP是从哪里来的?参见 Windows保护模式学习笔记(3)—— 任务状态段与任务门
0x02 调用门
前面我们介绍过,一致代码段低级别的程序可以在不提升CPL权限等级的情况下进行访问,并且不会破坏内核态的数据。但是如果是非一致代码段,是禁止不同级之间进行访问的。那如果这个时候一定要访问非一致代码段(普通代码段),必须通过"调用门"等方式提升CPL权限。
调用门用于在不同特权级之间实现受控的程序控制转移,通常仅用于使用特权级保护机制的操作系统中。本质上,它只是一个描述符,一个不同于代码段和数据段的描述符,可以安装在GDT或者LGT中,但是不能安装在IDT(中断描述符表)中。它主要是定义了目标代码对应段的选择子、入口地址的偏移和一些属性等。
指令格式:
CALL CS:EIP(EIP是废弃的)
执行步骤:
- 根据CS的值查GDT表,找到对应的段描述符 这个描述符是一个调用门
- 在调用门描述符中存储另一个代码段的段选择子
- 段选择子指向的段 段.Base + 偏移地址 就是真正要执行的地址(注意:这里的偏移地址是门描述符内的偏移地址,即门描述符的低四字节的0-15位 )
门描述符
结构跟代码段以及数据段描述符大不相同,门描述符结构如下
S位: 表示描述符的类型。1 数据段和代码段描述符;0 系统段描述符和门描述符。因此门描述符的S位必为0
Type域: 当S位为0,Type域为1100时,该描述符才为门描述符
Segment Selector: 低四字节的16~31位是决定 调用的代码存在于哪个段 的 段选择子
offset in segment:低四字节的0-15位是决定跳转位置的段内偏移
当长调用执行时,真正要执行的代码地址 = 门描述符中段选择子所指向的代码段的Base + 门描述符高四字节的16~31位 + 门描述符低四字节的0~15位
调用门权限检查
通过调用门进行程序的转移控制时,CPU会检查以下这几个字段:
- 当前代码段的CPL;
- 调用门描述符中的DPL;
- 调用门描述符中的RPL;
- 目的代码描述符的DPL;
- 目标代码段描述符中的一致性标志C(一致与非一致下面会提到)。
如下图:
并且调用门的权限检查对于call和jmp指令,因为jmp指令不可用调用门提升CPL,所以有着不同的优先级检查规则的:
-
对call来说:当前CPL<=调用门描述符DPL,RPL<=调用门描述符DPL,当前CPL>=目的代码段描述符DPL;
-
对jmp来说:除了跟call的 “当前CPL<=调用门描述符DPL,RPL<=调用门描述符DPL” 一样外,如果目的代码段的一致的话,CPL>=目的代码段的DPL,而如果目的代码段是非一致的话,CPL=目的代码段的DPL。
0x03 中断门&陷阱门
中断门、陷阱门与调用门有大量的相似之处,学习调用门是为了更好的理解中断门与陷阱门
中断
在介绍中断描述符和中断门之前有必要先来解释一下中断的概念。(这里对中断只是一个笼统的介绍,主要目的是对中断门的学习,关于中断与异常,详见Windows保护模式学习笔记(6)—— 中断与异常)
中断,即指当出现需要时,CPU暂时停止当前程序的执行转而执行处理新情况的程序和执行过程。即在程序运行过程中,系统出现了一个必须由CPU立即处理的情况,此时,CPU暂时中止程序的执行转而处理这个新的情况的过程就叫做中断。
比如:除零(0号中断)、断点(3号中断)、系统调用(2e号中断)、以及异常处理等都会引发中断,所以自然需要相应的中断例程去进行处理。这样操作系统就会用数据结构来维护这些中断例程,这个数据结构就是IDT(Interrupt Descriptor Table)。
中断描述符表(IDT)
IDT(Interrupt Descriptor Table)即中断描述符表,其存储着中断与异常的处理程序。IDT同GDT一样,IDT也是由一系列描述符组成的,每个描述符占8个字节,但要注意的是,IDT表中的第一个元素不是NULL。GDT有GDTR寄存器保存GDT表的入口地址,IDT也有着IDTR寄存器保存它的入口地址,IDTL则存储着IDT表边界的偏移值。从偏移值0x7FF可以看出,IDT表总长为2048字节。
IDT表的每个表项长8字节,其中每一项也称为“门描述符”,之所以这样称呼,是因为IDT表项的基本用途就是引领CPU从一个空间到另一个空间去执行,每个表项好像是一个空间到另一个空间的大门。IDT表中可以包含以下3种门描述符:
-
任务门描述符:用于任务切换,里面包含用于选择任务状态段(TSS)的段选择子。可以使用JMP或CALL指令通过任务门来切换到任务门所指向的任务,当CPU因为中断或异常转移到任务门时,也会切换到指定任务。
-
中断门描述符:用于描述中断例程的入口。
-
陷阱门描述符:用于描述异常处理例程的入口。
以下为三种门描述符的内存布局:
中断门(Interrupt Gate) 与陷阱门(Trap Gate)的区别
这二者直接几乎没有区别,唯一的区别在于中断门执行时,会将IF标志位清零,但陷阱门不会。IF是中断允许标志位,它用来控制8086是否允许接收外部中断请求。若IF=1,8086能响应外部中断,反之则屏蔽外部中断。这样的设计使得通过陷阱门进入的服务程序运行嵌套中断,而中断门进入的服务程序不允许嵌套中断的发生。
PS:这里说的IF=0时屏蔽的中断是 可屏蔽中断 ,还有一些中断是不可屏蔽的。比如程序正在运行时,我们通过键盘敲击了锁屏的快捷键,若IF位为1,CPU就能够接收到我们敲击键盘的指令并锁屏;然而断电时,电源会向CPU发出一个请求,这个请求叫作不可屏蔽中断,此时不管IF位是否为0,CPU都要去处理这个请求