C/C++代码中嵌套ARM汇编代码

起草人:马啸

在C/C++代码中嵌套汇编代码有两个目的:

  1. 为了性能优化,将非常关键的代码直接用汇编指令编写。。
  2. 程序需要频繁与底层硬件(例如:寄存器)打交道,也需要用汇编指令编写。

GCC编译器支持直接在C或者C++代码中,嵌入ARM汇编代码。其基本格式如下:

__asm__ [__volatile__] ( assembler template
            : [output operand list]            /*optional*/
            : [input operand list]            /*optional*/
            : [clobbered register list]        /*optional*/
);

下面将对上面的格式中前两个关键字进行进一步解释。

  1. 首先是关键字"__asm__",它将提醒GCC编译器,后面的将会是汇编代码。也可以使用"asm",但"__asm__"的兼容性更好。
  2. 其次是关键字"__volatile__",其作用是禁止编译器对后面编写的汇编指令再进行优化。这个关键字是可选的。同样的,也可以使用"volatile",但是兼容性不好。

括号中的就是真正的汇编代码了,由四部分组成:

  1. 具体的汇编代码;
  2. 输出参数列表;
  3. 输入参数列表;
  4. 修改寄存器列表。

各个部分间使用冒号“:”进行分割。如果前面的部分没有使用,而后面的部分使用了,则前面的部分也需要用冒号留空。

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”之后,编译器会在执行你的代码之前,保证将保存在寄存器中,没有更新到内存中的值全部都写入到内存中。

注意:此列表中的每一项都要用双引号("")括起来,每项之间都要用逗号(",")分割。

results matching ""

    No results matching ""