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提供瞭如下的約束類型:

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不支持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編譯器對於寄存器的一般使用規則:

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!微笑


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章