汇编语言入门之 call 指令
汇编语言作为最为底层的语言,是逆向无法绕过的一个门槛。在逆向 macOS 内核二进制的时候,发现execsw
符号调用发生变化,上文 已对此进行说明,os14 不再间接使用execsw
结构体常量数组进行函数调用,而是直接调用相关函数。虽然这两处函数调用均反汇编为call
指令,但对应的操作码并不一样。实际上call
指令的操作码有多个。
call 指令
call
指令的操作码及对应含义如下表。主要分为相对近调用、绝对间接近调用、绝对远调用等。表中cd
表示 4 字节,/2
表示所在字节为ModR/M
,用于指定存储操作数的寄存器或内存地址。这里的 near、far 是指针对当前代码段而言,近调用即段内调用,指 CS 寄存器在调用中不会发生更改,远调用即段间调用。另外近调用的过程返回时使用retn
操作码,远调用的过程返回时使用retf
操作码。感兴趣的可以阅读《Intel® 64 and IA-32 Architectures Software Developer’s Manual》 。
Opcode | Instruction | Description |
---|---|---|
E8 cd | call re/32 | Call near, relative, displacement relative to next instruction. |
FF /2 | call r/m32 | Call near, absolute indirect, address given in register or memory. |
9A cd | call ptr16:16 | Call far, absolute, address given in operand. |
FF /3 | call m16:32 | If selector points to a gate, then RIP = 64-bit displacement taken from gate; else RIP = zero extended 32-bit offset from far pointer referenced in the instruction. |
这里贴一下手册的 [Table 2-2. 32-Bit Addressing Forms with the ModR/M Byte],便于了解FF
开头的调用指令如何解释。注意红框圈出的部分,本文将涉及该指令。从表可知,FF15
和FF1D
指令的操作数均为 32 位立即数,只不过前者是近调用,后者是远调用。
call near
首先关注一下近调用。执行近调用时,处理器将 EIP 寄存器的值(包含当前call
指令到下一条指令的偏移)压栈,然后处理器执行目标地址的代码,目标地址的计算如下。
目标地址 = 下一条指令地址(当前指令地址 + 当前指令长度) + 相对偏移量
这里写一个 demo 查看两种近调用指令,代码如下。代码很简单,主要测试直接调用函数和使用结构体偏移间接调用函数最终的编码有何不同。
1 |
|
编译出的二进制使用 ida 反汇编,结果如下。main
函数中的两处函数调用的指令操作码是不同的,其中直接调用是E8
,通过结构体偏移调用是FF15
。E8C5FFFFFF
表示相对近调用,相对偏移量为FFFFFFC5
,这里高地址表示高字节,所以类似0x1234
在二进制中是以3412
显示的。偏移量高字节均是 F,说明偏移量为负,实际地址为0x100003F86+5+0xFFFFFFC5
,然后把进位抹掉,结果是0x100003F50
,即_func
自定义函数所在地址。
1 | __text:0000000100003F50 _func |
FF15
也表示近调用。根据官方解释,FF15
和E8
调用指令的区别在于,FF15
是间接函数调用,其中要调用的函数的地址是从内存或者寄存器加载的绝对地址,而E8
是一个直接函数调用,其中要调用的函数的地址通过相对 EIP 的偏移量指定。但在 macOS 中,感觉这两者都是通过偏移量指定函数地址,不同点在于FF15
可以调用非__text
代码段的符号。
感兴趣的可以尝试计算偏移,将这里的E8
指令后的偏移改为_user_func
结构体符号的偏移,运行后会报错 bus error。上文所提旧版本的 macOS 内核调用execsw
符号便是使用FF15
,os14 则是使用E8
直接调用。
call far
远调用正常的编码不太容易实现,这里使用汇编语言写一个最简的远调用例程。这里显式指定调用方式为call far
,需要注意被调用函数应使用retf
作为返回值。编译命令为nasm -fmacho64 ./far_call.asm
,生成 Mach-o 格式的 64 位二进制。
1 | rel |
编译后结果如下。这里使用的call far
被编译为FF1D
,从上表可知,指令表示远调用,且操作数为 32 位立即数。实际上上述代码实际编译出来的二进制很短,代码段数据仅有 8 字节:DB48FF1DF8FFFFFF
,那么根据上述计算,执行调用命令后 IP 寄存器位置是 0x8,加上偏移量-8,则函数地址为 0,符合编译结果。
1 | __text:0000000000000000 func proc far ; DATA XREF: _start↓r |
以上为个人学习总结,其中解释可能有错误之处,欢迎读者指出。
- Title: 汇编语言入门之 call 指令
- Author: 孙康
- Created at : 2023-07-28 22:12:00
- Updated at : 2023-08-30 11:29:14
- Link: https://conradsun.github.io/2023/0734337142.html
- License: This work is licensed under CC BY-NC-SA 4.0.