GCC編譯器支持直接在C或者C++代碼中,嵌入ARM匯編代碼。其基本格式非常簡單,大致如下:
__asm__ [__volatile__] ( assembler template : [output operand list] /* optional */ : [input operand list] /* optional */ : [clobbered register list] /* optional */ );
首先是關鍵字“__asm__”,其實也可以寫成“asm”。但是“asm”並不是所有版本的GCC編譯器都支持的,而且有可能和程序中別的地方定義的變量或函數名沖突,所以用“__asm__”的話,兼容性會好一點。
下面是“__volatile__”關鍵字,這個是可選的,其作用是禁止編譯器對後面編寫的匯編指令再進行優化。一般情況下,自己寫的匯編代碼肯定是自己進行設計優化過了的,如果編譯器再進行優化的話,很有可能效果還不如不優化,而且也有可能會出現奇怪的錯誤,所以通常都會帶上這個關鍵字。同樣,“__volatile__”也可以寫成“volatile”,但可能兼容性會沒那麼好。
下面,在括號裡面的,就是真正的匯編代碼了,其主要有四部分組成。第一個是具體的匯編代碼,這是必需的。而後面三個是一些輔助參數,這些參數是可選的。
各個部分間使用冒號“:”進行分割。如果前面的部分沒有使用,而後面的部分使用了,則前面的部分也需要用冒號留空。例如:
__asm__ __volatile__ ("msr cpsr, %0" : : "r" (status));
可以看出,本例中沒有第二部分(輸出參數列表),只有第三部分(輸入參數列表),但它們中間任然要留出冒號進行分割。同時,也沒有第四部分,但並不需要在第三部分後面加上冒號。
下面一一解釋各個部分的作用:
1)匯編代碼模板
所有的匯編代碼必須用雙引號括起來。如果有多行匯編代碼的話,每一條語句都要用雙引號括起來,並且在代碼後面要加上換行符(“\n”或者“\n\t”)。這樣做是因為GCC會將匯編代碼部分作為字符串形式直接傳給匯編器,加上換行符後,匯編器就能准確知道哪些字符串表示的是一條匯編語句。同時,為了增加可讀性,每條匯編語句都可以換行。
其具體形式如下:
__asm__ __volatile__ ( "instruction 1\n\t" "instruction 2\n\t" ...... "last instruction" );
因為匯編代碼部分是必需的,所以即使一行匯編代碼也沒有,也需要傳入空字符串(""),否則會報錯。
2)輸出操作數列表和輸入操作數列表
前面介紹了,第二部分和第三部分分別表示輸出操作數列表和輸入操作數列表。
輸入操作數表示要作為匯編代碼輸入的C表達式,而輸出操作數剛好相反,表示匯編代碼處理完後要輸出結果的C表達式。如果有多個輸出或輸入表達式,需要用逗號(“,”)將它們分隔開來。
可以再前面的匯編代碼模板中直接應用定義的輸出操作數和輸入操作數,其用法是使用百分號(“%”)後面接一個數字,0表示定義的第一個操作數,1表示定義的第二個操作數,依次類推。下面舉個例子:
__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的還是Thumb的,對限定符的定義也會不同:
看起來很復雜,但是常用的也就是r,f和m等幾個。
好,說完了限定符,下面來看看修改符。修改符是加在限定符之前的,並且是可選的,如果沒有修改符的話,則表明這個操作數是只讀的。這個對輸入操作數沒有問題,但是對輸出操作數來說,肯定是需要被修改的,那怎麼辦呢?答案就是使用修改符,修改這個操作數的屬性。目前,GCC中定義了三個修改符,分別是:
所以,作為輸出操作數,只需要在限定符前加上“=”就可以了。
在匯編代碼中,請絕對不要修改輸入操作數的值。
如果想讓一個C變量既作為輸入操作數,也作為輸出操作數的話,可以使用“+”限定符,並且這個操作數只需要在輸出操作數列表中列出就行了。例如:
__asm__("mov %0, %0, ror #1" : "+r" (y) );
上面的例子是將變量y中的值又移1位。因為輸入和輸出操作數是一個,所以該操作數要既可讀也可寫,因此添加了“+”修改符。
但是GCC的老版本不一定支持“+”修改符,如果也想達到前面的這種輸入和輸出操作數是一個的目的,可以用一種變通的做法,並且這種變通的做法GCC的新版本也是支持的。其實,在限定符中,也可以使用數字,其作用是指代前面定義的操作數,0代表第一個,1代表第二個,以此類推。例如:
__asm__("mov %0, %0, ror #1" : "=r" (y) : "0" (y) );
如果GCC編譯器支持的話,這個例子的效果和前面的例子是相同的。本例不同的是,先定義了一個可寫的輸出變量,同時在輸入變量列表中,明確用數字0指出了前面定義的第一個操作數同時也要用來作為輸入操作數。
好了,已經介紹了兩個修改符的用處了,還剩下最後一個“&”。前面的例子是明確要求輸出操作數和輸入操作數使用同一個寄存器,但有時候剛好相反,輸出操作數使用的寄存器一定不能和輸入操作數使用的寄存器一樣。但是,由於編譯器的優化,是完全有可能出現同一個寄存器既用作輸入操作數也用作輸出操作數的情況的。這時,可以在輸出操作數中使用“&”修改符,明確告訴編譯器,代表輸出操作數的寄存器一定不能使用輸入操作數已經使用過的寄存器。下面舉個例子:
__asm__ __volatile__("ldr %0, [%1]\n\t" "str %2, [%1, #4]" : "=&r" (rdv) : "r" (&table), "r" (wdv) : "memory"
本例中,將操作一個table數組,讀出它的第一個數存放到rdv中,然後修改第二個數為wdv中存放的值。乍看一下沒什麼問題,但是如果編譯器用同一個寄存器來表示輸入操作數&table(%1)和輸出操作數rdv(%0)怎麼辦呢?執行完第一條語句之後,table數組的地址就被修改掉了。所以,可以在輸出操作數中加上一個“&”修改符,強制保證輸出操作數不能和輸入操作數復用同一個寄存器,這個問題就解決了。如果匯編代碼中有輸入寄存器還沒有使用完畢,就對輸出操作數進行修改的情況,則特別需要用“&”修改符,保證不復用。
3)修改寄存器列表
在匯編指令中,有可能會用到一些指定的寄存器,但是在執行你定義的匯編程序時,那個指定的寄存器有可能另有別的用途,存放了非常重要的數據。等你的程序執行完成後,那個寄存器的值已經被你修改過了,肯定會造成執行錯誤。因此,在執行你的程序之前必須要做必要的備份和恢復的動作。但是,編譯器並不會分析你的匯編代碼,找出這種被你修改過,需要恢復的寄存器,因此你必須顯式的告訴編譯器,被你修改過的寄存器有哪些。這就是修改寄存器列表所起到的作用。
對於嵌入內聯ARM匯編來說,此列表中的值有下面三種類型:
對於“memory”來說,它並不是表示寄存器被讀取或修改了,而是表示內存中的值被修改了。出於優化的目的,在執行你的匯編代碼之前,編譯器將某些變量的值還保存在寄存器中,並沒有被寫到實際的內存中。但是,如果你的匯編代碼會讀取內存中的值,則很有可能新的值還在寄存器中,而內存中存放的還是老的值,這樣就會造成錯誤。添加了“memory”之後,編譯器會在執行你的代碼之前,保證將保存在寄存器中,沒有更新到內存中的值全部都寫入到內存中。
此列表中的每一項都要用雙引號("")括起來,每項之間要用逗號(“,”)分割。