6.47.2 Extended Asm - Assembler Instructions with C Expression Operands

徐德海
2023-12-01

使用扩展asm,您可以从汇编程序读取和写入 C 变量,并执行从汇编代码到 C 标号的跳转。扩展asm语法使用冒号(“:”)在汇编程序模板之后分隔操作数参数:

asm asm-qualifiers ( AssemblerTemplate 
                 : OutputOperands 
                 [ : InputOperands
                 [ : Clobbers ] ])

asm asm-qualifiers ( AssemblerTemplate 
                      : OutputOperands
                      : InputOperands
                      : Clobbers
                      : GotoLabels)

在后一种形式中,asm 限定符包含goto(在第一种形式中则没有)。

asm关键字是 GNU 扩展。在编写可以使用-ansi和各种-std选项进行编译的代码时,请使用__asm__而不是asm(请参见 Alternate Keywords)。

Qualifiers

  • volatile

    扩展asm语句的典型用法是操纵输入值以产生输出值。但是,asm语句也可能会产生副作用。如果是这样,您可能需要使用volatile限定符来禁用某些优化。 参见 Volatile

  • inline

    如果使用内联限定符,则出于内联目的,将asm语句的大小视为尽可能小的大小(请参见 Size of an asm)。

  • goto

    该限定符通知编译器,asm语句可能会跳转到 GotoLabels 中列出的某个标号。 请参阅 GotoLabels

Parameters

  • AssemblerTemplate

    这是一个文字字符串,它是汇编程序代码的模板,由固定文本和引用输入、输出及跳转参数的标记组合而成。请参见 AssemblerTemplate

  • OutputOperands

    由汇编模板中的指令修改的以逗号分隔的 C 变量列表。允许使用空列表。请参见 OutputOperands

  • InputOperands

    由 AssemblerTemplate 中的指令读取的以逗号分隔的 C 表达式列表。允许使用空列表。请参见 InputOperands

  • Clobbers

    除了输出之外,由汇编模板更改的以逗号分隔的寄存器或其它值的列表。允许空列表。请参阅 InputOperands

  • GotoLabels

    当使用asmgoto形式时,此部分包含 AssemblerTemplate 中的代码可能跳转到的所有 C 标号的列表。请参阅 GotoLabels

    asm语句不能跳转到其他asm语句中,只能跳转到列出的 GotoLabels 中。GCC 的优化器不知道其他跳转;因此在决定如何优化时不能考虑这些跳转。

输入+输出+跳转操作数的总数限制为30。

Remarks

asm语句允许您在 C 代码中直接包含汇编指令。这可以帮助您最大限度地提高对时间敏感的代码的性能,或者访问 C 程序不容易获得的汇编指令。

请注意,扩展asm语句必须在函数内部。只有基本asm可以在函数之外(请参阅 Basic Asm)。使用naked属性声明的函数也需要基本asm(请参见 Function Attributes)。

尽管asm的用法多种多样,但将asm语句看作一系列将输入参数转换为输出参数的低级指令可能会有所帮助。因此,一个使用asm的 i386的简单示例(如果不是特别有用)可能像这样:

int src = 1;
int dst;   

asm ("mov %1, %0\n\t"
    "add $1, %0"
    : "=r" (dst) 
    : "r" (src));

printf("%d\n", dst);

此代码将src复制到dst,并将dst加1。

6.47.2.1 Volatile

如果 GCC 的优化器确定不需要输出变量,则有时会丢弃asm语句。同样,如果优化器认为代码将始终返回相同的结果(即,两次调用之间的输入值均不变),则会将代码移出循环。使用volatile限定词将禁用这些优化。没有输出操作数的asm语句和asm goto语句是隐式易失的。

此 i386代码演示了一个不使用(或不需要)volatile限定符的情况。如果它执行下面的断言检查,则此代码使用asm执行验证;否则,任何代码都不会引用dwRes。结果,优化器可以丢弃asm语句,从而消除了对整个DoCheck例程的需求。通过在不需要时取消volatile限定符,可以使优化器生成尽可能高效的代码。

void DoCheck(uint32_t dwSomeValue)
{
   uint32_t dwRes;

   // Assumes dwSomeValue is not zero.
   asm ("bsfl %1,%0"
     : "=r" (dwRes)
     : "r" (dwSomeValue)
     : "cc");

   assert(dwRes > 3);
}

下一个示例显示了这样一种情况:优化器可以识别出输入(dwSomeValue)在函数执行期间从未改变,因此可以将asm移出循环以产生更有效的代码。同样,使用volatile限定符会禁用这种类型的优化。

void do_print(uint32_t dwSomeValue)
{
   uint32_t dwRes;

   for (uint32_t x=0; x < 5; x++)
   {
      // Assumes dwSomeValue is not zero.
      asm ("bsfl %1,%0"
        : "=r" (dwRes)
        : "r" (dwSomeValue)
        : "cc");

      printf("%u: %u %u\n", x, dwSomeValue, dwRes);
   }
}

