在 Linux 环境中运行内联汇编(使用 GCC/G++)

Running In-Line Assembly in Linux Environment (Using GCC/G++)

本文关键字:使用 GCC G++ 汇编 环境 Linux 运行      更新时间:2023-10-16

所以我有一个用C(.c文件)编写的非常基本的程序,带有内联汇编编码部分。我想将 .c 文件转换为我知道但不知道如何为 Linux 环境编译该代码的程序集输出。

将 gcc 或 g++ 用于.cpp文件时,我收到无法识别 asm 指令的错误。

现在,除了我将asm代码的括号更改为括号之外,此代码在Visual Studio中按预期工作。但是我仍然收到错误。对变量的一组未定义的引用。

我对工作代码所做的更改是将括号更改为括号,将汇编指令放在引号中(在线找到,可能是错误的)。

简而言之,我希望下面的代码能够使用命令 gcc 在 linux 环境中成功编译。我不知道语法,但代码有效,只是不适用于 linux/。

#include <stdio.h>
int main()
{
float num1, num2, sum, product;
float sum, product;
float f1, f2, f3, fsum, fmul;
printf("Enter two floating point numbers: n");
scanf("%f %f", &num1, &num2);

__asm__ 
(
    "FLD num1;"
    "FADD num2;"
    "FST fsum;"
);
printf("The sum of %f and %f " "is" " %fn", num1, num2, fsum);
printf("The hex equivalent of the numbers and sum is %x + %x = %xn", num1, num2, fsum);
return 0;
}

GNU C 内联 asm 被设计为在asm语句的开头/结尾不需要数据移动指令。 任何时候你写一个movfld或其他东西作为内联 asm 中的第一条指令,你就违背了约束系统的目的。 您应该首先要求编译器将数据放在您想要的位置。

此外,在 2016 年编写新的 x87 代码通常是浪费时间。 这很奇怪,与FP数学的正常方式(xmm寄存器中的标量或矢量指令)有很大不同。 通过将古老的asm代码转换为纯C,如果它是针对非常不同的微体系结构手动调整的,或者没有利用SSE指令,您可能会获得更好的结果。 如果您仍想编写 x87 代码,请参阅 x86 标记 wiki 中的指南。

如果你想用GNU C内联asm来学习asm,那就不要了。 选择任何其他方法来学习 asm,例如编写整个函数并从 C 调用它们。 另请参阅该答案的底部,以获取编写良好的GNU C内联asm的链接集合。


x87

浮点操作数有特殊规则,因为 x87 寄存器堆栈不是随机访问。 这使得内联 asm 比现在的"普通"东西更难使用。 获得最佳代码似乎也比正常情况更困难。


在我们的例子中,我们知道我们需要在 FP 堆栈的顶部有一个输入操作数,并在那里生成我们的结果。 要求编译器为我们执行此操作意味着我们不需要超出fadd的任何指令。

  asm (
    "fadd %[num2]nt"
    : "=t" (fsum)                                  // t is the top of the register stack
    : [num1] "%0" (num1), [num2] "f" (num2)         // 0 means same reg as arg 0, and the % means they're commutative.  gcc doesn't allow an input and output to both use "t" for somre reason.  For integer regs, naming the same reg for an input and an output works, instead of using "0".
    : // "st(1)"  // we *don't* pop the num2 input, unlike the FYL2XP1 example in the gcc manual
      // This is optimal for this context, but in other cases faddp would be better
      // we don't need an early-clobber "=&t" to prevent num2 from sharing a reg with the output, because we already have a "0" constraint
  );

有关%0的说明,请参阅约束修饰符的文档。

fadd之前:num2%st(0)num1要么在内存中,要么在另一个FP堆栈寄存器中。 编译器选择哪个,并填写寄存器名称或有效地址。

