【问题标题】:How do I translate an optimized x86-64 asm loop back to a C for loop?如何将优化的 x86-64 asm 循环转换回 C for 循环?
【发布时间】:2019-09-19 11:32:00
【问题描述】:

我有以下几点:

foo:
   movl $0, %eax                      //result = 0
   cmpq %rsi, %rdi                    // rdi = x, rsi = y?
   jle .L2

.L3:
   addq %rdi, %rax                    //result = result + i?
   subq $1, %rdi                      //decrement?
   cmp %rdi, rsi
   jl .L3

.L2
   rep
   ret

我正在尝试将其翻译为:

long foo(long x, long y)
{
    long i, result = 0;
    for (i=     ;               ;         ){

      //??

   }

 return result;
}

我不知道 cmpq %rsi, %rdi 是什么意思。 为什么long i没有另一个&eax?

我希望得到一些帮助来解决这个问题。我不知道我错过了什么——我一直在浏览我的笔记、教科书和互联网的其他部分,但我被困住了。这是一个复习题,我已经做了好几个小时了。

【问题讨论】:

  • 您的第一个任务是弄清楚调用约定——x 是什么,y 是什么。
  • 第二个 cmp 也不应该是 cmpq %rdi, %rsi
  • 回答:for (i = 0xDEADBEEF; x > y; x --) { result += x; }
  • @AnttiHaapala 请问为什么 i = 0xDEADBEEF?
  • 表明这个问题很愚蠢。标签后缺少冒号,基本上rsi 将是一个变量名...

标签: c assembly x86-64 reverse-engineering


【解决方案1】:

假设这是一个带有 2 个参数的函数。假设这是使用 gcc amd64 调用约定,它将传递 rdi 和 rsi 中的两个参数。在您的 C 函数中,您调用这些 x 和 y。

long foo(long x /*rdi*/, long y /*rsi*/)
{
    //movl $0, %eax
    long result = 0;  /* rax */

    //cmpq %rsi, %rdi
    //jle .L2
    if (x > y) {
        do {
            //addq %rdi, %rax
            result += x;

            //subq $1, %rdi
            --x;

            //cmp %rdi, rsi
            //jl .L3            
        } while (x > y);
    }

    return result;
}

【讨论】:

  • 是否可以合并long i 并将所有内容保留在“for”循环中?
  • @Ian:关键是您的汇编代码不会使用long i 转换为for 循环。它转化为一个 do-while 循环。
  • @MarkBenningfield:但这 C for() 循环如何编译为asm,作为在底部有条件分支的惯用循环. Why are loops always compiled into "do...while" style (tail jump)?。如果编译器第一次不能证明循环条件为真,那么如果它需要运行零次,你会得到一个 cmp/jcc 来完全跳过循环。这很可能是gcc -O1 输出(除了rsi 上缺少的%.L2 上缺少的:)Gcc 不会在-O1 上寻找异或归零窥视孔优化,但是会做正常的循环。
【解决方案2】:

我不知道cmpq %rsi, %rdi是什么意思

这是cmp rdi, rsi 的 AT&T 语法。 https://www.felixcloutier.com/x86/CMP.html

您可以在 ISA 手册中查看单个指令的详细信息。

更重要的是,cmp/jcc 就像 cmp %rsi,%rdi/jl 就像 jump if rdi<rsiAssembly - JG/JNLE/JL/JNGE after CMP。如果您详细了解cmp 如何设置标志,以及每个jcc 条件检查的标志,您可以验证它是否正确,但仅使用JL = 跳转到小于(假设标志由 cmp 设置)以记住它们的作用。

(由于 AT&T 语法,它被颠倒了;jcc 谓词对于 Intel 语法具有正确的语义。这是我通常更喜欢 Intel 语法的主要原因之一,但您可以习惯 AT&T 语法。)


