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的最新版本号仍然支持上述语法。但明显,上述语法更easy出错,且难以维护:如果你写一个较长的内联汇编。然后须要在某个位置插入一个新的输出操作数,此时。之后的操作数都须要又一次编号。


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


    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提供了例如以下的约束类型:

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 寄存器r0-r15
Not available
w Vector floating point registers s0 .. s31 Not available
X Any 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 仅仅用作输出

    对于输出操作数,它必须是仅仅写的,且相应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不支持asmkeyword来定义函数):

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编译器对于寄存器的一般使用规则:

Register Alt. Name Usage
r0 a1 First function argument
Integer function result
Scratch register
r1 a2 Second function argument
Scratch register
r2 a3 Third function argument
Scratch register
r3 a4 Fourth function argument
Scratch register
r4 v1 Register variable
r5 v2 Register variable
r6 v3 Register variable
r7 v4 Register variable
r8 v5 Register variable
r9 v6
rfp
Register variable
Real frame pointer
r10 sl Stack limit
r11 fp Argument pointer
r12 ip Temporary workspace
r13 sp Stack pointer
r14 lr Link register
Workspace
r15 pc Program 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) Change Thanks to
2014/02/11 Fixed the first constant example, where the constant must be an input operand. spider391Tang
2013/08/16 Corrected the example code of specific register usage and added a new pitfall section about the same topic. Sven Köhler
2012/03/28 Corrected the pitfall section about constant parameters and moved to the usage section. enh
Added a preprocessor macros pitfall.  
Added this history.  

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


posted @ 2017-04-22 17:18  jzdwajue  阅读(222)  评论(0编辑  收藏  举报