printf(char* format, ...)
反汇编后的 C 变量参数函数会是什么样子?
是否总是通过调用约定来识别,还是有更多的方法来识别它?
printf(char* format, ...)
反汇编后的 C 变量参数函数会是什么样子?
是否总是通过调用约定来识别,还是有更多的方法来识别它?
它在某些架构中非常简单,而在其他架构中则不是很明显。我将描述一些我熟悉的。
可能是最容易识别的。由于在 中指定使用的 XMM 寄存器数量的愚蠢决定al
,大多数 vararg 函数开始如下:
push rbp
mov rbp, rsp
sub rsp, 0E0h
mov [rbp+var_A8], rsi
mov [rbp+var_A0], rdx
mov [rbp+var_98], rcx
mov [rbp+var_90], r8
mov [rbp+var_88], r9
movzx eax, al
lea rdx, ds:0[rax*4]
lea rax, loc_402DA1
sub rax, rdx
lea rdx, [rbp+var_1]
jmp rax
movaps xmmword ptr [rdx-0Fh], xmm7
movaps xmmword ptr [rdx-1Fh], xmm6
movaps xmmword ptr [rdx-2Fh], xmm5
movaps xmmword ptr [rdx-3Fh], xmm4
movaps xmmword ptr [rdx-4Fh], xmm3
movaps xmmword ptr [rdx-5Fh], xmm2
movaps xmmword ptr [rdx-6Fh], xmm1
movaps xmmword ptr [rdx-7Fh], xmm0
loc_402DA1:
请注意它如何al
用于确定溢出到堆栈上的 xmm 寄存器的数量。
在 Win64 中,它不太明显,但有一个迹象:与椭圆形参数相对应的寄存器总是溢出到堆栈上,并位于与堆栈上传递的其余参数对齐的位置。例如,这是printf
的序言:
mov rax, rsp
mov [rax+8], rcx
mov [rax+10h], rdx
mov [rax+18h], r8
mov [rax+20h], r9
在这里,rcx
包含了固定的format
说法,和椭圆的参数传递中rdx
,r8
并且r9
然后在堆栈中。我们可以观察到rdx
,r8
和r9
被一个接一个地存储,并且刚好在以 开始的其余参数的下方rsp+0x28
。区域 [rsp+8..rsp+0x28]正是为此目的而保留的,但非可变参数函数通常不会在那里存储所有寄存器参数,或者将该区域重用于局部变量。例如,这是一个非-vararg 函数序言:
mov [rsp+10h], rbx
mov [rsp+18h], rbp
mov [rsp+20h], rsi
您可以看到它使用保留区域来保存非易失性寄存器,而不是溢出寄存器参数。
ARM 调用约定使用R0
-R3
作为第一个参数,因此 vararg 函数需要将它们溢出到堆栈上以与堆栈上传递的其余参数对齐。因此,您将看到R0
- R3
(或R1
- R3
,或R2
-R3
或只是R3
)被推入堆栈,这通常不会发生在非可变参数函数中。这不是一个 100% 万无一失的指标——例如微软的编译器有时会推送R0
——R1
到堆栈上并使用SP
而不是移动到其他寄存器并使用它来访问它们。但我认为这对 GCC 来说是一个非常可靠的迹象。以下是 GCC 编译函数的示例:
STMFD SP!, {R0-R3}
LDR R3, =dword_86090
STR LR, [SP,#0x10+var_14]!
LDR R1, [SP,#0x14+varg_r0] ; format
LDR R0, [R3] ; s
ADD R2, SP, #0x14+varg_r1 ; arg
BL vsprintf
LDR R3, =dword_86094
MOV R2, #1
STR R2, [R3]
LDR LR, [SP+0x14+var_14],#4
ADD SP, SP, #0x10
RET
它显然是一个 vararg 函数,因为它正在调用vsprintf
,我们可以看到R0
-R3
在开始时被推送(在此之前你不能推送任何其他东西,因为潜在的堆栈参数存在,SP
所以R0
-R3
必须在它们之前)。
(我的答案是特定于 x86 的)。
在函数内部,它看起来就像任何其他函数。唯一的区别是,在函数执行过程中的某个时刻,它将获取最后一个非变量参数的(堆栈)地址,并将其增加平台上的字长;然后将 this 用作指向变量参数基址的指针。在函数的外部,您将观察到不同数量的参数作为参数传递给函数(通常其中一个非可变参数将作为可变参数函数的一些明显指示符,例如硬编码格式字符串或相似的东西)。不能是可变参数函数__stdcall
,因为__stdcall
依赖于预编译ret XXh
指令,而可变参数函数的重点是可以传递未知数量的参数。因此,这些函数必须是__cdecl
,即调用者必须更正堆栈以移除所有推送的参数。