【问题标题】:Running In-Line Assembly in Linux Environment (Using GCC/G++)在 Linux 环境中运行内联汇编(使用 GCC/G++)
【发布时间】:2016-08-15 11:50:29
【问题描述】:

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

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

现在,除了我将 asm 代码的括号更改为圆括号之外,此代码在 Visual Studio 中可以正常工作。但是我仍然得到错误。一堆未定义的变量引用。

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

简而言之,我希望下面的代码能够在 linux 环境中使用命令 gcc 成功编译。我不知道语法,但代码有效,只是不适用于 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" " %f\n", num1, num2, fsum);
printf("The hex equivalent of the numbers and sum is %x + %x = %x\n", num1, num2, fsum);

return 0;
}

【问题讨论】:

    标签: c++ c linux gcc assembly


    【解决方案1】:

    GNU C inline asm 设计为在 asm 语句的开始/结束时不需要数据移动指令。每当您编写 movfld 或作为内联汇编中的第一条指令时,您就违背了约束系统的目的。您应该首先要求编译器将数据放在您想要的位置。

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

    If you're trying to learn asm using GNU C inline asm, just don't. Pick any other way to learn asm, e.g. writing whole functions and calling them from C. 另请参阅该答案的底部以获取编写良好 GNU C 内联汇编的链接集合。


    special rules for x87 floating-point operands,因为 x87 寄存器堆栈不是随机访问的。这使得 inline-asm 比“正常”的东西更难使用。 获得最佳代码似乎也比正常更难。


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

      asm (
        "fadd %[num2]\n\t"
        : "=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 的说明,请参阅the docs for constraint modifiers

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

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

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


    这是一个实际编译的完整版本,甚至可以在 64 位模式下工作,因为我为您编写了一个类型双关语包装函数。任何将 FP 寄存器中的 FP args 传递给 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]\n\t"
        : "=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 %f\n", 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 = %#x\n",
             pun(num1), pun(num2), pun(fsum));
    
      return 0;
    }
    

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

    取出-m32,看看在使用 SSE 进行 FP 数学运算的普通代码中,仅一次添加就将数据放入 x87 寄存器是多么愚蠢。 (尤其是因为在scanf 为我们提供单精度之后,它们还必须为printf 转换为双精度。)

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

    所以在这种情况下,"f" 约束比"m" 约束生成更好的代码,而且我没有看到使用 AT&T 语法为内存操作数指定单精度操作数大小而不破坏 asm 的简单方法用于寄存器操作数。 (fadds %st(1) 不汇编,但 fadd (mem) 也不与 clang 汇编。显然,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 参数的位置。

    【讨论】:

    • 我可能会选择类似的东西:godbolt.org/g/UrRcSg。如果您使用带有单个寄存器约束的输出操作数,例如 =t=u 并且至少有一个 f 输入操作数约束,则经验法则是将所有浮点(寄存器)输出操作数声明为早期 clobber .请参阅 i386 浮点 asm 操作数 部分中的 gcc.gnu.org/onlinedocs/gcc-4.2.0/gcc/Extended-Asm.html
    • @MichaelPetch:谢谢,我应该阅读 x87 inline-asm 规则。我只是对如何处理 FP 堆栈做了一些假设,事后看来这太简单了。在获取代码的过程中,我收到的一条错误消息建议对结果进行早期破坏,但我不明白为什么。如果我回到它,我可能会在明天更新这个答案。
    • 另一个观察结果。我注意到您在 cmets 中提到了 CLANG。如果传递了浮点寄存器,我不相信 CLANG 会正确编译fadds %[num1]\n\t(内存操作数会没问题)
    • @MichaelPetch:你是对的;如果您告诉 Godbolt 实际制作二进制文件,gcc 也会窒息。 gcc -S 甚至不尝试汇编,因此 gcc 不会检测到 asm 语法错误。如果使用内存操作数,英特尔语法会将操作数大小限定符放入内存操作数语法中,因此这里实际上是一个优势。无论如何,我认为对于这个程序,我们可能会通过强制内存操作数获得更好的代码,因为我们知道我们正在处理内存中的值。 (不过,通常这会很糟糕。)
    • @MichaelPetch:谢谢,我忘记了 % 做了什么。我记得有一次读到有一种方法可以告诉编译器有关可交换操作数的信息,但我忘记了它是什么,xD。
    【解决方案2】:

    GCC 中的内嵌汇编字面意思是生成的汇编源代码;由于程序集中不存在变量,因此您编写的内容无法正常工作。

    使其工作的方法是使用extended assembly,它使用修饰符对程序集进行注释,当编译源代码时,GCC 将使用这些修饰符来翻译程序集。

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

    【讨论】:

    • 好吧,这是有道理的,但是在进行更改时,我收到一条错误消息,指出输出约束 0 必须在第 13 行指定一个寄存器,所以我尝试添加以下 %eax\n 但没有运气。 (第 13 行是“fst %0”)
    • 尝试将=f 替换为=t。在这方面我几乎还是垃圾。
    • 在我添加了 '&' 以使最终成为 '"&=t"' 后,效果非常好。感谢您的帮助,我真的很感激,这确实是我正在处理的更大代码的一小部分,我明白了一般的想法,所以我应该能够做剩下的事情。再次感谢您的帮助,因为我不熟悉扩展程序集和 linux 本身。
    • 请注意,这是 GCC 本身的一部分,而不是 Linux;无论操作系统如何,同样的原则适用于 GCC 运行的所有平台。
    • 始终尽量避免在内联汇编的开始/结束处编写加载和存储指令。这是一个明显的迹象,您应该使用约束来让编译器将数据放在您想要的位置。我的版本在内联汇编中只有一个 insn。例如使用"=t" 作为结果意味着您不需要fst
    猜你喜欢
    • 1970-01-01
    • 2012-10-20
    • 1970-01-01
    • 2011-01-07
    • 1970-01-01
    • 2012-02-09
    • 2012-03-07
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多