翻译 | “扩展asm”——用C表示操作数的汇编程序指令

Posted ncdxlxk

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了翻译 | “扩展asm”——用C表示操作数的汇编程序指令相关的知识,希望对你有一定的参考价值。

技术分享图片

 

本文翻译自GNU关于GCC7.2.0版本的官方说明文档,第6.45.2小节。供查阅讨论,如有不当处敬请指正……

 

通过扩展asm,可以让你在汇编程序中使用C中的变量,并从汇编代码跳转到C语言标号。在汇编程序模板之后,扩展asm语法使用冒号“:”来界定操作参数。

asm [volatile] ( 汇编程序模板

                     : 输出操作数

                     : 输入操作数

                     : 互斥项 )

 

asm [volatile] goto ( 汇编程序模板

                      : 

                      : 输入操作数

                      : 互斥项

                      : 跳转标号)

“asm”是GNU扩展关键词,如果要使用“-ansi”或“-std”选项编译代码,使用“__asm__”代替“asm”(这两个选项会禁用一些特定的关键词如“asm”、“typeof”、“inline”等)。

 

限定符

  • volatile:使用扩展asm语句的主要目的是控制输入值使之产生相应的输出值。然而,这些asm语句也可能产生副作用,这时也许需要使用“volatile”限定符来禁用某些优化。

  • goto:该限定符告诉编译器汇编声明中的指令可能会跳转到所列出的跳转标号中去。

 

参数

  • 汇编程序模板:汇编样式的字符串,它由固定文本(汇编指令)和标识符组成,标识符引用输入、输出和跳转参数。

  • 输出操作数:由汇编程序模板更改的以逗号分隔的C变量列表。允许使用空列表。

  • 输入操作数:被汇编程序模板读取的以逗号分隔的C表达式列表。允许使用空列表。

  • 互斥项:由汇编程序模板更改的输出参数之外的寄存器或其它值列表。允许使用空列表。

  • 跳转标号:当你使用跳转指令,该部分包含了汇编程序模板中可能跳转到的C标号。但一个扩展asm语句只能到跳转列表中的标号,不能跳转到另一个扩展asm语句,GCC的优化器并不知道这些跳转,所以也就无法决定如何优化。

所有输入、输出以及跳转操作数的总数被限制在30以内。

 

附注

asm语句使得你可以在C代码中直接嵌入汇编代码,这可以帮助你在时间敏感的代码中获得最大化的性能,或是访问一些C语言不易表达的汇编指令。

注意扩展asm语句必须在函数内部,只有基础汇编才能存在于函数外。用 naked attribute 声明过的函数也需要基础汇编。

“asm”语法用处多样,它可帮助你将asm语句看作是一连串将输入参数转化为输出参数的底层指令。一个简单的i386处理器使用“asm”的例子如下:

int src = 1;

int dst;   

 

asm ("mov %1, %0 "

    "add $1, %0"

    : "=r" (dst) 

    : "r" (src));

 

printf("%d ", dst);

该段代码将src拷贝到dst,并对dst加1。

 

1. Volatile(易变的)

GCC优化器如果认定输出变量并无必要,它有时会将asm语句丢弃。同样,如果优化器相信某段代码始终返回相同的值,它会将该段代码移到循环外部。使用volatile限定符可以禁止这类优化,包含asm跳转语句在

内的无输出操作数的asm语句,都是默认使用了volatile的。

这段i386代码展示了一个没有使用volatile限定符的例子。假设它用于断言检查,其中使用“asm”实现验证;另外dwRes没有在它处被引用。结果就是,优化器丢弃了该asm语句,接着移除了整个DoCheck程序。 

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

}

当你不需要允许优化器来产生高效的代码时,可以省略volatile关键词。

下面展示了一个优化器能够识别出输入变量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 ", x, dwSomeValue, dwRes);

   }

}

下面展示了一个你需要使用volatile限定符的例子。它使用x86的rdtsc 指令来读取计算机的时间戳计数器。如果不使用volatile,优化器将可能会认为该asm语句段始终返回相同的值,进而在第二次调用时将其优化掉。

uint64_t msr;

 

