ARM GCC内联汇编 参考手册

目录

  • 关于本文档
  • GCC asm内联汇编语句
  • C代码优化
  • 输入操作数和输出操作数

关于本文档

GNU C编译器为ARM RISC处理器提供了在C语言中内嵌汇编语言的功能。这个特性可以被用来实现C语言中所不具备的功能,比如手动优化时间复杂度要求苛刻的软件模块,或者明确使用特定的处理器相关指令。因为本文档不是ARM汇编和C语言教程,所以要求读者已经熟练掌握ARM汇编和C语言。

本文档的所有示例均已使用GCC v4编译器进行了验证,并且大部分示例都可以在较早版本的编译器上正常工作。

GCC asm内联汇编语句

让我们以一个简单的例子来开始本文。下面的语句可以像其他C语句一样被包含到你的C代码里面。

/* NOP example */
asm("mov r0,r0");

上面这条语句将寄存器r0中的值赋给r0寄存器。也就是说,这条语句几乎什么都没有做。它就是一条经典的NOP(no operation)指令,经常被用来延迟一小段时间。

注意!在你把上面这条例子添加到你的C代码之前,你应该继续阅读本文档并了解为什么它并不会像我们期望的那样运行。

我们可以像是使用普通ARM汇编代码一样,在内联汇编中直接使用一模一样的汇编指令助记符。并且在内联汇编语句中,我们可以把几条汇编指令写在一行中。每条汇编语句之间使用分号隔开。但是为了增加可读性,最好还是一行一条指令。

asm(
"mov     r0, r0\n\t"
"mov     r0, r0\n\t"
"mov     r0, r0\n\t"
"mov     r0, r0"
);

使用换行符和制表符可以让汇编列表看起来美观一些。虽然第一次看起来有点儿奇怪,但是编译器就是按照这种方式将C语句编译成汇编语句的。

到目前为止,内联汇编语句和我们在纯汇编语言程序代码中看到的差不多。但是,相较于C语言表达式,编译器对 内联汇编中的寄存器和常数的处理方式是不同的。通用的内联汇编语句模板如下所示:

asm(code : output operand list : input operand list : clobber list);

内联汇编和C语言操作数之间的联系是通过asm语句中第二个(输出列表)和第三个(输入列表)可选项来提供的。至于第三个可选项(破坏列表)我们随后再进行解释。

接下来的例子将C语言变量传递给内联汇编语句,并进行移位操作。它将一个整型变量右移一位,并将结果赋给另外一个整型变量。

/* Rotating bits example */
asm("mov %[result], %[value], ror #1" : [result] "=r" (y) : [value] "r" (x));

每一条asm语句被冒号分隔成以下四部分:

  1. 以纯字符串的形式定义的汇编语句:
    "mov %[result], %[value], ror #1"
    
  2. 接下来是可选的输出列表。列表可以包含多项,每一项包括一个被中括号括起来的符号名,后面跟着一个限定字符串,再后面跟着被圆括号括起来的C表达式。我们这个示例中只有一项:
    [result] "=r" (y)
    
  3. 接下来是可选的输入列表,其语法结构和输出列表一样。再次说明一下,输入列表也是可选的,并且我们的示例中也只使用了一个操作数:
    [value] "r" (x)
    
  4. 最后一个可选的是破坏列表,被我们的示例省略了。

就像最开始的那个NOP示例一样,只要我们没有使用,asm语句后面的三个可选部分都可以省略。只包含纯汇编语句的内联asm语句被称为基本内联汇编,而包含可选部分的内联汇编语句被称之为扩展内联汇编。上面三个可选项,如果后面有一项使用了,那么前面未用到的部分必须使用冒号留出空位置。

接下来的示例是设置ARM处理器的CPSR寄存器。这里用到了输入列表,但是没有用到输出列表。

asm("msr cpsr,%[ps]" : : [ps]"r"(status));

虽然汇编语句部分可以为空,但是必须使用一个空字符串。接下来的例子创建了一个特殊的破坏列表,以告诉编译器内存中的内容被改变了。关于破坏列表,我们会在后面考察代码优化时对它进行介绍。

