如何在C代码中使用内联汇编之汇编指令模板

汇编指令模板
汇编模板:
汇编程序模板是包含汇编程序指令的文本字符串。
编译器替换模板中引用输入、输出和goto标签的标记,然后将生成的字符串输出给汇编程序。
字符串可以包含汇编程序识别的任何指令,包括指示符。
GCC不分析汇编程序指令本身,也不知道它们的含义,甚至不知道它们是否是有效的汇编程序输入。
可以将多个汇编程序指令放在一个asm字符串中,由系统汇编代码中通常使用的字符分隔。
在大多数地方工作的组合是换行符,加上一个制表符移动到指令字段(写为’\n\t’)。
有些汇编程序允许分号作为行分隔符。但是,请注意,一些汇编语言使用分号来开始注释。
不要期望汇编后asm语句序列保持完全连续,即使使用volatile限定符也是如此。
如果某些指令需要在输出中保持连续,请将它们放在一个多指令asm语句中
如果不使用输入/输出操作数(例如直接使用汇编器模板中的全局符号)就从C程序访问数据,可能无法像预期的那样工作。
类似地,直接从汇编器模板调用函数需要对目标汇编器和ABI有详细的了解。
由于GCC不解析汇编器模板,所以它对引用的任何符号都不可见。
这可能导致GCC将这些符号作为未引用丢弃,除非它们也被列出为输入、输出或goto操作数。

特殊的格式字符串:
除了输入、输出和goto操作数所描述的令牌之外,下面的令牌在汇编器模板中还有特殊的含义:
‘%%’:
将单个’ % ‘输出到汇编程序代码中。
‘%=’:
输出在整个编译过程中asm语句的每个实例所特有的数字
当在生成多个汇编器指令的单个模板中创建本地标签并多次引用它们时,此选项非常有用
‘%{’:
‘%|’:
‘%}’:
相应的将’ { ‘,’|以及’}'输出到汇编程序代码中。

asm模板中的多种汇编语言:

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

输出操作数的语法格式:
操作数使用逗号分隔。
约束条件
|
V
[ [asmSymbolicName] ] constraint (cvariablename)

asmSymbolicName:

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

constraint:

指定操作数位置约束的字符串常量。
输出约束必须以’ = ‘(覆盖现有值的变量)或’ + ‘(读写时)开始。
使用’ = '时,不要假定位置包含asm入口时的现有值,除非操作数绑定到输入
在前缀之后,必须有一个或多个附加约束来描述值所在的位置。
常见的约束包括r:表示寄存器(register),m:表示内存(memory)
当您列出多个可能的位置(例如,“=rm”)时,编译器将根据当前上下文选择最有效的位置
如果在asm语句允许的范围内列出尽可能多的备选项,则允许优化器生成尽可能好的代码。
如果您必须使用特定的寄存器,但是您的机器约束没有提供足够的控制来选择您想要的特定寄存器,那么本地寄存器变量可以提供一个解决方案

cvariablename:

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

输出操作数表达式必须是左值。使用“+”约束修饰符的操作数被计算为两个操作数(即同时作为输入和输出),每个asm语句的操作数最多为30个

对不能与输入重叠的所有输出操作数使用’ & '约束修饰符。
否则,GCC可以在相同的寄存器中将输出操作数分配为一个不相关的输入操作数,前提是汇编代码在生成输出之前使用它的输入。如果汇编程序代码实际上包含一条以上的指令,那么这种假设可能是错误的。
如果一个输出参数(a)允许寄存器约束,而另一个输出参数(b)允许内存约束,也会出现同样的问题。
GCC生成的用于访问b中的内存地址的代码可以包含可能由a共享的寄存器,GCC认为这些寄存器是asm的输入。
如上所述,GCC假设在编写任何输出之前都要使用这些输入寄存器
这种假设可能导致不正确的行为如果asm语句写在使用b。结合“&”修改器和寄存器约束确保修改不影响地址引用b。否则,b是未定义的位置如果修改使用前b。
asm支持操作数上的操作数修饰符(例如“%k2”而不是简单的“%2”)。通常,这些限定符依赖于硬件
如果asm后面的C代码没有使用任何输出操作数,那么使用volatile for asm语句,以防止优化器在不需要时丢弃asm语句
这段代码没有使用可选的asmSymbolicName。因此,它将第一个输出操作数引用为%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仅在本例中用于强调在何处使用哪些名称。可以重用名称索引和掩码。
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选择最佳位置。

输入操作数:
输入操作数使来自C的变量和表达式的值对汇编代码可用。
操作数之间使用逗号分隔。
输入操作数的语法格式:
[ [asmSymbolicName] ] constraint (cexpression)

asmSymbolicName:

