gcc中的內嵌匯編語言(Inteli386平臺)
一.聲明
雖然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代碼中很多地方都使用了這一語句。
三.AT&T的匯編語言語法格式
我想我們大部分人對Intel格式的匯編語言都很了解了。但是,在Linux核心代碼中,所有的匯編語言指令都是用AT&T格式的匯編語言書寫的。這兩種匯編語言在語法格式上有著很大的不同:
1.在AT&T的匯編語言中,用'$'前綴表示一個立即操作數;而在Intel的格式中,立即操作數的表示不帶任何前綴符。例如:下面兩個語句是完全相同的:
*AT&T: pushl $4
*Intel: push 4
2.AT&T和Intel的匯編語言格式中,源操作數和目標操作數的位置正好相反。Intel的匯編語言中,目標操作數在源操作數的左邊;而在AT&T的匯編語言中,目標操作數則在源操作數的右邊。例如:
*AT&T : addl $4,%eax
*Intel: add eax,4
3.在AT&T的匯編語言中,操作數的字長是由操作碼助記符的最后一個字母決定的,后綴'b'、'w'、'l'分別表示操作數的字長為8比特(字節,byte),16比特(字,word)和32比特(長字,long),而Intel格式中操作數的字長是用“wordptr”或者“byte ptr”等前綴來表示的。例如:
*AT&T: movb FOO,%al
*Intel: mov al,byte ptr FOO
4. 在AT&T匯編指令中,直接遠跳轉/調用的指令格式是“lcall/ljmp$SECTION,$OFFSET”,同樣,遠程返回的指令是“lret$STACK-ADJUST”;而在Intel格式中,相應的指令分別為“call/jmpfar SECTION:OFFSET”和“ret farSTACK-ADJUST”。
①AT&T匯編指令操作助記符命名規則
AT&T匯編語言中,操作碼助記符的后綴字符指定了該指令中操作數的字長。后綴字母'b'、'w'、'l'分別表示字長為8比特(字節,byte),16比特(字,word)和32比特(長字,long)的操作數。如果助記符中沒有指定字長后綴并且該指令中沒有內存操作數,匯編程序'as'會根據指令中指定的寄存器操作數補上相應的后綴字符。所以,下面的兩個指令具有相同的效果(這只是GNU的匯編程序as的一個特性,AT&T的Unix匯編程序將沒有字長后綴的指令的操作數字長假設為32比特):
mov %ax,%bx
movw %ax,%bx
AT&T中幾乎所有的操作助記符與Intel格式中的助記符同名,僅有一小部分例外。操作數擴展指令就是例外之一。在AT&T匯編指令中,操作數擴展指令有兩個后綴:一個指定源操作數的字長,另一個指定目標操作數的字長。AT&T的符號擴展指令的基本助記符為'movs',零擴展指令的基本助記符為'movz'(相應的Intel指令為'movsx'和'movzx')。因此,'movsbl%al,%edx'表示對寄存器al中的字節數據進行字節到長字的符號擴展,計算結果存放在寄存器edx中。下面是一些允許的操作數擴展后綴:
*bl: 字節->長字
*bw: 字節->字
*wl: 字->長字
還有一些其他的類型轉換指令的對應關系:
*Intel *AT&T
⑴ cbw cbtw
符號擴展:al->ax
⑵ cwde cwtl
符號擴展:ax->eax
⑶ cwd cwtd
符號擴展:ax->dx:ax
⑷ cdq cltd
符號擴展:eax->edx:eax
還有一個不同名的助記符就是遠程跳轉/調用指令。在Intel格式中,遠程跳轉/調用指令的助記符為“call/jmpfar”,而在AT&T的匯編語言中,相應的指令為“lcall”和“ljmp”。
②AT&T中寄存器的命名
在AT&T匯編語言中,寄存器操作數總是以'%'作為前綴。80386芯片的寄存器包括:
⑴8個32位寄存器:'%eax','%ebx','%ecx','%edx','%edi','%esi','%ebp','%esp'
⑵8個16位寄存器:'%ax','%bx','%cx','%dx','%si','%di','%bp','%sp'
⑶8個8位寄存器:'%ah','%al','%bh','%bl','%ch','%cl','%dh','%dl'
⑷6個段寄存器:'%cs','%ds','%es','%ss','%fs','%gs'
⑸3個控制寄存器:'%cr0','%cr1','%cr2'
⑹6個調試寄存器:'%db0','%db1','%db2','%db3','%db6','%db7'
⑺2個測試寄存器:'%tr6','%tr7'
⑻8個浮點寄存器棧:'%st(0)','%st(1)','%st(2)','%st(3)','%st(4)','%st(5)','%st(6)','%st(7)'
*注:我對這些寄存器并不是都了解,這些資料只是摘自as.info文檔。
如果真的需要寄存器命名的資料,我想可以參考一下相應GNU工具的機器描述方面的源文件。
③AT&T中的操作碼前綴
⑴段超越前綴'cs','ds','es','ss','fs','gs':當匯編程序中對內存操作數進行SECTION:MEMORY-OPERAND引用時,自動加上相應的段超越前綴。
⑵操作數/地址尺寸前綴'data16','addr16':這些前綴將32位的操作數/地址轉化為16位的操作數/地址。
⑶總線鎖定前綴'lock':總線鎖定操作。'lock'前綴在Linux核心代碼中使用很多,特別是SMP代碼中。
⑷協處理器等待前綴'wait':等待協處理器完成當前操作。
⑸指令重復前綴'rep','repe','repne':在串操作中重復指令的執行。
④AT&T中的內存操作數
在Intel的匯編語言中,內存操作數引用的格式如下:
SECTION : [BASE + INDEX*SCALE + DISP]
而在AT&T的匯編語言中,內存操作數的應用格式則是這樣的:
%SECTION : DISP(BASE,INDEX,SCALE)
下面是一些內存操作數的例子:
*AT&T *Intel
⑴ -4(%ebp) [ebp-4]
⑵foo(,%eax,4) [foo+eax*4]
⑶ foo(,1) [foo]
⑷ %gs:foo gs:foo
還有,絕對跳轉/調用指令中的內存操作數必須以'*'最為前綴,否則as總是假設這是一個相對跳轉/調用指令。
⑤AT&T中的跳轉指令
as匯編程序自動對跳轉指令進行優化,總是使用盡可能小的跳轉偏移量。如果8比特的偏移量無法滿足要求的話,as會使用一個32位的偏移量,as匯編程序暫時還不支持16位的跳轉偏移量,所以對跳轉指令使用'addr16'前綴是無效的。
還有一些跳轉指令只支持8位的跳轉偏移量,這些指令包括:'jcxz','jecxz','loop','loopz','loope','loopnz'和'loopne'。所以,在as的匯編源程序中使用這些指令可能會出錯。(幸運的是,gcc并不使用這些指令)
對AT&T匯編語言語法的簡單介紹差不多了,其中有些特性是as特有的。在Linux核心代碼中,并不涉及到所有上面這些提到的語法規則,其中有兩點規則特別重要:第一,as中對寄存器引用時使用前綴'%';第二,AT&T匯編語言中源操作數和目標操作數的位置與我們熟悉的Intel的語法正好相反。
四.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:這也是一個宏,定義為(*(volatilestruct __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。"+":操作數在指令中是讀-寫類型的(輸入-輸出操作數)。
15。"a":寄存器EAX。
16。"b":寄存器EBX。
17。"c":寄存器ECX。
18。"d":寄存器EDX。
19。"q":寄存器"a","b","c"或者"d"。
20。"A":寄存器"a"或者"d"。
21。"a":寄存器EAX。
22。"f":浮點數寄存器。
23。"t":第一個浮點數寄存器。
24。"u":第二個浮點數寄存器。
25。"D":寄存器di。
26。"S":寄存器si。
27。"I":0-31之間的立即數。(用于32位的移位指令)
28。"J":0-63之間的立即數。(用于64位的移位指令)
29。"N":0-255之間的立即數。(用于"out"指令)
30。"G":標準的80387浮點常數。
*注:還有一些不常見的限定字符并沒有在此說明,另外有一些限定字符,例如"%","&"等由于我缺乏編譯器方面的一些知識,所以我也不是很理解它們的含義,如果有高手愿意補充,不慎感激!不過在核心代碼中出現的限定字符差不多就是上面這些了。