asm("":::"memory");

我们可以通过插入空格、新行,甚至是C语言注释来增加代码的可读性:

asm("mov    %[result], %[value], ror #1"

           : [result]"=r" (y) /* Rotation result. */
           : [value]"r"   (x) /* Rotated value. */
           : /* No clobbers */
    );

在第一部分代码段中,我们用%后跟[ ]括起来的符号来表示操作数。它们对应后面的输入、输出列表中相同名字的操作数。具体来讲,在上面的移位操作例子中:

%[result] 指的就是输出列表中的C变量y
%[value] 指的就是输入列表中的C变量x

这些操作数的名字属于一个独立的命名空间。也就是说,这些符号和其他的符号表没有任何联系。再讲地直白一点,我们可以在内联汇编语句中随便选用变量名,而不用去考虑C代码中是否已经使用了相同的名字。但是,需要强调一点,每一个asm语句中我们必须使用不同的操作数名字。

如果你看过早期其他人写的内联汇编语句,你肯定会注意到有一点非常明显的差异。事实上,他们写的是旧式的经典写法,因为在GCC v3.1之后才引入了符号名称。对于早期的移位操作示例,必须写成如下形式:

asm("mov %0, %1, ror #1" : "=r" (result) : "r" (value));

早期版本使用百分号后面跟着一个一位的数字(0~9)来表示操作数,比如%0表示第一个操作数,%1表示第二个操作数。目前最新版本的GCC仍然支持这种格式。但是这样写很容易产生错误,也非常不方便维护。试想一下,当我们已经写了大量的汇编语句,而此时需要在输出列表中间添加一个新的操作数,那么我们要对汇编语句中的操作数进行重新编号。这非常繁琐,也非常容易引入错误。

C代码优化

我们之所以在C代码中使用内联汇编,主要有两个可能的原因:

  1. C语言限制了我们更加贴近底层地操作硬件。比如,C里面没有直接修改程序状态寄存器CPSR的语句。
  2. 写出高度优化的代码。

毫无疑问,GNU C编译器的代码优化工作做的很好。但是,编译器生成的汇编代码和精心手写的代码还是有很大的不同。

注意,这里有一点经常被我们忽略,那就是当我们使用内联汇编语句在C里面添加汇编代码时,这些汇编代码也是会被C编译器进行优化处理的。比如还是以上面的移位示例为例,它经过编译后再反汇编生成的汇编指令如下所示:

