如何在 C 代码中使用内联汇编语言

6.47 如何在 C 代码中使用内联汇编语言

使用 asm 关键字可以在 C 代码中嵌入汇编语言指令. GCC 提供两种形式的内联 asm 语句. 基本 asm 语句不带操作数(请参阅 “基本 asm - 不带操作数的汇编指令”), 而扩展 asm 语句(请参阅 “扩展 asm - 带 C 表达式操作数的汇编指令”)包含一个或多个操作数. 如果要在函数中混合使用 C 语言和汇编语言, 最好使用扩展形式, 但如果要在顶层包含汇编语言, 则必须使用基本 asm.

您还可以使用 asm 关键字覆盖 C 符号的汇编程序名称, 或将 C 变量置于特定寄存器中.

6.47.1 基本 asm - 不含操作数的汇编指令

基本 asm 语句的语法如下:

`asm` `asm`-qualifiers ( AssemblerInstructions  )

对于 C 语言, asm 关键字是 GNU 扩展. 在编写可以使用 -ansi 和 -std 选项编译的 C 代码时, 如果选择的 C 语言方言没有 GNU 扩展, 请使用 __asm__ 代替 asm(请参阅 替代关键字). 对于 C++ 语言, asm 是一个标准关键字, 但对于用 -fno-asm 编译的代码, 可以使用 __asm__ .

限定符

volatile

可选的 volatile 限定符不起作用. 所有基本的 asm 块都是隐式 volatile 的.

inline

如果使用 inline 限定符, 则为了内联的目的, asm 语句的大小将被视为尽可能小的大小(参见 asm 的大小).

参数

AssemblerInstructions

这是一个字面字符串, 用于指定汇编程序代码. 该字符串可以包含汇编程序识别的任何指令, 包括指令. GCC 不会解析汇编指令本身, 也不知道这些指令的含义, 甚至不知道它们是否是有效的汇编输入.

您可以将多条汇编指令放在一个 asm 字符串中, 并用系统汇编代码中通常使用的字符分隔. 在大多数情况下, 可以使用换行符来分行, 再加上制表符(写成"\n\t"). 有些汇编程序允许使用分号作为分隔符. 不过, 请注意有些汇编语言使用分号来开始注释.

备注

使用扩展 asm(请参阅扩展 asm - 使用 C 表达式操作符的汇编器指令)通常可以生成更小、更安全和更高效的代码, 在大多数情况下, 它是比基本 asm 更好的解决方案. 不过, 有两种情况只能使用基本 asm

扩展 asm 语句必须位于 C 函数内部, 因此要在文件作用域(“顶级”)、C 函数之外编写内联汇编语言, 必须使用 basic asm. 您可以使用这种技术来发射汇编指令、定义可在文件其他地方调用的汇编语言宏, 或用汇编语言编写整个函数. 函数外的 Basic asm 语句不得使用任何限定符.
使用裸属性声明的函数也需要使用基本 asm(请参阅声明函数属性).
从 basic asm 安全地访问 C 数据和调用函数比看上去要复杂得多. 要访问 C 数据, 最好使用扩展 asm.

不要期望编译后的 asm 语句序列能保持完全连续. 如果某些指令需要在输出中保持连续, 请将它们放在一条多指令 asm 语句中. 请注意, GCC 的优化器可以移动 asm 语句与其他代码的相对位置, 包括跨跳转.

asm 语句可能不会跳转到其他 asm 语句. GCC 不知道这些跳转, 因此在决定如何优化时无法考虑它们. 只有在扩展的 asm 中才支持从 asm 到 C 标签的跳转.

在某些情况下, GCC 在优化时可能会重复(或删除重复的)汇编代码. 如果您的汇编代码定义了符号或标签, 这可能会在编译过程中导致意想不到的重复符号错误.

警告: C 标准未指定 asm 的语义, 这可能导致编译器之间的不兼容性. 这些不兼容可能不会产生编译器警告/错误.

GCC 不会解析基本 asm 的 AssemblerInstructions, 这意味着编译器无法了解其中的内容. GCC 无法看到 asm 中的符号, 可能会将其作为未引用符号丢弃. 它也不知道汇编代码的副作用, 例如对内存或寄存器的修改. 与某些编译器不同的是, GCC 假定通用寄存器不会发生变化. 这一假设在未来的版本中可能会改变.

为避免未来语义变化带来的复杂性以及编译器之间的兼容性问题, 请考虑将基本 asm 替换为扩展 asm. 有关如何进行转换的信息, 请参阅如何从基本 asm 转换为扩展 asm.

编译器会将基本 asm 中的汇编器指令逐字复制到汇编语言输出文件, 而不会处理扩展 asm 中的方言或任何"%“操作符. 这导致基本 asm 字符串与扩展 asm 模板之间存在细微差别. 例如, 要引用寄存器, 在基本 asm 中可以使用”%eax", 而在扩展 asm 中则可以使用"%%eax".

在 x86 等支持多种汇编语言的目标机上, 所有基本 asm 块都使用 -mmas 命令行选项指定的汇编语言(请参阅 x86 选项). Basic asm 不提供为不同方言提供不同汇编器字符串的机制.

对于使用非空汇编字符串的基本 asm, GCC 假定汇编块不会更改任何通用寄存器, 但可以读取或写入任何全局可访问变量.

6.47.2 扩展 Asm - 带有 C 表达式操作符的汇编指令

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

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

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

在最后一种形式中, asm-qualifiers 包含 goto (而在第一种形式中, 不包含) .

asm 关键字是 GNU 扩展. 在编写可使用 -ansi 和各种 -std 选项编译的代码时, 请使用 __asm__ 代替 asm (请参阅替代关键字) .

限定符

volatile

扩展的 asm 语句的典型用途是处理输入值以产生输出值. 然而, 您的 asm 语句也可能产生副作用. 如果是这样, 您可能需要使用 volatile 限定符来禁用某些优化. 请参阅 volatile.

inline

如果您使用了 inline 限定符, 那么为了内联的目的, asm 语句的大小将被视为尽可能小的大小 (请参阅 asm 的大小) .

goto

该限定符通知编译器, asm 语句可以跳转到 GotoLabels 中列出的标签之一. 请参阅 GotoLabels.

参数

AssemblerTemplate

这是一个字面字符串, 是汇编代码的模板. 它是固定文本与指向输入、输出和转到参数的标记的组合. 请参阅 AssemblerTemplate.

OutputOperands

以逗号分隔的 C 语言变量列表, 包含被 AssemblerTemplate 指令修改过的变量. 允许使用空列表. 请参阅 OutputOperands.

InputOperands

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

Clobbers

以逗号分隔的寄存器或其他由 AssemblerTemplate 更改的值的列表, 不包括列为输出的值. 允许使用空列表. 请参阅 "缓冲区 "和 “擦除寄存器”.

GotoLabels

当使用 asm 的 goto 形式时, 该部分包含所有 C 标签的列表, AssemblerTemplate 中的代码可以跳转到这些标签. 请参阅 GotoLabels.

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

输入 + 输出 + goto 操作数的总数限制为 30

备注

通过 asm 语句, 您可以在 C 代码中直接包含汇编指令. 这可以帮助您最大限度地提高对时间敏感的代码的性能, 或访问 C 程序无法使用的汇编指令.

