1. 程式人生 > >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提供瞭如下的約束型別:

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使用者手冊。最新版的手冊連結:

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