以下示例演示了需要使用volatile限定符的情况。它使用x86 rdtsc指令,该指令读取计算机的时间戳记计数器。如果没有volatile限定符,优化器可能会假定asm块将始终返回相同的值,从而优化掉第二个调用。

uint64_t msr;

asm volatile ( "rdtsc\n\t"    // Returns the time in EDX:EAX.
        "shl $32, %%rdx\n\t"  // Shift the upper bits left.
        "or %%rdx, %0"        // 'Or' in the lower bits.
        : "=a" (msr)
        : 
        : "rdx");

printf("msr: %llx\n", msr);

// Do other work...

// Reprint the timestamp
asm volatile ( "rdtsc\n\t"    // Returns the time in EDX:EAX.
        "shl $32, %%rdx\n\t"  // Shift the upper bits left.
        "or %%rdx, %0"        // 'Or' in the lower bits.
        : "=a" (msr)
        : 
        : "rdx");

printf("msr: %llx\n", msr);

GCC 的优化程序不会像前面示例中的非易失性代码那样处理此代码。它们不会将其移出循环,也不会在假设前一次调用的结果仍然有效的前提下忽略它。

请注意,编译器甚至可以相对于其他代码移动volatile asm指令,包括跨跳转指令。例如,在许多目标处理器中都有一个系统寄存器来控制浮点运算的舍入模式。像下面的 PowerPC 示例一样,使用volatile asm语句进行设置并不可靠。

asm volatile("mtfsf 255, %0" : : "f" (fpenv));
sum = x + y;

编译器可能将加法移回volatile asm语句之前。为了使它按预期工作,请通过在后续代码中引用变量向“asm”添加一个人工依赖项,例如:

asm volatile ("mtfsf 255,%1" : "=X" (sum) : "f" (fpenv));
sum = x + y;

在某些情况下,GCC 可能会在优化时重复(或删除重复的)汇编代码。如果您的asm代码定义了符号或标号,则可能导致在编译期间出现意外的重复符号错误。 使用“%=”(请参阅 AssemblerTemplate)可以帮助解决此问题。

6.47.2.2 Assembler Template

汇编器模板是包含汇编器指令的文字字符串。编译器替换模板中引用输入、输出和跳转标号的标记,然后将结果字符串输出到汇编器。该字符串可以包含汇编程序可识别的任何指令,包括伪指令。GCC 不会自行解析汇编程序指令,也不知道它们的含义,甚至不知道它们是否为有效的汇编程序输入。但是,它会对语句进行计数(请参阅 Size of an asm)。

您可以将多个汇编程序指令放到一个asm字符串中,并以该系统中汇编代码常用的字符分隔。在大多数情况下有效的组合是换行符和制表符(写为“\n\t”)。一些汇编程序允许使用分号作为行分隔符。但是,请注意,某些汇编语法将分号作为注释符。

即使编译后使用volatile限定符,也不要期望汇编后的asm语句序列能完全保持连续。如果某些指令需要在输出中保持连续,请将它们放在单个多指令asm语句中。

不使用输入(输出)操作数(例如通过直接从汇编模板使用全局符号)从 C 程序访问数据可能无法按预期方式工作。同样,直接从汇编器模板调用函数需要详细了解目标汇编器和 ABI。

由于 GCC 不会解析汇编程序模板,因此它看不到所引用的任何符号。这可能导致 GCC 将这些符号作为未引用的符号丢弃,除非它们也作为输入、输出或跳转操作数列出。

Special format strings

除了输入、输出和跳转操作数所描述的标记外,这些标记在汇编程序模板中还有特殊的含义:

  • “%%”

    将单个“%”输出到汇编代码中。

  • “%=”

    输出asm语句的每个实例在整个编译中唯一的数字。在创建本地标号并在生成多个汇编指令的单个模板中多次引用它们时,此选项非常有用。

  • “%{”

  • “%|”

  • “%}”

    将“{”、“|”和“}”字符分别输出到汇编代码中。不转义时,这些字符在多种汇编语法中都具有特殊含义,如下所述。

Multiple assembler dialects in asm templates

在 x86等目标平台上,GCC 支持多种汇编语法。-masm选项控制 GCC 内联汇编程序的默认语法。-masm选项的特定目标文档包含受支持的语法列表,以及未指定时的默认语法。理解这些信息可能很重要,因为使用一种语法进行编译时可以正常工作的汇编代码,如果按照另一种语法编译可能会失败。请参阅 x86 Options

如果您的代码需要支持多种汇编语法(例如,如果您正在编写需要支持多种编译选项的公共头文件),请使用以下形式的构造:

{ dialect0 | dialect1 | dialect2... }

当使用 #0语法编译代码时,此构造输出dialect0;语法 1,输出dialect1,等等。如果括号内的替代项少于编译器支持的语法数量,则此构造不输出任何内容。