请注意, 扩展 asm 语句必须位于函数内部. 只有基本 asm 才可以位于函数之外(请参阅 “基本 asm - 不含操作数的汇编指令”). 使用裸属性声明的函数也需要基本 asm(请参阅函数属性声明).

虽然 asm 的用途多种多样, 但将 asm 语句视为一系列将输入参数转换为输出参数的低级指令可能会有所帮助. 因此, 在 i386 中使用 asm 的一个简单(如果不是特别有用)示例可能是这样的:

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 语句. 此外, 如果优化程序认为代码将始终返回相同的结果(即其输入值在调用之间不会发生变化), 则可能会将代码移出循环. 没有输出操作数的 asm 语句和 asm goto 语句是隐式 volatile 的.

这段 i386 代码演示了一种不使用(或不需要) volatile 限定符的情况. 如果要执行断言检查, 该代码会使用 asm 进行验证. 否则, 任何代码都不会引用 dwRes. 这样, 优化程序就可以放弃 asm 语句, 从而不再需要整个 DoCheck 例程. 通过在不需要 volatile 限定符时省略它, 优化程序就能生成最高效的代码.

void DoCheck(uint32_t dwSomeValue)
{
   uint32_t dwRes;

   // 假定 dwSomeValue 不为零. 
   // 注: bsfl 是 "Bit Scan Forward Long" 的缩写, 它用于查找一个32位操作数中最低位(从低位到高位)的第一个置位(为1)的位置
   //     bsfl dest, src
   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++)
   {
      // 假定 dwSomeValue 不为零. 
      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" // 返回以 EDX:EAX 为单位的时间. 
        "shl $32, %%rdx\n\t" // 左移高位. 
        "or %%rdx, %0" // 'Or' in the lower bits.
        : "=a" (msr)
        : 
        : "rdx");

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

// 执行其他工作

// 重印时间戳
asm volatile ( "rdtsc\n\t" // Returns the time in EDX:EAX.
        "shl $32, %%rdx\n\t" // 左移上面的位. 
        "or %%rdx, %0" // 'Or' in the lower bits.
        : "=a" (msr)
        : 
        : "rdx");

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

GCC 的优化器不会像前面例子中的非易失性代码那样处理这段代码. 它们不会将其移出循环, 也不会假设前一次调用的结果仍然有效而省略它.

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

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 汇编程序模板

汇编模板是包含 汇编指令的字面字符串. 编译器会替换模板中指向输入、输出和 goto 标签的标记, 然后将生成的字符串输出给汇编器. 该字符串可以包含汇编器识别的任何 instructions, 包括 directives. GCC 不会解析汇编指令本身, 也不知道这些指令的含义, 甚至不知道它们是否是有效的汇编输入. 不过, 它会计算 statements(请参阅 asm 的大小).

你可以将多条汇编指令放在一个 asm 字符串中, 并用系统汇编代码中通常使用的字符分隔. 在大多数情况下, 可以使用换行符来分行, 再加上一个制表符来移动到指令字段(写成"\n\t"). 有些汇编程序允许使用分号作为分隔符. 不过, 请注意有些汇编语言使用分号来开始注释.

即使在使用 volatile 限定符的情况下, 也不要指望一连串的 asm 语句在编译后保持完全连续. 如果某些指令需要在输出中保持连续, 请将它们放在一条多指令 asm 语句中.

在不使用 输入/输出 操作数的情况下 访问 C 程序中的数据(如直接使用汇编模板中的全局符号)可能无法达到预期效果. 同样, 直接从汇编器模板调用函数也需要详细了解目标汇编器和 ABI.

由于 GCC 不会解析汇编器模板, 因此无法看到其引用的任何符号. 这可能导致 GCC 将这些符号视为未引用符号, 除非它们也被列为输入、输出或 goto 操作数.

特殊格式字符串

除了输入、输出和 goto 操作符描述的符号外, 这些符号在汇编模板中还有特殊含义:

‘%%’

在汇编代码中输出单个’%'.

‘%=’

在整个编译过程中, 为 asm 语句的 每个实例输出一个唯一的数字. 当创建本地标签并在生成多条汇编指令的单个模板中多次引用这些标签时, 该选项非常有用.

‘%{’

‘%|’

‘%}’

分别在汇编代码中输出’{‘、’|‘和’}'字符. 这些字符在未转义时具有特殊含义, 可表示多种汇编语言, 详情如下.

asm 模板中的多种汇编语言

