C/C++代码中嵌套ARM汇编代码
起草人:马啸
在C/C++代码中嵌套汇编代码有两个目的:
- 为了性能优化,将非常关键的代码直接用汇编指令编写。。
- 程序需要频繁与底层硬件(例如:寄存器)打交道,也需要用汇编指令编写。
GCC编译器支持直接在C或者C++代码中,嵌入ARM汇编代码。其基本格式如下:
__asm__ [__volatile__] ( assembler template
: [output operand list] /*optional*/
: [input operand list] /*optional*/
: [clobbered register list] /*optional*/
);
下面将对上面的格式中前两个关键字进行进一步解释。
- 首先是关键字"__asm__",它将提醒GCC编译器,后面的将会是汇编代码。也可以使用"asm",但"__asm__"的兼容性更好。
- 其次是关键字"__volatile__",其作用是禁止编译器对后面编写的汇编指令再进行优化。这个关键字是可选的。同样的,也可以使用"volatile",但是兼容性不好。
括号中的就是真正的汇编代码了,由四部分组成:
- 具体的汇编代码;
- 输出参数列表;
- 输入参数列表;
- 修改寄存器列表。
各个部分间使用冒号“:”进行分割。如果前面的部分没有使用,而后面的部分使用了,则前面的部分也需要用冒号留空。
1)汇编代码模板:
汇编代码必须用双引号括起来。如果有多行汇编代码的话,每一条语句都要用双引号括起来,并且在代码后面要加上换行符(“\n”或者“\n\t”)。例如:
__asm__ __volatile__ ("instruction 1\n\t"
"instruction 2\n\t"
"instruction 3\n\t"
"instruction 4"
);
如果没有汇编代码,也需要传入空字符串(""),否则会报错。
2)输出参数列表
前面介绍了,第二部分表示输出参数列表。
输出参数表示汇编代码处理完后要输出结果的C表达式。如果有多个输出或输入表达式,需要用逗号(“,”)将它们分隔开来。下面举个例子:
__asm__("mov %0,%1, ror #1"
: "=r" (result)
: "r" (value)
);
例子中的%0代表后面定义的第一个操作数,即输出操作数,也就是C语言中的result变量。%1代表定义的第二个操作数,即输入操作数,就是C语言中的value变量。
该汇编代码的功能是将value的值右移一位,然后保存到result中。
从上面的例子可以看到,每一个操作数由三部分组成,分别是修改符(Modifier),限定符(Constraint)和C表达式,其中修改符是可选的。具体形式如下:
"[modifier]constraint"(C expression)
注意:修改符和限定符在双引号内,C表达式在括号内。
限定符:
由于并不是所有的汇编指令都可以接受C语言中任何类型的变量作为输入或输出变量的,因此汇编器需要知道这些变量到底用在什么地方,从而帮助其在传递操作数之前做一些转换。
下面是ARM下汇编指令中对常用限定符的定义:
限定符 | 在ARM指令集下 |
f | 浮点寄存器f0...f7 |
r | 通用寄存器r0...r15 |
m | 内存地址 |
修改符:
修改符加在限定符之前,可选。目前GCC定义了三个修改符,分别是:
修改符 | 含义 |
= | 只写操作数,通常用于输出操作数 |
+ | 可读且可写操作数,使用时必须要列在输出操作数中 |
& | 寄存器只能用于输出 |
作为输出操作数,只需要在限定符前加上“=”就可以了。
如果想让一个C变量既作为输入操作数,也作为输出操作数,可以使用“+”限定符,这个限定符加在输出操作数列表中。例如:
__asm__("mov %0, %0, ror #1"
: "+r" (y)
);
如果老版本GCC不识别"+"修改符,但是还想达到输入和输出操作符是一个变量的目的,可以用另外一种方法。在限定符中,也可以使用数字,其作用是指代前面定义的操作数,0代表第一个,1代表第二个,以此类推。例如:
__asm__("mov %0, %0", ror #1
: "=r" (y)
: "0" (y)
);
在输出操作数中使用“&”修改符,明确告诉编译器,输出操作数的寄存器一定不能使用输入操作数已经使用过的寄存器。下面举个例子:
__asm__ __volatile__("ldr %0, [%1]\n\t"
"str %2, [%1, #4]"
: "=&r" (rdv)
: "r" (&table), "r" (wdv)
: "memory"
);
在这个例子中,table是一个数组。它对应于汇编代码中的%1,rdv对应于汇编代码中的%0.所以第一条汇编代码的功能是把数组table中第一个数存放到rdv中。
第二条指令是将wdv中存放的值存放到table数组的第二个元素位置。
从代码上并没有看出问题,但是编译器可能会用同一个寄存器来表示输入操作数&table(%1)和输出操作数rdv(%0)。那么执行完第一条语句之后,table数组的地址就被修改掉了。
所以,可以在输出操作数中加上一个“&”修改符,强制保证输出操作数不能和输入操作数复用同一个寄存器,这个问题就解决了。
总结:如果汇编代码中输入寄存器还没有使用完毕,要对输出操作数进行修改时,则特别需要用“&”修改符,保证不复用。
3)输入参数列表
输入参数列表和输出参数列表类似。不同的地方是不用修改符,这样默认这个操作数只是只读的。
4) 修改寄存器列表
在执行你的程序时会用到一些寄存器,所以在执行之前必须要做必要的备份和恢复的动作。但是,编译器并不会分析你的汇编代码,找出这种被你修改过,需要恢复的寄存器,因此你必须显式的告诉编译器,被你修改过的寄存器有哪些。这就是修改寄存器列表所起到的作用。
对于嵌入内联ARM汇编来说,修改寄存器列表中的值有下面三种类型。
r0...r15 | 告诉编译器汇编代码中修改了通用寄存器r0...r15 |
cc | 告诉编译器汇编代码会导致CPU状态位的改变 |
memory | 告诉编译器汇编代码会读取或修改内存中某个地址存放的值 |
对于"memory",它保证了寄存器和内存之间的同步。比如:出于优化的目的,在执行你的汇编代码之前,编译器将某些变量的值还保存在寄存器中,并没有被写到实际的内存中。但是,如果你的汇编代码会读取内存中的值,则很有可能新的值还在寄存器中,而内存中存放的还是老的值,这样就会造成错误。添加了“memory”之后,编译器会在执行你的代码之前,保证将保存在寄存器中,没有更新到内存中的值全部都写入到内存中。
注意:此列表中的每一项都要用双引号("")括起来,每项之间都要用逗号(",")分割。