例如,如果 x86编译器支持两种语法(“att”,“intel”),则汇编程序模板如下:

"bt{l %[Offset],%[Base] | %[Base],%[Offset]}; jc %l2"

等价于其中之一

"btl %[Offset],%[Base] ; jc %l2"   /* att dialect */
"bt %[Base],%[Offset]; jc %l2"     /* intel dialect */

使用相同的编译器,以下代码:

"xchg{l}\t{%%}ebx, %1"

对应于

"xchgl\t%%ebx, %1"                 /* att dialect */
"xchg\tebx, %1"                    /* intel dialect */

不支持嵌套语法替代项。

6.47.2.3 Output Operands

asm语句具有零个或多个输出操作数,这些输出操作数指示由汇编代码修改的 C 变量的名称。

在下面这个 i386示例中,old(在模板字符串中称为%0)和*Base(称为%1)是输出,Offset%2)是输入:

bool old;

__asm__ ("btsl %2,%1\n\t" // Turn on zero-based bit #Offset in Base.
         "sbb %0,%0"      // Use the CF to calculate old.
   : "=r" (old), "+rm" (*Base)
   : "Ir" (Offset)
   : "cc");

return old;

操作数之间用逗号分隔。每个操作数都有以下形式:

[ [asmSymbolicName] ] constraint (cvariablename)
  • asmSymbolicName

    指定操作数的符号名称。在汇编程序模板中将名称括在方括号中即可引用该名称(即“%[Value]”)。名称的作用域为包含定义的asm语句。任何有效的 C 变量名称都是可接受的,包括周围代码中已定义的名称。同一asm语句中的两个操作数不能使用相同的符号名。

    如果不使用 asmSymbolicName,请使用汇编模板中操作数列表中操作数(从零开始)的位置。例如,如果有三个输出操作数,则在模板中使用“%0”引用第一个操作数,使用“%1”引用第二个操作数,“%2”表示第三个操作数。

  • constraint

    一个字符串常量,它指定对所放置操作数的约束;有关详细信息,请参见 Constraints

    输出约束必须以“=”(覆盖现有值的变量)或“+”(读取和写入时)开头。使用“=”时,除非操作数绑定到输入,否则不能假定该位置包含asm条目的现有值;请参阅 Input Operands

    在前缀之后,必须有一个或多个其他约束来描述值所在的位置(请参阅 Constraints)。常见的限制条件包括“r”代表寄存器,“m”代表内存。当您列出多个可能的位置(例如,“= rm”)时,编译器将根据当前上下文选择最有效的位置。如果您在asm语句允许的范围内列出尽可能多的替代项,则可以允许优化器产生最佳的代码。如果必须使用特定的寄存器,但是您的机器约束不能提供足够的控制来选择所需的特定寄存器,则可以使用局部寄存器变量来提供解决方案(请参阅 Local Register Variables)。

  • cvariablename

    指定一个 C 左值表达式来保存输出,通常是一个变量名。括号是语法的必需部分。

当编译器选择用于表示输出操作数的寄存器时,它不使用任何被破坏的寄存器(请参见 Clobbers and Scratch Registers)。

输出操作数表达式必须为左值。编译器无法检查操作数是否具有对于正在执行的指令合理的数据类型。对于不能直接寻址的输出表达式(例如位字段),约束必须允许寄存器。在这种情况下,GCC 将寄存器用作asm的输出,然后将该寄存器存储到输出中。

使用“+”约束修饰符的操作数计为两个操作数(即输入和输出),每个asm语句最多可包含30个操作数。

对不能与输入重叠的所有输出操作数使用“&”约束修饰符(请参阅 Modifiers)。否则,GCC 假设汇编代码在产生输出之前会消耗其输入,可能将输出操作数分配到与不相关的输入操作数相同的寄存器中。如果汇编代码实际上由多条指令组成,那么这种假设可能是错误的。

如果一个输出参数(a)允许寄存器约束,而另一个输出参数(b)允许内存约束,则可能发生相同的问题。GCC 生成的访问 b 中内存地址的代码可能包含与 a 共享的寄存器,并且 GCC 认为这些寄存器是asm的输入。如上所述,GCC 假定在写入任何输出之前就消耗了这些输入寄存器。如果asm语句在使用 b 之前写入 a,则此假设可能会导致错误的行为。在 a 上的寄存器约束中加入“&”修饰符可确保修改 a 时不会影响 b 引用的地址。否则,如果在使用 b 之前修改了 a,则 b 的位置未定义。

asm支持在操作数上应用操作数修饰符(例如,“%k2”而不是简单的“%2”)。通常,这些限定符取决于硬件。可在 x86 Operand modifiers 中找到 x86支持的修饰符列表。

如果asm后面的 C 代码没有使用任何输出操作数,请对asm语句使用volatile,以防止优化程序将asm语句作为不必要的代码丢弃(请参见 Volatile)。