在 x86 等目标机上, GCC 支持多种汇编语言. `-masm`` 选项控制 GCC 默认使用哪种方言作为内联汇编语言. 针对特定目标的 -mmasm 选项文档包含支持的方言列表, 以及未指定该选项时的默认方言. 了解这些信息可能很重要, 因为在使用一种方言编译时能正确运行的汇编代码, 在使用另一种方言编译时很可能会失败. 请参阅 x86 选项.

如果你的代码需要支持多种汇编语言(例如, 如果你编写的公共头文件需要支持多种编译选项), 请使用这种形式的结构:

{ 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 输出操作数

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;

返回 old;
操作数之间用逗号隔开. 每个操作数的格式如下

[ [asmSymbolicName] ] constraint (cVariableName)
asmSymbolicName

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

不使用 asmSymbolicName 时, 应使用操作数在汇编模板操作数列表中的位置(基于零). 例如, 如果有三个输出操作数, 在模板中使用"%0 "表示第一个, "%1 "表示第二个, "%2 "表示第三个.

constraint

指定操作数位置限制 的字符串常量; 详情请参见 asm 操作数的限制.

输出约束必须以"="(变量覆盖现有值)或 “+”(读写时)开头. 使用"="时, 不要假定该位置包含进入 asm 时的现有值, 除非操作数与输入绑定; 请参阅输入操作数.

在前缀之后, 必须有一个或多个附加约束(请参阅 asm 操作数的约束)来描述值的位置. 常见的约束包括 表示寄存器的 "r "表示内存的 “m”. 当您列出多个可能的位置(例如"=rm")时, 编译器会根据当前上下文选择最有效的位置. 如果在 asm 语句允许的范围内列出尽可能多的备选位置, 就能让优化器生成最佳代码. 如果您必须使用特定的寄存器, 但机器约束没有提供足够的控制来选择您想要的特定寄存器, 那么局部寄存器变量可能是一个解决方案(请参阅为局部变量指定寄存器).

cvariablename

指定一个 C lvalue 表达式来保存输出, 通常是一个变量名. 括号是语法的必要组成部分.

编译器在选择用于表示输出操作数的寄存器时, 不会使用任何 “缓冲寄存器”(参见 "缓冲寄存器 "和 “擦除寄存器”).

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

使用 "+"约束修饰符的操作数算作两个操作数(即输入和输出), 每个 asm 语句最多可有 30 个操作数.

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

Use the ‘&’ constraint modifier (see Constraint Modifier Characters) on all output operands that must not overlap an input. Otherwise, GCC may allocate the output operand in the same register as an unrelated input operand, on the assumption that the assembler code consumes its inputs before producing outputs. This assumption may be false if the assembler code actually consists of more than one instruction.

如果一个输出参数(a)允许寄存器约束, 而另一个输出参数(b)允许内存约束, 也会出现同样的问题. GCC 生成的用于访问 b 中内存地址的代码可能包含 a 可能共享的寄存器, 而 GCC 将这些寄存器视为 asm 的输入. 如上所述, GCC 假定在写入任何输出之前, 这些输入寄存器都会被消耗掉. 将"&"修饰符与 a 上的寄存器约束相结合, 可以确保修改 a 不会影响 b 引用的地址.
没看懂

asm 支持操作数上的操作数修饰符(例如"%k2", 而不是简单的"%2"). 通用操作数修饰符列出了在所有目标机上都可用的修饰符. 其他修改器取决于硬件. 例如, x86 支持的修改器列表请参见 x86 Operand modifiers.

如果 asm 后面的 C 代码不使用任何输出操作数, 请对 asm 语句使用 volatile, 以防止优化器将 asm 语句视为不需要而丢弃(请参阅 Volatile).

这段代码没有使用可选的 asmSymbolicName(没有用 %[Index] 这种表达). 因此, 它引用的第一个输出操作数为 %0(如果有第二个, 则为 %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 语法. 它产生的结果与上面的代码相同, 但有些人可能会认为它更具可读性或可维护性, 因为在添加或删除操作数时不需要对索引号重新排序. 在本示例中使用 aIndex 和 aMask 这两个名称只是为了强调哪个名称用于哪个位置. 重复使用 Index 和 Mask 这两个名称是可以接受的.

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 系列的标志输出约束格式为 “=@cccond” , 其中 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

有符号小于等于

拇指 1 模式不支持标志输出约束.

x86 系列

x86 系列的标志输出约束格式为 “=@cccond”, 其中 cond 是 ISA 手册中为 jcc 或 setcc 定义的标准条件之一.

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

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

不使用 asmSymbolicName 时, 应使用操作数在汇编模板操作数列表中的位置(基于零). 例如, 如果有两个输出操作数和三个输入操作数, 在模板中使用"%2 “表示第一个输入操作数, 使用”%3 “表示第二个操作数, 使用”%4 "表示第三个操作数.

constraint

指定操作数位置限制的字符串常量; 详情请参阅 asm 操作数的限制.

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

输入约束也可以是数字(例如 “0”). 这表示指定的输入必须与输出约束位于输出约束列表中(基于 0 的)索引的相同位置. 为输出操作数使用 asmSymbolicName 语法时, 可以使用这些名称(用括号"[]"括起来)代替数字.

cexpression

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

编译器在选择用于表示输入操作数的寄存器时, 不会使用任何 "lobber "寄存器(参见 "lobber "和 "Scratch "寄存器).

作者注:
Lobber(破坏寄存器):
Lobber指的是在函数调用过程中, 函数会修改(破坏)的寄存器, 通常这些寄存器会有push/pop操作保证内容得到恢复
Scratch(临时寄存器):
Scratch指的是在函数中用于存储临时数据的寄存器. 这些寄存器的内容在函数执行期间可以被修改, 并且在函数调用结束后并不需要被恢复到原始值. 

通常, EAX、ECX 和 EDX 这三个寄存器被认为是scratch寄存器. 

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

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

警告 不要修改只输入操作数的内容(与输出绑定的输入除外). 编译器会假定, 从 asm 语句退出时, 这些操作数的值与执行该语句前的值相同. 因此, 编译器无法使用 clobbers 来通知编译器这些输入的值正在发生变化. 一种常见的变通方法是将变化的输入变量与一个从未使用过的输出变量绑定. 但请注意, 如果 asm 语句后面的代码没有使用任何输出操作数, GCC 优化器可能会认为 asm 语句没有必要而将其丢弃(请参阅易失性).

asm 支持操作数上的操作数修饰符(例如, 用"%k2 “代替简单的”%2"). 通用操作数修饰符列出了所有目标机都可用的修饰符. 其他修改器取决于硬件. 例如, x86 支持的修改器列表见 x86 Operand modifiers.

在这个使用虚构的组合指令的示例中, 输入操作数 1 的约束条件 "0 "表示它必须与输出操作数 0 位于同一位置. 只有输入操作数可以在约束条件中使用数字, 而且每个数字必须指向一个输出操作数. 只有约束中的数字(或符号汇编名)才能保证一个操作数与另一个操作数位于同一位置. 仅仅因为 foo 是两个操作数的值, 还不足以保证它们在生成的汇编代码中处于相同位置.
asm (“combine %2, %0”
: “=r” (foo)
: “0” (foo), “g” (bar));

下面是一个使用符号名称的示例
```cpp
asm ("cmoveq %1, %2, %[result]" 
   : [result] "=r"(result) 
   : "r" (test), "r" (new), "[result]" (old));

6.47.2.6 Clobbers and Scratch Registers

虽然编译器知道输出操作数中列出的条目变化, 但内联 asm 代码可能修改的不仅仅是输出. 例如, 计算可能需要额外的寄存器, 或者处理器可能会覆盖某个寄存器作为特定汇编指令的副作用. 为了告知编译器这些变化, 请将它们列在缓冲区列表中. 寄存器列表项可以是寄存器名称, 也可以是特殊寄存器(如下所列). 每个控制寄存器列表项都是一个字符串常数, 用双引号括起来, 中间用逗号隔开.

缓冲区说明不得以任何方式与输入或输出操作数重叠. 例如, 在寄存器列表中列出只有一个成员的寄存器类时, 不能使用操作数来描述该寄存器. 声明存在于特定寄存器中的变量(参见指定寄存器中的变量), 以及用作 asm 输入或输出操作数的变量, 都不能在隐含说明中提及. 特别是, 如果不指定输入操作数为输出操作数, 就无法指定输入操作数被修改.

编译器在选择使用哪些寄存器来表示输入和输出操作数时, 不会使用任何 clobbered 的寄存器. 因此, clobbered 寄存器可以在汇编代码中任意使用.

另一个限制是, 迭代列表不应包含堆栈指针寄存器. 这是因为编译器要求堆栈指针在 asm 语句之后的值与进入该语句时的值相同. 然而, 以前版本的 GCC 并不执行这一规则, 而是允许堆栈指针出现在列表中, 其语义并不明确. 在未来的 GCC 版本中, 这种行为已被淘汰, 列出堆栈指针可能会成为一个错误.

下面是一个 VAX 的实际例子, 展示了如何使用 clobbered 寄存器:

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 "没有任何作用. 但无论在哪种机器上, "cc "都是有效的.

“memory”

memory 语法告诉编译器, 汇编代码将对输入和输出操作数中列出的项目以外的项目执行内存读取或写入操作(例如, 访问某个输入参数指向的内存). 为确保内存包含正确的值, GCC 可能需要在执行 asm 之前将特定寄存器的值刷新到内存中. 此外, 编译器不会假定在执行 asm 之前从内存读取的任何值在该 asm 之后保持不变; 编译器会根据需要重新加载这些值. 使用 "内存"缓冲器实际上为编译器形成了一个读/写内存屏障.

需要注意的是, 该缓冲器并不能阻止处理器在 asm 语句之后进行推测性读取. 要防止这种情况, 需要特定于处理器的 fence 指令.

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

下面是一条虚构的平方和指令, 它使用两个指向内存中浮点数值的指针, 并产生一个浮点寄存器输出. 注意 x 和 y 在 asm 参数中出现了两次, 一次用于指定访问的内存, 另一次用于指定 asm 使用的基寄存器. 这样做通常不会浪费寄存器, 因为 GCC 可以将同一个寄存器用于两种用途. 但是, 如果在这个 asm 中使用 %1 和 %3 表示 x, 并期望它们是相同的, 那就太愚蠢了. 事实上, %3 很可能不是寄存器. 它可能是 x 指向的对象的符号内存引用.

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