00309DE5    ldr   r3, [sp, #0]    @ x, x
E330A0E1    mov   r3, r3, ror #1  @ tmp, x
04308DE5    str   r3, [sp, #4]    @ tmp, y

通过反汇编后的代码可以看到,编译器选择了r3寄存器来进行位偏移。它本可以为每一个C变量都分配一个寄存器。当我们使用另一个不同版本的编译器并且使用不同的编译选项时,结果如下所示:

E420A0E1    mov r2, r4, ror #1    @ y, x

可以看到,这次编译器为每个操作数都分配了一个寄存器,使用已经缓存在r4中的值,循环右移1位,并把结果赋给r2寄存器。

这种情况算是好的了。有时候编译器甚至会决定把你添加的汇编语句完全优化掉。这些行为作为编译器优化策略的一部分,也取决于内联汇编语句所在的上下文。比如说,如果你在接下来的C语句中没有使用到任何输出列表中的操作数,那么编译优化器将很可能把你的内联汇编语句给优化掉。我们上面的NOP示例很可能就会被这样处理掉,因为对编译器来说这条内联汇编语句除了拖慢程序执行速度之外,没有任何用处。

解决方法是使用volatile关键字修饰asm内联汇编语句,使得编译器不再优化你的汇编代码。修改上边的NOP示例如下所示:

/* NOP example, revised */
asm volatile("mov r0, r0");

但是,即使添加了volatile关键字,麻烦也远没有结束。编译优化器还可能重排我们的代码。我们来观察一下下面的这个C代码片段:

i++;
if (j == 1)
    x += 3;
i++;

优化器会识别出这两条自增语句对于条件判断不产生任何影响,同时它会发现使用一条自增2语句就可以替代那两条自增1语句。所以它会将代码重新组合成如下形式:

if (j == 1)
    x += 3;
i += 2;

通过上面的例子我们可以发现,编译器并不能保证编译之后的代码仍然和源代码中的语句执行序列保持一致。

这可能会对我们的代码产生很严重的影响。我们继续举例子进行说明。下面的代码用来让c乘以b,而这两个变量可能会被中断处理例程修改。访问变量前禁止中断,并在访问完毕后开中断是个不错的主意。

asm volatile("mrs r12, cpsr\n\t"
    "orr r12, r12, #0xC0\n\t"
    "msr cpsr_c, r12\n\t" ::: "r12", "cc");
c *= b; /* This may fail. */
asm volatile("mrs r12, cpsr\n"
    "bic r12, r12, #0xC0\n"
    "msr cpsr_c, r12" ::: "r12", "cc");

对于上述代码,编译优化器可能会先执行乘法操作,之后再执行这两条内联汇编语句;或者反过来先执行这两条内联汇编语句,之后再执行乘法语句。总之,这使得我们的关中断操作毫无用处。

要解决这个问题,我们就必须借助于破坏列表。上面例子中的破坏列表如下所示:

"r12", "cc"

破坏列表在这里提示编译器汇编代码修改了r12寄存器的值,并更新了状态标志位。同时要说明一点,直接指明使用哪个寄存器,可能会影响优化结果。通常我们只需要传递一个变量,并让编译器来决定使用哪个寄存器。除了寄存器名和cc状态寄存器之外,memory也是一个合法的关键字。它告诉编译器汇编代码会修改内存。这样做会强制编译器在执行内联汇编代码前 保存所有的临时数据,并在执行后重新恢复它们的值。这将保留程序的执行顺序,因为在执行了一个带有修改内存属性的内联汇编语句后,所有变量的值都是不可预测的。

asm volatile("mrs r12, cpsr\n\t"
    "orr r12, r12, #0xC0\n\t"
    "msr cpsr_c, r12\n\t" :: : "r12", "cc", "memory");
    
c *= b; /* This is safe. */

asm volatile("mrs r12, cpsr\n"
    "bic r12, r12, #0xC0\n"
    "msr cpsr_c, r12" ::: "r12", "cc", "memory");

多说一句,通过memory关键字让编译器保存所有的临时变量并不是最优解。我们可以添加一个假的操作数来营造出一种假的依赖关系。如下所示:

asm volatile("mrs r12, cpsr\n\t"
    "orr r12, r12, #0xC0\n\t"
    "msr cpsr_c, r12\n\t" : "=X" (b) :: "r12", "cc");
    
c *= b; /* This is safe. */

asm volatile("mrs r12, cpsr\n"
    "bic r12, r12, #0xC0\n"
    "msr cpsr_c, r12" :: "X" (c) : "r12", "cc");

上面的代码通过输出列表中的b操作数,假装会修改变量b的值,并在第二条内联汇编中假装要使用变量c(实际汇编代码中并没有使用变量b和变量c,但编译器会认为在汇编语句中使用了)。这样编译器就会认为这三条语句具有严格的先后关系,从而会保留这三条语句的执行顺序,并且也不会去保存所有的临时变量,从而效率也不受影响。

我们一定要准确理解编译优化器是怎样影响内联汇编语句的。如果上面的例子你还没有百分之百弄懂,请再把本小节的内容仔细阅读一遍。

输入操作数和输出操作数

我们之前讲过,每一个输入和输出操作数,都由一个被方括号括起来的符号名,后面跟着一个限定字符串,之后跟着一个用圆括号括起来的C表达式构成。

那么什么是限定字符串呢?我们为什么要使用它们呢?你很可能知道每一条汇编指令都只接受特定类型的操作数。比如分支指令期望的操作数是一个将要跳转的目标地址。然而不是所有的内存地址都是有效的,因为最后的操作码部分只接受24位的偏移值。相反,分支交换指令则期望一个保存了32位目标地址的寄存器。在这两个例子中,从C传给内联汇编语句中的操作数可能是同一个函数指针变量。因此,当我们向内联汇编传递常量、指针或者变量时,内联汇编语句必须知道怎么样把传入的操作数用汇编代码表示出来。

对于ARM处理器核,GCC v4提供了如下一系列的限定性字符串:


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 e.g. ORR R0, R0, #operand Constant in the range 0 … 255 e.g. SWI operand
J Indexing constants -4095 … 4095 e.g. LDR R1, [PC, #operand] Constant in the range -255 … -1 e.g. SUB R0, R0, #operand
K Same as I, but inverted Same as I, but shifted
L Same as I, but negated Constant in the range -7 … 7 e.g. SUB R0, R1, #operand
l Same as r Registers r0…r7 e.g. PUSH operand
M Constant in the range of 0 … 32 or a power of 2 e.g. MOV R2, R1, ROR #operand Constant that is a multiple of 4 in the range of 0 … 1020 e.g. ADD R0, SP, #operand
m Any valid memory address
N Not available Constant in the range of 0 … 31 e.g. LSL R0, R1, #operand
O Not available Constant that is a multiple of 4 in the range of -508 … 508 e.g. ADD SP, #operand
r General register r0 … r15 e.g. SUB operand1, operand2, operand3 Not available
w Vector floating point registers s0 … s31 Not available
X Any operand

限定性字符串之前可以添加单个的限定性修饰符。如果不添加修饰符,则表明该操作数是只读的。可以使用的修饰符如下所示:


修饰符 说明
= 只写操作数,通常被用来修饰输出操作数
+ 可读可写操作数,只能用来修饰输出操作数
& 只能用来作为输出的寄存器

输出操作数必须是只写的,并且C表达式的值必须是一个左值。C编译器会去检查这一点。

输入操作数则必须是只读的。注意,C编译器不能够对这一点进行检查。大多数的问题都会在最后的汇编阶段被检测出来,但是此时编译器报的错误却千奇百怪。即使编译器提示它发现了一个内部的编译器错误并请求你向编译器开发人员汇报该问题,你也还是应该先去检查一下你的内联汇编代码。

有一条我们应该严格遵守的规则是:绝对不要向输入操作数执行写操作。
但是如果我们既想使用该操作数作为输入,同时又想使用该操作数作为输出呢?此时限定修饰符+可以达到我们的要求。示例代码如下所示:

asm("mov %[value], %[value], ror #1" : [value] "+r" (y));

这个例子很像之前那个移位操作的例子。它将变量value循环右移了1位。与之前例子不同的是,移位的结果并没有保存到另外一个变量中,移位结果又存储到了原来的变量中。

不幸的一点是早期的编译器并不支持修饰符+。庆幸的是有另外一中解决方法,并且在目前最新的编译器中依然有效。在限定性字符串中,对于输入操作符我们可以使用一位数字n(数字从0开始)来告诉编译器,让它对第n个操作数使用相同的寄存器。示例代码如下:

asm("mov %0, %0, ror #1" : "=r" (value) : "0" (value));

限定性字符串"0"表示 让编译器使用和第一个输出操作数相同的寄存器。
注意,反过来即使我们不告诉编译器这样做,它也有可能为输入和输出操作数选择相同的寄存器。像之前的循环移位的例子,编译器就为输入和输出操作数选择使用了相同的寄存器。

asm("mov %[result],%[value],ror #1":[result] "=r" (y):[value] "r" (x));

上述汇编语句编译之后得到的反汇编码如下所示:

00309DE5    ldr   r3, [sp, #0]    @ x, x
E330A0E1    mov   r3, r3, ror #1  @ tmp, x
04308DE5    str   r3, [sp, #4]    @ tmp, y

在大多数情况下,这也没什么问题。

你可能感兴趣的:(Arm开发板学习)