此代码不使用可选的 asmSymbolicName。因此,它将第一个输出操作数引用为%0(如果有第二个,则为%1,依此类推)。第一个输入操作数的数字比最后一个输出操作数的数字大1。在此 i386示例中,将Mask引用为%1

uint32_t Mask = 1234;
uint32_t Index;

  asm ("bsfl %1, %0"
     : "=r" (Index)
     : "r" (Mask)
     : "cc");

该代码将覆盖变量Index(“=”),并将值存储在寄存器(“r”)中。使用通用的“r”约束而不是直接指定某个特定寄存器,可使编译器选择要使用的寄存器,从而可以提高代码效率。如果汇编指令需要特定的寄存器,则可能无法实现。

以下 i386示例使用 asmSymbolicName 语法。它产生的结果与上面的代码相同,但是有些人可能认为它更具可读性或可维护性,因为在添加或删除操作数时不需要对索引号进行重新排序。名称aIndexaMask仅在此示例中用于强调哪些名称在何处使用。可以重用名称IndexMask

uint32_t Mask = 1234;
uint32_t Index;

  asm ("bsfl %[aMask], %[aIndex]"
     : [aIndex] "=r" (Index)
     : [aMask] "r" (Mask)
     : "cc");

下面是输出操作数的更多示例。

uint32_t c = 1;
uint32_t d;
uint32_t *e = &c;

asm ("mov %[e], %[d]"
   : [d] "=rm" (d)
   : [e] "rm" (*e));

这里,d可以在寄存器中,也可以在内存中。由于编译器可能已经在寄存器中拥有e指向的uint32_t位置的当前值,因此可以通过指定两个约束使它为d选择最佳位置。

6.47.2.4 Flag Output Operands

有些目标有一个特殊的寄存器,用于保存操作或比较结果的“标志”。通常,要么asm不修改该寄存器的内容,要么视为asm语句破坏了内容。

在某些目标上,存在一种特殊形式的输出操作数,根据这种形式,标志寄存器中的条件可以是asm的输出。支持的条件集是特定于目标的,但一般规则是输出变量必须是标量整数,并且值是布尔值。当受支持时,目标定义预处理器符号__GCC_ASM_FLAG_OUTPUTS__

由于标志输出操作数的特殊性质,约束不能包括替代项。

通常,目标只有一个标志寄存器,因此是许多指令的隐含操作数。在这种情况下,不应在汇编模板中通过%0等引用操作数,因为汇编语言中没有相应的文本。

  • ARM

  • AArch64

    ARM 系列的标志输出约束形式为“=@cc cond”,其中 cond 是 ARM 中为ConditionHolds定义的标准条件之一。

    eq
    Z 标志置位,或等于
    ne
    Z 标志清除或不相等
    cs
    hs
    C 标志置位或无符号大于等于
    cc
    lo
    C 标志清除或无符号小于
    mi
    N 标志置位或“减号”
    pl
    N 标志清除或“加号”
    vs
    V标志置位或有符号溢出
    vc
    V标志清除
    hi
    无符号大于
    ls
    无符号小于等于
    ge
    有符号大于等于
    lt
    有符号小于
    gt
    有符号大于
    le
    有符号小于等于

    在 thumb1模式下不支持标志输出约束。

  • x86 系列

    x86系列的标志输出约束形式为“=@cc cond”,其中 cond 是 ISA 手册中为jccsetcc定义的标准条件之一。
    a
    “高于”或无符号大于
    ae
    “高于或等于”或无符号大于或等于
    b
    “以下”或无符号小于
    be
    “低于或等于”或无符号小于或等于
    c
    进位标志置位
    e
    z
    “相等”或零标志置位
    g
    有符号大于
    ge
    有符号大于或等于
    l
    有符号小于
    le
    有符号小于或等于
    o
    溢出标志置位
    p
    奇偶校验标志置位
    s
    符号标志置位
    na
    nae
    nb
    nbe
    nc
    ne
    ng
    nge
    nl
    nle
    no
    np
    ns
    nz
    “not”标志,或上述标志的反向版本

6.47.2.5 Input Operands

输入操作数使汇编代码可使用 C 变量和表达式中的值。

操作数之间用逗号分隔。每个操作数具有以下格式:

[ [asmSymbolicName] ] constraint (cexpression)
  • asmSymbolicName

    指定操作数的符号名。在汇编程序模板中引用该名称的方法是将其括在方括号中(即“%[Value]”)。名称的作用域为包含其定义的asm语句。任何有效的 C 变量名称都是可接受的,包括上下文代码中已定义的名称。同一asm语句中的两个操作数都不能使用相同的符号名。

    如果不使用 asmSymbolicName,请在汇编器模板的操作数列表中使用操作数(从零开始)的位置。例如,如果有两个输出操作数和三个输入,则在模板中使用“%2”来引用第一个输入操作数,第二个为“%3”,第三个为“%4”。

  • constraint

    指定操作数位置约束的字符串常量;有关详细信息,请参见 Constraints

    输入约束字符串不能以“=”或“+”开头。当您列出多个可能的位置(例如"irm")时,编译器会根据当前上下文选择效率最优的位置。如果必须使用特定的寄存器,但是您的机器约束不能提供足够的控制来选择所需的特定寄存器,则可以使用局部寄存器变量来解决(请参阅 Local Register Variables)。

    输入约束也可以是数字(例如,"0")。这表明在输出约束列表中对应索引(从0开始)的输出约束与该输入共享同一位置。对输出操作数使用 asmSymbolicName 语法时,可以使用这些名称(用括号“[]”括起来)代替数字。

  • cexpression

    这是作为输入传递给asm语句的 C 变量或表达式。括号是语法的必需部分。

当编译器选择用于表示输入操作数的寄存器时,它不使用所有被破坏的寄存器(请参见 Clobbers and Scratch Registers)。

如果没有输出操作数,但有输入操作数,请在输出操作数的位置放置两个连续的冒号:

__asm__ ("some instructions"
   : /* No outputs. */
   : "r" (Offset / 8));

警告:请勿修改纯输入操作数的内容(绑定到输出的输入除外)。编译器假定在退出asm语句时,这些操作数包含与执行该语句之前相同的值。无法使用 Clobber 通知编译器这些输入中的值正在更改。一种常见的解决方法是将变化的输入变量绑定到永远不会使用的输出变量。但是请注意,如果asm语句后的代码不使用任何输出操作数,则 GCC 优化器可能会将asm语句视为不需要的内容而丢弃(请参见 Volatile)。

asm支持操作数上的操作数修饰符(例如“%k2”而不是简单的“%2”)。通常,这些限定符依赖于硬件。x86 支持的修饰符列表位于 x86 Operand modifiers

在本例中,使用虚构的combine指令,输入操作数1的约束"0"表示它必须与输出操作数0占据相同的位置。只有输入操作数约束中可以使用数字,并且它们必须分别对应到一个输出操作数。约束中只有数字(或符号汇编程序名称)可以保证一个操作数与另一个操作数位于同一位置。仅foo是两个操作数的值这一事实不足以保证它们在生成的汇编代码中位于同一位置。

asm ("combine %2, %0" 
   : "=r" (foo) 
   : "0" (foo), "g" (bar));

下面是一个使用符号名的示例。

asm ("cmoveq %1, %2, %[result]" 
   : [result] "=r"(result) 
   : "r" (test), "r" (new), "[result]" (old));

6.47.2.6 Clobbers and Scratch Registers

尽管编译器知道对输出操作数列表项所做的更改,但内联汇编代码可能会修改的不仅仅是输出。例如,计算可能需要额外的寄存器,或者处理器可能会由于特定汇编指令的副作用而覆盖寄存器。为了将这些更改通知编译器,请在 Clobber 列表中列出它们。Clobber 列表项可以是寄存器名称,也可以是特殊的损坏部分(如下所示)。每个损坏列表项都是一个字符串常量,用双引号引起来并用逗号分隔。

损坏描述不能以任何方式与输入或输出操作数重叠。例如,操作数不能使用损坏列表中列出的寄存器。声明为存在于特定寄存器中的变量(请参阅 Explicit Register Variables)和用作asm输入输出操作数的变量,不能出现在 Clobber 描述中。特别地,如果不同时将输入操作数指定为输出操作数,就无法标识对其进行了修改。

当编译器选择使用哪个寄存器表示输入和输出操作数时,它不会使用任何被破坏的寄存器。因此,汇编代码中可不受限制的使用损坏寄存器。

另一个限制是,损坏列表不应包含堆栈指针寄存器。这是因为编译器要求在asm语句之后,堆栈指针的值必须与进入该语句时相同。但是,以前版本的 GCC 并未强制执行此规则,,并且允许堆栈指针出现在列表中,但语义不清楚。不建议使用此行为,并且在未来的 GCC 版本中列出堆栈指针可能会成为错误。

下面是 VAX 的一个实际示例,显示了破坏性寄存器的使用:

asm volatile ("movc3 %0, %1, %2"
                   : /* No outputs. */
                   : "g" (from), "g" (to), "g" (count)
                   : "r0", "r1", "r2", "r3", "r4", "r5", "memory");

此外,还有两个特殊的损坏参数:

  • "cc"

"cc"损坏符表示汇编代码修改了标志寄存器。在某些机器上,GCC 将条件代码表示为特定的硬件寄存器。"cc"用于命名该寄存器。在其他机器上,条件代码的处理有所不同,并且指定"cc"不起作用。但无论目标是什么,它都是有效的。

  • "memory"

    "memory"损坏符告诉编译器,汇编代码对输入和输出操作数中未列出的项执行内存读取或写入操作(例如,访问由一个输入参数指向的内存)。为了确保内存中包含正确的值,GCC 可能需要在执行asm之前将特定的寄存器值刷新到内存中。此外,编译器不会假设在asm之前从内存读取的任何值在asm之后保持不变;它会根据需要重新加载它们。使用"memory"损坏项有效地构成了编译器的读/写内存屏障。

    请注意,此损坏项不会阻止处理器通过asm语句进行推测性读取。为避免这种情况,您需要特定于处理器的栅栏指令。