asm volatile ( "rdtsc "    // Returns the time in EDX:EAX.

        "shl $32, %%rdx "  // Shift the upper bits left.

        "or %%rdx, %0"        // ‘Or‘ in the lower bits.

        : "=a" (msr)

        : 

        : "rdx");

 

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

 

// Do other work...

 

// Reprint the timestamp

asm volatile ( "rdtsc "    // Returns the time in EDX:EAX.

        "shl $32, %%rdx "  // Shift the upper bits left.

        "or %%rdx, %0"        // ‘Or‘ in the lower bits.

        : "=a" (msr)

        : 

        : "rdx");

 

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

在上面的例子中,GCC优化器不会把这个asm语句当作non-volatile代码看待,就算认为前一次调用的结果依然有效,也不会将它移到循环外或是删除。

注意:编译器依然可能将带有volatile的asm语句和其它代码关联起来,包括做指令交叉。例如,在很多目标处理器中,由系统寄存器控制浮点操作数的取整,即使设置带volatile的asm语句,它也不一定能有效工作,就像下面PowerPC 的一个例子:

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

sum = x + y;

编译器可能会将加法移至asm语句语句之前。为了让它能正确执行,在后面加上一个变量引用使加法语句对asm语句语句产生人为的依赖关系,例如:

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

sum = x + y;

在特定的情况下,GCC可能会在优化时拷贝(或移除拷贝)你的汇编代码,如果你的汇编代码定义了符号或标号,这可能会在编译时造成多倍的符号错误。使用“%=”会帮助你解决这个问题。

 

2. Assembler Template(汇编模板)

汇编程序模板是一个含有汇编指令的字符串。编译器用对应的输入、输出和跳转标号替换模板中的标识符,然后将结果字符串交给汇编器。该字符串可以包含任何汇编器可以识别的内容。GCC不负责解析汇编指令,也不知道它们的含义以及它们是否为有效的汇编输入,但却会统计语句的个数。

你也许会把多条汇编指令放在一个asm字符串内,之间用汇编代码中常用的符号分隔。在多数场合,分隔的办法是重起一行,再加上一个tab制表符对应到指令区域(用“ ”表示)。一些汇编器接受用分号表示行分隔,但要注意的是有些汇编器也把分号当作注释的开始。

即使使用了volatile限定符,在编译之后也不要指望一组asm语句可以保持绝对的连续。如果有特定的指令需要在输出端保持连续,可以将它们放入单独的一个asm语句中。

如果不通过输入/输出操作数来访问C程序中的数据(例如在asm语句中直接使用全局符号),可能不会获得期望的结果。同样地,直接在asm语句中调用函数,需要你对目标汇编器和ABI(Application Binary Interface,应用程序二进制接口)有足够的了解。

由于GCC并不解析asm语句,它无法看到asm语句中引用的符号,除非它们在输入、输出或跳转操作数中被列出,否则可能导致GCC把这些符号当作未被引用的内容而丢弃掉。

 

特殊格式字符串

除了输入、输出以及跳转操作数所描述的标识符之外,一下这些标识符在asm语句中具有特殊的含义:

  • “%%”:在汇编代码中输出一个“%”。

  • “%=”:为整个编译代码中asm语句的每一个实例输出一个唯一的数字。当单个模板产生了本地标号,并被多次引用时,这一选项非常有用。

  • “%{”、“%|”、“%}”:分别在汇编代码中输出“{”、“|”、“}”。当为非转义字符时,它们对表现多种汇编格式具有特殊意义。

 

汇编模板中的多种汇编格式

以x86平台为例,GCC支持多种汇编格式。-masm选项控制GCC使用何种汇编格式,内联汇编默认使用缺省值。目标指定文件中的-masm选项包含了可支持格式的列表,如未指定则使用缺省值。当汇编代码使用某种格式能正常工作,却很有可能在使用另一种格式编译时失败,这时这一信息就变得很重要了。

如果你的代码需要支持多种汇编格式(例如,你要编写一个公共的头文件来支持多种不同的编译选项),使用如下的结构形式:

{ 格式0 | 格式1 | 格式2... }