下面是一条虚构的 *z++ = *x++ * *y++ 指令. 注意 x、y 和 z 指针寄存器必须指定为输入/输出, 因为 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 语句提供从头寄存器相比, 另一种方法是定义一个变量, 使其成为早期缓冲器的输出, 如下例中的 a2 和 a3. 这样编译器的寄存器分配器就有了更大的自由度. 你也可以定义一个变量, 使其成为一个与输入绑定的输出, 如 a0 和 a1, 分别与 ap 和 lda 绑定. 当然, 由于输出寄存器和输入寄存器是同一个寄存器, 因此在修改输出寄存器后, Asm 无法使用输入值. 更重要的是, 如果省略了输出端的早期缓冲器, 如果 GCC 可以证明它们在进入 asm 时具有相同的值, 那么 GCC 就有可能将相同的寄存器分配给另一个输入端. 这就是 a1 具有早期缓冲器的原因. 可以想象, 它的并列输入 lda 的值是 16, 如果没有早期缓冲器, 就会与 %11 共享同一个寄存器. 另一方面, ap 不可能与任何其他输入相同, 因此不需要在 a0 上设置早期缓冲器. 在这种情况下也不需要. 在 a0 上设置早期缓冲器会导致 GCC 为 "m"(*(const double (*)[]) ap) 输入分配一个单独的寄存器. 请注意, 将输入绑定到输出是通过 asm 语句修改初始化临时寄存器的方法. 不与输出绑定的输入被 GCC 认为是不变的, 例如下面的 “b”(16) 将 %11 设置为 16, 如果需要值 16, GCC 可能会在后续代码中使用该寄存器. 如果所有可能共享同一寄存器的输入都在使用 scratch(临时寄存器?) 之前被消耗掉, 您甚至可以使用普通的 asm 输出来使用 scratch . 除了 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  --> 与第 3 个操作数 a0 绑定
       "4" (lda) // 13  --> 与第 4 个操作数 a1 绑定
     :
       "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). 使用热标签和冷标签属性(参见标签属性)可以改进 asm goto 的优化.

如果汇编代码确实修改了任何内容, 请使用 "memory"缓冲器强制优化程序将所有寄存器值刷新到内存中, 必要时在 asm 语句后重新加载.

还要注意的是, asm goto 语句总是被隐含地视为易失性(volatile)语句.

当你只在某些可能的控制流路径上设置 asm goto 内的输出操作数时, 一定要小心. 如果不在给定路径上设置输出操作数, 也不在该路径上使用输出操作数, 则没有问题. 否则, 应使用 "+"约束修改器, 意思是操作数既是输入又是输出. 有了这个修改器, 你就能在 asm goto 的所有可能路径上获得正确的值.

要在汇编模板中引用一个标签, 请在其前缀加上"%l"(小写 “L”), 然后是它在 GotoLabels 中的位置(以零为基础), 再加上输入和输出操作数. 带有约束修饰符 “+“的输出操作数被视为两个操作数, 因为它被视为一个输出操作数和一个输入操作数. 例如, 如果 asm 有三个输入操作数、一个带约束修饰符 “+“的输出操作数和一个带约束修饰符”=“的输出操作数, 并引用了两个标签, 则第一个标签应称为”%l6”, 第二个标签应称为”%l7”).

另外, 也可以使用括号中的实际 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 的所有路径.

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 通用操作数修饰符

下表列出了所有目标函数支持的修饰符及其作用:

修改器描述示例
c要求使用常量操作数, 并打印不带标点符号的常量表达式.%c0
n与"%c "类似, 但常量值在打印前会被否定.%n0
a替换内存引用, 实际操作项被视为地址. 这在输出 "加载地址 "指令时可能有用, 因为这种指令的汇编语法通常要求将操作数当作内存引用来写.%a0
l打印不带标点符号的标签名称%l0
6.47.2.9 x86 Operand Modifiers

在扩展 asm 语句的汇编器模板中引用输入、输出和 goto 操作数时, 可以使用修改器来影响操作数在输出到汇编器的代码中的格式. 例如, 以下代码使用了 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

下表列出了支持的修改器及其效果.

修改器说明操作数“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 个字节添加到偏移表内存引用. 在访问 SSE 值的高 8 字节时很有用. 对于 (%rax) 中的 memref, 它生成%H08(%rax)8[rax]
k打印寄存器的 SImode 名称.%k0%eaxeax
l打印不带标点符号的标签名称.%l3.L3.L3
L打印 l 的操作码后缀.%L0l
N打印 maskz.%N7{z}{z}
p打印原始符号名称(没有特定于语法的前缀).%p24242
P如果用于函数, 则打印 PLT 后缀并生成 PIC 代码. 例如, foo@PLT对于函数 foo(), 使用 emmit 而不是 ‘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/之一). lq%z0l
Z例如z, 带有 x87 指令的特殊后缀.
6.47.2.10 x86 Floating-Point asm Operands

在 x86 目标机上, asm 操作数中堆栈类寄存器的使用有几条规则. 这些规则只适用于类堆栈寄存器的操作数:

  1. 给定一组在 asm 中死亡的输入寄存器, 有必要知道哪些寄存器会被 asm 隐式弹出, 哪些寄存器必须被 GCC 显式弹出.
    被 asm 隐式弹出的输入寄存器必须被显式弹出, 除非它受限于匹配输出操作数.

  2. 对于任何被 asm 隐式弹出的输入寄存器, 必须知道如何调整堆栈以补偿弹出. 如果任何非弹出输入比隐式弹出寄存器更接近reg堆栈的顶端, 那么就无法知道堆栈是什么样子–也就不清楚堆栈的其他部分是如何 "向上滑动 "的.

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

如果一个输入在 asm 中死亡, 编译器可能会将该输入寄存器用于输出重载. 请看下面这个例子

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

这段代码说明输入 b 没有被 asm 弹出, 并且 asm 将结果推入了堆栈, 也就是说, 堆栈在 asm 之后比之前更深了一层. 但是, reload 有可能认为可以将同一个寄存器用于输入和输出.

– 沒看懂

为了防止这种情况发生, 如果任何输入操作数使用了 "f “约束, 那么所有输出寄存器约束都必须使用”&"早期缓冲修饰符.

上面的示例正确的写法是

asm ("foo" : "=&t" (a) : "f" (b));
  1. 有些操作数需要位于堆栈的特定位置. 所有输出操作数都属于这一类, 除非您在约束中指明, 否则 GCC 无法知道输出操作数出现在哪个寄存器中.
    输出操作数必须明确指出输出出现在 asm 后的哪个寄存器中. 不允许使用"=f":操作数约束必须选择具有单个寄存器的类.

  2. 输出操作数不得 "插入 "现有堆栈寄存器之间. 由于没有 387 操作码使用读/写操作数, 因此所有输出操作数在 asm 之前都是死的, 并被 asm 推入. 除了 reg 堆栈的顶端, 推入其他任何地方都没有意义.
    输出操作数必须从寄存器栈顶开始:输出操作数不能 "跳过 "寄存器.

  3. 某些 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.11 MSP430 Operand Modifiers

下面的列表描述了 MSP430 支持的修改器及其效果.