将寄存器刷新到内存会影响性能,对于时间敏感的代码可能是一个问题。您可以向 GCC 提供更好的信息来避免这种情况,如以下示例所示。至少,别名规则允许 GCC 知道不需要刷新哪些内存。

下面是一个虚拟的平方和指令,它使用两个指针指向的内存中的浮点值,并产生浮点寄存器输出。请注意,xy都在asm参数中出现两次,一次是指定要访问的内存,另一次是指定asm使用的基址寄存器。通常,这样做不会浪费寄存器,因为 GCC 可以将同一寄存器用于这两个目的。但是,在这个asm中为x使用%1%3并期望它们相同是不明智的。实际上,%3可能不是寄存器。它可能是对x指向的对象的符号内存引用。

asm ("sumsq %0, %1, %2"
     : "+f" (result)
     : "r" (x), "r" (y), "m" (*x), "m" (*y));

以下是一个虚构的*z++ = *x++ * *y++指令。请注意,必须将xyz指针寄存器指定为输入(输出),因为asm会对它们进行修改。

asm ("vecmul %0, %1, %2"
     : "+r" (z), "+r" (x), "+r" (y), "=m" (*z)
     : "m" (*x), "m" (*y));

一个 x86示例,其中字符串内存参数的长度未知。

asm("repne scasb"
    : "=c" (count), "+D" (p)
    : "m" (*(const char (*)[]) p), "0" (-1), "a" (0));

如果您知道上面的命令只读取一个10字节的数组,那么可以使用类似于"m" (*(const char (*)[10]) p)的内存输入。

下面是一个在汇编中实现的 PowerPC 矢量缩放的示例,包括矢量和条件代码损坏项,以及一些已初始化且经asm不变的偏移寄存器。

void
dscal (size_t n, double *x, double alpha)
{
  asm ("/* lots of asm here */"
       : "+m" (*(double (*)[n]) x), "+&r" (n), "+b" (x)
       : "d" (alpha), "b" (32), "b" (48), "b" (64),
         "b" (80), "b" (96), "b" (112)
       : "cr0",
         "vs32","vs33","vs34","vs35","vs36","vs37","vs38","vs39",
         "vs40","vs41","vs42","vs43","vs44","vs45","vs46","vs47");
}

与其通过损坏列表为asm语句分配固定寄存器用于暂存,还可以定义变量并将其设置为早期损坏输出,如下面的示例中的a2a3所示。这在寄存器分配上赋予了编译器更多的自由。您还可以定义一个变量,并将其作为与输入绑定的输出,如a0a1,分别与aplda绑定。当然,对于绑定输出,asm不能在修改输出寄存器后使用输入值,因为它们是同一个寄存器。更重要的是,如果您省略了输出上的早期损坏,那么如果 GCC 能够证明它们在输入asm时具有相同的值,那么 GCC 可能会将相同的寄存器分配给另一个输入。这就是为什么a1具有早期损坏的原因。可以想象,lda的绑定输入值为16,若没有早期损坏标识,它与%11共享同一个寄存器。另一方面,ap不能与任何其他输入相同,因此不需要对a0上进行早期提示。在这种情况下也是不可取的。 a0上的早期损坏会导致 GCC 为"m" (*(const double (*)[]) ap)输入分配一个单独的寄存器。请注意,设置由asm语句修改的初始化临时寄存器的方法是将输入绑定到输出。GCC 假定没有绑定到输出的输入是不变的,例如,下面的"b" (16)%11设置为16,如果恰好需要值16,则 GCC 可以在以下代码中使用该寄存器。如果在使用暂存器之前已消耗了可能共享同一寄存器的所有输入,则甚至可以将普通的asm输出用于暂存器。除非 GCC 对asm参数数量的限制,被asm语句破坏的 VSX 寄存器可能已经使用了此技术。

static void
dgemv_kernel_4x4 (long n, const double *ap, long lda,
                  const double *x, double *y, double alpha)
{
  double *a0;
  double *a1;
  double *a2;
  double *a3;

  __asm__
    (
     /* lots of asm here */
     "#n=%1 ap=%8=%12 lda=%13 x=%7=%10 y=%0=%2 alpha=%9 o16=%11\n"
     "#a0=%3 a1=%4 a2=%5 a3=%6"
     :
       "+m" (*(double (*)[n]) y),
       "+&r" (n),	// 1
       "+b" (y),	// 2
       "=b" (a0),	// 3
       "=&b" (a1),	// 4
       "=&b" (a2),	// 5
       "=&b" (a3)	// 6
     :
       "m" (*(const double (*)[n]) x),
       "m" (*(const double (*)[]) ap),
       "d" (alpha),	// 9
       "r" (x),		// 10
       "b" (16),	// 11
       "3" (ap),	// 12
       "4" (lda)	// 13
     :
       "cr0",
       "vs32","vs33","vs34","vs35","vs36","vs37",
       "vs40","vs41","vs42","vs43","vs44","vs45","vs46","vs47"
     );
}

