将 O2 优化的 for 循环从汇编翻译成 C

Translating O2 optimized for-loop from assembly to C

这是一道作业题。 我试图从以下汇编代码(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    , %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    , %edx           // value of edx = 1; Increment loop by one;
    salq    , %rcx           // value of rcx = 8; Shift left rcx;
    andl    35928559, %eax  // value of eax = 1; Value AND 1 = 1;
    orq     %rax, %rsi         // value of rsi = 1; 1 OR 0 = 1;
    cmpl    , %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);
}

我有几个问题和健全性检查,我想确保我做对了,因为我发现的类似示例中 none 是针对优化代码的。在自己编译一些试验后,我得到了一些接近的东西,但 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 , %edx;

下一部分对我来说意义不大

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

salq    , %rcx      
andl    35928559, %eax  
orq     %rax, %rsi

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

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

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

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

任何帮助都会很棒,谢谢!

答案是:

#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 中,参数存储在 rdi 中, rsirdx 和其他一些,按顺序)。

x 是设置为 0xDEADBEEF 的常量。这个肯定不能扣,但是有道理,因为在C代码里好像是设置成常量,之后好像就没有设置了。

现在剩下的,它被 GCC 的反优化混淆了。你看,GCC 检测到循环将执行 21 次,并且认为破坏条件并用无用的计数器替换它是聪明的。知道了,我们看到 edx 是无用的计数器,而 rcxmask。然后我们可以推断出真实情况和真实的 "increment" 操作。我们可以看到汇编中的<<= 3,注意到如果你将一个64位的int左移22次,它就变成了0(shift 3, 22次就是shift 66位,所以全部移出)。

遗憾的是,这种反优化对于 GCC 来说确实很常见。该程序集可以替换为:

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

它做的事情完全一样,但是我们删除了无用的计数器并节省了 3 条汇编指令。它还能更好地匹配 C 代码。

让我们倒退一点。我们知道 result 必须是 printf() 的第二个参数。在 x86_64 调用约定中,即 %rsi。循环是 .L2 标签和 jne .L2 指令之间的所有内容。我们在模板中看到循环末尾有一个 result |= 行,实际上,那里有一个 orl 指令,目标是 %rsi,所以检查出来了。我们现在可以在 .main.

的顶部看到它被初始化的内容

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

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

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