修饰符描述
A选择常量/寄存器/存储器操作数的低 16 位.
B选择常量/寄存器/存储器操作数的高 16 位.
C选择常量/寄存器/存储器操作数的位 32-47.
D选择常量/寄存器/存储器操作数的位 48-63.
H相当于B(为了向后兼容).
INOT打印常量值的逆(逻辑).
J打印一个不带前缀的整数#.
L相当于A(为了向后兼容).
O当前帧距堆栈顶部的偏移量.
Q使用A指令后缀.
R条件代码的逆, 用于无符号比较.
W从常数值中减去 16.
X使用X指令后缀.
Y从常数值中减去 4.
Z从常数值中减去 1.
b根据模式, 将.B,.W或.A附加到指令中.
d内存引用或常量值的偏移 1 个字节.
e内存引用或常量值的偏移 3 个字节.
f内存引用或常量值的偏移 5 个字节.
g内存引用或常量值的偏移 7 个字节.
p打印 2 的值, 并计算给定常量的幂. 用于选择指定的位位置.
r条件代码的逆, 用于有符号比较.
x相当于X, 但仅适用于指针.
6.47.2.12 LoongArch Operand Modifiers

下面的列表描述了 LoongArch 支持的修改器及其效果.

修饰符描述
d与 相同c.
ii如果操作数不是寄存器, 则打印字符"i".
m与 相同c, 但打印的值为 operand - 1.
X以十六进制打印常量整数操作数.
z以未修改的形式打印操作数, 后跟逗号.
6.47.2.13 RISC-V Operand Modifiers

下面的列表描述了 RISC-V 支持的修饰符及其效果.

修饰符描述
zzero如果操作数是值为零的立即数, 则打印"zero"而不是 0.
ii如果操作数是立即数, 则打印字符"i".

6.47.3 Constraints for asm Operands

下面具体介绍了可以在 asm 操作数中使用的约束字母. 约束可以说明操作数是否可以在寄存器中, 以及寄存器的种类; 操作数是否可以是内存引用, 以及内存引用的地址种类; 操作数是否可以是立即常数, 以及立即常数的可能值. 约束还可以要求两个操作数匹配. 除非使用"<“或”>"约束, 否则不允许在内联 asm 的操作数中使用副作用, 因为无法保证副作用会在一条可以更新寻址寄存器的指令中准确发生一次.

6.47.3.1 Simple Constraints

最简单的约束是一个由字母组成的字符串, 每个字母描述一种允许使用的操作数. 以下是允许使用的字母:

whitespace

空白字符会被忽略, 可以插入到除第一个字符外的任何位置. 这样, 即使不同操作数的约束条件和修饰符数量不同, 也能在机器描述中直观地对齐.

‘m’

允许内存操作数, 可使用机器一般支持的任何地址. 请注意, 后端可以使用 TARGET_MEM_CONSTRAINT 宏重新定义用于一般内存约束的字母.

‘o’

允许使用内存操作数, 但必须是可偏移地址. 这意味着可以在地址中加入一个小整数(实际上是操作数的字节宽度, 由其机器模式决定), 其结果也是一个有效的内存地址.

例如, 常量地址是可偏移的; 寄存器和常量之和的地址也是可偏移的(只要稍大的常量也在机器支持的地址偏移范围内); 但自动递增或自动递减地址不是可偏移的. 更复杂的间接/索引地址可能是可偏移的, 也可能不是, 这取决于机器支持的其他寻址模式.

请注意, 在可与其他操作数匹配的输出操作数中, 约束字母’o’只有在同时伴有’<‘(如果目标机器具有预增寻址功能)和’>'(如果目标机器具有预增寻址功能)时才有效.

‘V’

不可偏移的内存操作数. 换句话说, 任何符合 "m"约束条件但不符合 “o” 约束条件的操作数.

‘<’

允许使用自动递减寻址(前递减或后递减)的内存操作数. 在内联 asm 中, 只有当操作数在能处理副作用的指令中被精确使用一次时, 才允许使用该约束. 在内联 asm 模式中, 完全不使用约束字符串中带有"<“的操作数或在多条指令中使用该操作数都是无效的, 因为副作用不会被执行或将被执行多次. 此外, 在某些目标机上, 约束字符串中带有”<"的操作数必须带有特殊的指令后缀, 如 PowerPC 上的 %U0 指令后缀或 IA-64 上的 %P0.

‘>’

允许使用自动递增寻址(前置递增或后置递增)的内存操作数. 在内联 asm 中, 适用与’<'相同的限制.

‘r’

允许使用寄存器操作数, 但必须是普通寄存器中的操作数.

‘i’

允许使用直接整数操作数(具有常数值的操作数). 这包括只有在汇编时或稍后才知道其值的符号常数.

‘n’

允许使用已知数值的直接整数操作数. 许多系统无法为宽度小于一个字的操作数提供汇编时常数. 这些操作数的约束条件应使用’n’而不是’i’.

‘I’, ‘J’, ‘K’, … ‘P’

'I’至’P’范围内的其他字母可根据机器的不同情况进行定义, 以允许在指定范围内具有明确整数值的直接整数操作数. 例如, 在 68000 上, "I "被定义为 1 至 8 的数值范围. 这是移位指令中允许作为移位计数的范围.

‘E’

允许使用直接浮点操作数(表达式代码 const_double), 但前提是目标浮点格式与主机(编译器运行在主机上)的浮点格式相同.

‘F’

允许使用直接浮点操作数(表达式代码 const_double 或 const_vector).

G’、'H

'G’和’H’可根据机器情况定义, 允许在特定数值范围内使用直接浮动操作数.

‘s’

允许使用其值不是显式整数的直接整数操作数.

这可能看起来很奇怪; 如果一个 insn 允许使用在编译时未知值的常量操作数, 那么它当然必须允许使用任何已知值. 那么, 为什么要使用 "s "而不是 "i "呢?有时, 这样可以生成更好的代码.

例如, 在 68000 的全字指令中, 可以使用立即操作数; 但如果立即操作数的值介于 -128 和 127 之间, 则将该值载入寄存器并使用寄存器会产生更好的代码. 这是因为可以使用 "moveq "指令将数值载入寄存器. 我们可以通过定义字母 “K “表示”-128 至 127 范围之外的任何整数”, 然后在操作数约束中指定 "Ks "来实现这一点.

‘g’

允许任何寄存器、内存或直接整数操作数, 非通用寄存器的寄存器除外.

‘X’

允许任何操作数.

'0', '1', '2', … '9'

允许使用与指定操作数相匹配的操作数. 如果数字与字母一起使用, 数字应放在最后.

该数字允许多于一位数. 如果连续出现多个数字, 它们将被解释为一个十进制整数. 由于 “10” 从未被解释为与操作数 1 或操作数 0 相匹配, 因此出现歧义的可能性很小.

这就是所谓的 “匹配约束”, 它的真正含义是汇编器只有一个操作数, 而该操作数同时扮演两个角色, asm 可以将这两个角色区分开来. 例如, 加法指令使用两个输入操作数和一个输出操作数, 但在大多数 CISC 机器上, 加法指令实际上只有两个操作数, 其中一个是输入输出操作数:

addl #35,r12

在这种情况下, 需要使用匹配约束. 更准确地说, 匹配的两个操作数必须包括一个输入操作数和一个输出操作数. 此外, 数字必须小于在约束中使用它的操作数的数字.

‘p’

