为什么不能在 GNU C 基本内联 asm 语句中使用局部变量?

Why can't local variable be used in GNU C basic inline asm statements?

为什么我不能在基本 asm 内联中使用 main 中的局部变量?它只允许在扩展 asm 中使用,但为什么呢?

(我知道局部变量在 return 地址之后的堆栈上(因此一旦函数 return 就不能使用),但这不应该是不使用它们的原因)

以及基本 asm 示例:

int a = 10; //global a
int b = 20; //global b
int result;

int main() {
    asm ( "pusha\n\t"
          "movl a, %eax\n\t"
          "movl b, %ebx\n\t"
          "imull %ebx, %eax\n\t"
          "movl %eax, result\n\t"
          "popa");

    printf("the answer is %d\n", result);
    return 0;
}

扩展示例:

int main (void) {
    int data1 = 10;  //local var - could be used in extended
    int data2 = 20;
    int result;

    asm ( "imull %%edx, %%ecx\n\t"
          "movl %%ecx, %%eax" 
          : "=a"(result)
          : "d"(data1), "c"(data2));

    printf("The result is %d\n",result);
    return 0;
}

编译: gcc -m32 somefile.c

平台: uname -a: Linux 5.0.0-32-generic #34-Ubuntu SMP Wed Oct 2 02:06:48 UTC 2019 x86_64 x86_64 x86_64 GNU/Linux

"Basic asm"和"Extended asm"区别不大; "basic asm" 只是一种特殊情况,其中 __asm__ 语句没有输出、输入或破坏列表。编译器不会在 Basic asm 的汇编字符串中进行 % 替换。如果你想要输入或输出,你必须指定它们,这就是人们所说的 "extended asm".

实际上,可以从 "basic asm" 访问外部(甚至文件范围静态)对象。这是因为这些对象将(分别可能)在汇编级别具有符号名称。但是,要执行此类访问,您需要注意它是否与位置无关(如果您的代码将 linked 到库或 PIE 可执行文件中)并满足可能在 [=33= 处施加的其他 ABI 约束]ing 时间,并且对于与 link 时间优化和编译器可能执行的其他转换的兼容性有多种考虑。简而言之,这是一个坏主意,因为您无法告诉编译器一条基本的 asm 语句修改了内存。没有办法让它安全。

"memory" clobber(扩展 asm)可以使从 asm 模板按名称访问静态存储变量变得安全。

基本 asm 的用例是仅修改机器状态的东西,例如 asm("cli") 在内核中禁用中断,而不读取或写入任何 C 变量。 (即使那样,您也经常使用 "memory" 破坏来确保编译器在更改机器状态之前完成了较早的内存操作。)

局部(自动存储,不是静态的)变量基本上没有符号名称,因为它们不存在于单个实例中;在运行时,它们在其中声明的块的每个活动实例都有一个对象。因此,访问它们的唯一可能方法是通过 input/output 约束。

来自 MSVC 领域的用户可能会感到惊讶,因为 MSVC 的内联汇编方案通过将其内联汇编版本中的局部变量引用转换为堆栈指针相对访问等方式解决了该问题。然而,它提供的内联 asm 版本与优化编译器不兼容,并且在使用该类型的内联 asm 的函数中几乎不会发生优化。 GCC 和从 unix 中与 C 一起成长的更大的编译器世界并没有做任何类似的事情。

这是因为 asm 是一种已定义的语言,对于同一处理器系列上的所有编译器都是通用的。使用 __asm__ 关键字后,您可以可靠地使用任何好的处理器手册,然后开始编写有用的代码。

但是它没有为 C 定义的接口,老实说,如果您不将汇编器与 C 代码连接起来,那它为什么在那里?

有用的非常简单的asm示例:生成调试中断;设置浮点寄存器模式(exceptions/accuracy);

每个编译器作者都发明了自己的机制来与 C 接口。例如,在一个旧的编译器中,您必须在 C 代码中将要共享的变量声明为命名寄存器。在 GCC 和 clang 中,它们允许您使用非常混乱的两步系统来引用输入或输出索引,然后将该索引与局部变量相关联。

这个机制是 "extension" 的 asm 标准。

当然,asm 并不是真正的标准。更改处理器,您的 asm 代码就是垃圾。当我们一般性地谈论坚持 c/c++ 标准而不使用扩展时,我们不会谈论 asm,因为你已经打破了所有可移植性规则。

然后,最重要的是,如果您要调用 C 函数,或者您的 asm 声明了可由 C 调用的函数,那么您将必须与编译器的调用约定相匹配。这些规则是隐含的。它们限制了您编写 asm 的方式,但根据某些标准,它仍然是合法的 asm。

但是,如果您只是编写自己的 asm 函数,并从 asm 中调用它们,您可能不会受到 c/c++ 约定的太多限制:制定自己的寄存器参数规则; return 您想要的任何寄存器中的值;制作堆栈框架,或者不制作;通过异常保留堆栈帧——谁在乎呢?

