【问题标题】:x86 explanation, number of function arguments and local variablesx86 解释,函数参数和局部变量的数量
【发布时间】:2020-08-08 12:03:36
【问题描述】:

x86-64 系统的 C ABI 如下:寄存器 rdi、rsi、rdx、rcx、r8、r9 用于按顺序传递参数。堆栈用于第 7 个参数。返回值使用 rax 寄存器。 rsp 寄存器包含堆栈指针。

吹函数bloop中定义了多少个函数参数?

我认为只有一个函数参数rdi。这是正确的吗?

在下面的函数bloop中声明了多少个局部变量(不是参数)?

我认为没有局部变量。这是正确的吗?

0000000000001139 <bloop>:
    1139:       55                      push   %rbp
    113a:       48 89 e5                mov    %rsp,%rbp
    113d:       48 83 ec 10             sub    $0x10,%rsp
    1141:       48 89 7d f8             mov    %rdi,-0x8(%rbp)
    1145:       48 83 7d f8 29          cmpq   $0x29,-0x8(%rbp)
    114a:       7f 1b                   jg     1167 <bloop+0x2e>
    114c:       48 8b 05 dd 2e 00 00    mov    0x2edd(%rip),%rax
    1153:       48 89 c6                mov    %rax,%rsi
    1156:       48 8d 3d b5 0e 00 00    lea    0xeb5(%rip),%rdi
    115d:       b8 00 00 00 00          mov    $0x0,%eax
    1162:       e8 c9 fe ff ff          callq  1030 <printf@plt>
    1167:       90                      nop
    1168:       c9                      leaveq
    1169:       c3                      retq

【问题讨论】:

  • mov 是一条指令。 mov 作为函数参数是什么意思?与nop 相同。这也是一个指令。
  • 鉴于您对汇编的基本概念(指令、寄存器、函数参数、变量等)感到困惑,阅读教程以正确理解基本术语可能会有所帮助。
  • 很抱歉给您带来了困惑。我的意思是 rdi 中的 mov 作为函数参数。

标签: assembly x86-64 reverse-engineering


【解决方案1】:

由于这个 asm 显然是反优化调试模式的编译器输出(默认 -O0 优化级别),您可以假设所有寄存器参数都溢出到函数堆栈条目。 (Why does clang produce inefficient asm with -O0 (for this simple floating point sum)?)

所以是的,这使逆向工程变得微不足道,并排除了任何未使用的函数 args 或在它们到达的同一寄存器中传递给 printf 的 args。

杂散nopleave 指令的使用意味着这可能是GCC 输出,而不是clang 或ICC。仅与排除const int foo = 0x29; 或其他东西的可能性真正相关,GCC 不会在-O0 进行优化。 ICC 和 clang 为让 GCC 生成此 asm 的源代码生成不同的 asm。我没有检查每个编译器版本,只检查这些编译器的最新版本。

(此外,这看起来像是对 PIE 可执行文件或共享库的反汇编。在传统的位置相关 ELF 可执行文件中,左侧的地址列将具有更高的地址,并且编译器会使用 mov $imm32, %edi 来放置静态寄存器中的地址。)


所以是的,有一个 64 位整数/指针 arg(当然会到达 RDI),并且对 printf 的调用传递了一个全局或静态 64 位变量的值mov 0x2edd(%rip), %rsi,以及将全局/静态格式字符串的地址放入带有 LEA 的寄存器中。

是的,我看不到本地人,除非他们完全未使用。在-O0,gcc 将优化掉int unused;,但不会优化int foo = 123;。有任何本地人,即使register const compare = 0x29; 也会让 GCC 到 subq $24, %rsp 而不是 16 (0x10)。 (请参阅下面的 Godbolt 链接。)它实际上不会进行持续传播。


我可以让 GCC9.3 -O0 从这个源代码中准确地生成这个 asm:

#include <stdio.h>
long global_var;

void bloop(long x) {
    if (!(x>0x29))
        printf("%ld", global_var);
}

on Godbolt with gcc9.3 -O0 -fpie -fverbose-asm:

# godbolt strips out directives like .section .rodata
.LC0:
        .string "%ld"

bloop:
        pushq   %rbp  #
        movq    %rsp, %rbp      #,
        subq    $16, %rsp       #,
        movq    %rdi, -8(%rbp)  # x, x
        cmpq    $41, -8(%rbp)   #, x
        jg      .L3 #,
        movq    global_var(%rip), %rax  # global_var, global_var.0_1
        movq    %rax, %rsi      # global_var.0_1,
        leaq    .LC0(%rip), %rdi        #,
        movl    $0, %eax        #,
        call    printf@PLT      #
