2012年(1008)
分类:
2012-08-01 10:45:24
原文地址:ARM GCC 内嵌汇编手册 作者:booktree
这篇文章是本人为方便各位业界同仁而翻译,方便大家开发底层代码使用,转载请注明出处,谢谢。要是你E文功底好,本人还是建议阅读E文版的。
http://www.ethernut.de/en/documents/arm-inline-asm.html
对于基于ARM的RISC处理器,GNU C编译器提供了在C代码中内嵌汇编的功能。这种非常酷的特性提供了C代码没有的功能,比如手动优化软件关键部分的代码、使用相关的处理器指令。
这里设想了读者是熟练编写ARM汇编程序读者,因为该片文档不是ARM汇编手册。同样也不是C语言手册。
这篇文档假设使用的是GCC 4 的版本,但是对于早期的版本也有效。
|
该语句的作用是将r0移动到r0中。换句话讲他并不干任何事。典型的就是NOP指令,作用就是短时的延时。
|
|
汇编和C语句这间的联系是通过上面asm声明中可选的output operand list和input operand list。Clobber list后面再讲。
|
每一个asm语句被冒号(:)分成了四个部分。
l 汇编指令放在第一部分中的“”中间。
|
l 接下来是冒号后的可选择的output operand list,每一个条目是由一对[](方括号)和被他包括的符号名组成,它后面跟着限制性字符串,再后面是圆括号和它括着的C变量。这个例子中只有一个条目。
|
l 接着冒号后面是输入操作符列表,它的语法和输入操作列表一样
|
l 破坏符列表,在本例中没有使用
就像上面的NOP例子,asm声明的4个部分中,只要最尾部没有使用的部分都可以省略。但是有有一点要注意的是,上面的4个部分中只要后面的还要使用,前面的部分没有使用也不能省略,必须空但是保留冒号。下面的一个例子就是设置ARM Soc的CPSR寄存器,它有input但是没有output operand。
|
即使汇编代码没有使用,代码部分也要保留空字符串。下面的例子使用了一个特别的破坏符,目的就是告诉编译器内存被修改过了。这里的破坏符在下面的优化部分在讲解。
|
为了增加代码的可读性,你可以使用换行,空格,还有C风格的注释。
|
在代码部分%后面跟着的是后面两个部分方括号中的符号,它指的是相同符号操作列表中的一个条目。
%[result]表示第二部分的C变量y,%[value]表示三部分的C变量x;
符号操作符的名字使用了独立的命名空间。这就意味着它使用的是其他的符号表。简单一点就是说你不必关心使用的符号名在C代码中已经使用了。在早期的C代码中,循环移位的例子必须要这么写:
|
在汇编代码中操作数的引用使用的是%后面跟一个数字,%1代表第一个操作数,%2代码第二个操作数,往后的类推。这个方法目前最新的编译器还是支持的。但是它不便于维护代码。试想一下,你写了大量的汇编指令的代码,要是你想插入一个操作数,那么你就不得不从新修改操作数编号。
有两种情况决定了你必须使用汇编。1st,C限制了你更加贴近底层操作硬件,比如,C中没有直接修改程序状态寄存器(PSR)的声明。2nd就是要写出更加优化的代码。毫无疑问GNU C代码优化器做的很好,但是他的结果和我们手工写的汇编代码相差很远。
这一部分有一点很重要,也是被别人忽视最多的就是:我们在C代码中通过内嵌汇编指令添加的汇编代码,也是要被C编译器的优化器处理的。让我们下面做个试验来看看吧。
下面是代码实例。
bigtree@just:~/embedded/basic-C$ arm-linux-gcc -c test.c
bigtree@just:~/embedded/basic-C$ arm-linux-objdump -D test.o
编译器选择r3作为循环移位使用。它也完全可以选择为每一个C变量分配寄存器。Load或者store一个值并不显式的进行。下面是其它编译器的编译结果。
|
编译器为每一个操作数选择一个相应的寄存器,将操作过的值cache到r4中,然后传递该值到r2中。这个过程你能理解不?
有的时候这个过程变得更加糟糕。有时候编译器甚至完全抛弃你嵌入的汇编代码。C编译器的这种行为,取决于代码优化器的策略和嵌入汇编所处的上下文。如果在内嵌汇编语句中不使用任何输出部分,那么C代码优化器很有可能将该内嵌语句完全删除。比如NOP例子,我们可以使用它作为延时操作,但是对于编译器认为这影响了程序的执行速速,认为它是没有任何意义的。
上面的解决方法还是有的。那就是使用volatile关键字。它的作用就是禁止优化器优化。将NOP例子修改过后如下:
|
下面还有更多的烦恼等着我们。一个设计精细的优化器可能重新排列代码。看下面的代码:
|
优化器肯定是要从新组织代码的,两个i++并没有对if的条件产生影响。更进一步的来讲,i的值增加2,仅仅使用一条ARM汇编指令。因而代码要重新组织如下:
|
这样节省了一条ARM指令。结果是:这些操作并没有得到许可。
这些将对你的代码产生很到的影响,这将在下面介绍。下面的代码是c乘b,其中c和b中的一个或者两个可能会被中断处理程序修改。进入该代码前先禁止中断,执行完该代码后再开启中断。
|
但是不幸的是针对上面的代码,优化器决定先执行乘法然后执行两个内嵌汇编,或相反。这样将会使得我们的代码变得毫无意义。
我们可以使用clobber list帮忙。上面例子中的clobber list如下:
|
上面的clobber list将会将向编译器传达如下信息,修改了r12和程序状态寄存器的标志位。Btw,直接指明使用的寄存器,将有可能阻止了最好的优化结果。通常你只要传递一个变量,然后让编译器自己选择适合的寄存器。另外寄存器名,cc(condition registor 状态寄存器标志位),memory都是在clobber list上有效的关键字。它用来向编译器指明,内嵌汇编指令改变了内存中的值。这将强迫编译器在执行汇编代码前存储所有缓存的值,然后在执行完汇编代码后重新加载该值。这将保留程序的执行顺序,因为在使用了带有memory clobber的asm声明后,所有变量的内容都是不可预测的。
|
使所有的缓存的值都无效,只是局部最优(suboptimal)。你可以有选择性的添加dummy operand 来人工添加依赖。
|
上面的第一个asm试图修改变量先b,第二个asm试图修改c。这将保留三个语句的执行顺序,而不要使缓存的变量无效。
理解优化器对内嵌汇编的影响很重要。如果你读到这里还是云里雾里,最好是在看下个主题之前再把这段文章读几遍^_^。
前面我们学到,每一个input和output operand,由被方括号[]中的符号名,限制字符串,圆括号中的C表达式构成。
这些限制性字符串有哪些,为什么我们需要他们?你应该知道每一条汇编指令只接受特定类型的操作数。例如:跳转指令期望的跳转目标地址。不是所有的内存地址都是有效的。因为最后的opcode只接受24位偏移。但矛盾的是跳转指令和数据交换指令都希望寄存器中存储的是32位的目标地址。在所有的例子中,C传给operand的可能是函数指针。所以面对传给内嵌汇编的常量、指针、变量,编译器必须要知道怎样组织到汇编代码中。
对于ARM核的处理器,GCC 4 提供了一下的限制。
Constraint |
Usage in ARM state |
Usage in Thumb state |
f |
Floating point registers f0 .. f7 |
Not available |
h |
Not available |
Registers r8..r15 |
G |
Immediate floating point constant |
Not available |
H |
Same a G, but negated |
Not available |
I |
Immediate value in data processing instructions |
Constant in the range 0 .. 255 |
J |
Indexing constants -4095 .. 4095 |
Constant in the range -255 .. -1 |
K |
Same as I, but inverted |
Same as I, but shifted |
L |
Same as I, but negated |
Constant in the range -7 .. 7 |
l |
Same as r |
Registers r0..r7 |
M |
Constant in the range of 0 .. 32 or a power of 2 |
Constant that is a multiple of |
m |
Any valid memory address | |
N |
Not available |
Constant in the range of 0 .. 31 |
O |
Not available |
Constant that is a multiple of |
r |
General register r0 .. r15 |
Not available |
w |
Vector floating point registers s0 .. s31 |
Not available |
X |
Any operand |
限制字符可能要单个modifier指示。要是没有modifier指示的默认为read-only operand。
Modifier |
Specifies |
= |
Write-only operand, usually used for all output operands |
+ |
Read-write operand, must be listed as an output operand |
& |
A register that should be used for output only |
Output operands必须为write-only,相应C表达式的值必须是左值。Input operands必须为read-only。C编译器是没有能力做这个检查。
比较严格的规则是:不要试图向input operand写。但是如果你想要使用相同的operand作为input和output。限制性modifier(+)可以达到效果。例子如下:
|
和上面例子不一样的是,最后的结果存储在input variable中。
|
限制性字符串“0”告诉编译器,使用和第一个output operand使用同样input register。
请注意,在相反的情况下不会自动实现。如果我没告诉编译器那样做,编译器也有可能为input和output选择相同的寄存器。第一个例子中就为input和output选择了r3。
在多数情况下这没有什么,但是如果在input使用前output已经被修改过了,这将是致命的。在input和output使用不同寄存器的情况下,你必须使用&modifier来限制output operand。下面是代码示例:
|
在以张表中读取一个值然后在写到该表的另一个位置。
要是经常使用使用部分汇编,最好的方法是将它以宏的形式定义在头文件中。使用该头文件在严格的ANSI模式下会出现警告。为了避免该类问题,可以使用__asm__代替asm,__volatile__代替volatile。这可以等同于别名。下面就是个例程:
|
宏定义包含的是相同的代码。这在大型routine中是不可以接受的。这种情况下最好定义个桩函数。
|
|
这个声明告诉编译器使用了符号名clock代替了具体的值。
为了改变函数名,你需要一个原型声明,因为编译器不接受在函数定义中出现asm关键字。
|
调用函数calc()将会创建调用函数CALCULATE的汇编指令。
局部变量可能存储在一个寄存器中。你可以利用内嵌汇编为该变量指定一个特定的寄存器。
|
汇编指令“eor r3, r3, r
如果你使用了寄存器,而你没有在input或output operand传递,那么你就必须向编译器指明这些。下面的例子中使用r3作为scratch 寄存器,通过在clobber list中写r3,来让编译器得知使用该寄存器。由于ands指令跟新了状态寄存器的标志位,使用cc在clobber list中指明。
asm volatile(
"ands r3, %1, #3" "\n\t"
"eor %0, %0, r3" "\n\t"
"addne %0, #4"
: "=r" (len)
: "0" (len)
: "cc", "r3"
);
最好的方法是使用桩函数并且使用局部临时变量。
比较好的方法是分析编译后的汇编列表,并且学习C 编译器生成的代码。下面的列表是编译器将ARM核寄存器的典型用途,知道这些将有助于理解代码。
Register |
Alt. Name |
Usage |
r0 |
a1 |
First function argument |
r1 |
a2 |
Second function argument |
r2 |
a3 |
Third function argument |
r3 |
a4 |
Fourth function argument |
r4 |
v1 |
Register variable |
r5 |
v2 |
Register variable |
r6 |
v3 |
Register variable |
r7 |
v4 |
Register variable |
r8 |
v5 |
Register variable |
r9 |
v6 |
Register variable |
r10 |
sl |
Stack limit |
r11 |
fp |
Argument pointer |
r12 |
ip |
Temporary workspace |
r13 |
sp |
Stack pointer |
r14 |
lr |
Link register |
r15 |
pc |
Program counter |