这篇笔记录的内容来自GCC手册中关于内嵌汇编的说明。
通过asm关键字,可以实现在C/C++代码中插入汇编代码,GCC提供了两种格式的内嵌汇编代码:1)基本asm汇编(无操作数);2)扩展asm汇编(有一个或多个操作数)。推荐使用扩展asm汇编,但也有些场景是必须使用基本asm汇编来完成。
基本asm汇编
基本asm汇编语法如下:
asm [ volatile ] ( AssemblerInstructions )
asm是GNU扩展的关键字,如果可以使用__asm__(对编译选项有要求),那么建议使用__asm__(手册上是这么介绍的,但是我们可以简单的将这二者认为是一样的)。
参数
AssemblerInstructions就是汇编代码,它就是个纯粹的字符串,GCC对其内容不做解释,它会被交给汇编程序处理。代码中通常用类似如下的写法来表示。
asm ("mov r0, r1\n\t"
"add r0, r2\n\t");
说明
使用扩展asm汇编往往可以生成更加高效的代码,但是有些场景是必须由基本asm汇编来完成的,比如:扩展asm汇编必须在C/C++函数内部,但是基本asm汇编可以独立在函数外,使用基本asm汇编可以在函数外定义宏,甚至实现某个函数;
基本asm汇编有很多的局限性,比如:如果在基本asm汇编代码中需要修改了某个寄存器,那么必须在执行前西先保存现场,执行完毕后再恢复现场,否则等基本asm汇编执行完毕后,GCC并不知到该寄存器的内容已经发生了变化,那么随后的程序大概率无法正确运行。上述的保护和恢复现场都必须由程序员在基本asm汇编中自己实现,很麻烦,使用扩展asm汇编可以高效洁净的解决这一问题。
扩展asm汇编
基本asm汇编只能包含语句,而扩展asm汇编却可以指定指令的操作数,可以让汇编指令和C变量有更好的互动。扩展asm语法如下:
asm [volatile] ( AssemblerTemplate
: OutputOperands
[ : InputOperands
[ : Clobbers ] ])
asm [volatile] goto ( AssemblerTemplate
:: InputOperands
: Clobbers
: GotoLabels)
上述每个部分都是可选的。下面代码是ARM平台使能中断的asm汇编实现,下面我们以该代码为例对扩展asm汇编的基本语法进行说明。
void enable_interrupts (void)
{
unsigned long temp;
__asm__ __volatile__("mrs %0, cpsr\n"
"bic %0, %0, #0x80\n"
"msr cpsr_c, %0"
: "=r" (temp)
:
: "memory");
}
汇编模板
汇编模板包含了被插入到 C 程序的汇编指令集。其格式为:每条指令用双引号圈起,或者整个指令组用双引号圈起。同时每条指令应以分界符结尾。有效的分界符有换行符\n和分号。实际编程中,通常使用\n 后紧随一个制表符(\t),制表符就是为了输出的汇编文件排版更加清晰。
对C程序变量的访问必须使用输入输出操作数,不能在指令中直接引用C语言符号。
转义字符
汇编模板中规定如下特殊字符的含义:
- '%%': 在输出的汇编代码中表示一个'%'字符;
- '%=': 在输出的汇编代码中生成一个唯一的数字,该数字可以代表编写的内嵌汇编程序,这相当于生成了一个本地标号;
- '%{','%|','%}': 在出书的汇编代码中分别表示一个'{','|','}'。之所以有这三个转义符是因为这三个符号在asm汇编语句中有特殊的含义。
输出操作数
asm汇编语句中可以有零个或多个输出操作数,这些操作数是会被汇编代码操作的C变量,当有多个输出操作数时用逗号隔开即可。示例代码中的%0引用的就是C变量temp。操作数有如下语法格式:
[ [asmSymbolicName] ] constraint (cvariablename)
[asmSymbolicName]可选,如果指定,那么相当于为C变量绑定了一个名字,在汇编代码中可以用%[asmSymbolicName]格式的语法来引用该变量,如果不为C变量绑定显式的名字,那么使用的是基于位置(从0开始)的名字来引用C变量。
constraint是一个字符串,用来对后面的C变量进行限定,细节在后面会单独介绍。对于输出操作数,该字段必须以'+'(表示该变量既要读又要写,这种情况需要变量有初值)或者'='(表示变量直接赋值,这种情况变量不需要有初值)开头。之后必须有一个或多个额外的限定符,其中'r'要求编译器将该变量放在寄存器中,'m'要求该变量放在内存中,当指定多个限定的时候,编译器会基于上下文选择一个最高效的。
一个asm语句所能包含的输入输出操作数总个数是有限制的,对于使用'+'限定符的操作数,其会占用2个操作数限制。
(cvariablename)代表一个C语言左值表达式,通常是一个变量名。
输入操作数
asm汇编语句中可以有零个或多个输入操作数,这些操作数会作为汇编语句的输入,它们可以是任意的C表达式,当有多个输入操作数时用逗号隔开即可。每个输入操作数遵守如下语法:
[ [asmSymbolicName] ] constraint (cexpression)
[asmSymbolicName]和前面输出操作数中的含义相同。
constraint对于输入操作数来说,不能以'='和'+'来开头。输入操作数可用使用数字来作为限定(比如'0'),这表示编译器需要将该输入操作数和对应编号的输出操作数放到同一个位置,即要么放到相同的寄存器,要么放到相同的内存中。如果输出操作数使用了[asmSymbolicName],那么可以用[名字]的方式指定。这种方式叫做匹配约束。
破坏列表
Clobbers部分的作用是让程序员告诉GCC汇编指令可能会修改哪些寄存器,这样GCC就会认为在asm语句执行完毕后,这些内容是无效的,进而在编译时会插入相应的保护现场和恢复现场的代码。
对于输入输出操作数占用的寄存器(r修饰符),GCC是知道对应的寄存器会被使用的,这些寄存器不需要出现在Clobbers列表中,不过我们往往只是说明要将变量放到寄存器中,具体放到哪个寄存器中我们并不知道。
Clobbers列表是由一个个用逗号分割的字符串组成的,字符串中可以是寄存器名字,也可以是其它特殊的clobber。
有两个特殊的clobber,它们含义如下:
- "cc":表示asm汇编代码会修改条件码寄存器。即使对应平台没有flag寄存器,使用它也不会有问题;
- "memory": 表示asm汇编代码会修改除了输入输出操作数以外的内存,那么需要使用该clobber。假设在执行asm汇编语句之前,已经将部分内存中的值读到了寄存器中,此时执行了asm汇编,这些汇编指令修改了相关的内存,之后如果直接使用之前寄存器中的值,那么逻辑有可能就是非预期的。GCC遇到memory后,会在执行asm汇编之前,保证内存中的值是最新的(需要写的会刷新到内存),再执行完毕asm汇编后,将重新从内存中读取最新的值。
goto标签
goto标签指定了asm汇编代码可能跳转到的C标签,如果有多个,那么用逗号隔开即可。goto类型的asm汇编是不能有输出操作数的,如果代码修改了某些内容,那么应该在clobbers部分使用"memory"。
asm汇编中使用"%l"(小写L)+基于0的编号来引用goto标签。此外,也可使用"%l[C label]"的语法格式引用C语言中的标签。
volatile
volatile用于关闭编译器优化(__volatile__是一样的效果)。使能中断的示例代码中,temp变量最后并没有被使用,此时GCC就可能会认为该语句没有作用,进而将其优化掉,这和实际预期是不符的。此时可以用volatile关键字让GCC不做优化。
如果我们的汇编只是用于一些计算并且没有任何副作用,不使用 volatile 关键词会更好。此时GCC可以对代码进行优化,使其更加高效。
对于基本asm汇编,volatile关键字是隐式指定的。
操作数约束
这部分详细介绍了asm汇编语句中操作数部分可使用的contraints(约束),约束告诉GCC操作数是否应该在寄存器中,以及在哪类寄存器中;是否在内存中,以及在哪类内存中;是否是常量,以及它可能的取值;此外约束还可以匹配输入操作和输出操作数。
约束是编程人员告诉GCC,在汇编asm语句是要遵守的条件。
简单约束符
最简单的限定符是由一组字母组成的字符串,其中每个字符描述了允许的一种类型操作数,常用的限定符如下:
- 空白字符: 空白字符可以出现在字符串的除第一个字符外的任意位置,它会被忽略,支持空白字符是为了编程时能够进行合理的对齐;
- 'r': 操作数可以位于任意一个通用寄存器中;此时GCC会将对应的变量先读到寄存器中,指令执行完毕后,再将其值写入内存中的变量中(如果该变量是输出操作数);只有需要高效操作的场景才应该这么指定;
- 'm': 操作数可以位于内存的任意地址;和r相反,此时对该变量的操作都是再内存中;
- 'i': 操作是一个立即数;
- 匹配约束:将输入操作数的约束指定为和某个输出操作数相同的约束,见输入操作数部分介绍;
上述简单限定符组合到一起组成复合限定符,如"rm"表示操作数既可以位于寄存器中,也可以位于内存中。
约束修饰符
对于输出操作数,在上面的简单约束符开头可以增加下面的约束修饰符来决定它们的可读写特性。
- '=': 表示操作数会被指令直接赋值,之前的值会被清除,这意味这该操作数之前的值并不重要;
- '+': 表示操作数既会被指令读,也会被指令写;对于既没有指定'=',也没有指定'+'的操作数,会被认为是只读的;
- '&': 没理解,以后补充吧;
寄存器变量
GCC支持将一个全局变量或者局部变量和一个寄存器进行关联。一般情况下都是GCC负责规划这些寄存器的使用,但在某些场景下会有这样的诉求。如下是u-boot中对寄存器变量的使用示例。
#define DECLARE_GLOBAL_DATA_PTR register volatile gd_t *gd asm ("r8")
上述代码将寄存器r8和全局变量gd绑定到一起,r8中保存了u-boot全局信息的指针。由于寄存器是物理资源,所以即使代码中有多个DECLARE_GLOBAL_DATA_PTR,也不会有重定义的问题,因为他们实际中只占用一个寄存器。