gcc内联汇编表现得很奇怪

时间:2018-03-29 11:49:43

标签: gcc assembly x86 inline-assembly att

我正在学习GCC的扩展内联汇编。我写了一个A + B函数,想要检测ZF标志,但事情表现得很奇怪。

我使用的编译器是x86-64 Arch Linux上的gcc 7.3.1。

我从以下代码开始,此代码将正确打印a + b

int a, b, sum;
scanf("%d%d", &a, &b);
asm volatile (
  "movl %1, %0\n"
  "addl %2, %0\n"
  : "=r"(sum)
  : "r"(a), "r"(b)
  : "cc"
);
printf("%d\n", sum);

然后我只是添加了一个变量来检查标志,它给了我错误的输出。

int a, b, sum, zero;
scanf("%d%d", &a, &b);
asm volatile (
  "movl %2, %0\n"
  "addl %3, %0\n"
  : "=r"(sum), "=@ccz"(zero)
  : "r"(a), "r"(b)
  : "cc"
);
printf("%d %d\n", sum, zero);

GAS程序集输出

  movl  -24(%rbp), %eax  # %eax = a
  movl  -20(%rbp), %edx  # %edx = b
#APP
# 6 "main.c" 1
  movl %eax, %edx
  addl %edx, %edx

# 0 "" 2
#NO_APP
  sete  %al
  movzbl  %al, %eax
  movl  %edx, -16(%rbp)  # sum = %edx
  movl  %eax, -12(%rbp)  # zero = %eax

这次,sum将成为a + a。但是,当我刚刚交换%2%3时,输出将是正确的a + b

然后我在wandbox.org上检查了各种gcc版本(似乎clang不支持它,当输出是一个标志时),从版本4.5.4到版本4.7.4给出了正确的结果a + b,并开始从版本4.8.1开始,输出都是a + a

我的问题是:我写错了代码还是gcc有什么问题?

1 个答案:

答案 0 :(得分:1)

问题是,在所有输入(在您的情况下为%0)被消耗之前,您是否%2

"movl %1, %0\n"
"addl %2, %0\n"
在使用%0之前,第一个 MOV 正在修改

%2。优化编译器可以将寄存器重新用于输出约束,该输入约束用于输出约束。在您的情况下,其中一个编译器选择使用%2%0的相同寄存器,这会导致错误的结果。

要解决在消耗所有输入之前更改正在修改的寄存器的问题,请使用&标记输出约束。 &是一个表示Early Clobber的修饰符:

  

<强>“&安培;”   意味着(在一个特定的替代方案中)该操作数是一个earlyclobber操作数,它是在使用输入操作数完成指令之前写入的。因此,该操作数可能不在于由指令读取的寄存器或作为任何存储器地址的一部分。

     

'&amp;'仅适用于编写它的替代方案。在有多种选择的约束条件下,有时一种替代方案需要“&amp;”而其他方案则不需要。例如,参见68000的'movdf'insn。

     

如果在写入早期结果之前,只将其用作输入,则该指令读取的操作数可以绑定到earlyclobber操作数。添加这种形式的替代方案通常允许GCC在只有一些读取操作数可能受早期影响者影响时产生更好的代码。例如,参见ARM的“mulsi3”insn。

     

此外,如果earlyclobber操作数也是一个读/写操作数,那么该操作数只有在使用后才会写入。

     

'&amp;'不会消除写'='或'+'的需要。由于早期的操作数总是被写入,因此只读的早期操作数是不正确的,并且会被编译器拒绝。

对代码的更改是将"=r"(sum)修改为"=&r"(sum)。这将阻止编译器使用用于输出约束之一的输出约束的寄存器。

警告语。 GCC内联汇编是强大而邪恶的。如果你不知道自己在做什么,很容易出错。只有在必要时才使用它,如果可以的话,请避免使用它。