首页 > 代码库 > Android漫游记(5)---ARM GCC 内联汇编烹饪书(附实例分析)

Android漫游记(5)---ARM GCC 内联汇编烹饪书(附实例分析)

    原文链接(点击打开链接)

   

    关于本文档

    

    GNU C编译器针对ARM RISC处理器,提供了内联汇编支持。利用这一非常酷炫的特性,我们可以用来优化软件代码中的关键部分,或者可以使用针对特定处理的汇编处理指令。

    本文假定,你已经熟悉ARM汇编语言。本文不是一篇ARM汇编教程,也不是C语言教程。

    

    GCC汇编声明

    让我们从一个简单的例子开始。下面的一条ARM汇编指令,你可以添加到C源码中。

 /* NOP example-空操作 */

asm("mov r0,r0");
    上面的指令,讲r0寄存器的值移动到r0,换言之,实际上是一条空操作指令,可以用来实现短延迟。

    停!在我们把上面的代码添加到C源码之前,请继续阅读下文,否则,可能程序不会像你想象的那样工作。

    内联汇编可以使用纯汇编程序一样的指令助记符集,你也可以写一个多行的内联汇编,为了使代码易读,你可以在每行添加一个换行符。

    

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

    上面的\n\t换行符和制表符,会使的汇编器更易于处理,更可读。尽管看起来有些奇怪,但这却是C编译器在编译C源码时的处理方式。

     到目前为止,我们看到的内联汇编的表现形式和普通的纯汇编程序一样。但当我们要引用C表达式的时候,情况会有所不同。一条标准的内联汇编格式如下:

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

    内联汇编和C操作数之前的关联性体现在上面的input和out操作数上,对于第三个操作数clobber(覆盖、破坏),我们后面再解释。

    下面的一个例子讲C变量x进行循环移位操作,x为整形。循环右移位的结果保存在y中。

/* Rotating bits example */
asm("mov %[result], %[value], ror #1" : [result] "=r" (y) : [value] "r" (x));
    我们用冒号,讲每条扩展的asm指令分成了4个部分:

    1,指令码:

"mov %[result], %[value], ror #1"
    2,可选的输出数列表(多个输出数用逗号分隔)。每个输出数的符号名用方括号包围,后面跟一个约束串,然后再加上一个括号包围的C表达式。

[result] "=r" (y) /*result:符号名   "=r":约束串*    (y):C表达式/

    3,可选的输入操作数列表,语法规则和上面的输出操作数相同。我们的例子中就一个输入操作数:

[value] "r" (x)

    实例分析:    

    先写段小程序:

/*
 *  arm inline asm cookbook
 *  Demo Program
 *  Created on: 2014-6
 *  Author: Chris.Z
 */
#include <stdio.h>
#include <stdlib.h>

/**
 * x ROR 1bit to y
 * return y if SUCCESS
 */
int  value_convert(int x)
{
    int y;
    asm volatile
    (
        "mov %[result], %[value], ror #1"
        : [result] "=r" (y)
        : [value] "r" (x)
        :
    );
    return y;
}

int main()
{
    printf("GCC ARM Inline Assembler CookBook Demo!\n");
    int x = 4;
    printf("call func value_convert with input x:%d,output y:%d\n",x,value_convert(x));
    return 0;
}
程序编译运行后的输出:



这段程序的作用是将变量x,循环右移1位(相当于除以2),结果保存到变量y。我们看看IDA生成的convert_value的汇编: 

.text:00008334 ; =============== S U B R O U T I N E =======================================

