我不知道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<rsi。
Assembly - JG/JNLE/JL/JNGE after CMP。如果您详细了解cmp 如何设置标志,以及每个jcc 条件检查的标志,您可以验证它是否正确,但仅使用JL = 跳转到小于(假设标志由 cmp 设置)以记住它们的作用。
(由于 AT&T 语法,它被颠倒了;jcc 谓词对于 Intel 语法具有正确的语义。这是我通常更喜欢 Intel 语法的主要原因之一,但您可以习惯 AT&T 语法。)
从使用rdi 和rsi 作为输入(在不写入/ 之前读取它们),它们是传递参数的寄存器。所以这是 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 源代码会在循环内修改 i 和 result,并且可能不会修改它的输入变量 x 和 y。执行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 += x 但 i-- 的版本转错了方向后,我意识到 i 是一种干扰,最初通过不使用 @987654418 进行了简化@ 一点也不。我刚刚使用x--,而第一次反转它,因为显然 RDI=x。 (我非常了解 x86-64 System V 调用约定,可以立即看到。)
查看循环体后,result += x 和 x-- 从 add 和 sub 指令中完全显而易见。
cmp/jl 显然是一个涉及 2 个输入变量的 something < something 循环条件。
我不确定它是 x<y 还是 y<x,并且较新的 gcc 版本使用 jne 作为循环条件。我认为当时我作弊并查看了布赖恩的答案以检查它是否真的是x > y,而不是花一分钟时间来研究实际的逻辑。 但一旦我发现它是x--,只有x>y 才有意义。如果它完全进入循环,另一个在环绕之前将是正确的,但有符号溢出是 C 中未定义的行为.
然后我查看了一些较旧的 gcc 版本,看看是否有任何使 asm 更像问题中的内容。
然后我返回并在循环内将x 替换为i。
如果这看起来有点随意和草率,那是因为这个循环是如此之小,以至于我没想到会遇到任何麻烦,而且我更感兴趣的是找到完全复制它的源 + gcc 版本,而不是而不是完全扭转它的原始问题。
(我并不是说初学者应该觉得这很容易,我只是在记录我的思维过程,以防有人好奇。)