GCC中的內聯組合語言
來源:互聯網
上載者:User
一.聲明
雖然Linux的核心代碼大部分是用C語言編寫的,但是不可避免的其中還是有一部分是用組合語言寫成的。有些組合語言代碼是直接寫在彙編來源程式中的,特別是Linux的啟動代碼部分;還有一些則是利用gcc的內嵌組合語言嵌在C語言程式中的。這篇文章簡單介紹了gcc中的內嵌式組合語言,主要想協助那些才開始閱讀Linux核心代碼的朋友們能夠更快的入手。
寫這篇文章的主要資訊來源是GNU的兩個info檔案:as.info和gcc.info,如果你覺得這篇文章中的介紹還不夠詳細的話,你可以查閱這兩個檔案。當然,直接查閱這兩個檔案可以獲得更加權威的資訊。如果你不想被這兩篇文檔中的一大堆資訊搞迷糊的話,我建議你先閱讀一下這篇文章,然後在必要時再去查閱更權威的資訊。
二.簡介
在Linux的核心代碼中,還是存在相當一部分的組合語言代碼。如果你想順利閱讀Linux代碼的話,你不可能繞過這一部分代碼。在Linux使用的組合語言代碼中,主要有兩種格式:一種是直接寫成組合語言來源程式的形式,這一部分主要是一些Linux的啟動代碼;另一部分則是利用gcc的內嵌式組合語言語句asm嵌在Linux的C語言代碼中的。這篇文章主要是介紹第二種形式的組合語言代碼。
首先,我介紹一下as支援的組合語言的文法格式。大家知道,我們現在學習的組合語言的格式主要是Intel風格的,而在Linux的核心代碼中使用的則是AT&T格式的組合語言代碼,應該說大部分人對這種格式的組合語言還不是很瞭解,所以我覺得有必要介紹一下。
接著,我主要介紹一下gcc的內嵌式組合語言的格式。gcc的內嵌式組合語言提供了一種在C語言來源程式中直接嵌入彙編指令的很好的辦法,既能夠直接控制所形成的指令序列,又有著與C語言的良好介面,所以在Linux代碼中很多地方都使用了這一語句。
三.gcc的內嵌組合語言語句asm
利用gcc的asm語句,你可以在C語言代碼中直接嵌入組合語言指令,同時還可以使用C語言的運算式指定彙編指令所用到的運算元。這一特性提供了很大的方便。
要使用這一特性,首先要寫一個彙編指令的模板(這種模板有點類似於機器描述檔案中的指令模板),然後要為每一個運算元指定一個限定字串。例如:
extern __inline__ void change_bit(int nr,volatile void *addr)
{
__asm__ __volatile__( LOCK_PREFIX
"btcl %1,%0 "
: "=m " (ADDR)
: "ir " (nr));
}
上面的函數中:
LOCK_PREFIX:這是一個宏,如果定義了__SMP__,擴充為 "lock; ",用於指定匯流排鎖定首碼,否則擴充為 ""。
ADDR:這也是一個宏,定義為(*(volatile struct __dummy *) addr)
"btcl %1,%0 ":這就是嵌入的組合語言指令,btcl為指令作業碼,%1,%0是這條指令兩個運算元的預留位置。後面的兩個限定字串就用於描述這兩個運算元。
: "=m " (ADDR):第一個冒號後的限定字串用於描述指令中的“輸出”運算元。刮號中的ADDR將運算元與C語言的變數聯絡起來。這個限定字串表示指令中的“%0”就是addr指標指向的記憶體運算元。這是一個“輸出”類型的記憶體運算元。
: "ir " (nr):第二個冒號後的限定字串用於描述指令中的“輸入”運算元。這條限定字串表示指令中的“%1”就是變數nr,這個的運算元可以是一個立即運算元或者是一個寄存器運算元。
*註:限定字串與運算元預留位置之間的對應關係是這樣的:在所有限定字串中(包括第一個冒號後的以及第二個冒號後的所有限定字串),最先出現的字串用於描述運算元“%0”,第二個出現的字串描述運算元“%1”,以此類推。
①彙編指令模板
asm語句中的彙編指令模板主要由彙編指令序列和限定字串組成。在一個asm語句中可以包括多條彙編指令。彙編指令序列中使用運算元預留位置引用C語言中的變數。一條asm語句中最多可以包含十個運算元預留位置:%0,%1,...,%9。彙編指令序列後面是運算元限定字串,對指令序列中的預留位置進行限定。限定的內容包括:該預留位置與哪個C語言變數對應,可以是什麼類型的運算元等等。限定字串可以分為三個部分:輸出運算元限定字串(指令序列後第一個冒號後的限定字串),輸入運算元限定字串(第一個冒號與第二個冒號之間),還有第三種類型的限定字串在第二個冒號之後。同一種類型的限定字串之間用逗號間隔。asm語句中出現的第一個限定字串用於描述預留位置“%0”,第二個用於描述預留位置“%1”,以此類推(不管該限定字串的類型)。如果指令序列中沒有任何輸出運算元,那麼在語句中出現的第一個限定字串(該字串用於描述輸入運算元)之前應該有兩個冒號(這樣,編譯器就知道指令中沒有輸出運算元)。
指令中的輸出運算元對應的C語言變數應該具有左實值型別,當然對於輸出運算元沒有這種左值限制。輸出運算元必須是唯寫的,也就是說,asm對取出某個運算元,執行一定計算以後再將結果存回該運算元這種類型的彙編指令的支援不是直接的,而必須通過特定的格式的說明。如果彙編指令中包含了一個輸入-輸出類型的運算元,那麼在模板中必須用兩個預留位置對該運算元的不同功能進行引用:一個負責輸入,另一個負責輸出。例如:
asm ( "addl %2,%0 ": "=r "(foo): "0 "(foo), "g "(bar));
在上面這條指令中,
"%0”是一個輸入-輸出類型的運算元,
"=r "(foo)用於限定其輸出功能,該指令的輸出結果會存放到C語言變數foo中;
指令中沒有顯式的出現“%1”運算元,但是針對它有一個限定字串 "0 "(foo),事實上指令中隱式的“%1”運算元用於描述“%0”運算元的輸入功能,它的限定字串中的 "0 "限定了“%1”運算元與“%0”
具有相同的地址。可以這樣理解上述指令中的模板:該指令將“%1”和“%2”中的值相加,計算結果存放回“%0”中,指令中的“%1”與“%0”具有相同的地址。注意,用於描述“%1”的 "0 "限定字元足以保證“%1”與“%0”具有相同的地址。
但是,如果用下面的指令完成,這種輸入-輸出操作就不會正常工作:
asm ( "addl %2,%0 ": "=r "(foo): "r "(foo), "g "(bar));
雖然該指令中“%0”和“%1”同樣引用了C語言變數foo,但是gcc並不保證在產生的組譯工具中它們具有相同的地址。
還有一些彙編指令可能會改變某些寄存器的值,相應的彙編指令模板中必須將這種情況通知編譯器。所以在模板中還有第三種類型的限定字串,它們跟在輸入運算元限定字串的後面,之間用冒號間隔。這些字串是某些寄存器的名稱,代表該指令會改變這些寄存器中的內容。
在內嵌的彙編指令中可能會直接引用某些硬體寄存器,我們已經知道AT&T格式的組合語言中,寄存器名以“%”作為首碼,為了在產生的組譯工具中保留這個“%”號,在asm語句中對硬體寄存器的引用必須用“%%”作為寄存器名稱的首碼。如果彙編指令改變了硬體寄存器的內容,不要忘記通知編譯器(在第三種類型的限定串中添加相應的字串)。還有一些指令可能會改變CPU標誌寄存器EFLAG的內容,那麼需要在第三種類型的限定字串中加入 "cc "。
為了防止gcc在最佳化過程中對asm中的彙編指令進行改變,可以在 "asm "關鍵字後加上 "volatile "修飾符。
可以在一條asm語句中描述多條組合語言指令;各條彙編指令之間用“;”或者“/n”隔開。
②運算元限定字元
運算元限定字串中利用規定的限定字元來描述相應的運算元,一些常用的限定字元有:(還有一些沒有涉及的限定字元,參見gcc.info)
1. "m ":運算元是記憶體變數。
2. "o ":運算元是記憶體變數,但它的定址方式必須是“位移量”類型的,也就是基址定址或者基址加變址定址。
3. "V ":運算元是記憶體變數,其定址方式非“位移量”類型。
4. " ":運算元是記憶體變數,其地址自動增量。
6. "r ":運算元是通用寄存器。
7. "i ":運算元是立即運算元。(其值可在彙編時確定)
8. "n ":運算元是立即運算元。有些系統不支援除字(雙位元組)以外的立即運算元,這些運算元要用 "n "而不是 "i "來描述。
9. "g ":運算元可以是立即數,記憶體變數或者寄存器,只要寄存器屬於通用寄存器。
10. "X ":運算元允許是任何類型。
11. "0 ", "1 ",..., "9 ":運算元與某個指定的運算元匹配。也就是說,該運算元就是指定的那個運算元。例如,如果用 "0 "來描述 "%1 "運算元,那麼 "%1 "引用的其實就是 "%0 "運算元。
12. "p ":運算元是一個合法的記憶體位址(指標)。
13. "= ":運算元在指令中是唯寫的(輸出運算元)。
14. "+ ":運算元在指令中是讀-寫類型的(輸入-輸出運算元)。
22. "f ":浮點數寄存器。
23. "t ":第一個浮點數寄存器。
24. "u ":第二個浮點數寄存器。
27. "I ":0-31之間的立即數。(用於32位的移位指令)
28. "J ":0-63之間的立即數。(用於64位的移位指令)
29. "N ":0-255之間的立即數。(用於 "out "指令)
30. "G ":標準的80387浮點常數。
註:還有一些不常見的限定字元並沒有在此說明,另外有一些限定字元,例如 "% ", "& "等由於我缺乏編譯器方面的一些知識,所以我也不是很理解它們的含義,如果有高手願意補充,不慎感激。不過在核心代碼中出現的限定字元差不多就是上面這些了。
對《gcc中的內嵌組合語言》一文的補充說明
初次接觸到AT&T格式的彙編代碼,看著那一堆莫名其妙的怪符號,真是有點痛不欲生的感覺,只好慢慢地去啃gcc文檔,在似懂非懂的狀態下過了一段時間。後來又在網上找到了靈溪寫的《gcc中的內嵌組合語言》一文,讀後自感大有裨益。幾個月下來,接觸的原始碼多了以後,慢慢有了一些經驗。為了使初次接觸AT&T格式的彙編代碼的同志不至於遭受我這樣的痛苦,就整理出該文來和大家共用.如有錯誤之處,歡迎大家指正,共同提高。
本文主要以舉例的方式對gcc中的內嵌組合語言進行進一步的解釋。
一、gcc對內嵌組合語言的處理方式
gcc在編譯內嵌組合語言時,採取的步驟如下:
變數輸入:根據限定符的內容將輸入運算元放入合適的寄存器,如果限定符指定為立即數( "i ")或記憶體變數( "m "),則該步被省略,如果限定符沒有具體指定輸入運算元的類型(如常用的 "g "),gcc會視需要決定是否將該運算元輸入到某個寄存器.這樣每個預留位置都與某個寄存器,記憶體變數,或立即數形成了一一對應的關係.這就是對第二個冒號後內容的解釋.如:: "a "(foo), "i "(100), "m "(bar)表示%0對應eax寄存器,%1對應100,%2對應記憶體變數bar.
產生代碼:然後根據這種一一對應的關係(還應包括輸出操作符),用這些寄存器,記憶體變數,或立即數來取代彙編代碼中的預留位置(則有點像宏操作),注意,則一步驟並不檢查由這種取代操作所產生的彙編代碼是否合法,例如,如果有這樣一條指令asm( "movl %0,%1 ":: "m "(foo), "m "(bar));如果你用gcc -c -S選項編譯該源檔案,那麼在產生的彙編檔案中,你將會看到產生了movl foo,bar這樣一條指令,這顯然是錯誤的.這個錯誤在稍後的編譯檢查中會被發現.
變數輸出:按照輸出限定符的指定將寄存器的內容輸出到某個記憶體變數中,如果輸出運算元的限定符指定為記憶體變數( "m "),則該步驟被省略.這就是對第一個冒號後內容的解釋,如:asm( "mov %0,%1 ": "=m "(foo), "=a "(bar):);編譯後為:
#APP
movl foo,eax
#NO_APP
movl eax,bar
該語句雖然有點怪怪的,但它很好的體現了gcc的運作方式。
再以arch/i386/kernel/apm.c中的一段代碼為例,我們來比較一下它們編譯前後的情況:
來源程式 編譯後的彙編代碼
__asm__ (
"pushl %%edi/n/t "
"pushl %%ebp/n/t "
"lcall %%cs:/n/t "
"setc %%al/n/t "
"addl %1,%2/n/t "
"popl %%ebp/n/t "
"popl %%edi/n/t "
: "=a "(ea), "=b "(eb),
"=c "(ec), "=d "(ed), "=S "(es)
: "a "(eax_in), "b "(ebx_in), "c "(ecx_in)
: "memory ", "cc ");
movl eax_in,%eax
movl ebx_in,%ebx
movl ecx_in,%ecx
#APP
pushl %edi
pushl %ebp
lcall %cs:
setc %al
addl eb,ec
popl %ebp
popl %edi
#NO_APP
movl %eax,ea
movl %ebx,eb
movl %ecx,ec
movl %edx,ed
movl %esi,es
二、對第三個冒號後面內容的解釋
第三個冒號後面內容主要針對gcc最佳化處理,它告訴gcc在本段彙編代碼中對寄存器和記憶體的使用方式,以免gcc在最佳化處理時產生錯誤。它可以是 "eax ", "ebx ", "ecx "等寄存器名,表示本段彙編代碼對該寄存器進行了顯式操作,如 asm ( "mov %%eax,%0 ",: "=r "(foo):: "eax ");這樣gcc在最佳化時會避免使用eax作臨時變數,或者避免cache到eax的記憶體變數通過該段彙編碼。
下面的代碼均用gcc的-O2級最佳化,它顯示了嵌入彙編中第三個冒號後 "eax "的作用
來源程式 編譯後的彙編代碼
int main()
{
int bar=1;
bar=fun();
bar++;
return bar;
}
pushl %ebp
movl %esp,%ebp
call fun
incl %eax #顯然,bar預設使用eax寄存器
leave
ret
加了彙編後:
來源程式 編譯後的彙編代碼
int main()
{
int bar=1;
bar=fun();
asm volatile( "" : : : "eax ");
bar++;
return bar;
}
pushl %ebp
movl %esp,%ebp
call fun
movl %eax,%edx #bar改為使用edx寄存器
incl %edx
movl %edx,%eax #放入main()的傳回值
ret
"merory "是一個常用的限定,它表示彙編代碼以不可預知的方式改變了記憶體,這樣gcc在最佳化時就不會讓cache到寄存器的記憶體變數使用該寄存器通過彙編代碼,否則可能會發生同步出錯.有了上面的例子,這個問題就很好理解了
三、對 "& "限定符的解釋
這是一個較常見用於輸出的限定符,它告訴gcc輸出運算元使用的寄存器不可再讓輸入運算元使用。
對於 "g ", "r "等限定符,為了有效利用為數不多的幾個通用寄存器,gcc一般會讓輸入運算元和輸出運算元選用同一個寄存器。但如果代碼沒編好,會引起一些意想不到的錯誤,例如:
asm( "call fun;mov ebx,%1 ": "=a "(foo): "r "(bar));
gcc編譯的結果是foo和bar同時使用eax寄存器:
movl bar,eax
#APP
call fun
movl ebx,eax
#NO_APP
movl eax,foo
本來這段代碼的意圖是將fun()函數的傳回值放入foo變數,但半路殺出個程咬金,用ebx的值衝掉了傳回值,所以這是一段錯誤的代碼,解決的方法是加上一個給輸出運算元加上一個 "& "限定符:
asm( "call fun;mov ebx,%1 ": "=&a "(foo): "r "(bar));
這樣gcc就會讓輸入運算元另尋高就,不再使用eax寄存器了
補充說明:
其實&對讀代碼根本沒有意義,只有GCC或寫彙編的才關心。
如果你非要知道&的作用,這裡解釋一下。
1. GCC處理嵌入彙編時,如果兩個輸入運算元值相同,可能會分配到同一個寄存器,以減少寄存器的使用。
2. GCC把嵌入彙編看成一個整體,它並不知道是一條指令還是多條指令,通常它認為彙編指令輸出結果時,輸入運算元還沒有改變,單條指令時,這種假設大多成立,但多條指令時可能不成立。比如定義如下:
#define add1(a,b) asm( "incl %0/n/taddl %2,%0 ": "=r "(res): "0 "(a), "r "(b))
計算a+b+1。我們看看add1(a,a)產生的指令,假設%eax中包含a的值:
#%0=eax,%2=eax
#APP
incl %eax
addl %eax, %eax
#NO_APP
結果不對,讀或寫彙編的人很容易看出這點,但是GCC不知道,加個&,定義寫成:
#define add1(a, b) asm( "addl %2,%0 ": "=&r "(res): "0 "(a), "r "(b))
告訴GCC參作數%0是earlyclobber,不要和%2分配到同一個寄存器,這樣add1(a,a)產生指令:
movl %eax, %edx
#%0=edx, %2=eax
#APP
incl %edx
addl %eax, %edx # edx = output
#NO_APP
四、對%quot;& "限定符的解釋(老鐵補充)
%:說明指令中可與下一運算元交換的那個運算元,這意味著編譯可以交換這兩個運算元以使得能以代價更小的方法來滿足運算元約束,這常常用於真正只有兩個運算元的加法指令的指令樣板中,這種加法指令的結果必須存放在兩個運算元之一中。