.text:00008334
.text:00008334 ; Attributes: bp-based frame
.text:00008334
.text:00008334                 EXPORT value_convert
.text:00008334 value_convert                           ; CODE XREF: main+28p
.text:00008334
.text:00008334 var_10          = -0x10
.text:00008334 var_8           = -8
.text:00008334
.text:00008334                 STMFD   SP!, {R4,R11}
.text:00008338                 ADD     R11, SP, #4
.text:0000833C                 SUB     SP, SP, #0x10
.text:00008340                 STR     R0, [R11,#var_10]
.text:00008344                 LDR     R3, [R11,#var_10]
.text:00008348                 MOV     R4, R3,ROR#1    ;编译器将我们的内联汇编直接“搬”了过来,同时使用了R3保存x,R4保存结果y
.text:0000834C                 STR     R4, [R11,#var_8]
.text:00008350                 LDR     R3, [R11,#var_8]
.text:00008354                 MOV     R0, R3
.text:00008358                 SUB     SP, R11, #4
.text:0000835C                 LDMFD   SP!, {R4,R11}
.text:00008360                 BX      LR
.text:00008360 ; End of function value_convert
.text:00008360
.text:00008364
.text:00008364 ; =============== S U B R O U T I N E =======================================

上面的汇编代码我不会一行行说明,重点关注下红色标注部分。可以看出,编译器汇编我们的内联汇编时,指定R3为输入寄存器,R4为输出寄存器(不同的编译器可能会选择有所不同),同时将R4、R11入堆栈。


    4,被覆盖(破坏)寄存器列表,我们的例子中没有使用。

     正如我们第一个例子看到的NOP一样,内联汇编的后面3个部分可以省略,这叫做“基础内联汇编”,反之,则称为“扩展内联汇编”。扩展内联汇编中,如果某个部分为空,则同样需要用冒号分隔,如下例,设置当前程序状态寄存器(CSPR),该指令有一个输入数,没有输出数:

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和value),自定义的符号引用输入或输出操作数列表中的对应符号(同名),如上例中:

%[result]    引用输出操作数,C变量y

%[value]    引用输入操作数,C变量x

    这里的符号名采用了独立的命名空间,也就是说和其他符号表无关,你可以选一个易记的符号(即使C代码中用同名也不影响)。但是,在同一个内联汇编代码段中,必须保持符号名唯一性。

如果你曾经阅读过一些其他程序员写的内联汇编代码,你可能发现和我这里的语法有些不同。实际上,GCC从3.1版开始支持上述的新语法。而在此之前,一直是如下的语法:

asm("mov %0, %1, ror #1" : "=r" (result) : "r" (value));
 
    操作数用一个带百分号的数字来表示,上述0%和1%分别表示第一个、第二个操作数。GCC的最新版本仍然支持上述语法,但明显,上述语法更容易出错,且难以维护:假设你写一个较长的内联汇编,然后需要在某个位置插入一个新的输出操作数,此时,之后的操作数都需要重新编号。


    到此,你可能会觉得内联汇编语法有些晦涩难懂,请不要担心,下面我们详细来说明。除了上面所提的神秘的“覆盖、破坏”操作数列表外,你可能会觉得还有些地方没搞清楚,是么?实际上,比如我们并没有真正解释“约束串”的含义。我希望你可以通过自己的实践来加深理解。下面,我会讲一些更深入的东西。


    C代码优化过程

    选择内联汇编的两个原因:

    第一,如果我们需要操作一些底层硬件的时候,C很多时候无能为力。如没有一条C函数可以操作CSPR寄存器(译者注:实际上Linux C提供了一个函数调用:ptrace。可以用来操作寄存器,大名鼎鼎的GDB就是基于此调用)。

    第二,内联汇编可以构造高度优化的代码。事实上,GNU 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

    该编译器选择了r2、r4寄存器来分别表示y和x变量。到这里你发现什么没有?

    不同的C优化器,可能会优化出“不同的结果”!在有些情况,这些优化可能会适得其反,比如你的内联汇编可能会被“忽略”掉。这点依赖于编译器的优化策略,以及你的代码的上下文。例如:如果在程序的剩余部分,从未使用前面的内联汇编输出操作数,那么优化器很有可能会移除你的汇编。再如我们上面的NOP操作,优化器可能会认为这会降低程序性能的无用操作,而将其“忽略”!

针对这一问题的解决方法是增加volatile属性,这一属性告诉编译器不要对本代码段进行优化。针对上面的NOP汇编代码,修订如下:

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

    除了上面的情况外,还有种更复杂的情况:优化器可能会重新组织我们的代码!如:

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

    对于上面的代码,优化器会认定两个i++操作,对于if条件没有任何影响,此外,优化器会选择用i+2这一条指令来替换两个i++。因此,代码会被重新组织为:

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

    

    这样的结果是:无法保证编译的代码和原始代码保持一致性!

    这点可能会对你的编码造成巨大影响。如下面的代码段:

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");

    上面的代码c *=b,c或者b这两个变量可能会在执行过程中由于中断发生修改,因此在代码的上下各增加一段内联汇编:在乘法操作前禁止中断,乘法完成后再继续允许中断。

    译者注:上面的mrs和msr分别是对于程序状态寄存器(CPSR(SPSR))操作指令,我们看看CPSR的位分布图:


上面的两段内联汇编实际上就是首先将CPSR的bit0-bit7即CPRS_c寄存器的bit6和bit7置为1,也就是禁止FIQ和IRQ,c *=b结束后,再将bit6和bit7清零,即允许FIQ和IRQ。


    然后,不幸的是,优化器可能会选择首先c*=b,然后再执行两段汇编,或者反过来!这就会让我们的汇编代码不起作用!

    针对这个问题,我们可以通过clobber操作数列表来解决!针对上例的clobber列表:

"r12", "cc"

    通过这个clobber,通知编译器,我们的汇编代码段修改了r12,并且修改了CSPR_c。此外,如果我们在内联汇编中使用硬编码的寄存器(如r12),会干扰优化器产生最优的代码优化结果。一般情况下,你可以通过传变量的方式,来让编译器决定选择哪个寄存器。上例中的cc表示条件寄存器。此外,memory表示内存被修改,这会让编译器在执行内联汇编段之前存储所有需缓存的值到内存,在执行汇编代码段之后,从内存再重新reload。加了clobber后,编译器必须保持代码顺序,因为在执行完一个带有clobber的带代码段后,所操作的变量的内容是不可预料的!

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");

    上面的方式不是最好的方式,你还可以通过添加一个“伪操作数”来实现一个“人造的依赖”!

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("=X"(b)),第二个代码段冒充把c变量作为输入操作数(“X”(c))。通过这种方法,可以在不刷新缓存值的情况来维护我们正确的代码顺序。

实际上,理解优化器是如何影响内联汇编的编译过程是十分重要的。如果有时候,编译后的程序执行结果有些让你云里雾里,那么在你看下一章节之前,好好看看这一部分的内容十分必要!

    译者注:这段内容的翻译比较费劲,也比较难以理解,实际上可以总结为:由于C优化器的特性,我们在嵌入内联汇编的时候,一定要十分注意,往往编译的结果会和我们预想的结果不同,常见的一种就是上面所说的,优化器可能会改变原始的代码顺序,针对这种情况,上文也提供了一种聪明的解决方法:伪造操作数!

    

     

    实例分析:    

    关于内联汇编的clobber操作数,相信和大家一样,译者刚理解起来也是云山雾罩,我们不妨还是用一个小程序来加深我们的理解。这里我们将上一个小程序稍微做些修改如下:

/*
 *  arm inline asm cookbook
 *  Demo Program
 *  Created on: 2014-6
 *  Author: Chris.Z
 */
#include <stdio.h>
#include <stdlib.h>

int g_clobbered = 0;<span style="font-family: Arial, Helvetica, sans-serif;">/*新增加*/</span>
/**
 * x ROR 1bit to y
 * return y if SUCCESS
 */
int  value_convert(int x)
{
    int y;
    asm volatile
    (
        "mov %[result], %[value], ror #1\n\t"
        "mov r7, %[result]\n\t"  /*新增加*/
        "mov %[r_clobberd], r7"  <span style="font-family: Arial, Helvetica, sans-serif;">/*新增加*/</span>
        : [result] "=r" (y),[r_clobberd] "=r" (g_clobbered)
        : [value] "r" (x)
        : "r7"  <span style="font-family: Arial, Helvetica, sans-serif;">/*新增加*/</span>
    );

    return y;
}

int main()
{
    printf("GCC ARM Inline Assembler CookBook Demo!\n");
    int x = 4;
    printf("call func value_convert with input x:%d,output y:%d,and g_clobbered:%d\n",x,value_convert(x),g_clobbered);
    return 0;
}

我们新增加了一个全局变量g_clobbered(主要是为了演示),重点是在上面的clobberlist新增加了一个r7,首先,我们查看编译后的汇编:

.text:00008334 ; =============== S U B R O U T I N E =======================================
.text:00008334
.text:00008334 ; Attributes: bp-based frame
.text:00008334
.text:00008334                 EXPORT value_convert
.text:00008334 value_convert                           ; CODE XREF: main+30p
.text:00008334
.text:00008334 var_18          = -0x18
.text:00008334 var_10          = -0x10
.text:00008334
.text:00008334                 STMFD   SP!, {R4,R7,R11}
.text:00008338                 ADD     R11, SP, #8
.text:0000833C                 SUB     SP, SP, #0x14
.text:00008340                 STR     R0, [R11,#var_18]
.text:00008344                 LDR     R3, =_GLOBAL_OFFSET_TABLE_ ; PIC mode
.text:00008348                 NOP
.text:0000834C                 LDR     R2, [R11,#var_18]
.text:00008350                 MOV     R4, R2,ROR#1
.text:00008354                 MOV     R7, R4
.text:00008358                 MOV     R2, R7
.text:0000835C                 STR     R4, [R11,#var_10]
.text:00008360                 LDR     R1, =(g_clobbered_ptr - 0x9FE4)
.text:00008364                 LDR     R3, [R3,R1]
.text:00008368                 STR     R2, [R3]
.text:0000836C                 LDR     R3, [R11,#var_10]
.text:00008370                 MOV     R0, R3
.text:00008374                 SUB     SP, R11, #8
.text:00008378                 LDMFD   SP!, {R4,R7,R11}
.text:0000837C                 BX      LR
.text:0000837C ; End of function value_convert
.text:0000837C
.text:0000837C ; ---------------------------------------------------------------------------

  然后我们把r7从clobberlist去掉,再看看生成后的汇编输出:

.text:00008334 ; =============== S U B R O U T I N E =======================================
.text:00008334
.text:00008334 ; Attributes: bp-based frame
.text:00008334
.text:00008334                 EXPORT value_convert
.text:00008334 value_convert                           ; CODE XREF: main+30p
.text:00008334
.text:00008334 var_10          = -0x10
.text:00008334 var_8           = -8
.text:00008334
.text:00008334                 STMFD   SP!, {R4,R11}
.text:00008338                 ADD     R11, SP, #4
.text:0000833C                 SUB     SP, SP, #0x10
.text:00008340                 STR     R0, [R11,#var_10]
.text:00008344                 LDR     R3, =_GLOBAL_OFFSET_TABLE_ ; PIC mode
.text:00008348                 NOP
.text:0000834C                 LDR     R2, [R11,#var_10]
.text:00008350                 MOV     R4, R2,ROR#1
.text:00008354                 MOV     R7, R4
.text:00008358                 MOV     R2, R7
.text:0000835C                 STR     R4, [R11,#var_8]
.text:00008360                 LDR     R1, =(g_clobbered_ptr - 0x9FE4)
.text:00008364                 LDR     R3, [R3,R1]
.text:00008368                 STR     R2, [R3]
.text:0000836C                 LDR     R3, [R11,#var_8]
.text:00008370                 MOV     R0, R3
.text:00008374                 SUB     SP, R11, #4
.text:00008378                 LDMFD   SP!, {R4,R11}
.text:0000837C                 BX      LR
.text:0000837C ; End of function value_convert
.text:0000837C
.text:0000837C ; ---------------------------------------------------------------------------

相信到这里,我们应该基本理解了所谓的“clobber list”的作用:

通过在clobber list添加被破坏的寄存器(这里是r7)或者是内存(符号是:memory),通知编译器,在我们的内联汇编段中,我们修改了某个特定的寄存器或者内存区域。编译器会将被破坏的寄存器先保存到堆栈,执行完内联汇编后再出栈,也就是保护寄存器原始的值!对于内存,则是在执行完内联汇编后,重新刷新已用的内存缓存值。

     输入和输出操作数

     前面的文章里,我们提到对于每个输入或输出操作数,我们可以用一个方括号包围的符号名来表示,后面需要加上一个带有c表达式的约束串。

    那么,什么是约束串?为什么我们需要使用约束串?我们知道,不同类型的汇编指令需要不同类型的操作数。如,跳转指令branch(b指令)的操作数是一个跳转的目标地址。但是,并不是每个合法的内存地址都是可以作为b指令的立即数,实际上b指令的立即数为24位偏移量

译者注:这里作者是以32位ARM指令集的Branch指令为例的,如果是Thumb,情况有所不同。下图是ARM7TDMI指令集的Branch指令编码图:


可以看出,bit0-bit23表示Branch指令的目标偏移值。

在实际编码中,b指令的操作数往往是一个包含了32位数值目标地址的寄存器。在上述的两种类型操作数中,传输给内联汇编的操作数可能会是同一个C函数指针,因此在我们传输常量或者变量给内联汇编的时候,内联汇编器必须要知道如何处理我们的参数输入。

    对于ARM处理器,GCC4提供了如下的约束类型:

ConstraintUsage in ARM stateUsage in Thumb state
fFloating point registers f0 .. f7  浮点寄存器Not available
hNot availableRegisters r8..r15
GImmediate floating point constant 浮点型立即数常量Not available
HSame a G, but negatedNot available
IImmediate value in data processing instructions
e.g. ORR R0, R0, #operand 立即数
Constant in the range 0 .. 255
e.g. SWI operand
JIndexing constants -4095 .. 4095
e.g. LDR R1, [PC, #operand] 偏移常量
Constant in the range -255 .. -1
e.g. SUB R0, R0, #operand
KSame as I, but invertedSame as I, but shifted
LSame as I, but negatedConstant in the range -7 .. 7
e.g. SUB R0, R1, #operand
lSame as rRegisters r0..r7
e.g. PUSH operand
MConstant 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
mAny valid memory address 内存地址
NNot availableConstant in the range of 0 .. 31
e.g. LSL R0, R1, #operand
ONot availableConstant that is a multiple of 4 in the range of -508 .. 508
e.g. ADD SP, #operand
rGeneral register r0 .. r15
e.g. SUB operand1, operand2, operand3 寄存器r0-r15
Not available
wVector floating point registers s0 .. s31Not available
XAny operand

    上面的约束字符前面可以增加一个约束修改符(如无约束修改符,则该操作数只读)。有如下预定义的修改符:

ModifierSpecifies
=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 只用作输出

    对于输出操作数,它必须是只写的,且对应C表达式的左值。C编译器可以检查这个约束。而对于输入操作数,是只读的。

注意:C编译器无法检查内联汇编指令中的操作数是否合法。大部分的合法性错误可以再汇编阶段检查到,汇编器会提示一些奇异的错误信息。比如汇编器报错提升你遇到了一个内部编译器错误,此时,你最好先仔细检查下你的代码。

    首先一条约定是:从来不要试图回写输入操作数!但是,如果你需要输入和输出使用同一个操作数怎么办?此时,你可以用上面的约束修改符“+”:

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

这和我们上面的位循环的例子很类似。该指令右循环value 1位(译者注:相当于value除以2)。和前例不同的是,移位结果也保存到了同一个变量value中。注意,最新版本的GCC可能不再支持“+”符号,此时,我们还有另外一个解决方案:

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

    大部分情况下,上面的汇编输出不会产生什么问题。但如果上述的输入操作数寄存器在使用之前,输出操作数寄存器就被修改的话,会产生致命错误!对于这种情况,我们可以使用上面的"&"约束修改符:

asm volatile("ldr %0, [%1]"     "\n\t"
             "str %2, [%1, #4]" "\n\t"
             : "=&r" (rdv)
             : "r" (&table), "r" (wdv)
             : "memory");

    上述代码中,一个值从内存表读到寄存器,同时将另外一个值从寄存器写到内存表的另外一个位置。上面的代码中,如果编译器为输入和输出操作数选择同一个寄存器的话,那么输出值在第一条指令中就已经被修改。幸运的是,"&"符号告诉编译器为输出操作数另外选择一个不同于输入操作数的寄存器。


    更多的食谱

    内联汇编预处理宏

    通过定义预处理宏,我们可以实现内联汇编段的复用。如果我们直接按照上述的语法来定义宏并引用的话,在强ANSI编译模式下,会产生很多的编译告警。通过__asm__ __volatie__定义可以避免上述的告警。下面的代码宏,将一个long型的值从小端转为大端(或反之)。

#define BYTESWAP(val)     __asm__ __volatile__ (         "eor     r3, %1, %1, ror #16\n\t"         "bic     r3, r3, #0x00FF0000\n\t"         "mov     %0, %1, ror #8\n\t"         "eor     %0, %0, r3, lsr #8"         : "=r" (val)         : "0"(val)         : "r3", "cc"     );

    译者注:这里的大端(Big-Endian)和小端(Little-Endian)是指字节存储顺序。大端:高位在前,低位在后,小端正好相反。

如:我们将0x1234abcd写入到以0x0000开始的内存中,则结果为:
               big-endian    little-endian
0x0000        0x12               0xcd
0x0001        0x34               0xab
0x0002        0xab               0x34
0x0003        0xcd               0x12

    C存根函数

    内联汇编宏定义在编译的时候,只是用预定义的代码直接替换。当我们要定义一个很长的代码段时候,这种方式会造成代码尺寸的大幅度增加,这时候,可以定义一个C存根函数。上面的预定义宏我们可以重定义如下:

unsigned long ByteSwap(unsigned long val)
{
asm volatile (
        "eor     r3, %1, %1, ror #16\n\t"
        "bic     r3, r3, #0x00FF0000\n\t"
        "mov     %0, %1, ror #8\n\t"
        "eor     %0, %0, r3, lsr #8"
        : "=r" (val)
        : "0"(val)
        : "r3"
);
return val;
}

    重命名C变量

    默认情况下,GCC在C和汇编中使用一致的函数名或变量名符号。使用下面的汇编声明,我们可以为汇编代码定义一个不同的符号名。

unsigned long value asm("clock") = 3686400;

    上面的声明,将long型的value声明为clock,在汇编中可以用clock这个符号来引用value。当然,这种声明方式只适用于全局变量。本地变量(自动变量)不适用。

    重命名C函数

    要重命名一个C函数,首先需要一个函数原型声明(因为C不支持asm关键字来定义函数):

extern long Calc(void) asm ("CALCULATE");
    上述代码中,如果我们调用函数Cal,将会生成调用CALCULATE函数的指令。

    强制指定寄存器

    寄存器可以用来存储本地变量。你可以指定内联汇编器使用一个特定的寄存器来存储变量。

void Count(void) {
register unsigned char counter asm("r3");

... some code...
asm volatile("eor r3, r3, r3" : "=l" (counter));
... more code...
}

上面的指令(译者注:eor为逻辑异或指令)清零r3寄存器,也就是清零counter变量。在大部分情况下,上面的代码是劣质代码,因为会干扰优化器工作。此外,优化器在有些情况下也并不会因为“强制指定”而预先保留好r3寄存器,例如,优化器发现counter变量在后续的代码中并没有引用的情况下,r3可能会被再次用作其他地方。同时,在预指定寄存器的情况下,编译器是无法判断寄存器使用冲突的。如果你预指定了太多的寄存器,在代码生成阶段,编译器可能会用完所有的寄存器,从而导致错误!

    零时寄存器

    有时候,你需要临时使用一个寄存器,你需要告诉编译器你临时使用了某寄存器。下面的代码实现将一个数调整为4的倍数。代码使用了r3临时寄存器,同时在clobber列表指定r3。另外,ands指令修改了状态寄存器,因此指定了cc标志。                                                                                                                                                                                                    

asm volatile(
    "ands    r3, %1, #3"     "\n\t"
    "eor     %0, %0, r3" "\n\t"
    "addne   %0, #4"
    : "=r" (len)
    : "0" (len)
    : "cc", "r3"
  );

    需要说明的是,上面的硬编码使用寄存器的方式不是一个良好的编码习惯。更好的方法是实现一个C存根函数,用本地变量来存储临时值。                                                                    

    常量

    MOV指令可以用来将一个立即数赋值到寄存器,立即数范围为0-255(译者注:和上面的Branch指令类似,由于指令位数的限制) 

 

asm("mov r0, %[flag]" : : [flag] "I" (0x80));

    更大的值可以通过循环右移位来实现(偶数位),也就是n * 2X  

其中0<=n<=255,x为0到24范围内的偶数。   由于是循环移位,x可以设置为26\28\32,此时,位32-37折叠到位5-0。 当然,也可以使用MVN(取反传送指令)。  

译者注:这段译者没理解原文作者的意思,一般意义上,32位ARM指令的合法立即数生成规则为:<immediate>=immed_8 循环右移(2×rotate_imm),其中immed_8 表示8位立即数,rotate_imm表示4位的移位值,即用12位表示32位的立即数。  

指令位图如下:

  

    有时候,你可能需要跳转到一个固定的内存地址,该地址由一个预定义的标号来表示:

ldr  r3, =JMPADDR
bx   r3

JMPADDR可以取到任何合法的地址值。 如果立即数为合法立即数,那么上面的指令会被转换为:

mov  r3, #0x20000000
bx   r3
译者注:0x20000000,可以由0x02循环右移0x4位获得。

如果立即数不合法(比如立即数0x00F000F0),那么立即数会从文字池中读取到寄存器,上面的代码会被转换为:

ldr  r3, .L1
bx   r3
...
.L1: .word 0x00F000F0

上面描述的规则同样适用于内联汇编,上面的代码在内联汇编中可以表示如下:

asm volatile("bx %0" : : "r" (JMPADDR));
编译器会根据JMPADDR的实际值,来选择翻译成MOV、LDR或者其他方式来加载立即数。比如,JMPARDDR=0xFFFFFF00,那么上面的内联汇编会被转换为:

 mvn  r3, #0xFF
 bx   r3

    现实世界往往会比理论情况更复杂。假设,我们需要调用一个main子程序,但是希望在子程序返回的时候直接跳转到JMPADDR,而不是返回到bx指令的下一条指令。这在嵌入式开发中很常见。此时,我们需要使用lr寄存器(译者注:lr为链接寄存器,保存子程序调用时的返回地址):
 ldr  lr, =JMPADDR
 ldr  r3, main
 bx   r3

    我们看看上面的这段汇编,用内联汇编如何实现:

asm volatile(
 "mov lr, %1\n\t"
 "bx %0\n\t"
 : : "r" (main), "I" (JMPADDR));

    有个问题,如果JMPADDR是合法立即数,那么上面的内联汇编会被解释成和之前纯汇编一样的代码,但如果不是合法立即数,我们可以使用LDR吗?答案是NO。内联汇编中不能使用如下的LDR伪指令:

ldr  lr, =JMPADDR

内联汇编中,我们必须这么写:

asm volatile(
    "mov lr, %1\n\t"
    "bx %0\n\t"
    : : "r" (main), "r" (JMPADDR));

上面的代码会被编译器解释为:

  ldr     r3, .L1
  ldr     r2, .L2
  mov     lr, r2
  bx      r3

    寄存器用法

    通过分析C编译器的汇编输出来加深我们对于内联汇编的理解,始终是一个好办法。下面的表格中,给出了C编译器对于寄存器的一般使用规则:

RegisterAlt. NameUsage
r0a1First function argument
Integer function result
Scratch register
r1a2Second function argument
Scratch register
r2a3Third function argument
Scratch register
r3a4Fourth function argument
Scratch register
r4v1Register variable
r5v2Register variable
r6v3Register variable
r7v4Register variable
r8v5Register variable
r9v6
rfp
Register variable
Real frame pointer
r10slStack limit
r11fpArgument pointer
r12ipTemporary workspace
r13spStack pointer
r14lrLink register
Workspace
r15pcProgram counter

    内联汇编中的常见“陷阱”

    指令序列

    一般情况下,程序员总是认定最终生存的代码中的指令序列和源码的序列是一致的。但实际上,事实并非如此。在允许情况下,C优化器会像处理C源码一样的方式来优化处理内联汇编,包括重新排序等优化。前面的“C代码优化”一节,我们已经说明了这点。

    本地变量绑定到寄存器

    即使我们硬编码,指定一个变量绑定到某个寄存器,在实际的生成代码中,往往和我们的预期有出入:

int foo(int n1, int n2) {
  register int n3 asm("r7") = n2;
  asm("mov r7, #4");
  return n3;
}

    上述代码中,指定r7寄存器来保持本地变量n3,同时初始化为值n2,然后将r7赋值为常数4,最后返回n3。经过编译后,输出的最终代码可能会让你大跌眼镜,因为编译器对于内联汇编段内的代码是“不敏感”的,但对于C代码却会“自主”的进行优化:

foo:
  mov r7, #4
  mov r0, r1
  bx  lr

    实际上返回的是n2,而不是返回r7。(译者注:按照ATPCS规则,foo的参数传递规则为n1通过r0传递,n2通过r1传递,返回值保存到r0

到底发生了什么?我们可以看到最终的代码确实包含了我们内联汇编中的mov指令,但是C代码优化器可能会认为n3在后续没有使用,因此决定直接返回n2参数。

    可以看出,即使我们绑定一个变量到寄存器,C编译器也不一定会使用那个变量。这种情况下,我们需要告诉编译器我们在内联汇编中修改了变量:

asm("mov %0, #4" : "=l" (n3));

通过增加一个输出操作数,C编译器知道,n3已经被修改。看看输出的最终代码:

foo:
  push {r7, lr}
  mov  r7, #4
  mov  r0, r7
  pop  {r7, pc}

    Thumb下的汇编

    注意,编译器依赖于不同的编译选项,可能会转换到Thumb状态,在Thumb状态下,内联汇编是不可用的!


    汇编代码大小

    大部分情况下,编译器能正确的确定汇编指令代码大小,但是如果我们使用了预定义的内联汇编宏,可能就会产生问题。因此,在内联汇编预定义宏和C预处理宏之间,我们最好选择后者。


    标签

    内联汇编可以使用标签作为跳转目标,但是要注意,目标标签不是只包含一条汇编指令,优化器可能会产生错误的结果输出。

    

    预处理宏

    在内联汇编中,不能包含预处理宏,因为对于内联汇编来说,这些宏只是一些字符串,不会解释。


    外链

    要了解更详细的内联汇编知识,可以参考GCC用户手册。最新版的手册链接:

    http://gcc.gnu.org/onlinedocs/


    版权

    

Copyright (C) 2007-2013 by Harald Kipp.
Permission is granted to copy, distribute and/or modify this document
under the terms of the GNU Free Documentation License, Version 1.3
or any later version published by the Free Software Foundation.

    文档历史

  

Date (YMD)ChangeThanks to
2014/02/11Fixed the first constant example, where the constant must be an input operand.spider391Tang
2013/08/16Corrected the example code of specific register usage and added a new pitfall section about the same topic.Sven K?hler
2012/03/28Corrected the pitfall section about constant parameters and moved to the usage section.enh
Added a preprocessor macros pitfall. 
Added this history. 

转载请注明出处:生活秀                Enjoy IT!微笑