引言
这篇文章从机器码的角度介绍32位汇编下控制转移类指令的分类。搞懂这些分类很重要,因为所有的基本块都是以控制转移指令结尾的,分清这些指令对应的机器码有助于我们对基本块进行修改和变形,从而实现对程序的修改。
总体描述
控制转移指令包括四大类指令:
- 函数调用指令(call)
- 函数返回指令(ret)
- 无条件转移指令(jmp)
- 条件转移(如jne)
学过汇编的知友应该都知道,很久以前在DOS系统下,控制转移类指令都分为段内跳转和段间跳转,但是CPU进入32位后,寄存器大小已经足够放下整个内存地址,所以也就不存在什么段内和段间跳转了。但是还存在相对地址和绝对地址的区别,这个后面具体分析。
很多书上都提到如直接跳转与间接跳转,立即数寻址和内存寻址,这些专业术语很教条,而且不同书上说的也会有出入,其实指令的本质是机器码,下面就让我们一起站在机器码的视角,从本质上吃透这些概念彻底搞定控制转移类指令。
Call指令
一条call指令的字节数可以是(2,5,6)byte,对应不同的操作数,下面分情况讨论。
- 假设我们在函数src中调用函数dst,对应的call指令是的机器码长度是5 byte,其中第一个字节是e8代表指令,后面四个字节是一个相对偏移offset,dst=src_next+offset,其中src_next是下一条指令的地址,也可以看出是当前指令的地址加上该指令的长度。可以称这种情况为直接转移。
- 假设有一个全局函数指针变量g_pDst指向dst函数,我们在函数src中call这个变量,则对应的机器码长度是6 byte,前两个字节是ff 15代表指令,后面四个字节是一个绝对地址即变量g_pDst的地址。可以称这种情况为内存间接寻址。
- 假设函数src中有一个函数指针变量pDst指向dst函数,在函数src中call该变量,因为这个指针式保存在栈中的,编译时无法知道它的全局位置,只知道它相对于栈底的位置,所以根据pDst距离栈底ebp的距离。当距离小于0xff时,机器码长度为3,前两个字节是ff 55代表指令,后面一个字节代表相对ebp的偏移(注意偏移都是负数,比如f8,则ebp-8即为实际内存地址)。当距离大于0xff时,机器码长度为6,前两个字节为ff 95表指令,后四个字节是相对ebp的偏移,计算方法与上面相同。可以称这种情况为栈间接寻址。
- call寄存器,比如call eax、call ebx;还有call eax + 4。可以称这种情况为寄存器直接寻址。
- call寄存器间接转移,比如call [eax],call [ebx];以及call [eax] + 4。可以称这种情况为寄存器间接寻址。
注:对于第一种情况,还有短转移,也就是偏移为一个字节,但是有些编译器(如MSVC)不会编译这种短的call,总之现在比较少见。另外可能有些写过汇编的知友会问,为什么没有call functionA + 10 这类代码呢,这是因为编译器在编译的时候会自动计算相加后的地址。
Ret指令
ret指令比较简单,只有两种情况,ret和ret n。
Jmp指令
jmp指令的分类和call指令几乎完全一样,不同之处仅在于指令的机器码不同(废话)。
条件转移指令
条件转移类指令与jmp指令的分类大体类似,但是有一个重要的区别就是,条件转移类指令只有直接转移,没有后面四种,所以条件转移类指令全部是相对偏移的,每种指令可分为长跳转和短跳转。
指令机器码对照表
(不知如何在知乎插表格,只能用图片代替了)