从使用rdirsi 作为输入(在不写入/ 之前读取它们),它们是传递参数的寄存器。所以这是 x86-64 System V 调用约定,其中整数参数在 RDI、RSI、RDX、RCX、R8、R9 中传递,然后在堆栈上。 (What are the calling conventions for UNIX & Linux system calls on i386 and x86-64 涵盖函数调用和系统调用)。另一个主要的 x86-64 调用约定是 Windows x64,它传递 RCX 和 RDX 中的前 2 个参数(如果它们都是整数类型)。

所以是的,x=RDI 和 y=RSI。是的,结果=RAX。 (写入 EAX 零扩展到 RAX)。


从代码结构(不是在语句之​​间将每个 C 变量存储/重新加载到内存),它是在启用了某种程度的优化的情况下编译的,所以 for() 循环变成了一个普通的 asm 循环,条件分支位于底端。 Why are loops always compiled into "do...while" style (tail jump)?(@BrianWalker 的回答显示 asm 循环被音译回 C,没有尝试将其重新形成为惯用的 for 循环。)

从循环前面的 cmp/jcc 可以看出,编译器无法证明循环运行的迭代次数非零。所以无论for() 循环条件是什么,第一次它都可能是假的。 (考虑到有符号整数,这不足为奇。)

由于我们没有看到 i 使用了单独的寄存器,因此我们可以得出结论,优化重用了 i 的另一个 var 寄存器。可能像for(i=x; 一样,然后x 的原始值未被用于函数的其余部分,它是“死的”,编译器可以只使用RDI 作为i,破坏x 的原始值。

我猜是i=x 而不是y,因为RDI 是在循环内修改的arg 寄存器。我们预计 C 源代码会在循环内修改 iresult,并且可能不会修改它的输入变量 xy。执行i=y 然后执行x-- 之类的操作是没有意义的,尽管这将是另一种有效的反编译方式。

cmp %rdi, %rsi / jl .L3 表示(重新)进入循环的循环条件是rsi-rdi < 0(有符号),或i<y

cmp/jcc before 循环正在检查相反的条件;请注意,操作数是相反的,它正在检查jle,即jng。所以这是有道理的,它确实是从循环中剥离出来并以不同方式实现的相同循环条件。因此,它与 C 源代码兼容,即具有一个条件的普通 for() 循环。

sub $1, %rdi 显然是i----i。我们可以在for() 内或在循环体的底部执行此操作。最简单、最惯用的地方是for(;;) 语句的第三部分。

addq %rdi, %rax 显然是将i 添加到result。我们已经知道这个函数中的 RDI 和 RAX 是什么。

拼凑起来,我们得出:

long foo(long x, long y)
{
    long i, result = 0;
    for (i= x    ;    i>y    ;    i-- ){
        result += i;
    }

    return result;
}

这段代码是哪个编译器编写的?

.L3: 标签名称来看,这看起来像来自gcc 的输出。 (不知何故损坏了,从.L2 中删除:,更重要的是在一个cmp 中从%rsi 中删除%。确保将代码复制/粘贴到SO 问题中以避免这种情况。)

因此,可以使用正确的 gcc 版本/选项来准确获取此 asm 以获取某些 C 输入。可能是gcc -O1,因为movl $0, %eax 排除了-O2 和更高版本(GCC 会寻找xor %eax,%eax 窥视孔优化以有效地清零寄存器)。但它不是-O0,因为这会将循环计数器存储/重新加载到内存中。并且-Og(优化一点,用于调试)喜欢在循环条件中使用jmp 而不是单独的cmp/jcc 来跳过循环。这种详细程度基本上与简单地反编译成做同样事情的 C 无关。

rep ret 是 gcc 的另一个标志;由于 AMD K8/K10 分支预测,gcc7 和更早的版本在其默认的 ret 输出中使用了它作为分支目标或从 jcc 中退出的 tune=generic 输出。 What does `rep ret` mean?

gcc8 及更高版本仍将与-mtune=k8-mtune=barcelona 一起使用。但我们可以排除这种可能性,因为该调整选项将使用dec %rdi 而不是subq $1, %rdi。 (只有少数现代 CPU 对 inc/dec 不修改 CF 用于寄存器操作数有任何问题。INC instruction vs ADD 1: Does it matter?

gcc4.8 及更高版本将rep ret 放在同一行。 gcc4.7 及更早版本如您所见打印它,前面的行带有 rep 前缀。

gcc4.7 及更高版本喜欢将初始分支放在之前 mov $0, %eax,这看起来像是错过了优化。这意味着他们需要一个单独的 return 0 路径出函数,其中包含另一个 mov $0, %eax

gcc4.6.4 -O1 复制您的输出完全正确,对于上面显示的来源,on the Godbolt compiler explorer

# compiled with gcc4.6.4 -O1 -fverbose-asm
foo:
        movl    $0, %eax        #, result
        cmpq    %rsi, %rdi      # y, x
        jle     .L2       #,
.L3:
        addq    %rdi, %rax      # i, result
        subq    $1, %rdi        #, i
        cmpq    %rdi, %rsi      # i, y
        jl      .L3 #,
.L2:
        rep
        ret

使用i=y 的其他版本也是如此。当然,我们可以添加很多可以优化的东西,比如i=y+1,然后有一个循环条件,比如x>--i。 (有符号溢出在 C 中是未定义的行为,因此编译器可以假设它不会发生。)

// also the same asm output, using i=y but modifying x in the loop.
long foo2(long x, long y) {
  long i, result = 0;
  for (i= y    ;    x>i    ;    x-- ){
      result += x;
   }
   return result;
}

在实践中我实际上扭转了这一点:

  • 我将 C 模板复制/粘贴到 Godbolt (https://godbolt.org/)。我可以立即看到(从 mov $0 而不是 xor-zero 和标签名称)它看起来像 gcc -O1 输出,所以我输入了该命令行选项并选择了一个旧版本的 gcc gcc6. (原来这个 asm 实际上来自一个更老的 gcc)。
  • 我尝试了基于 cmp/jcc 和 i++ 的初步猜测,例如 x<y(在我真正仔细阅读 asm 的其余部分之前根本),因为 for 循环经常使用i++。看似微不足道的无限循环 asm 输出告诉我这显然是错误的:P

  • 我猜想 i=x,但在使用 result += xi-- 的版本转错了方向后,我意识到 i 是一种干扰,最初通过不使用 @987654418 进行了简化@ 一点也不。我刚刚使用x--,而第一次反转它,因为显然 RDI=x。 (我非常了解 x86-64 System V 调用约定,可以立即看到。)

  • 查看循环体后,result += xx--addsub 指令中完全显而易见。

  • cmp/jl 显然是一个涉及 2 个输入变量的 something < something 循环条件。

  • 我不确定它是 x<y 还是 y<x,并且较新的 gcc 版本使用 jne 作为循环条件。我认为当时我作弊并查看了布赖恩的答案以检查它是否真的是x > y,而不是花一分钟时间来研究实际的逻辑。 但一旦我发现它是x--,只有x>y 才有意义。如果它完全进入循环,另一个在环绕之前将是正确的,但有符号溢出是 C 中未定义的行为.

  • 然后我查看了一些较旧的 gcc 版本,看看是否有任何使 asm 更像问题中的内容。

  • 然后我返回并在循环内将x 替换为i

如果这看起来有点随意和草率,那是因为这个循环是如此之小,以至于我没想到会遇到任何麻烦,而且我更感兴趣的是找到完全复制它的源 + gcc 版本,而不是而不是完全扭转它的原始问题。

(我并不是说初学者应该觉得这很容易,我只是在记录我的思维过程,以防有人好奇。)

【讨论】:

    猜你喜欢
    • 2015-12-24
    • 2010-11-15
    • 1970-01-01
    • 2018-12-23
    • 1970-01-01
    • 2012-10-13
    • 2018-09-02
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多