为操作数指定符号名称。
通过将名称括在方括号中,在汇编器模板中引用该名称(即’ %[Value] ‘)。
名称的作用域是包含定义它的asm语句。
任何有效的C变量名都是可以接受的,包括已经在周围代码中定义的名称。
同一asm语句中的任何两个操作数都不能使用相同的符号名。
当不使用asmSymbolicName时,请在汇编器模板中的操作数列表中使用操作数的(从零开始的)位置。
例如,如果有两个输出操作数和三个输入,在模板中使用’ %2 ‘引用第一个输入操作数,’ %3 ‘引用第二个,’ %4 ‘引用第三个。
constraint:
指定操作数位置约束的字符串常量
输入约束字符串不能以’ = ‘或’ + '开头
当您列出多个可能的位置(例如,“irm”)时,编译器将根据当前上下文选择最有效的位置。
如果您必须使用特定的寄存器,但是您的机器约束没有提供足够的控制来选择您想要的特定寄存器,那么本地寄存器变量可以提供一个解决方案
输入约束也可以是数字(例如,“0”)。这表明指定的输入必须与输出约束列表中(从零开始)索引处的输出约束位于相同的位置。当为输出操作数使用asmSymbolicName语法时,您可以使用这些名称(括在括号“[]”中)代替数字。
cexpression:
这是作为输入传递给asm语句的C变量或表达式。括号是语法中必需的一部分。
如果没有输出操作数,但有输入操作数,则在输出操作数的位置放置两个连续冒号:
asm (“some instructions”
: /* No outputs. */
: “r” (Offset / 8));

警告:不要修改仅输入操作数的内容(与输出绑定的输入除外)。编译器假设从asm语句中退出时,这些操作数包含与执行语句之前相同的值。
不可能使用clobbers通知编译器这些输入中的值正在更改。一种常见的解决方法是将正在更改的输入变量绑定到从未使用过的输出变量。

但是,请注意,如果asm语句后面的代码没有使用任何输出操作数,GCC优化器可能会将asm语句作为不需要的语句丢弃(请参阅Volatile)
在本例中,使用虚构的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));

Clobbers and Scratch Registers:
虽然编译器知道对输出操作数中列出的条目的更改,但是内联asm代码可能修改的不仅仅是输出
例如,计算可能需要额外的寄存器,或者作为特定汇编指令的副作用,处理器可能覆盖寄存器。
为了将这些更改通知编译器,请在clobber列表中列出它们。
Clobber列表项可以是寄存器名,也可以是特殊的Clobber(如下所示)。每个clobber列表项都是一个字符串常量,用双引号括起来,并用逗号分隔。
Clobber描述不能以任何方式与输入或输出操作数重叠。
另一个限制是clobber列表不应该包含堆栈指针寄存器。
这是因为编译器要求堆栈指针的值在asm语句之后与在语句入口时相同。
但是,以前版本的GCC没有强制执行这个规则,允许堆栈指针出现在列表中,语义不清楚。这种行为是不赞成的,在GCC的未来版本中,列出堆栈指针可能会成为一个错误。

下面是一个实际的VAX例子,展示了如何使用Clobber寄存器:
	asm volatile ("movc3 %0, %1, %2"
					   : /* No outputs. */
					   : "g" (from), "g" (to), "g" (count)
					   : "r0", "r1", "r2", "r3", "r4", "r5", "memory");

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

“cc”:
“cc”clobber表示汇编程序代码修改标志寄存器。
在某些机器上,GCC将条件代码表示为一个特定的硬件寄存器;“cc”用于命名此寄存器。在其他机器上,条件代码处理是不同的,指定“cc”没有效果。但无论目标是什么,它都是有效的。
“memory”:
“memory”clobber告诉编译器,汇编代码执行对输入和输出操作数中列出的项以外的项的内存读写(例如,访问由输入参数之一指向的内存)。
为了确保内存中包含正确的值,GCC可能需要在执行asm之前将特定的寄存器值刷新到内存中。
此外,编译器并不假设在asm之前从内存中读取的任何值在asm之后保持不变;它会根据需要重新加载它们。使用“内存”clobber可以有效地为编译器形成读/写内存屏障。
注意,这个clobber并不阻止处理器执行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)

Goto Label:
asm goto允许汇编代码跳转到一个或多个C标签。
asm goto语句中的GotoLabels部分包含一个逗号分隔的列表,其中包含汇编代码可能跳转到的所有C标签。
GCC假设asm的执行会一直执行到下一个语句(如果不是这样,请考虑在asm语句之后使用__builtin_unreachable )
asm goto的优化可以通过使用hot和cold标签属性来改进
asm goto语句不能有输出。
是由于编译器的内部限制:控件传输指令不能有输出
如果汇编代码确实修改了任何东西,那么使用“memory”clobber强制优化器将所有寄存器值刷新到内存中,并在asm语句之后根据需要重新加载它们。
还要注意,asm goto语句总是被隐式地认为是volatile语句。
要引用汇编器模板中的标签,请在其前面加上“%l”(小写的“l”),
在GotoLabels中加上它的(从零开始的)位置和输入操作数。
例如,如果asm有三个输入并引用两个标签,则将第一个标签引用为“%l3”,第二个标签引用为“%l4”)。
另外,您可以使用括号内的实际C标签名称引用标签。例如,要引用一个名为carry的标签,可以使用“%l[carry]”。
使用这种方法时,标签必须仍然列在GotoLabels部分中。
下面是一个例子asm goto for i386:
asm goto (
“btl %1, %0\n\t”
“jc %l2”
: /* No outputs. */
: “r” (p1), “r” (p2)
: “cc”
: carry);

	return 0;

	carry:
	return 1;


下面的例子显示了一个使用内存clobber的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;
	}

猜你喜欢

转载自blog.csdn.net/wzc18743083828/article/details/100543968