这应该有望让编译器在之后弹出堆栈正确的次数。 (请注意,当输出约束必须是FP堆栈寄存器时,fst %0非常愚蠢。 它很可能最终会像fst %st(0)之类的无操作。

如果两个 FP 值都已经在寄存器中,我没有看到一种简单的方法来优化它以使用faddp %st。 例如 如果num1以前处于%st1状态,但在FP寄存器中仍然不需要,那么faddp %st1将是理想的选择。


这是一个实际编译的完整版本,甚至可以在 64 位模式下工作,因为我为您编写了一个类型双关包装函数。 对于将 FP 寄存器中的某些 FP 参数传递给 varargs 函数的任何 ABI,都需要这样做。

#include <stdio.h>
#include <stdint.h>
uint32_t pun(float x) {
  union fp_pun {
    float single;
    uint32_t u32;
  } xu = {x};
  return xu.u32;
}

int main()
{
  float num1, num2, fsum;
  printf("Enter two floating point numbers: n");
  scanf("%f %f", &num1, &num2);
  asm (
    "fadd %[num2]nt"
    : "=t" (fsum)
    : [num1] "%0" (num1), [num2] "f" (num2)  // 0 means same reg as arg 0, and the % means it's commutative with the next operand.  gcc doesn't allow an input and output to both use "t" for some reason.  For integer regs, naming the same reg for an input and an output works, instead of using "0".
    : // "st(1)"  // we *don't* pop the num2 input, unlike the FYL2XP1 example in the gcc manual
      // This is optimal for this context, but in other cases faddp would be better
      // we don't need an early-clobber "=&t" to prevent num2 from sharing a reg with the output, because we already have a "0" constraint
  );
  printf("The sum of %f and %f is %fn", num1, num2, fsum);
  // Use a union for type-punning.  The %a hex-formatted-float only works for double, not single
  printf("The hex equivalent of the numbers and sum is %#x + %#x = %#xn",
         pun(num1), pun(num2), pun(fsum));
  return 0;
}

看看它是如何在 Godbolt Compiler Explorer 上编译的。

拿出-m32,看看在使用 SSE 进行 FP 数学运算的普通代码中,将数据放入 x87 寄存器中仅用于一次添加是多么愚蠢。 (特别是因为它们也必须转换为双精度才能printf scanf给我们单精度。

GCC 最终也为 32 位制作了一些看起来效率非常低的 x87 代码。 它最终在 regs 中有两个参数,因为它从单精度加载它以准备存储为双精度。 出于某种原因,它会复制 FP 堆栈上的值,而不是在执行fadd之前存储为双精度值。

因此,在这种情况下,"f"约束比"m"约束生成更好的代码,并且我看不到AT&T语法在不破坏寄存器操作数的asm的情况下为内存操作数指定单精度操作数大小的简单方法。 (fadds %st(1)不会组装,但fadd (mem)也不会用叮当声组装。 显然,GNU 默认为单精度内存操作数。 使用 Intel 语法时,修改的操作数大小附加到内存操作数,如果编译器选择内存操作数,则操作数大小将附加到内存操作数,否则不会。

无论如何,这个序列会比 gcc 发出的更好,因为它避免了fld %st(1)

    call    __isoc99_scanf
    flds    -16(%ebp)
    subl    $12, %esp      # make even more space for args for printf beyond what was left after scanf
    fstl    (%esp)         # (double)num1
    flds    -12(%ebp)
    fstl    8(%esp)        # (double)num2
    faddp  %st(1)          # pops both inputs, leaving only fsum in %st(0)
    fsts    -28(%ebp)      # store the single-precision copy
    fstpl   16(%esp)       # (double)fsum
    pushl   $.LC3
    call    printf

但显然,gcc并没有想到这样做。 编写内联 asm 以使用 faddp 会使 gcc 在faddp之前做额外的fld %st(1),而不是说服它在添加之前为 printf 存储double参数。

更好的是,如果设置单精度存储,以便它们可以成为类型双关 printf 的参数,而不必为此再次复制。 如果手动编写函数,我会将 scanf 结果存储到作为 printf 参数的位置。

GCC 中的内联程序集被逐字地转换为生成的程序集源;由于程序集中不存在变量,因此您编写的内容无法工作。

使其工作的方法是使用扩展程序集,它使用修饰符注释程序集,GCC 将在编译源代码时使用修饰符来翻译程序集。

__asm__
(
  "fld %1nt"
  "fadd %2nt"
  "fst %0"
  : "=f" (fsum)
  : "f" (num1), "f" (num2)
  :
);