欢迎您访问365答案网,请分享给你的朋友!
生活常识 学习资料

【高级C】GNUC/C++内联——进阶——语法详解

时间:2023-04-30

本人就职于国际知名终端厂商,负责modem芯片研发。
在5G早期负责终端数据业务层、核心网相关的开发工作,目前牵头6G算力网络技术标准研究。


博客内容主要围绕:
       5G协议讲解
       算力网络讲解(云计算,边缘计算,端计算)
       高级C语言讲解
       Rust语言讲解

文章目录

GNU C/C++ 内联汇编——进阶——语法详解

语法介绍

asm-qualifiersOperands注意事项 asm-qualifiers——volatile详解Assembler Template详解

模板中一些特殊的字符串asm模板中的多种汇编语言(也称为方言 dialect) Output Operands详解

每个操作数的格式特别说明示例Flag Output Operands Input Operands详解

注意事项例子 Clobbers and Scratch Registers详解

注意事项两个特殊的clobber参数示例 Goto Labels详解x86 Operand Modifiers(应用在 assembler template 的操作数引用中)stack-like registers使用事项 文章链接 GNU C/C++ 内联汇编——进阶——语法详解

何为进阶?
就是我们可以在内联汇编中引入参数,并指定参数存放的位置。

语法介绍

通过扩展asm,可以从汇编程序读取和修改C变量,并执行从汇编程序代码到 C label 的跳转。扩展asm语法使用冒号(’:’)分隔操作数:

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

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

一共有两种形式的语法格式,在最后一种形式中,asm-qualifiers 包含goto(在第一种形式中不包含goto)。

asm是GUN的扩展,如果想编写适用于标准语言的内嵌汇编,需要将 asm 替换为 __asm__。


asm-qualifiers

volatile:禁用某些优化(下面会详细介绍);inline:内联这段code;goto:这个限定符告诉编译器,asm语句可以跳转到 gotolabel 中列出的某个 label。 Operands

AssemblerTemplate:这是一个字面值字符串,它是汇编代码的模板(有点像C++ 的STL)。OutputOperands:一个由逗号分隔的一连串的C变量,这些变量被 AssemblerTemplate 中的指令修改。这个参数允许为空。InputOperands:一个由逗号分隔的C表达式,被AssemblerTemplate 中的指令读取。这个参数允许为空。Clobbers:逗号分隔的寄存器或者其它的在AssemblerTemplate 中发生改变的值的一个列表(不包括OutputOperands中的值)。这个参数允许为空。GotoLabels:当您使用asm的goto形式时,这里包含了所有在AssemblerTemplate 中的代码可能跳转到的 C labels(asm语句不能跳转到其他asm语句,只能跳转到列在 gotolabel 中的 labels)。

OutputOperands+InputOperands+ Clobbers 的操作数总数被限制为 30 个。

注意事项

       asm 语句允许直接在C代码中包含汇编指令。这可以帮助最大限度地提高对时间敏感的代码的性能,或者通过汇编指令实现C 函数库中没有的功能。

       注意,扩展的asm语句必须在函数中。只有basic asm可以在函数的外部。用naked属性声明的函数也需要使用 basic asm。



asm-qualifiers——volatile详解

       GCC的优化器在确定这条asm语句之后不会再引用OutputOperands 中的变量时可能会丢弃这条asm语句。此外,如果优化器认为代码总是返回相同的结果(也就是说在调用前后,代码的输入值都没有改变),那么优化器可能会将代码移出循环。使用volatile限定符将禁用这些优化。

       没有OutputOperands 参数的asm语句和asm goto语句都是隐式使用volatile修饰的。

下面这段i386代码演示了一个不使用 volatile 限定符的情况。如果它正在执行断言检查,这段代码使用asm来执行验证。因为没有任何代码引用 dwRes。因此,优化器可能丢弃这条 asm 语句,这就会导致下面的 assert 函数出现意料之外的结果。当然通过在不需要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 %un", x, dwSomevalue, dwRes); }}

