gcc內嵌彙編介紹

閱讀AndroidLinux的源碼時,有時會遇到使用內嵌彙編的代碼。閱讀內嵌彙編代碼不是一件特別容易的事,如果只瞭解普通彙編語言,沒學習過內嵌彙編,從語言上大概能明白內嵌彙編代碼的作用,但是要精確的瞭解每行代碼,每個寄存器的含義就不太可能了。內嵌彙編其實並不複雜,只不過gcc的內嵌彙編必須是AT&T格式,而且有自己一套獨特的標記,加上資料很少,有限的資料寫的又很晦澀,所以學習起來比較困難。本文爭取用簡短的篇幅介紹明白內嵌彙編的規則和作用。如果對AT&T彙編格式還不瞭解,請先參考前一篇博文:AT&T彙編格式介紹。

 

一、簡單內嵌彙編格式

內嵌彙編是用來和C語言混合編程的,所以要有方法把它的語句和C語句隔開,下面看一個簡單的例子:

__asm__ __volatile__ ("movl $5, % eax");

內嵌彙編語句必須放在__asm__開頭,並且加上括號和引號。__volatile__修飾符不是必須的,它的作用是告訴編譯器不要調整我們寫的彙編語句(通常編譯器會因爲優化的原因這麼做)。

也可以把多條彙編語句放在一個__asm__中,例如:

__asm__ __volatile__ ("movl $5, % eax; movl $5, % ebx ");

彙編語句之間要使用分號‘;’隔開。除了用分號‘;’作爲語句間的分割符,還可以用"\n"來作爲分割符。本人也試過“\n\r,或者"\n\t"也能編譯通過。如果看到有些內嵌彙編語句使用它們作爲分隔符,不要奇怪。

其實,最簡單的內嵌彙編就這點內容,把彙編語句放到__asm__塊中就完了。這種簡單的內嵌彙編用處並不是很大(當然我們可以用它來調用一些特殊的CPU指令),主要是因爲它不能和C代碼交流,只是孤零零的彙編代碼用處有限。

 

二、複雜的內嵌彙編格式

如何才能讓彙編代碼要和C語句交流呢?答案很簡單,就是讓彙編中能使用和改變C語言中的變量。因此內嵌彙編需要用一種方式來表示這種意圖。首先,我們需要告訴編譯器我們要在哪個地方使用變量,因此內嵌彙編用“%0到“%910個符號來代替彙編語句中的操作數,同時把c的表達式放在彙編語句後面的括號中,例如:

int kk = 0

__asm__ __volatile__ ("movl %0, %% eax":"=m"(kk));    //把變量kk的值放到eax寄存器中

上面彙編語句中的“%0又被稱爲佔位符,09的數值表示後面變量的位置。例如下面的例子中有兩個變量:

int kk = 0mm

__asm__ __volatile__ ("movl %0, %% eax; movl %% eax, %1":"=m"(kk),"=m"(mm));

上面的例子中%0代表變量kk%1代表變量mm。可能大家已經注意到上面的例子中繼承器前面已經多了一個‘%號,這也是內嵌彙編的要求,爲了和佔位符區別,使用兩個百分號來表示寄存器。

彙編語句和後面的變量之間用‘:’隔開。整個語句中有無分割符‘:’正是區別簡單內嵌彙編和複雜內嵌彙編的標誌。而且整個語句中的分隔符‘:’可以多達三個,這三個‘:’把整個語句分成四段,分別是模板指令域,output域,input域和Clobber/Modify域。後面的三個不必都有,可以空着。例如:

__asm__ __volatile__ ("movl %0, %% eax"::"m"(kk));

__asm__ __volatile__ ("movl %0, %% eax":::"memory");

這些“域”都是做什麼的呢?第一個當然很容易理解,就是帶有佔位符的彙編語句。第四個最後再解釋。中間的兩個output域和input域都是用來放C表達式的,這裏的所謂表達式可能是變量,也可是一個數。爲什麼要兩個域呢?因爲我們對變量有兩種操作,讀值或存值。如果一個變量被放到了output域,在彙編語句結束後編譯器會插入給變量賦值的指令,而放到input域的變量則不會,所以這裏需要兩個域把它們分開。例如:

