【问题标题】:Translating O2 optimized for-loop from assembly to C将 O2 优化的 for 循环从程序集转换为 C
【发布时间】:2015-12-15 07:27:35
【问题描述】:

这是一道作业题。 我正在尝试从以下汇编代码(x86 linux 机器,使用 gcc -O2 优化编译)中获取信息。我已经评论了每个部分以显示我所知道的。我的大部分假设可能是错误的,但我已经做了足够多的搜索,以至于我知道我应该在这里问这些问题。

.section        .rodata.str1.1,"aMS",@progbits,1

.LC0:
    .string "result %lx\n"  //Printed string at end of program
    .text

main:
.LFB13: 
    xorl    %esi, %esi         // value of esi = 0; x
    movl    $1, %ecx           // value of ecx = 1; result
    xorl    %edx, %edx         // value of edx = 0; Loop increment variable (possibly mask?)
.L2:
    movq    %rcx, %rax         // value of rax = 1; ?
    addl    $1, %edx           // value of edx = 1; Increment loop by one;
    salq    $3, %rcx           // value of rcx = 8; Shift left rcx;
    andl    $3735928559, %eax  // value of eax = 1; Value AND 1 = 1;
    orq     %rax, %rsi         // value of rsi = 1; 1 OR 0 = 1;
    cmpl    $22, %edx          // edx != 22 
    jne     .L2                // if true, go back to .L2 (loop again)
    movl    $.LC0, %edi        // Point to string
    xorl    %eax, %eax         // value of eax = 0;
    jmp     printf             // print
.LFE13: ret                    // return

我应该把它变成下面填空的C代码

#include <stdio.h>
int main()
{
 long x = 0x________;
 long result = ______;
 long mask;
 for (mask = _________; mask _______; mask = ________) {
   result |= ________;
}
 printf("result %lx\n",result);
}

我有几个问题和健全性检查,我想确保我做对了,因为我发现的类似示例都不是针对优化代码的。在自己编译一些试验后,我得到了一些接近的结果,但 L2 的中间部分总是关闭。

我的理解

一开始,esi 与自身异或,结果为 0,用 x 表示。然后将 1 添加到 ecx,这将由变量 result 表示。

x = 0;     result = 1;

然后,我相信一个循环增量变量存储在 edx 中并设置为 0。这将用于 for 循环的第三部分(更新表达式)。我还认为这个变量必须是掩码,因为稍后将 1 添加到 edx,表示循环增量(掩码 = mask++),以及在 for 循环的中间部分比较 edx(测试表达式又名掩码!= 22 )。

mask = 0; (in a way)

然后进入循环,将 rax 设置为 1。我不明白它在哪里使用,因为我没有声明第四个变量,尽管它稍后显示被和归零。

movq %rcx, %rax;

然后循环变量加一

addl $1, %edx;

下一部分对我来说意义最小

我觉得接下来的三个操作构成了循环的主体表达式,但是我不知道如何处理它们。它会导致类似于 result |= x ... 但我不知道还有什么

salq    $3, %rcx      
andl    $3735928559, %eax  
orq     %rax, %rsi

其余的我觉得我掌握得很好。进行比较(如果 mask != 22,再次循环),并打印结果。

我遇到的问题 我不明白几件事。

1) 我不明白如何计算我的变量。似乎有 3 个硬编码的,以及在程序集中找到的一个增量或临时存储变量(rax、rcx、rdx、rsi)。我认为 rsi 将是 x ,而 rcx 将是 result,但我不确定 mask 是 rdx 还是 rax,无论哪种方式,最后一个变量是什么是吗?

2) 我不确定的 3 个表达式是做什么的?我觉得我以某种方式将它们与增量混淆了,但是在不知道变量的情况下我不知道如何解决这个问题。

任何和所有的帮助都会很棒,谢谢!

【问题讨论】:

  • 我查看了系统如何处理 for 循环,增量是比较之前的行,但是这些值似乎不会对齐,因为增量存储在掩码中,并且比较之前的行没有提到掩码

标签: c loops assembly x86 reverse-engineering


【解决方案1】:

答案是:

#include <stdio.h>
int main()
{
    long x = 0xDEADBEEF;
    long result = 0;
    long mask;
    for (mask = 1; mask != 0; mask = mask << 3) {
        result |= mask & x;
    }
    printf("result %lx\n",result);
}

在大会中:

rsiresult。我们推断因为它是获得ORed 的唯一值,并且它是printf 的第二个参数(在x64 linux 中,参数存储在rdirsirdx 和一些其他,按顺序)。

x 是一个设置为0xDEADBEEF 的常量。这肯定不能扣除,但它是有道理的,因为它似乎在 C 代码中设置为常量,之后似乎没有设置。

现在剩下的,它被 GCC 的反优化混淆了。你看,GCC 检测到循环将被准确执行 21 次,并且认为巧妙地破坏条件并将其替换为无用的计数器。知道了这一点,我们看到edx 是无用的计数器,rcxmask。然后我们可以推导出真实的条件和真正的“增量”操作。我们可以在汇编中看到&lt;&lt;= 3,注意到如果你将一个64位的int左移22次,它就变成了0(移位3,22次意味着移位66位,所以全部移出)。

遗憾的是,这种反优化对于 GCC 来说非常普遍。该程序集可以替换为:

.LFB13: 
    xorl    %esi, %esi
    movl    $1, %ecx
.L2:
    movq    %rcx, %rax
    andl    $3735928559, %eax
    orq     %rax, %rsi
    salq    $3, %rcx // implicit test for 0
    jne     .L2
    movl    $.LC0, %edi
    xorl    %eax, %eax
    jmp     printf

它的作用完全相同,但我们删除了无用的计数器并保存了 3 条汇编指令。它也更好地匹配 C 代码。

【讨论】:

  • maskresult 必须是 unsigned long 类型,否则行为未定义。
  • @mch 位运算即使对于有符号整数也有很好的定义,result 不需要无符号。严格来说,mask 确实应该是无符号的,但实际上 100% 的编译器不会费心为负值添加特殊情况(因为它无论如何都是未定义的),这就解释了为什么它在这里工作。
  • 1 左移到符号位或符号位上方是未定义的行为,因此如果mask 不是无符号类型,编译器可以将for 循环作为无限循环处理。 printf 中的 "%lx" 也需要 unsigned long
  • @mch:mask 的左移在最后一次迭代中肯定会是未定义的行为,但只有在 unsigned long 可以的情况下将 long int 传递给 printf("%lx") 才是未定义的t 代表long int 的值。所以这是可能未定义的行为。
  • 反优化的词是“悲观化”。这对 gcc 来说非常愚蠢。通常它只是额外的 mov 指令,尤其是。使用向量内在函数时。
【解决方案2】:

让我们向后一点。我们知道result 必须是printf() 的第二个参数。在 x86_64 调用约定中,它是 %rsi。循环是.L2 标签和jne .L2 指令之间的所有内容。我们在模板中看到在循环的末尾有一个result |= 行,实际上,那里有一个orl 指令,其目标是%rsi,因此可以签出。我们现在可以在.main 的顶部看到它的初始化内容。

ElderBug 是正确的,编译器通过添加计数器进行虚假优化。但是我们仍然可以弄清楚:当循环重复时,哪条指令在|= 之后立即运行?那一定是循环的第三部分。在循环体之前运行的是什么?那一定是循环初始化。不幸的是,您必须弄清楚原始循环的第 22 次迭代会发生什么,以对循环条件进行逆向工程。 (但sal 是左移,那条线是原始循环条件的痕迹,在插入%rdx 测试之前,它后面会跟着一个条件分支。)

请注意,代码在%rcx 中保留了mask 值的副本,然后在%rax 中修改它,并且x 被折叠成一个常量(仔细查看andl 行)。

还请注意,您可以将 .S 文件提供给 gas 以获取 .o 并查看它的作用。

【讨论】:

    猜你喜欢
    • 2019-09-19
    • 2012-10-13
    • 1970-01-01
    • 1970-01-01
    • 2018-12-23
    • 1970-01-01
    • 2021-10-15
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多