允许使用有效内存地址的操作数. 这适用于 "加载地址 "和 "推送地址 "指令.

约束中的’p’必须与 address_operand 一起作为 match_operand 中的谓词. 该谓词将 match_operand 中指定的模式解释为地址有效的内存引用模式.

其他字母

其他字母可以根据机器定义, 代表特定类别的寄存器或其他任意操作数类型. 在 68000/68020 中, “d”、"a "和 "f "分别代表数据、地址和浮点寄存器.

6.47.3.2 Multiple Alternative Constraints

有时, 一条指令有多组可供选择的操作数. 例如, 在 68000 上, 逻辑或指令可以将寄存器或立即值组合到内存中, 也可以将任何类型的操作数组合到寄存器中;但不能将一个内存位置组合到另一个内存位置中.

这些限制被表示为多个备选方案. 每个操作数可以用一系列字母来描述. 一个操作数的总体限制由第一个备选方案中该操作数的字母、逗号、第二个备选方案中该操作数的字母、逗号组成, 依此类推, 直到最后一个备选方案. 单条指令的所有操作数必须有相同数量的备选数.

因此, 68000 的逻辑或的第一个备选指令可以写成 “+m”(输出): “ir”(输入). 第二条指令可以写成 “+r”(输出):“irm”(输入): “irm”(输入). 不过, 由于一条指令中不能使用两个内存位置, 因此不能简单地使用 “+rm”(输出): “irm”(输入). 使用多重替代指令, 可以写成 “+m,r”(输出): “ir,irm”(输入). 这就向编译器描述了所有可用的备选方案, 使其能够根据当前条件选择最有效的方案.

在模板中没有办法确定选择了哪种方案. 不过, 你可以用内置函数(如 __builtin_constant_p )来封装你的 asm 语句, 以达到所需的结果.

6.47.3.3 Constraint Modifier Characters

‘=’

表示该操作数被该指令写入:之前的值被丢弃, 取而代之的是新数据.

‘+’

表示这条指令既读取操作数, 也写入操作数.

编译器为满足约束条件而调整操作数时, 需要知道哪些操作数被指令读取, 哪些被指令写入. '=‘表示操作数只被写入;’+'表示操作数既被读出又被写入;所有其他操作数都被假定为只被读出.

如果在约束中指定’=‘或’+', 则将其放在约束字符串的第一个字符中.

‘&’

表示(在特定情况下)该操作数是一个早期捕获操作数, 在指令使用完输入操作数之前就被写入. 因此, 该操作数不能位于指令读取的寄存器中, 也不能作为内存地址的一部分.

“&” 仅适用于写入该操作数的选择项. 在有多个选择项的约束中, 有时一个选择项需要使用’&', 而其他选择项则不需要. 例如, 请参阅 68000 的 "movdf "insn.

被指令读取的操作数, 如果只是在写入早期结果之前作为输入使用, 则可以与早期结果操作数绑定. 当只有部分读取操作数会受早期循环影响时, 添加这种形式的替代指令往往能让 GCC 生成更好的代码. 例如, 请参见 ARM 的 "mulsi3 "insn.

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

&“并不能消除写入”="或 "+"的必要性. 由于 earlyclobber 操作数总是被写入的, 因此只读的 earlyclobber 操作数是格式错误的, 会被编译器拒绝.

‘%’

声明指令对该操作数和后面的操作数是换元指令. 这意味着编译器可以交换这两个操作数, 如果这是使所有操作数符合约束条件的最经济的方法. %“适用于所有备选操作数, 并且必须作为第一个字符出现在约束中. 只有只读操作数可以使用”%".

GCC 在一个 asm 中只能处理一个交换对, 如果使用更多, 编译器可能会失败. 请注意, 如果两个选项完全相同, 则无需使用修饰符;这样做只会浪费重载的时间.

6.47.3.4 Constraints for Particular Machines

只要有可能, 就应该在 asm 参数中使用通用约束字母, 因为它们更容易向阅读代码的人传达含义. 如果做不到这一点, 就使用在不同架构中通常具有非常相似含义的约束字母. 最常用的约束是 "m "和 “r”(分别表示内存和通用寄存器;请参阅 “简单约束”), 以及 “I”, 通常是表示最常用的即时常数格式的字母.

每个体系结构都定义了额外的约束. 这些约束被编译器本身用于指令生成以及 asm 语句;因此, 有些约束对 asm 并不特别有用. 以下是一些特定机器上可用的机器相关约束的摘要;其中既包括对 asm 有用的约束, 也包括对 asm 无益的约束. 表格标题中提到的每个体系结构的编译器源文件是该体系结构约束含义的权威参考.

AArch64 family—config/aarch64/constraints.md

k

堆栈指针寄存器 (SP)

w

浮点寄存器、高级 SIMD 向量寄存器或 SVE 向量寄存器

x

与 w 类似, 但仅限于 0 至 15(含 15)寄存器.

y

与 w 相同, 但仅限于 0 至 7(含 7)寄存器.

Upl

SVE 八个低谓词寄存器之一(P0 至 P7).

Upa

任何一个 SVE 谓词寄存器(P0 至 P15)

I

在 ADD 指令中作为立即操作数有效的整数常量

J

在 SUB 指令中作为直接操作数有效的整数常量(一旦被否定)

K

可与 32 位逻辑指令一起使用的整数常量

L

可与 64 位逻辑指令一起使用的整数常量

M

在 32 位 MOV 伪指令中作为直接操作数有效的整数常量. 根据值的不同, MOV 可装配为几种不同的机器指令之一

N

在 64 位 MOV 伪指令中作为直接操作数有效的整数常量

S

绝对符号地址或标签引用

Y

浮点常数 0

Z

整数常量零

Ush