请注意,您可能仍会受到平台的可重定位代码约定的限制(这些不是 "C" 约定,但通常使用 C 语法进行描述),但这仍然是您可以编写块的一种方式"portable" asm 函数,然后使用 "extended" 嵌入式 asm 调用它们。

您可以在扩展程序集中使用局部变量,但您需要将它们告知扩展程序集结构。考虑:

#include <stdio.h>


int main (void)
{
    int data1 = 10;
    int data2 = 20;
    int result;

    __asm__(
        "   movl    %[mydata1], %[myresult]\n"
        "   imull   %[mydata2], %[myresult]\n"
        : [myresult] "=&r" (result)
        : [mydata1] "r" (data1), [mydata2] "r" (data2));

    printf("The result is %d\n",result);

    return 0;
}

在这个[myresult] "=&r" (result)中说到select一个寄存器(r)将用作左值[=15=的输出(=)值],并且该寄存器将在汇编中称为 %[myresult],并且必须不同于输入寄存器 (&)。 (您可以在两个地方使用相同的文本,result 而不是 myresult;我只是为了说明而将其不同。)

类似[mydata1] "r" (data1)表示将表达式data1的值放入一个寄存器中,它在汇编中将被称为%[mydata1]

我修改了程序集中的代码,使其只修改输出寄存器。您的原始代码修改了 %ecx 但没有告诉编译器它正在这样做。您可以通过将 "ecx" 放在第三个 : 之后告诉编译器,这是“破坏”寄存器列表所在的位置。但是,由于我的代码让编译器分配一个寄存器,所以我不会在被破坏的寄存器中列出一个特定的寄存器。可能有一种方法可以告诉编译器其中一个输入寄存器将被修改但输出不需要,但我不知道。 (文档为 here。)对于此任务,更好的解决方案是告诉编译器对其中一个输入使用与输出相同的寄存器:

    __asm__(
        "   imull   %[mydata1], %[myresult]\n"
        : [myresult] "=r" (result)
        : [mydata1] "r" (data1), [mydata2] "0" (data2));

在此,0data2表示使其与操作数0相同。操作数按照它们出现的顺序编号,第一个输出操作数从0开始并继续进入输入操作数。因此,当汇编代码开始时,%[myresult] 将引用一些存放 data2 值的寄存器,编译器将期望 result 的新值位于该寄存器中组装完成后注册。

执行此操作时,您必须将约束与事物在装配中的使用方式相匹配。对于 r 约束,编译器提供了一些可以在接受通用处理器寄存器的汇编语言中使用的文本。其他包括用于内存引用的 m 和用于立即操作数的 i

您也不能安全地在 Basic Asm 语句中使用全局变量;它恰好在禁用优化的情况下工作,但它不安全并且你在滥用语法。

几乎没有理由曾经使用Basic Asm。即使是像asm("cli")这样的机器状态控制来禁用中断,您通常需要 "memory" 破坏者来订购它。加载/存储到全局变量。事实上,GCC 的 https://gcc.gnu.org/wiki/ConvertBasicAsmToExtended 页面建议永远不要使用 Basic Asm,因为它在编译器之间有所不同,并且 GCC 可能会将其视为破坏一切而不是什么都没有(因为现有的错误代码做出错误的假设)。如果编译器还在其周围生成存储和重新加载,这将使使用 push/pop 的 Basic Asm 语句更加低效。

基本上 Basic Asm 的唯一用例是编写 __attribute__((naked)) 函数的主体,其中数据 inputs/outputs / 与其他代码的交互遵循 ABI 的调用约定,而不是任何自定义约定约束/破坏描述了一个真正的内联代码块。


GNU C 内联 asm 的设计是将文本注入编译器的正常 asm 输出(然后馈送到汇编器,as)。扩展 asm 使字符串成为可以将操作数替换到其中的模板。约束描述了 asm 如何适应程序逻辑的数据流,以及如何注册它。

您需要使用语法来准确描述它的作用,而不是解析字符串。解析 var 名称的模板只能解决操作数需要解决的部分语言设计问题,并且会使编译器的代码更加复杂。 (它必须更多地了解每条指令,才能知道是否允许使用内存、寄存器或立即数,以及类似的东西。通常它的机器描述文件只需要知道如何从逻辑操作到 asm,而不是其他方向.)

您的 Basic asm 块已损坏,因为您修改了 C 变量而没有告知编译器。这可能会破坏启用的优化(也许只有更复杂的周围代码,但碰巧工作与实际安全不是一回事。这就是为什么仅测试 GNU C 内联汇编代码甚至不足以使其成为未来证明的原因针对新的编译器和周围代码的更改)。没有隐含的 "memory" 破坏。 (基本 asm 与扩展 asm 相同,只是不对字符串文字进行 % 替换。因此您不需要 %% 来在 asm 输出中获得文字 %。它是像没有输出的扩展 asm 一样隐式易变。)

另请注意,如果您的目标是 i386 MacOS,则您的 asm 中需要 _resultresult 只是碰巧起作用,因为 asm 符号名称与 C 变量名称完全匹配。 使用扩展的 asm 约束将使它在 GNU/Linux(无前导下划线)与其他使用前导 _.