int kk = 0

__asm__ __volatile__ ("movl $5, %% eax":"=a"(kk));

上面的例子中彙編代碼的功能只是把立即數5放到了寄存器eax中,可是運行的結果是變量kk的值也變成了5。這就是因爲變量kk放在了output域,編譯器自動把eax中的值賦給了kk。這樣,新的問題又來了,編譯器是如何知道要把eax的值賦給了kk,難道是因爲彙編指令中只用到了eax?當然不是這個原因,其實是我們自己指定的。上面的例子中所有變量的前面都帶有一個字符串,這個字符串稱爲限定符,我們通過限定符來告訴編譯器我們要使用哪個寄存器。上面例子中的字母‘a’就是寄存器eax的縮寫。其實不僅僅是eax,根據後面變量的類型,編譯器會把字母‘a’解釋爲al/ax/eax/rax中的一種。常用的幾種寄存器的縮寫如下:

a 代表al/ax/eax/rax寄存器

b 代表bl/bx/ebx/rbx寄存器

c 代表cl/cx/ecx/rcx寄存器

d 代表dl/dx/edx/rdx寄存器

D 代表di寄存器

S 代表si寄存器

如果我們不打算指定一個固定的寄存器,可以使用縮寫‘q’或‘r’。

如果希望使用浮點寄存器,可以使用縮寫‘f’。 而縮寫‘t’和‘u'分別表示要使用第一浮點寄存器和第二浮點寄存器。

在什麼情況下我們會使用‘q’或‘r’來讓編譯器給我們選擇一個寄存器呢?前面的output域的例子肯定不行,這個例子中我們必須很明白的告訴編譯器我們希望最後把哪個寄存器的值賦給輸出變量kk。我們要考慮另外一種情況,在彙編語句中,地址表達式的使用是受限的,x86中規定了彙編語句的兩個操作數至少有一個是寄存器。因此我們會先把地址表達式的值放到一個寄存器中,然後再使用它。寄存器限定符的另一個作用就是告訴編譯器將變量的值放到哪個寄存器後再使用。這時縮寫‘q’或‘r’就能派上用場。正因爲寄存器限定符的第二個作用,所以input域中的變量前面同樣也要使用限定符。

寄存器限定符解決了變量使用寄存器的問題,但是有一個副作用,使用限定符會讓編譯器產生將變量值放進寄存器的指令,這條指令對於需要在彙編語句中使用變量的情況是有用的,但是如果只需要改變一個變量的值而不需要在彙編代碼中用到它,那麼這條指令就多餘了。解決的辦法是再加一個 =’號限定符。這就是前面例子中經常出現的‘=’號的作用。一旦使用了‘=’號,寄存器限定符的第二個作用就消失了。假如我們兩個作用都需要呢?答案是使用‘+’號。不管是‘=’或‘+’號,它們都只能用在output域的變量前,input域的變量前是不能加上‘=’或‘+’號的,因爲input域變量的作用就是輸入。output域的變量前必須使用‘=’或者“+”號,否則編譯不通過。看一個例子。

__asm__ __volatile__ ("movl %0, %% ebx":"=a"(kk));

上面這個例子編譯可以通過。表面上看它的功能是把變量kk的值放到了寄存器ebx中了,實際上這條彙編直接被編譯器扔掉了。原因就是變量kk的前面已經使用了‘=’限定符,它不能做爲輸入用在彙編語句中了。如果一定要用,必須使用限定符‘+’,例如:

__asm__ __volatile__ ("movl %0, %% ebx":"+a"(kk));

到這裏,內嵌彙編的基本原理和語法規則就完了。其實也就這點東西,用一些符號來告訴編譯器如何使用變量而已。

 

三、其餘的限定符

但是還沒有結束,我們還需要學習另外的一些限定符,這些限定符用來處理一些特殊的情況。

1)       如果我們使用變量的時候不希望通過寄存器中轉,希望直接使用。這時可以使用限定符‘m’,例如:

__asm__ __volatile__ ("movl %0, %% ebx"::"m"(kk));

這個例子產生的彙編指令會直接使用變量kk作爲第一操作數。請注意,上面的例子中使用了兩個‘:’分隔符,說明變量kk放在了intput域。我們這裏的例子需要變量kk作爲輸入,放在input域當然沒問題。但是限定符‘m’同樣也可以放到output域的變量前,但是一旦這樣用來,會導致output域的變量失去輸出作用,不管這時變量的前面使用了‘=’還是‘+’號。

__asm__ __volatile__ ("movl %0, %% ebx":"=m"(kk));

前面我們講過,如果output域的變量前加上‘=’是不能用在彙編語句中的,但是使用這個‘m’限定符後就可以了,所以上面的彙編語句會把變量kk的值賦給寄存器ebx,但是結束後編譯器將不會自動給變量kk賦值了。這是使用限定符‘m’需要注意的。

2)         我們前面的例子都很簡單,只使用了output域或者input域。如果兩者同時使用,會有一種衝突情況:我們知道input域的變量前可以使用通用寄存器限定符‘q’或‘r’來讓編譯器來挑選寄存器。如果編譯器挑選的寄存器是我們在output域中指定的寄存器,有可能產生衝突,例如:

__asm__ __volatile__ ("movl %0, %% ebx":"+a"(kk):"r"(mm));

上面這個例子中,如果變量mm的寄存器也被選定爲寄存器eax,邏輯上有可能會和變量kk使用的eax發生衝突,導致最後的結果出錯。解決的辦法是使用限定符‘&’告訴編譯器不要挑選eax,例如上例改成下面這樣就可以了:

__asm__ __volatile__ ("movl %0, %% ebx":"+&a"(kk):"r"(mm));

3)         一種簡單的情況是input域使用的不是變量,而是立即數(output域是不能出現立即數的)。需要使用‘i’限定符告訴編譯器後面括號中的是整數,或者使用‘f’限定符告訴編譯器後面括號中的是浮點數。

__asm__ __volatile__ ("movl %0, %% ebx"::"i"(5));

4)         最後一種限定符是數字09,只能用在input域中,表示和前面的第noutput域的變量使用相同的寄存器。例如:

__asm__ __volatile__ ("movl %0, %% ebx":"+a"(kk):"0"(mm));

 

四、Clobber/Modify

最後,我們看看第四個域:Clobber/Modify域。

1)        如果我們在內嵌彙編的語句中使用了某些寄存器,但是這些寄存器可能之前已經被使用了,爲了在內嵌彙編語句結束後這些寄存器的值保持原樣,可以把它們的縮寫放在Clobber/Modify域。這樣編譯器將自動產生pushpop指令來保存這些寄存器的值。例如:

__asm__ ("movl %0, %% ebx" : : "a"(kk) : "bx");

上面的例子中彙編語句中使用了寄存器ebx,因爲擔心破壞以前的值,在Clobber/Modify域加上了縮寫“bx”。

一旦在Clobber/Modify域中指定了某個寄存器,這個寄存器將不能再用在inputoutput域的限定符中,否則編譯通不過。

Clobber/Modify域寄存器的縮寫和代表的寄存器如下:

ax 代表al/ax/eax/rax寄存器

bx 代表bl/bx/ebx/rbx寄存器

cx 代表cl/cx/ecx/rcx寄存器

dx 代表dl/dx/edx/rdx寄存器

di 代表di寄存器

si 代表si寄存器

2)        Clobber/Modify域中還可以使用“memroy”,例如:

__asm__ ("movl %0, %% ebx" :: "a"(kk): "memory");

memory在這裏的作用是“編譯優化屏障”。目的是防止編譯器把前面的代碼和內嵌彙編中的代碼一起優化。

3)        如果內嵌彙編中的語句會影響條件寄存器中的值,比如z標識,溢出,進位標誌等,需要在Clobber/Modify域中使用“cc”向編譯器聲明。

 

以上介紹是以x86爲基礎進行的,如果是arm體系,語法規則是一樣的,只不過寄存器名不一樣了。

 

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