指令 4GB 内符号的 pc 相对地址的高位部分(第 12 位及以上

Q

使用单一基寄存器且无偏移的内存地址

Ump

适用于 SI、DI、SF 和 DF 模式中加载/存储对指令的内存地址

ARM family-config/arm/constraints.md

h

在 Thumb 状态下, 内核寄存器 r8-r15.

k

堆栈指针寄存器.

l

在 Thumb 状态下, 核心寄存器 r0-r7. 在 ARM 状态下, 这是 r 约束的别名.

t

VFP 浮点寄存器 s0-s31. 用于 32 位数值.

w

VFP 浮点寄存器 d0-d31, 以及基于命令行选项的相应子集 d0-d15. 仅用于 64 位数值. 不适用于 Thumb1.

y

iWMMX 协处理器寄存器.

z

iWMMX GR 寄存器.

G
浮点常数 0.0

I

在数据处理指令中作为直接操作数有效的整数. 即 0 至 255 范围内旋转 2 倍的整数.

J

范围在 -4095 至 4095 之间的整数

K

反转时满足约束条件 "I "的整数(1 的补码)

L

反转时满足约束 ‘I’ 的整数(二进制补码)

M

范围在 0 至 32 之间的整数

Q

精确地址位于单个寄存器中的内存引用("m "最好用于 asm 语句)

R

常量池中的一个项目

S

当前文件文本段中的一个符号

Uv

适合 VFP 加载/存储的内存引用(注册+常量偏移量)

Uy

适用于 iWMMXt 加载/存储指令的内存引用.

Uq

适用于 ARMv4 ldrsb 指令的内存引用.

RISC-V—config/riscv/constraints.md

f

浮点寄存器(如有).

I

I 型 12 位有符号立即寄存器.

J

零整数.

K

用于 CSR 访问指令的 5 位无符号立即计数.

A

保存在通用寄存器中的地址.

S

与绝对符号地址匹配的约束.

vr

矢量寄存器(如有).

vd

矢量寄存器, 不包括 v0(如有).

vm

矢量寄存器, 不包括 v0(如有).

x86 family—config/i386/constraints.md

R

传统寄存器–所有 i386 处理器上都有的八个整数寄存器(a、b、c、d、si、di、bp、sp).

q

可作为 rl 访问的任何寄存器. 在 32 位模式下为 a、b、c 和 d;在 64 位模式下为任何整数寄存器.

Q

可作为 rh 访问的任何寄存器:a、b、c 和 d.

a

a 寄存器.

b

b 寄存器.

c

c 寄存器.

d

d 寄存器.

S

si 寄存器.

D

di 寄存器.

A

a 和 d 寄存器. 该类用于在 ax:dx 寄存器对中返回双字结果的指令. 单字数值将分配到 ax 或 dx 中. 例如, 在 i386 上, 以下指令实现了 rdtsc:

unsigned long long rdtsc (void)
{
  unsigned long long tick;
  __asm__ __volatile__("rdtsc":"=A"(tick));
  return tick;
}

这在 x86-64 上是不正确的, 因为它会在 ax 或 dx 中分配 tick. 您必须使用以下变体来代替:

unsigned long long rdtsc (void)
{
  unsigned int tickl, tickh;
  __asm__ __volatile__("rdtsc":"=a"(tickl),"=d"(tickh));
  return ((unsigned long long)tickh << 32)|tickl;
}

U

调用循环整数寄存器.

f

任何 80387 浮点(堆栈)寄存器.

t

80387 浮点堆栈的顶部(%st(0)).

u

80387 浮点堆栈顶部第二个寄存器(%st(1)).

y

任何 MMX 寄存器.

x

任何 SSE 寄存器.

v

任何 EVEX 编码 SSE 寄存器(%xmm0-%xmm31).

Yz

第一个 SSE 寄存器(%xmm0).

I

范围为 0 … 31 的整数常量, 用于 32 位移位.

J

范围为 0 … 63 的整数常数, 用于 64 位移位.

K

8 位有符号整数常量.

L

0xFF 或 0xFFFF, 用于作为零扩展移动的 andsi.

M

0、1、2 或 3(用于 lea 指令的移位).

N

无符号 8 位整数常量(用于 in 和 out 指令).

G

标准 80387 浮点常量.

C

SSE 常数零操作数.

e

32 位带符号整数常量, 或已知符合该范围的符号引用(用于符号扩展 x86-64 指令中的立即操作数).

我们
32 位带符号整数常量, 或已知符合该范围的符号引用(用于需要非 VOID 模式立即操作数的符号扩展转换操作).

Wz

32 位无符号整数常量, 或已知符合该范围的符号引用(用于需要非 VOID 模式立即操作数的零扩展转换操作).

Wd

128 位整数常量, 高位和低位 64 位字均满足 e 约束条件.

Z

32 位无符号整数常量, 或已知符合该范围的符号引用(用于零扩展 x86-64 指令中的立即操作数).

Tv

VSIB 地址操作数.

Ts

不带段寄存器的地址操作数.

6.47.4 Controlling Names Used in Assembler Code

您可以通过在声明符后写入 asm(或 asm )关键字, 指定 C 函数或变量在汇编代码中使用的名称. 您需要确保您选择的汇编名称不会与任何其他汇编符号或引用寄存器相冲突.

数据的汇编名

本示例展示了如何指定数据的汇编名:

int foo asm ("myfoo") = 2;

该示例指定汇编代码中变量 foo 的名称应为 “myfoo”, 而不是通常的"_foo".

在系统中, C 变量的名称通常以下划线作为前缀, 而该功能允许您为链接器定义不以下划线开头的名称.

GCC 不支持将此功能用于非静态局部变量, 因为此类变量没有汇编程序名称. 如果您想将变量放在特定寄存器中, 请参阅 指定寄存器中的变量.

函数的汇编程序名称

要指定函数的汇编程序名称, 请在函数定义前编写函数声明, 并在其中输入 asm, 如下所示:

int func (int x, int y) asm ("MYFUNC");
     
int func (int x, int y)
{
   /* … */

这就指定了函数 func 在汇编代码中的名称应为 MYFUNC.

6.47.5 Variables in Specified Registers

GNU C 允许将特定的硬件寄存器与 C 变量关联起来. 几乎在所有情况下, 允许编译器分配寄存器都能产生最佳代码. 但在某些特殊情况下, 需要对变量存储进行更精确的控制.

全局变量和局部变量都可以与寄存器关联. 执行这种关联的后果在两者之间有很大不同, 下文将对此进行解释.

6.47.5.1 Defining Global Register Variables

可以这样定义全局寄存器变量, 并将其与指定寄存器关联:

register int *foo asm ("r12");

这里 r12 是应使用的寄存器名称. 请注意, 这种语法与定义局部寄存器变量的语法相同, 但对于全局变量而言, 声明出现在函数之外. register 关键字是必需的, 不能与 static 结合使用. 寄存器名称必须是目标平台的有效寄存器名称.

不要使用 const 和 volatile 等类型限定符, 因为结果可能与预期相反. 特别是, 使用 volatile 限定符并不能完全阻止编译器优化对寄存器的访问.

在大多数系统中, 寄存器是一种稀缺资源, 允许编译器管理寄存器的使用通常会产生最佳代码. 不过, 在特殊情况下, 全局保留一些寄存器也是有意义的. 例如, 在编程语言解释器等程序中, 如果有几个全局变量经常被访问, 那么这种做法就很有用.

为当前编译单元定义全局寄存器变量后:

  • 如果寄存器是 调用保存(call-saved) 寄存器, 调用 ABI 将受到影响:在变量赋值后的函数尾声序列中, 寄存器将不会恢复. 因此, 函数无法安全地返回到假设标准 ABI 的调用者.
  • 相反, 如果寄存器是 调用缓冲(call-clobbered) 寄存器, 调用使用标准 ABI 的函数可能会丢失变量内容. 编译器可能会创建这种调用, 即使在原始程序中并不明显, 例如, 当使用 libgcc 函数来弥补不可用的指令时.
  • 只要变量的可观测值不受影响, 对变量的访问可以像往常一样进行优化, 寄存器仍可用于分配和计算.
  • 如果变量在内联汇编中被引用, 则必须通过约束向编译器提供访问类型(请参阅 asm 操作数的约束). 不支持从基本 asms 访问.
    请注意, 这些要点仅适用于与定义一起编译的代码. 仅链接进来的代码(例如来自库的代码)的行为不受影响.

如果你想重新编译那些没有实际使用全局寄存器变量的源文件, 使它们不会将指定的寄存器用于任何其他目的, 你不需要在它们的源代码中实际添加全局寄存器声明. 只需指定编译器选项 -ffixed-reg(参见 “代码生成约定选项”)来保留寄存器即可.

声明变量

全局寄存器变量不能有初始值, 因为可执行文件无法为寄存器提供初始内容.

在选择寄存器时, 应选择通常由机器上的函数调用保存和恢复的寄存器. 这样可以确保不知道这一保留的代码(如库例程)在返回前恢复它.

在带有寄存器窗口的机器上, 一定要选择一个不受函数调用机制影响的全局寄存器.

使用变量

在调用未注意到保留的例程时, 如果这些例程回调到使用变量的代码中, 请务必谨慎. 例如, 如果你调用系统库版本的 qsort, 它可能会在执行过程中破坏你的寄存器, 但(如果你选择了适当的寄存器)它会在返回前恢复你的寄存器. 不过, 在调用 qsort 的比较函数之前, 它不会恢复这些寄存器. 因此, 除非重建 qsort 函数本身, 否则比较函数将无法可靠地使用全局值.

同样, 从信号处理器或多个控制线程访问全局寄存器变量也不安全. 除非为当前任务专门重新编译, 否则系统库例程可能会暂时将寄存器用于其他用途. 此外, 由于寄存器不是专门为变量保留的, 因此从异步信号处理程序访问寄存器时, 可能会观察到寄存器中不相关的临时值.

在大多数机器上, longjmp 会恢复每个全局寄存器变量在 setjmp 时的值. 但在某些机器上, longjmp 不会改变全局寄存器变量的值. 为了实现可移植性, 调用 setjmp 的函数应该做出其他安排, 保存全局寄存器变量的值, 并在 longjmp 中恢复它们. 这样, 无论 longjmp 做了什么, 都会发生同样的事情.

6.47.5.2 Specifying Registers for Local Variables

您可以这样定义一个本地寄存器变量, 并将其与指定寄存器关联:

register int *foo asm ("r12");

这里 r12 是应使用的寄存器名称. 请注意, 这种语法与定义全局寄存器变量的语法相同, 但对于局部变量, 其声明是在函数中进行的. register 关键字是必需的, 不能与 static 结合使用. 寄存器名称必须是目标平台的有效寄存器名称.

不要使用 const 和 volatile 等类型限定符, 因为结果可能与预期相反. 特别是使用 const 限定符时, 编译器可能会在 asm 语句中用初始化器代替变量, 这可能会导致相应的操作数出现在不同的寄存器中.

与全局寄存器变量一样, 建议您选择一个通常由机器上的函数调用保存和恢复的寄存器, 这样库例程调用就不会破坏它.

该功能唯一受支持的用途是在调用 Extended asm 时为输入和输出操作数指定寄存器(参见 Extended Asm - Assembler Instructions with C Expression Operands). 如果特定机器的约束条件无法提供足够的控制来选择所需的寄存器, 则有必要这样做. 要将操作数强制输入寄存器, 可创建一个局部变量, 并在变量声明后指定寄存器名称. 然后将局部变量用于 asm 操作数, 并指定与寄存器相匹配的约束字母:

register int *p1 asm ("r0") =;
register int *p2 asm ("r1") =;
register int *result asm ("r0");
asm ("sysint" : "=r" (result) : "0" (p1), "r" (p2));

警告 在上述示例中, 请注意寄存器(例如 r0)可能会被后续代码调用, 包括函数调用和库中对其他变量的算术运算符调用(例如 p2 的初始化). 在这种情况下, 应在寄存器赋值之间使用临时变量来表达:

int t1 =;
register int *p1 asm ("r0") =;
register int *p2 asm ("r1") = t1;
register int *result asm ("r0");
asm ("sysint" : "=r" (result) : "0" (p1), "r" (p2));

定义寄存器变量并不保留寄存器. 除了调用 Extended asm 外, 不保证指定寄存器的内容. 因此, 明确不支持以下用途. 如果它们看起来可以工作, 那也只是偶然, 可能会因为周围代码(看似)无关的变化, 甚至是未来版本 gcc 优化的微小变化而停止工作:

  • 向 Basic asm 传递参数或从 Basic asm 传递参数
  • 在不使用输入或输出操作数的情况下, 向 Extended asm 传递参数或从 Extended asm 传递参数.
  • 向使用非标准调用约定的汇编语言(或其他语言)编写的例程传递参数, 或从这些例程传递参数.
  • 有些开发者使用本地寄存器变量, 试图改进 gcc 的寄存器分配, 尤其是在大型函数中. 在这种情况下, 寄存器名称基本上是对寄存器分配器的一个提示. 虽然在某些情况下, 这样做可以生成更好的代码, 但改进效果受分配器/优化器的影响. 由于不能保证你的改进不会丢失, 所以不鼓励使用本地寄存器变量.

在 MIPS 平台上, 本地寄存器变量的相关用途与此略有不同(参见 GNU Compiler Collection (GCC) Internals 中为 MIPS 目标定义协处理器特性).

6.47.6 Size of an asm

有些目标要求 GCC 跟踪所使用的每条指令的大小, 以便生成正确的代码. 由于只有汇编程序才知道 asm 语句生成的代码的最终长度, 因此 GCC 必须估算出代码的长度. 为此, GCC 会计算 asm 模式中的指令数, 然后乘以该处理器支持的最长指令的长度. (在计算指令数时, 它假定任何换行符或汇编器支持的语句分隔符(通常是";")的出现都表示指令的结束).

通常情况下, GCC 的估计值足以确保生成正确的代码, 但如果使用了 扩展为多条实际指令的伪指令汇编宏, 或者使用了在对象文件中扩展的空间大于单条指令所需的空间的汇编指令, 就有可能使编译器感到困惑. 如果出现这种情况, 汇编程序可能会产生一个诊断结果, 指出某个标签无法访问.

这一大小也用于内联决策. 如果使用 asm inline 而不只是 asm, 那么在内联时, asm 的大小将作为最小值, 而不会考虑 GCC 认为它有多少条指令.

  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
C语言源程序嵌入汇编语言语句,可以使用内联汇编(inline assembly)的方式实现。内联汇编指的是在C语言代码直接嵌入汇编语句,这些汇编语句会被编译器直接嵌入到生成的机器码,从而实现对底层硬件的直接控制。具体实现方法如下: 1. 编写汇编语句,使用AT&T格式或Intel格式。 2. 在C语言程序使用__asm__关键字将汇编语句嵌入到C语句。 3. 在嵌入的汇编语句使用%加数字或变量名表示寄存器,$加数字或常量表示立即数,以及方括号表示内存地址。 以下是一个简单的例子,演示如何在C语言程序嵌入汇编语句: ``` #include <stdio.h> int main() { int a = 10, b = 20, sum; __asm__ ( "movl %1, %%eax;" "addl %2, %%eax;" "movl %%eax, %0;" :"=r"(sum) :"r"(a), "r"(b) :"%eax" ); printf("sum=%d\n", sum); return 0; } ``` 上述代码使用内联汇编计算了变量a和b的和,并将结果保存在变量sum。其,movl指令将变量a的值移动到寄存器eax,addl指令将变量b的值加到eax,movl指令将eax的结果移动到变量sum。在内联汇编代码使用了%1、%2、%0等符号来表示变量a、b、sum,使用了%eax符号来表示寄存器eax。同时,在内联汇编代码使用了输出操作数(output operand)、输入操作数(input operand)和修改操作数(clobbered operand)等参数来告知编译器在寄存器保存哪些变量,以及哪些寄存器被修改了。 需要注意的是,内联汇编虽然可以直接控制底层硬件,但使用不当容易引发安全问题或者不可移植性问题。因此,在使用内联汇编时应该谨慎,避免出现不必要的风险。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值