6.47.2.7 Goto Labels

asm goto允许汇编代码跳转到一个或多个 C 标号。asm goto语句中的 GotoLabels 部分包含用逗号分隔的列表,其中列出了汇编代码可能跳转到的所有 C 标号。GCC 假定asm的执行会进入下一条语句(如果不是这种情况,请考虑在asm语句之后使用__builtin_unreachable内在函数)。通过使用hotcold标号属性(请参见 Label Attributes),可以改善asm goto的优化。

如果汇编代码确实进行了任何修改,请使用"memory"损坏符强制优化器将所有寄存器值刷新到内存中,并在asm语句之后根据需要重新加载它们。

还要注意,asm goto语句总是隐式地被认为是易失性的。

asm goto中若仅在某些可能的控制流路径上设置输出操作数时要小心。如果您没有在给定路径上设置输出,并且从不在此路径上使用它,那是可以的。否则,您应使用“ +”约束修饰符,这意味着操作数是输入也是输出。使用此修饰符,您将在asm goto`的所有可能路径上获得正确的值。

要在汇编器模板中引用标号,请在标号前添加“%l”(字母“L”的小写),然后加上其在 GotoLabels 中(从零开始)位置与输入操作数数量之和。例如,如果asm具有三个输入并引用了两个标号,则将第一个标号称为“%l3”,将第二个标号称为“%l4”)。

或者,您可以使用括号内的实际 C 标号名称引用标号。例如,要引用名为carry的标号,可以使用“%l[carry]”。使用此方法时,标号仍必须在 GotoLabels 部分中列出。

以下是 i386的asm goto示例:

asm goto (
    "btl %1, %0\n\t"
    "jc %l2"
    : /* No outputs. */
    : "r" (p1), "r" (p2) 
    : "cc" 
    : carry);

return 0;

carry:
return 1;

以下示例显示了使用一个内存损坏的asm goto

int frob(int x)
{
  int y;
  asm goto ("frob %%r5, %1; jc %l[error]; mov (%2), %%r5"
            : /* No outputs. */
            : "r"(x), "r"(&y)
            : "r5", "memory" 
            : error);
  return y;
error:
  return -1;
}

以下示例显示了使用一个输出的asm goto

int foo(int count)
{
  asm goto ("dec %0; jb %l[stop]"
            : "+r" (count)
            :
            :
            : stop);
  return count;
stop:
  return 0;
}

以下人工示例显示了一个asm goto,它仅在asm goto内部的一条路径上设置输出。 使用=约束修饰符而不是+是错误的,因为从asm goto的所有路径上都使用factor

int foo(int inp)
{
  int factor = 0;
  asm goto ("cmp %1, 10; jb %l[lab]; mov 2, %0"
            : "+r" (factor)
            : "r" (inp)
            :
            : lab);
lab:
  return inp * factor; /* return 2 * inp or 0 if inp < 10 */
}

6.47.2.8 x86 Operand Modifiers

扩展asm语句的汇编程序模板中对输入、输出和跳转操作数的引用,在代码输出到汇编器时可以使用修饰符来影响操作数的格式化方式。例如,以下代码对 x86使用“h”和“b”修饰符:

uint16_t  num;
asm volatile ("xchg %h0, %b0" : "+a" (num) );

这些修饰符生成以下汇编代码:

xchg %ah, %al

本讨论的其余部分使用以下代码进行说明。

int main()
{
   int iInt = 1;

top:

   asm volatile goto ("some assembler instructions here"
   : /* No outputs. */
   : "q" (iInt), "X" (sizeof(unsigned char) + 1), "i" (42)
   : /* No clobbers. */
   : top);
}

如果没有修饰符,这就是汇编器的“att”和“intel”语法的操作数输出:

Operand‘att’‘intel’
%0%eaxeax
%1$22
%3$.L3OFFSET FLAT:.L3
%4$88
%5%xmm0xmm0
%7$00

下表显示了受支持的修饰符及其效果。

ModifierDescriptionOperand‘att’‘intel’
A打印绝对内存引用。%A0*%raxrax
b打印寄存器的 QImode 名称。%b0%alal
B打印b的操作码后缀。%B0b
c需要一个常量操作数,并打印不带标点符号的常量表达式。%c122
d打印 AVX 指令的重复寄存器操作数。%d5%xmm0, %xmm0xmm0, xmm0
E当目标为64位时,以双整数(DImode)模式(8字节)打印地址。否则模式未指定(VOIDmode)。%E1%(rax)[rax]
g打印寄存器的 V16SFmode 名称。%g0%zmm0zmm0
h打印“高”寄存器的 QImode 名称。%h0%ahah
H向可偏移内存引用添加8个字节。在访问高8字节的 SSE 值时非常有用。对于(%rax)中的 memref,它会生成%H08(%rax)8[rax]
k打印寄存器的 SImode 名称。%k0%eaxeax
l打印不带标点符号的标号名称。%l3.L3.L3
L打印l的操作码后缀。%L0l
N打印掩码z%N7{z}{z}
p打印原始符号名称(不带特定于语法的前缀)。%p24242
P如果用于函数,则打印 PLT 后缀并生成 PIC 代码。例如,为函数 foo() 发出foo@PLT而不是“foo”。 如果用于常量,则删除所有特定于语法的前缀并发出裸常量。 参见上面的p
q打印寄存器的 DImode 名称。%q0%raxrax
Q打印q的操作码后缀。%Q0q
R打印嵌入式舍入和sae%R4{rn-sae},, {rn-sae}
r仅打印sae%r4{sae},, {sae}
s打印移位双计数,然后使用汇编程序参数定界符打印s的操作码后缀。%s1$2,2,
S打印s的操作码后缀。%S0s
t打印寄存器的 V8SFmode 名称。%t5%ymm0ymm0
T打印t的操作码后缀。%T0t
V打印不带%的裸整数寄存器名。%V0eaxeax
w打印寄存器的 HImode 名称。%w0%axax
W打印w的操作码后缀。%W0w
x打印寄存器的 V4SFmode 名称。%x5%xmm0xmm0
y打印“st(0)”而不是“st”作为寄存器。%y6%st(0)st(0)
z打印当前整数操作数大小的操作码后缀b/w/l/q之一)。%z0l
Zz一样,带有 x87指令的特殊后缀。

6.47.2.9 x86 Floating-Point asm Operands

在 x86目标上,关于asm操作数中堆栈式寄存器的使用有几个规则。这些规则仅适用于堆栈式寄存器的操作数:

  1. 给定一组在asm中消失的输入寄存器,有必要知道asm隐式弹出了哪些输入寄存器,以及哪些必须由GCC显式弹出。
    除非受约束以匹配输出操作数,否则必须显式以损坏修饰由asm隐式弹出的输入寄存器。

  2. 对于由asm隐式弹出的任何输入寄存器,必须知道如何调整堆栈以补偿弹出。如果有任何未弹出的输入比隐式弹出的寄存器更靠近寄存器堆栈的顶部,则将无法知道堆栈看起来是什么样子,也不清楚堆栈的其余部分如何“向上滑动”。

    所有隐式弹出的输入寄存器必须比任何未隐式弹出的输入寄存器更接近寄存器堆栈的顶部。

    如果输入在asm中消失,编译器可能会使用输入寄存器重新加载输出。考虑以下示例:

    asm ("foo" : "=t" (a) : "f" (b));
    

    这段代码表示asm不会弹出输入basm将结果推送到寄存器堆栈上,即asm之后的堆栈比之前更深一层。但是,重载可能认为它可以对输入和输出使用相同的寄存器。

    为了防止这种情况的发生,如果任何输入操作数使用“f”约束,则所有输出寄存器约束都必须使用“&” 早期损坏修饰符。

    上面的示例正确编写为:

    asm ("foo" : "=&t" (a) : "f" (b));
    
  3. 一些操作数需要放在堆栈中的特定位置。所有输出操作数都属于这一类 GCC,没有其他方法知道输出出现在哪个寄存器中,除非您在约束中指出这一点。

    输出操作数必须明确指示asm后输出在哪个寄存器中。不允许“=f”:操作数约束必须选择具有单个寄存器的类。

  4. 不能在现有堆栈寄存器之间“插入”输出操作数。由于 387 操作码不使用读/写操作数,因此所有输出操作数在asm之前都是无效的,并由asm推送。除了堆栈寄存器的顶部之外,将其推入其他任何地方都没有意义。

输出操作数必须从寄存器堆栈的顶部开始:输出操作数不能“跳过”寄存器。

  1. 有些asm语句可能需要额外的堆栈空间来进行内部计算。这可以通过损坏与输入和输出无关的堆栈寄存器来保证。

这个asm接受一个内部弹出的输入,并产生两个输出。

asm ("fsincos" : "=t" (cos), "=u" (sin) : "0" (inp));

asm接受两个输入,这些输入由fyl2xp1操作码弹出,并将它们替换为一个输出。要使编译器知道fyl2xp1会同时弹出两个输入,st(1)损坏项是必需的。

asm ("fyl2xp1" : "=t" (result) : "0" (x), "u" (y) : "st(1)");

6.47.2.10 MSP430 Operand Modifiers


参考资料:

 类似资料:

相关阅读

相关文章

相关问答