.L3:
        nop     
        leave   
        ret

nop 没有目的;我不知道为什么未优化的 GCC 输出有时会有一个。

有关查看编译器输出的更多信息,另请参阅 How to remove "noise" from GCC/clang assembly output?

【讨论】:

  • @Kai 正如我的回答所暗示的,你不能肯定地说。
  • @fuz:如果有的话,它们是未使用的,只是没有初始化器声明。甚至 register const foo = 0x29 也使 GCC -O0 保留额外的堆栈空间! (当然我是基于 gcc9.3 -O0 实际做了什么,忽略了它保留的 8 字节未使用的堆栈空间。)
  • @PeterCordes 这是一种可能的情况。 OP 没有提供任何关于他使用什么选项的编译器的信息,所以我不能肯定地说。
  • @fuz:那里的流浪nop,以及leave 的使用,都是证明这是GCC -O0 -fpie 的有力证据。 clang 不使用leave,ICC 恰好没有在那里留下nop。我只检查了 Godbolt 拥有的编译器,而不是 Sun、Portland Group 或其他不太常见的编译器。
【解决方案2】:

movnop 都是指令。指令是处理器执行的东西,是构成机器程序的东西。如果您不熟悉这个概念,阅读汇编编程教程可能会有所帮助。

函数使用什么指令在很大程度上与它有多少参数和局部变量无关。 nop 和一些 mov 指令的存在不会告诉您有关函数的参数和变量的任何信息。

告诉你的是这些指令有什么操作数。如果您不熟悉什么是操作数或 x86 指令如何使用它们的操作数,我必须再次请您参考教程,因为这超出了本问题的范围。

识别函数参数的一般方法是检查函数使用的调用者保存的寄存器,而无需事先为它们分配值。虽然这不是万无一失的方法,但它通常是最好的启发式方法。

在您的函数中,使用了调用者保存的寄存器rdirsirax。其中,只有rdi 的原始值对函数有影响。至于rsirax,函数不看就覆盖了它们的原始值。因此,这些不太可能是函数参数(rax 在 SysV 调用约定中从未用于函数参数)。因此,该函数可能在rdi 中有一个参数。我看不到调用者分配的堆栈槽的任何访问权限,因此那里也不太可能隐藏任何额外的参数。

仍然可能是该函数被编写为在rsi 或其他一些寄存器中具有参数,而这些参数只是未被使用。如果没有额外的信息(例如调试符号、调用站点的反汇编等),我们永远无法确定。

至于局部变量:通常无法重建 C 函数在编译成程序集时使用的局部变量,因为编译器可以将局部变量优化到无法识别其存在的程度。它还可以为各种目的添加额外的局部变量。

但是,在您的具体情况下,该函数很可能是在关闭优化的情况下编译的。在这种情况下,许多 C 编译器以一种非常直接且可预测的方式编译 C 代码,其中为每个局部变量分配一个堆栈槽,并且对局部变量的每次内存访问都会对该堆栈槽产生一次加载或存储。

但是仍然不能绝对肯定地说这些变量可能具有什么类型,或者两个相邻的堆栈槽是两个独立的变量,一个是特别大的变量(例如long double)还是一个结构变量或数组类型。我们再也不会知道了。

在您的示例中,两个 8 字节的堆栈槽由指令 sub $0x10, %rsp 分配。由于编译器必须以 16 字节增量分配堆栈槽以进行对齐,这意味着原始函数至少有一个变量(64 位类型),但可能有多达九个(其他为char 类型) .

由于只有一个堆栈槽 (-0x8(%rbp)) 最终被访问,我们只能确定该函数至少有一个变量。由于访问以 64 位宽发生,因此所述变量很可能具有 64 位宽的类型。该函数可能有额外的未使用的局部变量,或者它拥有的变量可能是一个具有多个成员的结构或一个数组,每个成员只有第一个成员被访问。我们不能肯定地说。

也有可能不存在局部变量,并且编译器出于某种原因决定使用-0x8(%rbp) 溢出某些表达式(它喜欢在关闭优化时进行这样的无意义溢出),但这似乎不太可能。

因此,总而言之:根据机器代码判断,通常不可能准确地找出 C 函数的样子,但您通常可以做出有根据的猜测,让您走得更远。

因此,从“带有这种机器代码的 C 函数会是什么样子?”的角度来思考通常更有用。而不是“生成此机器代码的 C 函数是什么样的?”因为你永远无法确定。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2022-08-02
    • 1970-01-01
    • 2023-03-24
    • 1970-01-01
    • 1970-01-01
    • 2011-02-14
    • 2015-02-26
    • 1970-01-01
    相关资源
    最近更新 更多