下面的示例演示了需要使用volatile限定符的情况。它使用x86 rdtsc指令来读取计算机的时间戳计数器。如果没有volatile限定符,优化器可能会认为asm块总是返回相同的值,因此会优化掉第二次调用。

uint64_t msr;asm volatile ( "rdtscnt" // Returns the time in EDX:EAX. "shl $32, %%rdxnt" // Shift the upper bits left. "or %%rdx, %0" // 'Or' in the lower bits. : "=a" (msr) : : "rdx");printf("msr: %llxn", msr);// Do other work...// Reprint the timestampasm volatile ( "rdtscnt" // Returns the time in EDX:EAX. "shl $32, %%rdxnt" // Shift the upper bits left. "or %%rdx, %0" // 'Or' in the lower bits. : "=a" (msr) : : "rdx");printf("msr: %llxn", msr);

请注意,编译器甚至可以相对于其他代码移动volatile 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;


Assembler Template详解

       汇编程序模板是一个包含汇编程序指令的字面值字符串。编译器替换模板中引用Inputs、Outputs和gotolabels的标记,然后将结果字符串输出给汇编器。该字符串可以包含汇编器识别的任何指令。GCC本身不解析汇编指令,也不知道它们的意思,甚至不知道它们是否是有效的汇编器输入。但是,它对语句进行了计数(用于计算 asm 的 size)。

       可以将多个汇编程序指令放在一个asm字符串中,使用分隔符将多条指令分隔。大多数情况下的组合是用换行符加上一个制表符(写成’ nt ')。有些汇编程序允许用分号作为行分隔符。但是,请注意,一些汇编语言使用分号表示一段注释的开始。

       如果有多个 asm 汇编语句,即使你对每个asm 都使用了 volatile限定符修饰,汇编之后这些 asm 语句的相对顺序也可能会发生改变。如果你希望汇编指令的相对顺序在汇编之后任然保持汇编前的顺序,你需要将这些汇编指令写在一个单独的 asm 中,而不是分散在多个 asm 中。

       由于GCC不解析 Assembler Template ,因此 Assembler Template 所引用的任何符号对于GCC都不可见。这可能会导致 GCC 将这些不在 InputOperands、OutputOperands 或 gotolabels 列表中的符号作为未引用的符号而丢弃。

模板中一些特殊的字符串

%%:在汇编code中输出一个单独的 ‘%’。

%=:在整个汇编过程中,为每一个 asm statement 实例生成一个唯一的数字。当在包含多个汇编指令的 asm 中创建一些 local label 并多次引用它们时,此选项非常有用。例如下面的code,如果这样写汇编器会报 Error: symbol `ONE’ is already defined.

int main(){ int a=2; int b[2]={1,2}; __asm__ ("cmp $2,%1nt" "jne ONEnt" "mov $4,%0nt" "ONE:NOP" :"+r"(b[1]):"r"(a):"cc");b[1] = 8;__asm__ ("cmp $4,%1nt" "jne ONEnt" "mov $4,%0nt" "ONE:NOP" :"+r"(b[1]):"r"(a):"cc"); printf("%dn",b[1]); return 0;}int main(){ int a=2; int b[2]={1,2}; __asm__ ("cmp $2,%1nt" "jne ONE%=nt" "mov $4,%0nt" "ONE%=:NOP" :"+r"(b[1]):"r"(a):"cc");b[1] = 8;__asm__ ("cmp $4,%1nt" "jne ONE%=nt" "mov $4,%0nt" "ONE%=:NOP" :"+r"(b[1]):"r"(a):"cc"); printf("%dn",b[1]); return 0;}

%{、%|、%}:将’{’ 、’ | ’ 和 ‘}’ 字符(分别)输出到汇编代码中。当未转义时,这些字符具有特殊的含义,表示多种汇编程序方言,如下所述。

asm模板中的多种汇编语言(也称为方言 dialect)

       GCC支持多种汇编语言。-masm 选项控制GCC使用哪种方言作为内联汇编程序的默认方言。理解这些信息很重要,因为在使用一种方言编译时能够正常工作的汇编代码在使用另一种方言编译时可能会失败。

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

{ dialect0 | dialect1 | dialect2… }

这个结构在使用dialect0 #0 编译代码时输出dialect0,dialect0 #1 时输出dialect1,等等。如果大括号内的方言选项比编译器支持的方言数量少,则上面的结构不会输出任何内容。

例如,一个 X86的汇编器支持两种汇编语言 (‘att’, ‘intel’),assembler template 如下:

bt{l %[Offset],%[base] | %[base],%[Offset]}; jc %l2//等价于btl %[Offset],%[base] ; jc %l2 bt %[base],%[Offset]; jc %l2 xchg{l}t{%%}ebx, %1//等价于xchglt%%ebx, %1 xchgtebx, %1

不支持嵌套dialect


Output Operands详解

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

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

bool old;__asm__ ("btsl %2,%1nt" // 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 (c variable name)

asmSymbolicName:指定操作数的符号名称。在汇编器模板中引用该名称,将其括在方括号中(即’ %[Value] ‘)。名称的作用域是包含定义的asm语句。任何有效的C变量名都是可以接受的,包括周围代码中已经定义的名称。同一asm语句中的两个操作数不能使用相同的符号名。当不使用asmSymbolicName时,使用汇编器模板中操作数列表中操作数的(从零开始)位置。例如,如果有三个输出操作数,在模板中使用’ %0 ‘来引用第一个,’ %1 ‘引用第二个,’ %2 '引用第三个;

constraint:一个字符串,定义了与它相关的操作数的约束。输出约束必须以 ’ = '(变量覆写现有值)或 ’ + '(读取和写入时)开头。当使用’ = '时,不要假设该位置的值会和asm 输入操作数中的某个值相等,除非输出操作数同时被绑定到一个输入操作数(会在输入参数中详细介绍这句话的含义)。在前缀之后,必须有一个或多个额外的约束来描述值驻留的位置。常见的约束包括寄存器的 ’ r ’ 和内存的 ’ m '。当您列出多个可能的位置时(例如,"=rm"),编译器会根据当前上下文选择最有效的位置。可以在asm语句允许的范围内列出尽可能多的备选项,这样就允许优化器生成尽可能好的代码。如果必须使用特定的寄存器,但Machine Constraints没有提供足够的控制来选择所需的特定寄存器,则 local register variables 可以提供一种解决方案(参考《GNU C/C++ 内联汇编——补充介绍》);

c variable name:定义了一个左值表达式,通常是一个C变量名来接收输出。必须使用括号将其括起来

特别说明

当编译器选择用来表示输出操作数的寄存器时,它不会使用任何clobbered registers;输出操作数必须是左值表达式。编译器不能检查操作数的数据类型是否适合正在执行的指令。对于不能直接寻址的输出表达式(例如 bit-field),constraint 必须允许寄存器。在这种情况下,GCC使用寄存器作为asm的输出,然后将该寄存器存储到输出中;使用’ + '约束修饰符的操作数,被认为是两个操作数(即,同时作为输入和输出)计入总数,每个asm语句的操作数总数最多为30个;对所有不能与输入重叠的输出操作数使用’ & '约束修饰符。否则,GCC可能会给输出操作数分配一个已经与输入操作数绑定的寄存器,因为它假定汇编程序代码在产生输出之前会消耗它的输入。如果汇编代码实际上包含多条指令,那么这个假设可能是错误的。如果一个输出参数(a)允许寄存器约束,而另一个输出参数(b)允许内存约束,同样的问题也会发生。GCC为访问b中的内存地址而生成的代码中可能包含被a共享的寄存器,并且GCC认为这些寄存器是对asm的输入。如上所述,GCC假定在写入任何输出之前会使用这些输入寄存器中的值。如果asm语句在使用b之前写到a,这个假设可能会导致错误的行为。将’ & '修饰符与a上的寄存器约束相结合,可以确保修改a不会影响b引用的地址。否则,如果在使用b之前修改了a,则b的位置是未定义的。如果asm后面的C代码没有使用任何输出操作数,请为asm语句使用volatile,以防止优化器因不需要而丢弃这个asm语句。 示例

       这段代码没有使用可选的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语法。它产生的结果与上面的代码相同,但有些人可能认为它更易读或更易于维护,因为在添加或删除操作数时不需要重新排序索引号。名称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 的值选择最佳的存储位置。

Flag Output Operands

       一些目标平台有一个特殊的寄存器,用来保存操作或比较的结果的“标志”(标志寄存器)。通常,该寄存器的内容要么无法被asm修改,要么asm语句被认为破坏了这个寄存器中的内容。在某些目标平台上,存在一种特殊形式的输出操作数,通过这种输出操作数,flags寄存器中的条件可以是asm的输出。支持的条件集是特定于目标平台的,但是一般的规则是输出变量必须是一个标量整数,值是布尔值。当支持时,目标平台会定义预处理符号__GCC_ASM_FLAG_OUTPUTS__。

       通常,目标平台只有一个标志寄存器,因此会是多个指令的隐含操作数。在这种情况下,不应该在 assembler template 中通过 %0 等方式来引用操作数,因为标志寄存器在汇编语言中没有相应的符号。下面给出ARM 和 X86 平台的一些 flag :

output constraintsAArch64x86格式‘=@cccond’‘=@cccond’cond 的类型Z flag set, or equaleqe、zZ flag clear or not equalnene、nzC flag setcscC flag clearccncN flag set or “minus”miN flag clear or “plus”plV flag set or signed overflowvsV flag clearvcunsigned greater thanhiaunsigned less than equallsbeunsigned less thanlobunsigned greater than equalhsaesigned greater than equalgegesigned less thanltlsigned greater thangtgsigned less than equalleleoverflow flag setooverflow flag clearnoparity flag setpparity flag clearnpsign flag setssign flag clearns

例子:

int main(){ int a=2; int b=0; #ifdef __GCC_ASM_FLAG_OUTPUTS__ __asm__ ("cmp $24,%1" :"=@cce"(b):"r"(a));#endif printf("%dn",b); // 输出0 #ifdef __GCC_ASM_FLAG_OUTPUTS__ __asm__ ("cmp $2,%1" :"=@cce"(b):"r"(a));#endif printf("%dn",b); // 输出1 return 0;}


Input Operands详解

输入操作数使C变量和表达式的值可用于汇编代码中。操作数之间用逗号分隔。格式如下:

[[asmSymbolicName] ] constraint (cexpression)

asmSymbolicName:指定输入操作数的符号名称。在 Assembler Template 中引用该名称,将其括在方括号中(即’ %[Value] ‘)。名称的作用域是包含定义的asm语句。任何有效的C变量名都是可以接受的,包括周围代码中已经定义的名称。同一asm语句中的两个操作数不能使用相同的符号名。当不使用asmSymbolicName时,使用汇编器模板中操作数列表中操作数的(从零开始)位置来引用。例如,如果有两个输出操作数和三个输入操作数,则在模板中使用’ %2 ‘引用第一个输入操作数,’ %3 ‘引用第二个输入操作数,’ %4 '引用第三个输入操作数。

constraint:输入约束字符串不能以’ = ‘或’ + '开头。当列出多个可能的位置(例如,’ “irm” ')时,编译器会根据当前上下文选择最有效的位置。如果必须使用特定的寄存器,但Machine Constraints没有提供足够的控制来选择所需的特定寄存器,则本地寄存器变量可能提供一种解决方案(参考《GNU C/C++ 内联汇编——补充介绍》)。输入约束也可以是数字(例如,“0”)。这表明指定的输入必须与输出列表中(从零开始)相应位置的输出位于相同的位置。当对输出操作数使用asmSymbolicName语法时,你可以使用这些名称(用括号 ‘[]’ 括起来)代替数字。

int main(){ int a=4; int b=0; printf("%dn",b); // 输出 0 __asm__ ("NOP" :"=r"(b):"0"(a)); printf("%dn",b); // 输出 4,因为a和b使用的是同一个寄存器,进行了绑定 return 0;}

cexpression:这是传递给asm语句作为输入的C变量或表达式。必须使用括号括起来(语法要求)。

注意事项

当编译器选择寄存器来表示输入操作数时,它不会使用任何 clobbered registers。

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

__asm__ ("some instructions" : : "r" (Offset / 8));

不要修改仅输入操作数的内容(input-only operands)(除非输入绑定到了输出)。编译器假定从asm语句退出时,这些操作数包含的值与执行语句之前的值相同。不可能使用 clobbers 来通知编译器这些输入的值发生了改变。一种常见的解决方法是将发生变化的输入变量与永远不会使用的输出变量绑定在一起。但是,请注意,如果asm语句后面的C代码不使用任何输出操作数,GCC优化器可能会丢弃不需要的asm语句(可使用volatile避免这种优化行为)。

例子

       这里使用虚构的combine指令,输入操作数 foo 的约束“0”表示它必须占据与输出操作数0相同的位置。只有输入操作数可以在约束中使用数字,并且这些数字都有对应的输出操作数。只有约束中的数字(或 symbolic assembler name)才能保证一个操作数与另一个操作数处于相同的位置。仅仅foo是两个操作数的值这一事实不足以保证它们在生成的汇编代码中处于相同的位置。

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

下面是一个使用 symbolic assembler name 的例子:

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


Clobbers and Scratch Registers详解

       虽然编译器知道输出操作数中列出的条目会发生变化,但内联asm代码可能修改的不仅仅是输出操作数中的条目。例如,计算可能需要额外的寄存器,或者某些特定的汇编指令会覆写某些寄存器。为了将这些更改通知编译器,请在clobber列表中列出它们。clobber 列表要么是寄存器名称,要么是特殊的clobbers (如下所示)。每个clobber 列表项都是一个字符串常量,用双引号括起来,用逗号分隔。

注意事项

出现在 clobbers 中的条目,不能与 input 或者 output 操作数有交集;当编译器选择使用某些寄存器来表示输入和输出操作数时,它不会使用任何clobbered 寄存器。因此,clobbered 寄存器可以在汇编代码中任意使用;另一个限制是,clobber 列表不应该包含堆栈指针寄存器。这是因为编译器要求堆栈指针的值在asm语句之后和语句进入时相同。 两个特殊的clobber参数

"cc" :“cc” clobber 表示汇编程序代码修改了标志寄存器。在一些机器上,GCC将条件代码表示为一个特定的硬件寄存器,“cc”用于命名此寄存器。在其它机器上,条件代码处理是不同的,指定“cc”可能没有任何效果。但无论目标是什么,在语法上它都是有效的。

"memory":“memory” clobber 告诉编译器,汇编代码对输入和输出操作数中列出的以外的项执行内存读或写操作(例如,访问输入参数所指向的内存)。为了确保内存包含正确的值,GCC可能需要在执行asm之前将特定的寄存器值刷新到内存中。此外,编译器不会假设在asm之前从内存中读取的任何值在asm之后保持不变,它会根据需要重新加载。使用 “memory” clobber 有效地为编译器形成了一个读/写内存屏障(注意,这个 clobber 不会阻止 processor 在asm语句之后进行 speculative 读取。为了防止这种情况发生,您需要使用 fence 指令)。将寄存器刷新到内存会影响性能,对于时间敏感的代码来说可能是个问题。您可以向GCC提供更详细的信息来避免这种情况,如下面的示例所示。至少,aliasing 规则允许GCC知道哪些内存不需要刷新。

示例

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

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

       下面是一个虚构的平方和指令,它接受两个指向内存中的浮点值的指针,并产生一个浮点寄存器输出。请注意,x和y都在asm参数中出现了两次,一次用于指定访问的内存,一次用于指定asm使用的基寄存器。这样做通常不会浪费寄存器,因为GCC可以将同一个寄存器用于这两种目的(一种是获取地址,一种是基于这个地址进行寻址)。然而,在这个asm中同时使用%1和%3表示x,并期望它们相同是愚蠢的。事实上,%3很可能不是一个寄存器。它可能是对 x 所指向内存的symbolic memory reference 。

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));


Goto Labels详解

跳转的 labels 是 C程序中的 labels,不是这条 asm 语句中的labels

       asm goto允许汇编代码跳转到一个或多个C标签。asm goto语句中的 gotolabels 部分包含了一个以逗号分隔的C标签列表,汇编代码可以跳转到这些标签。如果汇编代码做了任何修改,使用“memory”clobber 强制优化器将所有寄存器值刷新到内存中,并在必要时在asm语句之后重新加载它们。还要注意,asm goto语句总是被默认为是volatile的。

       当你仅在一些可能的控制流路径上设置了输出操作数时,要小心。如果你没有在给定的路径上设置输出,并且永远不会在这个路径上使用它,这是可以的。否则,你应该使用’ + '约束修饰符,表示操作数为输入和输出。使用这个修饰符,从asm goto开始的所有可能路径上都将有正确的值。

       要使用 assembler template 中的一个标签,在它前面加上’ %l '(小写的 ‘L’ ),然后加上它在gotolabel中的(从零开始)位置,加上输入和输出操作数的数量。带有约束修饰符 ’ + ’ 的输出操作数被计算为两个操作数,因为它被认为是一个输出操作数和一个输入操作数。例如,如果asm有三个输入,一个输出操作数带有约束修饰符’ + ‘,一个输出操作数带有约束修饰符’ = ',并引用两个标签,则将第一个标签引用为 ’ %l6 ‘,第二个标签引用为’ %l7 ’ 。

       你也可以使用C语言中的 labels 名。例如,要引用名为 ‘carry’的标签,可以使用 ’ %l[carry] '。使用这种方法时,标签仍然必须在 gotolabels 部分中列出。最好使用标签的命名引用,因为在这种情况下,您可以避免计算输入和输出操作数。

下面是一个关于i386的例子:

asm goto ("btl %1, %0nt" "jc %l2" : : "r" (p1), "r" (p2) : "cc" : carry);return 0;carry:return 1;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; }

特别提示
更早的版本中 asm goto不允许有输出操作数。
这是 GNU 的决定。
在c-parser.c的函数c_parser_for_statement中,可以找到:

https://github.com/gcc-mirror/gcc/blob/releases/gcc-10/gcc/c/c-parser.c

然而,这种情况可能会改变,因为在主分支中,此注释不再存在。


x86 Operand Modifiers(应用在 assembler template 的操作数引用中)

       在扩展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" : : "q" (iInt), "X" (sizeof(unsigned char) + 1), "i" (42) : : top);}

在没有修饰符的情况下,对于assembler的’ att ‘和’ intel '方言,操作数的输出如下:

Operand‘att’‘intel’0%%eaxeax1%$223%$.L3OFFSET FLAT:.L34%$885%%xmm0xmm07%$00

下表显示了支持的修饰符(modifiers )及其效果:

ModifierDescriptionOperand‘att’‘intel’A打印一个绝对内存引用。%A0*%raxraxb打印寄存器的QImode(单字节)名称。%b0%alalB答应操作码后缀 ‘b’%B0bc要求一个常量操作数并输出不带标点符号的常量表达式。%c122d为AVX指令打印重复的寄存器操作数。%d5%xmm0, %xmm0xmm0, xmm0E当目标器是64位时,以Double Integer (DImode)模式(8字节)打印地址。否则mode是未指定的(VOIDmode)。%E1%(rax)[rax]g打印寄存器的V16SFmode名称。%g0%zmm0zmm0h打印一个“high”寄存器的QImode名称。%h0%ahahH内存地址加 8 的偏移量。当访问高8字节的SSE值时很有用。%H08(%rax)8[rax]k打印寄存器的SImode名称。%k0%eaxeaxl打印不带标点符号的标签名称。%l3.L3.L3L打印操作码后缀 l。%L0lN打印maskz。%N7{z}{z}p打印原始符号名(不包含语法特定的前缀)。%p24242P如果用于函数,则打印PLT后缀并生成PIC代码。例如,对于函数foo(), emit为foo@PLT而不是’ foo '。如果用于常量,则删除所有特定于语法的前缀,并发出裸常量。参见上面的p。q打印寄存器的DImode名称。%q0%raxraxQ打印操作码后缀 q。%Q0qR打印 embedded rounding和sae。%R4{rn-sae},, {rn-sae}r只打印sae。%r4{sae},, {sae}s打印一个 shift double count,后面跟着汇编器参数分隔符。%s1$2,2,S打印操作码后缀 s。%S0st打印寄存器的V8SFmode名称。%t5%ymm0ymm0T打印操作码后缀 t。%T0tV打印不带%的裸整数寄存器名。%V0eaxeaxw打印寄存器的HImode名称。%w0%axaxW打印操作码后缀 w。%W0wx打印寄存器的v44sfmode名称。%x5%xmm0xmm0y打印"st(0)"而不是"st"作为寄存器。%y6%st(0)st(0)z根据当前整数操作数大小,打印 opcode后缀(b/w/l/q)。%z0l
stack-like registers使用事项

       在x86目标上,在asm的操作数中使用 stack-like registers 有几个规则。这些规则只适用于 stack-like registers 的操作数:

给定asm中的一组输入寄存器,有必要知道哪些是asm隐式弹出的,哪些必须由GCC显式弹出。asm隐式弹出的输入寄存器必须被显式 clobbered,除非它与输出操作数绑定;

对于任何由asm隐式弹出的输入寄存器,必须知道如何调整堆栈以补偿弹出带来的影响。如果任何未弹出的输入比隐式弹出的寄存器更接近reg-stack的顶部,就不可能知道堆栈的样子——不清楚堆栈的其余部分是如何“slides up”的。所有隐式弹出的输入寄存器必须比任何未隐式弹出的输入更接近reg-stack的顶部。

如果asm中的一个输入失效,编译器可能会使用输入寄存器来重新加载输出。考虑一下这个例子:

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

上面这段代码说输入 ‘b’ 没有被asm弹出,并且asm将结果推送到reg-stack上。但是,重载(reload)时可能认为输入和输出可以使用相同的寄存器。

为了防止这种情况的发生,如果任何输入操作数使用了’ f '约束,所有输出寄存器约束必须使用 ’ & ’ early-clobber 修饰符。

上面的例子正确的写法是:

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

有些操作数需要放在堆栈的特定位置上。所有属于这一类别的输出操作数——GCC是没有方法知道输出会出现在哪个寄存器中,所以你必须在 constraints 中指出这一点。输出操作数必须明确指出输出出现在asm之后的哪个寄存器中。不允许使用 ’ =f ’ ,操作数约束必须选择一个具有单一寄存器的类(a class with a single register)(能够明确指明是哪个寄存器的约束符);

输出操作数不能在现有堆栈寄存器之间 “插入”。输出操作数必须从reg-stack的顶部开始;

一些asm语句可能需要额外的堆栈空间来进行内部计算。这可以通过 clobber 那些与输入和输出无关的堆栈寄存器来实现。


这里是从善若水的博客,感谢您的阅读⌨


文章链接

《GNU C/C++ 内联汇编编程指南全集》
《GNU C/C++ 内联汇编——入门级》
《GNU C/C++ 内联汇编——进阶——语法详解》
《GNU C/C++ 内联汇编——进阶——约束详解》
《GNU C/C++ 内联汇编——补充介绍》
《GNU C/C++ 内联汇编——实例参考》
《GNU C/C++ 内联汇编——Intel与ATT汇编语法对比》




Copyright © 2016-2020 www.365daan.com All Rights Reserved. 365答案网 版权所有 备案号:

部分内容来自互联网,版权归原作者所有,如有冒犯请联系我们,我们将在三个工作时内妥善处理。