使用格式#0编译时,这种结构输出格式0代码进行编译,使用格式#1编译时,输出格式1代码进行编译……如果代码分支的条目少于编译器支持的格式,这种结构将输出空。

例如,假设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} {%%}ebx, %1"

等价于其中的一句:

"xchgl %%ebx, %1"                 /* att dialect */

"xchg ebx, %1"                    /* intel dialect */

不支持格式间的嵌套!

 

3. Output Operands(输出操作数)

一个asm语句有零到多个输出操作数,用C变量表示并在汇编代码中被修改。

在下面这个i386平台的例子中,old(与汇编模板中的%0对应)和*Base(%1)是输出,offset(%2)是输入。

bool old;

 

__asm__ ("btsl %2,%1 " // 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]”)。名称的范围是有效的汇编语句,也包括任何C变量名以及周边已经定义过的名字。一个asm语句中的两个操作数不能使用相同的标识符。

当不使用汇编标识符名时,可用asm语句中操作数的位置(从0开始)代替。例如:假设有三个输出操作数,在汇编模板中使用“%0”表示第一个,“%1”表示第二个,“%2”表示第三个。

 

constraint(约束)

用于约束操作数放置位置的字符串。

输出约束必须以“=”(可覆盖已存在值)或“+”(读写)开头。当使用“=”时,不要假定输出的位置就位于已存在的汇编输入中,除非该操作数和输入绑定在一起。

在前缀之后,还需要一到多个附加约束来描述操作值存放的位置。常用的约束有“r”表示寄存器,“m”表示内存。当你列出多于一个位置时(如“=rm”),编译器基于上下文选择效率最优的一个。如果你列出了

所有asm语句支持的情况,则表示许可优化器选择最优可能的代码。如果你必须指定一个寄存器,但机器约束使得你无法充分选择到指定的那个寄存器,这时本地寄存器可以提供解决方案。

 

cvariablename(C变量名)

为输出指定一个C左值表达式,通常为变量名。圆括号是语法的一部分。

当编译器使用寄存器来表示输出操作数,它就绝不会使用互斥过的寄存器。

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

使用“+”约束的操作数,在asm语句最多30个操作数的统计中记为两个操作数(同时为输入和输出)。

在所有不可覆盖输入的输出操作数中使用“&”约束修饰符。否则,GCC可能将输出操作数分配到与之不相关的输入操作数相同的寄存器中,前提是汇编代码在生成输出之前已将输入使用完毕。如果汇编代码实际上包含多个指令,那么这个假设可能是错误的!

如果一个输出参数(a)允许寄存器约束,而另一个输出参数(b)允许内存约束,同样的问题也会发生。GCC生成的用于访问B内存地址的代码可以包含一个可能由A共享的寄存器,而GCC将这些寄存器考虑进asm的输入中。如上所述,GCC假设这样的输入寄存器在写入任何输出之前被消耗掉,如果汇编在使用b之前写到a,这个假设可能会导致不正确的行为。

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

如果asm后面的C代码没有使用它的任何输出操作数,可以使用volatile关键词来防止优化器丢弃asm语句。

下面这个代码没有使用可选的汇编标识符,因此,它将第一个输出操作数引用为%0(如有第二个为%1……)。第一个输入操作数的值比后一个输出操作数大1,在这个i386的例子中,mask引用为%1。

uint32_t Mask = 1234;

uint32_t Index;

 

  asm ("bsfl %1, %0"

     : "=r" (Index)

     : "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的最佳位置。

 

4. Flag Output Operands(标志输出操作数)

有些目标有一个特殊的寄存器,它保存操作或比较结果的“标志”。通常情况下,这种寄存器的内容被asm登记为不可修改,或是被放进asm的修改列表中。

在某些目标上,存在一种特殊的输出操作数,其中标志寄存器中的状态可以是asm的输出。所支持的状态集是特定于目标的,但一般规则是输出变量必须是整数标量,而值是布尔值。如果支持,目标定义预处理器符号 __GCC_ASM_FLAG_OUTPUTS__。

由于标志输出操作数的特殊性质,没有替代的约束方案。

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

x86家族

对于x86家族的标志输出约束的形式为“[email protected]条件”,其中条件是在jcc或setcc的ISA(指令集)中定义的标准条件。

"a" :大于或无符号大于

"ae":大于等于或无符号大于等于

"b" :小于或无符号小于

"be":小于等于或无符号小于等于

"c" :进位标志置位

"e"/

"z" :等于或零标志置位

"g" :有符号大于

"ge":有符号大于等于

"l" :有符号小于

"le":有符号小于等于

"o" :溢出标志置位

"p" :奇偶标志置位

"s" :符号标志置位

n前缀表示以上条件的反面

"na"

"nae"

"nb"

"nbe"

"nc"

"ne"

"ng"

"nge"

"nl"

"nle"

"no"

"np"

"ns"

"nz"

 

5. Input Operands(输入操作数)

输入操作数从C变量和对汇编代码有效的表达式中取值。操作数用逗号分隔,每个操作数格式如下:

[ [asmSymbolicName] ] constraint (cexpression)

 

asmSymbolicName(汇编标识符名)

为操作数指定一个标识符,在汇编模板中用方括号将标识符名括起来(如“%[Value]”)。名称的范围是有效的汇编语句,也包括任何C变量名以及周边已经定义过的名字。一个asm语句中的两个操作数不能使用相同的标识符。

当不使用汇编标识符名时,可用asm语句中操作数的位置(从0开始)代替。例如:假设有2个输出操作数和3个输入操作数,在汇编模板中使用“%2”表示第一个输入操作数,“%3”表示第二个输入操作数,“%4”表示第三个输入操作数。

 

constraint(约束)

用于约束操作数放置位置的字符串。

输入约束不能用“=”或“+”开头。当你列出多于一个位置时(如“=irm”),编译器基于上下文选择效率最优的一个。如果你必须指定一个寄存器,但机器约束使得你无法充分选择到指定的那个寄存器,这时本地寄存器可以提供解决方案。

输入约束也可以是数字(例如,“0”)。这表明指定的输入必须与输出约束列表中该对应索引(从0开始)的输出约束共享同一位置。当使用汇编标识符语法输出操作数,你可以使用它们的名称(括在[]中)代替数字。

 

cexpression(C表达式)

可将C变量或表达式作为输入传递给asm,其中括号是必须的语法。

当编译器使用寄存器来表示输入操作数,它就绝不会使用互斥过的寄存器。

如果没有输出操作数,但有输入操作数,输出操作数处使用两个连续的冒号:

__asm__ ("some instructions"

   : /* No outputs. */

   : "r" (Offset / 8));

警告:不要修改单输入操作数的内容(与输出相关联的输入除外)。编译器假定在退出asm语句时,这些操作数包含与执行语句之前相同的值。这时不可能使用互斥量告知编译器这些输入值的变化,一个常见的手法是将不断变化的输入变量绑定到一个从未使用过的输出变量。

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

如果asm后面的C代码没有使用它的任何输出操作数,优化器可能会出人意料地丢弃asm语句。

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

在这个示例中,使用虚构的组合指令,输入操作数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. Clobbers(互斥项)

当编译器意识到输出操作数列表中条目的变化时,内联汇编代码修改的可能不仅仅是输出。例如,计算可能需要附加寄存器,或是处理器可能覆盖寄存器作为特定汇编指令的副作用。为了通知编译器这些变化,将它们列在互斥列表中,互斥列表的内容可以是寄存器名或特殊的互斥项(在下方列出)。每个互斥项为用双括号括起的字符串,并用逗号分隔。

互斥项不能与任何的输入或输出操作数重合。例如,操作数不能使用互斥项列表中列出的寄存器;输入输出操作数中指定寄存器的变量不能在互斥项列表中描述。特别地,除非把输入操作数同时指定为输出操作数,否则没法判断输入操作数会被修改。

编译器在选择哪个寄存器用于输入输出操作数时,它不会选择互斥列表中列出的寄存器。结果就是,互斥寄存器在汇编代码中可以用于任何用途。

这里以VAX为例,显示了互斥寄存器的使用实例:

asm volatile ("movc3 %0, %1, %2"

                   : /* No outputs. */

                   : "g" (from), "g" (to), "g" (count)

                   : "r0", "r1", "r2", "r3", "r4", "r5");

另外,还有两个特殊的互斥项:

  • "cc":它预示着汇编代码修改了标志寄存器。在某些器件上,GCC将条件代码表示为一个特定的硬件寄存器;“cc”用来命名这个寄存器。在其他器件上,条件代码的表示不同,指定“cc”没有作用。但不管目标器件是什么,它都是有效的。

  • "memory":告诉编译器,汇编代码对输入和输出操作数列表之外的其它项执行内存读或写(例如,访问由某个输入参数指向的内存)。为了确保内存中包含正确的值,GCC可能需要在执行asm之前将特定寄存器值刷新到内存。此外,编译器不保证任何在asm语句执行前从内存中读的值,在asm执行完后依然保持不变,它将会根据需要重载。使用“memory”互斥项,有效地为编译器形成一个读/写内存保护。

注意:使用该互斥项并不能阻止处理器投机地越过asm语句做数据读取,为防止这种情况发生,你需要特定于处理器的栅栏指令。

把寄存器的值刷进内存会影响性能,这对时间敏感型代码可能会是个问题,如果被访问内存的大小在编译阶段已知的话,你可以使用一个小技巧来避免它。例如,假设访问一个10字节大小的字符串,使用下面的内存输入操作数:

{"m"( ({ struct { char x[10]; } *p = (void *)ptr ; *p; }) )}.

 

7. Goto Labels(跳转标号)

asm允许汇编代码跳转到一个或多个C标号。在asm语句的跳转标号部分包含所有汇编代码可能跳转的标号,并用逗号分隔。GCC假定asm执行后转到下一条语句(如果不是这样的话,考虑在asm语句后使用__builtin_unreachable属性)。通过使用热和冷标签属性,可以改进对asm跳转的优化。

asm跳转语句不能有输出,这是由编译器内部限制的:控制转移指令不能有输出。如果汇编代码不修改任何东西,使用“memory”互斥项来强制将所有寄存器的值刷入内存,并在asm语句后重新载入它们。

需要注意的是,asm跳转语句总是隐式地使用volatile。

要在汇编模板中引用标号,使用“%l”(或“%L”前缀),后面跟上标号在跳转标号中的位置(从0开始+输入操作数)。例如,假设asm有三个输入和两个跳转标号,用“%l3”表示第一个跳转标号,“%l4”表示第2个跳转标号。

另外,可以使用包含在括号中的实际C标号名称来引用标号。例如,为了引用名为“carry”的标号,可以使用“%l[carry]”。用这个方法时,标签必须仍然被列在跳转标号中。

这里是一个i386使用asm跳转的例子:

asm goto (

    "btl %1, %0 "

    "jc %l2"

    : /* No outputs. */

    : "r" (p1), "r" (p2) 

    : "cc" 

    : carry);

return 0;

 

carry:

return 1;

下面展示了一个使用memory互斥项的asm跳转:

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;

}

 

关于x86平台的两个小节略去:

8. x86 Operand Modifiers

……

9. x86 Floating-Point asm Operands

……

 

参考资料

【1】Extended Asm - Assembler Instructions with C Expression Operands. 

https://gcc.gnu.org/onlinedocs/gcc-7.2.0/gcc/Extended-Asm.html#Extended-Asm

 

 

·END·

 

想进一步跟踪本博客动态,欢迎关注我的个人微信订阅号:信号君

 

信号君:寻求简单之道

技术成长 | 读书笔记 | 认知升级

技术分享图片

扫描二维码关注信号君

  

 

以上是关于翻译 | “扩展asm”——用C表示操作数的汇编程序指令的主要内容,如果未能解决你的问题,请参考以下文章

单片机编程用C语言还是汇编?

20165232 第二周学习总结

使用C语言按照GPIO操作流程点亮LED灯

编译原理-第一章 引论-C和Java编译系统

指令系统-第三节1:X86汇编语言基础

51单片机汇编中的寄存器R0、R1、R2如果用c语言写的话怎么表示