汇编语言入门之 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开头的调用指令如何解释。注意红框圈出的部分,本文将涉及该指令。从表可知,FF15FF1D指令的操作数均为 32 位立即数,只不过前者是近调用,后者是远调用。

ModR/M 32位寻址表
ModR/M 32位寻址表

call near

首先关注一下近调用。执行近调用时,处理器将 EIP 寄存器的值(包含当前call指令到下一条指令的偏移)压栈,然后处理器执行目标地址的代码,目标地址的计算如下。

目标地址 = 下一条指令地址(当前指令地址 + 当前指令长度) + 相对偏移量

这里写一个 demo 查看两种近调用指令,代码如下。代码很简单,主要测试直接调用函数和使用结构体偏移间接调用函数最终的编码有何不同。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <stdio.h>

int func(void) {
printf("call func\n");
return 0;
}

struct user_func {
int(*const func)(void);
const char *func_name;
} const user_func[] = {
{ func, "func" },
{ NULL, NULL}
};

int main(int argc, const char * argv[]) {
func();
user_func[0].func();
return 0;
}

编译出的二进制使用 ida 反汇编,结果如下。main函数中的两处函数调用的指令操作码是不同的,其中直接调用是E8,通过结构体偏移调用是FF15E8C5FFFFFF表示相对近调用,相对偏移量为FFFFFFC5,这里高地址表示高字节,所以类似0x1234在二进制中是以3412显示的。偏移量高字节均是 F,说明偏移量为负,实际地址为0x100003F86+5+0xFFFFFFC5,然后把进位抹掉,结果是0x100003F50,即_func自定义函数所在地址。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
__text:0000000100003F50                 public _func
__text:0000000100003F50 _func proc near ; CODE XREF: _main+16↓p
__text:0000000100003F50 push rbp
__text:0000000100003F51 mov rbp, rsp
__text:0000000100003F54 lea rdi, aCallFunc ; "call func\n"
__text:0000000100003F5B mov al, 0
__text:0000000100003F5D call _printf
__text:0000000100003F62 xor eax, eax
__text:0000000100003F64 pop rbp
__text:0000000100003F65 retn
__text:0000000100003F65 _func endp

__text:0000000100003F70 public _main
__text:0000000100003F70 _main proc near
__text:0000000100003F70 push rbp
__text:0000000100003F71 mov rbp, rsp
__text:0000000100003F74 sub rsp, 10h
__text:0000000100003F78 mov [rbp+var_4], 0
__text:0000000100003F7F mov [rbp+var_8], edi
__text:0000000100003F82 mov [rbp+var_10], rsi
__text:0000000100003F86 call _func ; Hex: E8C5FFFFFF
__text:0000000100003F8B call cs:_user_func ; Hex: FF157F000000
__text:0000000100003F91 xor eax, eax
__text:0000000100003F93 add rsp, 10h
__text:0000000100003F97 pop rbp
__text:0000000100003F98 retn
__text:0000000100003F98 _main endp

__got:0000000100004000 _printf_ptr dq 8020000100004030h ; DATA XREF: _printf↑r

__const:0000000100004010 public _user_func
__const:0000000100004010 _user_func dq 10000000003F50h ; DATA XREF: _main+1B↑r

FF15也表示近调用。根据官方解释,FF15E8调用指令的区别在于,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
2
3
4
5
6
7
default rel
func:
retf

global _start
_start:
call far [func]

编译后结果如下。这里使用的call far被编译为FF1D,从上表可知,指令表示远调用,且操作数为 32 位立即数。实际上上述代码实际编译出来的二进制很短,代码段数据仅有 8 字节:DB48FF1DF8FFFFFF,那么根据上述计算,执行调用命令后 IP 寄存器位置是 0x8,加上偏移量-8,则函数地址为 0,符合编译结果。

1
2
3
4
5
6
7
8
__text:0000000000000000 func            proc far                ; DATA XREF: _start↓r
__text:0000000000000000 retf
__text:0000000000000000 func endp

__text:0000000000000001 public _start
__text:0000000000000001 _start proc near
__text:0000000000000001 call qword ptr cs:func ; Hex: FF1DF8FFFFFF
__text:0000000000000001 _start endp

以上为个人学习总结,其中解释可能有错误之处,欢迎读者指出。

  • 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.
Comments
On this page
汇编语言入门之 call 指令