的平台之间可移植

您的扩展 asm 已损坏,因为您修改了输入 ("c")(没有告诉编译器寄存器也是一个输出,例如使用相同的寄存器)。 它也很低效:如果 mov 是你模板的第一条或最后一条指令,你几乎总是做错了,应该使用更好的约束。

相反,您可以这样做:

    asm ("imull %%edx, %%ecx\n\t"
          : "=c"(result)
          : "d"(data1), "c"(data2));

或者更好的是,使用 "+r"(data2)"r"(data1) 操作数让编译器在进行寄存器分配时自由选择,而不是潜在地强制 编译器 发出不必要的mov 说明。 (请参阅@Eric 使用命名操作数和 "=r" 以及匹配的 "0" 约束的回答;这等同于 "+r" 但允许您对输入和输出使用不同的 C 名称。)

查看编译器的 asm output 以查看代码生成是如何围绕您的 asm 语句发生的,如果您想确保它是有效的。


由于本地变量在 asm 文本中没有符号/标签(相反它们存在于寄存器中或位于堆栈或帧指针的某个偏移处,即自动存储),因此无法使用符号它们在 asm 中的名称。

即使对于全局变量,您希望编译器能够尽可能地围绕您的内联 asm 进行优化,因此您希望让编译器可以选择使用已经在寄存器中的全局变量的副本,而不是让内存中的值与存储同步,这样你的 asm 就可以重新加载它。

让编译器尝试解析您的 asm 并找出哪些 C 局部变量名称是输入和输出是可能的。 (但会很复杂。)

但是如果你想让它高效,你需要弄清楚asm中的x什么时候可以是像EAX这样的寄存器,而不是像总是把x存入内存那样做一些脑残的事情在 asm 语句之前,然后将 x 替换为 8(%rsp) 或其他任何内容。 如果你想让 asm 语句控制输入的位置,你需要某种形式的约束。在每个操作数的基础上这样做是完全有意义的,这意味着内联 asm处理不必知道 bts 可以获取立即数或注册源但不是内存,以及其他机器特定的细节。 (记住;GCC 是一个可移植的编译器;将大量每台机器的信息烘焙到内联 asm 解析器中是不好的。)

(MSVC 强制 _asm{} 块中的所有 C 变量成为内存。 有效地 包装单个指令是不可能的,因为输入必须通过内存反弹,即使你将它包装在一个函数中,这样你就可以使用官方支持的技巧,即在 EAX 中保留一个值并从非空函数的末尾脱落。What is the difference between 'asm', '__asm' and '__asm__'? 实际上,MSVC 的实现显然非常脆弱并且难以维护,以至于他们为 x86-64 删除了它,并且它被记录为即使在 32 位模式下也不受寄存器参数的功能支持!但这不是语法设计的错,只是实际的实施。)

Clang 确实支持 -fasm-blocks _asm { ... } MSVC 风格的语法,它解析 asm 并使用 C var 名称。它可能会强制输入和输出到内存中,但我没有检查过。


另请注意,GCC 的带约束的内联 asm 语法是围绕与 GCC 内部机器描述文件用于向编译器描述 ISA 的相同约束系统设计的。 (GCC 源代码中的 .md 文件告诉编译器有关添加数字的指令,该指令在 "r" 寄存器中输入,并具有助记符的文本字符串。注意 "r""m"https://gcc.gnu.org/onlinedocs/gccint/RTL-Template.html 中的某些示例中)。

GNU C中asm的设计模型是优化器的黑盒;您必须使用约束完整地描述代码(对优化器)的影响。 如果您破坏了寄存器,则必须告诉编译器。如果您有一个要销毁的输入操作数,则需要使用具有匹配约束的虚拟输出操作数,或 "+r" 操作数来更新相应的 C 变量的值。

如果您读取或写入寄存器输入指向的内存,则必须告诉编译器。

如果你使用堆栈,你必须告诉编译器(但你不能,所以你必须避免踩到红区:/ ) See also the inline-assembly tag wiki

GCC 的设计使编译器可以在寄存器中为您提供输入,并使用相同的 寄存器获得不同的输出。 (如果不行,请使用 early-clobber 约束;GCC 的语法旨在有效地包装单个指令,该指令在写入其任何输出之前读取其所有输入。)

如果 GCC 只能从出现在 asm 源代码中的 C var 名称中推断出所有这些东西,我认为这种控制级别是不可能的。 (至少不合理。)并且可能到处都有令人惊讶的效果,更不用说错过的优化了。只有当你想最大限度地控制事物时,你才会使用内联汇编,所以你最不想要的就是编译器使用大量复杂的不透明逻辑来确定要做什么。

(内联汇编在其当前设计中已经足够复杂,与普通 C 相比使用不多,因此需要非常复杂的编译器支持的设计可能最终会出现很多编译器错误。 )


GNU C 内联汇编并非为低性能低工作量而设计。如果你想要简单,只需用纯 C 编写或使用内部函数,让编译器完成它的工作。(如果它生成次优代码,则文件错过优化错误报告。)