诸如printf(char* format, ...)之类的C变量自变量函数在反汇编时会是什么样子?

总是通过调用约定来标识它,还是有更多的方法来标识它?

#1 楼

在某些架构中,它非常简单,而在另一些架构中,则不是很明显。我将介绍一些我熟悉的东西。

SystemV x86_64(Linux,OS X,BSD)

可能最容易识别。由于决定在al中指定已使用的XMM寄存器的数目的决定很笨拙,因此大多数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的寄存器溢出到堆栈上。

Windows x64又名AMD64

在Win64中不太明显,但这是一个信号:与椭圆参数相对应的寄存器总是溢出到堆栈上堆栈以及与堆栈上传递的其余参数对齐的位置。例如。这是printf的序言:

  mov     rax, rsp
  mov     [rax+8], rcx
  mov     [rax+10h], rdx
  mov     [rax+18h], r8
  mov     [rax+20h], r9


这里,rcx包含固定的format参数,并且椭圆参数在rdxr8r9中传递,然后在堆栈上。我们可以观察到,rdxr8r9准确地一个接一个地存储,并且刚好在以rsp+0x28开头的其余参数的下方。区域[rsp + 8..rsp + 0x28]正是为此目的而保留的,但是non-vararg函数通常不会在其中存储所有寄存器参数,也不会将该区域用于局部变量。例如,这是一个非易失性函数的序言:

  mov     [rsp+10h], rbx
  mov     [rsp+18h], rbp
  mov     [rsp+20h], rsi


您可以看到它正在使用保留区来保存非易失性寄存器,而不溢出寄存器参数。

ARM

ARM调用约定的第一个参数使用R0-R3,因此vararg函数需要将它们溢出到堆栈上,以与在堆栈上传递的其余参数保持一致。因此,您将看到R0-R3(或R1-R3R2-R3或仅R3)被压入堆栈,这通常在非可变参数函数中不会发生。这不是100%可靠的指标-例如Microsoft的编译器有时会将R0-R1压入堆栈,并使用SP访问它们,而不是移至其他寄存器并使用它们。但是我认为这对海湾合作委员会是一个非常可靠的信号。这是一个由GCC编译的函数的示例:您不能在此之前执行其他任何操作,因为在vsprintf处存在潜在的堆栈参数,因此R0-R3必须在它们之前。)

评论


太棒了,感谢您通过示例分解了不同的场景!

–滑翔
13年3月21日在17:39

#2 楼

(我的答案是特定于x86的)。

在函数内部,它看起来与其他任何函数一样。唯一的区别是,在函数执行过程中的某个时刻,它将采用最后一个不变变量的(堆栈)地址,并在平台上增加字长。然后将其用作指向变量参数基础的指针。在函数的外部,您将观察到不同数量的参数作为参数传递给函数(通常,非变量参数之一将是变量参数函数的明显指示,例如硬编码格式字符串或相似的东西)。可变参数函数不能是__stdcall,因为__stdcall依赖于预编译的ret XXh指令,而可变参数函数的要点是可以传递未知数量的参数。因此,这些函数必须为__cdecl,即调用者必须更正堆栈以删除所有推送的参数。