AT&T匯編語(yǔ)言與GCC內嵌匯編簡(jiǎn)介
版本 0.1
時(shí)間04/3/30
EMAIL chforest_chang@hotmail.com
1 AT&T 與INTEL的匯編語(yǔ)言語(yǔ)法的區別
1.1大小寫(xiě)
1.2操作數賦值方向
1.3前綴
1.4間接尋址語(yǔ)法
1.5后綴
1.6指令
2 GCC內嵌匯編
2.1簡(jiǎn)介
2.2內嵌匯編舉例
2.3語(yǔ)法
2.3.1匯編語(yǔ)句模板
2.3.2輸出部分
2.3.3輸入部分
2.3.4限制字符
2.3.5破壞描述部分
2.4GCC如何編譯內嵌匯編代碼
3后記
本節先介紹
AT&T匯編語(yǔ)言語(yǔ)法與INTEL匯編語(yǔ)法的差別,然后介紹GCC內嵌匯編語(yǔ)法。閱讀本節需要讀者具有INTEL匯編語(yǔ)言基礎。
1 AT&T 與INTEL的匯編語(yǔ)言語(yǔ)法的區別
1.1
指令大小寫(xiě)
INTEL格式的指令使用大寫(xiě)字母,而AT&T
格式的使用小寫(xiě)字母。
例:
INTEL AT&T
MOV EAX,EBX movl %ebx,%eax
1.2
指令操作數賦值方向
在INTEL語(yǔ)法中,第一個(gè)表示目的操作數,第二個(gè)表示源操作數,賦值方向從右向左。
AT&T語(yǔ)法第一個(gè)為源操作數,第二個(gè)為目的操作數,方向從左到右,合乎自然。
例:
INTEL AT&T
MOV EAX,EBX movl %ebx,%eax
1.3
指令前綴
在INTEL語(yǔ)法中寄存器和立即數不需要前綴;
AT&T中寄存器需要加前綴"%";立即數需要加前綴"$"。
例:
INTEL AT&T
MOV EAX,1 movl $1,%eax
符號常數直接引用,不需要加前綴,如:
movl value , %ebx
value為一常數;
在符號前加前綴 $, 表示引用符號地址,
如
movl $value, %ebx
是將value的地址放到ebx中。
總線(xiàn)鎖定前綴"lock":
總線(xiàn)鎖定操作。"lock"前綴在Linux
核心代碼中使用很多,特別是SMP
代碼中。當總線(xiàn)鎖定后其它CPU
不能存取鎖定地址處的內存單元。
遠程跳轉指令和子過(guò)程調用指令的操作碼使用前綴"l",分別為ljmp,lcall,
與之相應的返回指令偽lret。
例:
INTEL AT&T
lcall $secion:$offset
JMP FAR SECTION:OFFSET ljmp $secion:$offset
RET FAR SATCK_ADJUST lret $stack_adjust
1.4 間接尋址語(yǔ)法
INTEL中基地址使用"["、"]",而在A(yíng)T&T"("、")";
另外處理復雜操作數的語(yǔ)法也不同,
INTEL為Segreg:[base+index*scale+disp]
,而在A(yíng)T&T中為%segreg:disp(base,index,sale),其中segreg
,index,scale,disp都是可選的,在指定index而沒(méi)有顯式指定Scale
的情況下使用默認值1。Scale,disp不需要加前綴"&"。
INTEL AT&T
Instr foo,segreg:[base+index*scale+disp] instr %segreg:disp(base,index,scale),foo
1.5
指令后綴
AT&T
語(yǔ)法中大部分指令操作碼的最后一個(gè)字母表示操作數大小,"b"表示byte
(一個(gè)字節);"w"表示word(2,個(gè)字節);"l"表示long(4,個(gè)字節)。
INTEL中處理內存操作數時(shí)也有類(lèi)似的語(yǔ)法如:
BYTE PTR、WORD PTR、DWORD PTR。
例:
INTEL AT&T
mov al, bl movb %bl,%al
mov ax,bx movw %bx,%ax
mov eax, dword ptr [ebx] movl (%ebx), %eax
AT&T匯編指令中,操作數擴展指令有兩個(gè)后綴,一個(gè)指定源操作數的字長(cháng),另一個(gè)指定目標操作數的字長(cháng)。AT&T的符號擴展指令的為"movs",零擴展指令為"movz
"(相應的Intel指令為"movsx"和"movzx")。因此,"movsbl %al,%edx"表示對寄存器al
中的字節數據進(jìn)行字節到長(cháng)字的符號擴展,計算結果存放在寄存器edx
中。下面是一些允許的操作數擴展后綴:
l
bl: ,字節>->長(cháng)字 l
bw: ,字節>->字 l
wl: ,字->長(cháng)字
跳轉指令標號后的后綴表示跳轉方向,"f"表示向前(forward),
"b,"表示向后(back)。
例:
jmp 1f
jmp 1f
1.6 指令
INTEL匯編與AT&T匯編指令基本相同,差別僅在語(yǔ)法上。關(guān)于每條指令的語(yǔ)法可以參考I386Manual。
2 GCC內嵌匯編
2.1 簡(jiǎn)介
內核代碼絕大部分使用C
語(yǔ)言編寫(xiě),只有一小部分使用匯編語(yǔ)言編寫(xiě),例如與特定體系結構相關(guān)的代碼和對性能影響很大的代碼。GCC提供了內嵌匯編的功能,可以在C代碼中直接內嵌匯編語(yǔ)言語(yǔ)句,大大方便了程序設計。
簡(jiǎn)單的內嵌匯編很容易理解
例:
__asm__
__volatile__("hlt");
"__asm__"表示后面的代碼為內嵌匯編,"asm"是"__asm__"的別名。
"__volatile__"表示編譯器不要優(yōu)化代碼,后面的指令保留原樣,
"volatile"是它的別名。括號里面是匯編指令。
2.2 內嵌匯編舉例在內嵌匯編中,可以將C
語(yǔ)言表達式指定為匯編指令的操作數,而且不用去管如何將C
語(yǔ)言表達式的值讀入哪個(gè)寄存器,以及如何將計算結果寫(xiě)回C
變量,你只要告訴程序中C語(yǔ)言表達式與匯編指令操作數之間的對應關(guān)系即可, GCC
會(huì )自動(dòng)插入代碼完成必要的操作。
使用內嵌匯編,要先編寫(xiě)匯編指令模板,然后將C語(yǔ)言表達式與指令的操作數相關(guān)聯(lián),并告訴
GCC對這些操作有哪些限制條件。例如在下面的匯編語(yǔ)句:
__asm__ __violate__
("movl %1,%0" : "=r" (result) : "m" (input));
"movl %1,%0"是指令模板;"%0"和"%1"代表指令的操作數,稱(chēng)為占位符,內嵌匯編靠它們將C
語(yǔ)言表達式與指令操作數相對應。指令模板后面用小括號括起來(lái)的是C
語(yǔ)言表達式,本例中只有兩個(gè):"result"和"input",他們按照出現的順序分別與指令操作
數"%0","%1,"對應;注意對應順序:第一個(gè)C表達式對應"%0";第二個(gè)表達式對應"%1
",依次類(lèi)推,操作數至多有10個(gè),分別用"%0","%1"…."%9,"表示。在每個(gè)操作數前
面有一個(gè)用引號括起來(lái)的字符串,字符串的內容是對該操作數的限制或者說(shuō)要求。"result"前面
的限制字符串是"=r",其中"="表示"result"是輸出操作數,"r
"表示需要將"result"與某個(gè)通用寄存器相關(guān)聯(lián),先將操作數的值讀入寄存器,然后
在指令中使用相應寄存器,而不是"result"本身,當然指令執行完后需要將寄存器中的值
存入變量"result",從表面上看好像是指令直接對"result"進(jìn)行操作,實(shí)際上GCC
做了隱式處理,這樣我們可以少寫(xiě)一些指令。"input"前面的"r"表示該表達式需要先放入
某個(gè)寄存器,然后在指令中使用該寄存器參加運算。
我們將上面的內嵌代碼放到一個(gè)C源文件中,然后使用gcc ?c?S得到該C
文件源代碼相對應的匯編代碼,然后查看一下匯編代碼,看看GCC是如何處理的。
C源文件如下內容如下,注意該代碼沒(méi)有實(shí)際意義,僅僅作為例子。
extern int
input,result;
void test(void)
{
input
= 1;
__asm__ __volatile__ ("movl %1,%0" :
"=r" (result) : "r" (input));
return
;
}
對應的匯編代碼如下;
行號 代碼 解釋
1
7
8 movl $1, input 對應C語(yǔ)言語(yǔ)句input = 1;
9 input, %eax
10 #APP GCC插入的注釋?zhuān)硎緝惹秴R編開(kāi)始
11 movl %eax,%eax 我們的內嵌匯編語(yǔ)句
12 #NO_APP GCC 插入的注釋?zhuān)硎緝惹秴R編結束
13 movl %eax, result 將結果存入result變量
14
-
18
。。。。。。
從匯編代碼可以看出,第9行和第13行是GCC,自動(dòng)增加的代碼,GCC
根據限定字符串決定如何處理C表達式,本例兩個(gè)表達式都被指定為"r"型,所以先使用指令:
movl input, %eax
將input讀入寄存器%eax;GCC,也指定一個(gè)寄存器與輸出變量result
相關(guān),本例也是%eax,等得到操作結果后再使用指令:
movl %eax, result
將寄存器的值寫(xiě)回C變量result中。從上面的匯編代碼我們可以看出與result
和input,相關(guān)連的寄存器都是%eax,GCC使用%eax,替換內嵌匯編指令模板中的
%0,%1
movl %eax,%eax
顯然這一句可以不要。但是沒(méi)有優(yōu)化,所以這一句沒(méi)有被去掉。
由此可見(jiàn),C表達式或者變量與寄存器的關(guān)系由GCC自動(dòng)處理,我們只需使用限制字符串指導GCC
如何處理即可。限制字符必須與指令對操作數的要求相匹配,否則產(chǎn)生的匯編代碼
將會(huì )有錯,讀者可以將上例中的兩個(gè)"r",都改為"m"(m,表示操作數放在內存,而不是寄
存器中),編譯后得到的結果是:
movl input, result
很明顯這是一條非法指令,因此限制字符串必須與指令對操作數的要求匹配。例如指令movl
允許寄存器到寄存器,立即數到寄存器等,但是不允許內存到內存的操作,因此兩個(gè)操作數
不能同時(shí)使用"m"作為限定字符。
2.3 語(yǔ)法
內嵌匯編語(yǔ)法如下:
__asm__(
匯編語(yǔ)句模板:
輸出部分:
輸入部分:
破壞描述部分)
共四個(gè)部分:匯編語(yǔ)句模板,輸出部分,輸入部分,破壞描述部分,各部分使用":"格
開(kāi),匯編語(yǔ)句模板必不可少,其他三部分可選,如果使用了后面的部分,而前面部分為空,
也需要用":"格開(kāi),相應部分內容為空。例如:
__asm__ __volatile__(
"cli":
:
:"memory")
2.3.1 匯編語(yǔ)句模板
匯編語(yǔ)句模板由匯編語(yǔ)句序列組成,語(yǔ)句之間使用";"、"\n"或"\n\t"分開(kāi)。
指令中的操作數可以使用占位符引用C語(yǔ)言變量,操作數占位符最多10個(gè),名稱(chēng)如下:%0,%1…,%9。
指令中使用占位符表示的操作數,總被視為long型(4,個(gè)字節),但對其施加的操作
根據指令可以是字或者字節,當把操作數當作字或者字節使用時(shí),默認為低字或者低字節。
對字節操作可以顯式的指明是低字節還是次字節。方法是在%和序號之間插入一個(gè)字母,
"b"代表低字節,"h"代表高字節,例如:%h1。
2.3.2 輸出部分
輸出部分描述輸出操作數,不同的操作數描述符之間用逗號格開(kāi),每個(gè)操作數描述符由限定字符串和
C語(yǔ)言變量組成。每個(gè)輸出操作數的限定字符串必須包含"="表示他是一個(gè)輸出操作數。
例:
__asm__ __volatile__("pushfl ; popl %0 ; cli":"=g" (x) )
描述符字符串表示對該變量的限制條件,這樣GCC就可以根據這些條件決定如何
分配寄存器,如何產(chǎn)生必要的代碼處理指令操作數與C表達式或C變量之間的聯(lián)系。
2.3.3 輸入部分
輸入部分描述輸入操作數,不同的操作數描述符之間使用逗號格開(kāi),每個(gè)操作數描述符由
限定字符串和C語(yǔ)言表達式或者C語(yǔ)言變量組成。
例1:
__asm__ __volatile__ ("lidt %0" : : "m" (real_mode_idt));
例二(bitops.h):
Static __inline__ void __set_bit(int nr,
volatile void * addr)
{
__asm__(
"btsl%1,%0" :
"=m"(ADDR) :
"Ir"(nr));
}
后例功能是將(*addr)的第nr位設為1。第一個(gè)占位符%0與C,語(yǔ)言變量ADDR
對應,第二個(gè)占位符%1與C,語(yǔ)言變量nr對應。因此上面的匯編語(yǔ)句代碼與下面的偽代碼等價(jià):
btsl nr, ADDR,該指令的兩個(gè)操作數不能全是內存變量,因此將nr的限定字符串指定為"Ir",
將nr,與立即數或者寄存器相關(guān)聯(lián),這樣兩個(gè)操作數中只有ADDR為內存變量。
2.3.4 限制字符
2.3.4.1 限制字符列表
限制字符有很多種,有些是與特定體系結構相關(guān),此處僅列出常用的限定字符和i386
中可能用到的一些常用的限定符。它們的作用是指示編譯器如何處理其后的C
語(yǔ)言變量與指令操作數之間的關(guān)系,例如是將變量放在寄存器中還是放在內存中等,
下表列出了常用的限定字母。
分類(lèi)
限定符 描述 通用寄存器
"a"將輸入變量放入eax
這里有一個(gè)問(wèn)題:假設eax已經(jīng)被使用,那怎么辦?
其實(shí)很簡(jiǎn)單:因為GCC知道eax已經(jīng)被使用,它在這段匯編代碼的起始處插入一條
語(yǔ)句pushl %eax,將eax內容保存到堆棧,然后在這段代碼結束處再增加一條
語(yǔ)句popl %eax,恢復eax的內容
"b"將輸入變量放入ebx
"c"將輸入變量放入ecx
"d"將輸入變量放入edx
"s"將輸入變量放入esi
"d"將輸入變量放入edi
"q"將輸入變量放入eax,ebx ,ecx ,edx中的一個(gè)
"r"將輸入變量放入通用寄存器,也就是eax ,ebx,ecx,edx,esi,edi中的一個(gè)
"A"把eax和edx,合成一個(gè)64位的寄存器(uselong longs)
"m"內存變量
"o"操作數為內存變量,但是其尋址方式是偏移量類(lèi)型,也即是基址尋址,或者是基址加變址尋址
"V"操作數為內存變量,但尋址方式不是偏移量類(lèi)型
"," 操作數為內存變量,但尋址方式為自動(dòng)增量
"p"操作數是一個(gè)合法的內存地址(指針)
寄存器或內存
"g" 將輸入變量放入eax,ebx,ecx ,edx中的一個(gè)或者作為內存變量
"X"操作數可以是任何類(lèi)型
立即數
"I" 0-31 之間的立即數(用于32位移位指令)
"J" 0-63 之間的立即數(用于64 位移位指令)
"N" 0-255 ,之間的立即數(用于out 指令)
"i" 立即數
"n" 立即數,有些系統不支持除字以外的立即數,這些系統應該使用"n"而不是"i"
匹配
"0","1 ,"... "9 "
表示用它限制的操作數與某個(gè)指定的操作數匹配,也即該操作數就是指定的那個(gè)操作數,
例如用"0 "去描述"%1"操作數,那么"%1"引用的其實(shí)就是"%0"操作數,注意作為
限定符字母的0-9 ,與指令中的"%0"-"%9"的區別,前者描述操作數,后者代表操作數。
后面有詳細描述 & 該輸出操作數不能使用過(guò)和輸入操作數相同的寄存器
后面有詳細描述
操作數類(lèi)型
"=" 操作數在指令中是只寫(xiě)的(輸出操作數)
"+" 操作數在指令中是讀寫(xiě)類(lèi)型的(輸入輸出操作數)
浮點(diǎn)數
"f"
浮點(diǎn)寄存器
"t"第一個(gè)浮點(diǎn)寄存器
"u"第二個(gè)浮點(diǎn)寄存器
"G"標準的80387
浮點(diǎn)常數
% 該操作數可以和下一個(gè)操作數交換位置
例如addl的兩個(gè)操作數可以交換順序(當然兩個(gè)操作數都不能是立即數)
# 部分注釋?zhuān)瑥脑撟址狡浜蟮亩禾栔g所有字母被忽略
* 表示如果選用寄存器,則其后的字母被忽略
現在繼續看上面的例子,
"=m" (ADDR)表示ADDR為內存變量("m"),而且是輸出變量("=");"Ir" (nr)表示nr,為
0-31之間的立即數("I")或者一個(gè)寄存器操作數("r")。
2.3.4.2
匹配限制符
I386
指令集中許多指令的操作數是讀寫(xiě)型的(讀寫(xiě)型操作數指先讀取原來(lái)的值然后參加運算,最后
將結果寫(xiě)回操作數),例如addl %1,%0,它的作用是將操作數%0與操作數%1的和存入操作數%0,
因此操作數%0是讀寫(xiě)型操作數。老版本的GCC對這種類(lèi)型操作數的支持不是很好,它將操作數嚴格
分為輸入和輸出兩種,分別放在輸入部分和輸出部分,而沒(méi)有一個(gè)單獨部分描述讀寫(xiě)型操作數,
因此在GCC中讀寫(xiě)型的操作數需要在輸入和輸出部分分別描述,靠匹配限制符將兩者關(guān)聯(lián)到一起
注意僅在輸入和輸出部分使用相同的C變量,但是不用匹配限制符,產(chǎn)生的代碼很可能不對,后
面會(huì )分析原因。
匹配限制符是一位數字:"0"、"1"……"9,",分別表示它限制的C表達式分別與
占位符%0,%1,……%9對應的C變量匹配。例如使用"0"作為%1,的限制字符,那么
%0和%1表示同一個(gè)C,變量。
看一下下面的代碼就知道為什么要將讀寫(xiě)型操作數,分別在輸入和輸出部分加以描述。
該例功能是求input+result的和,然后存入result:
extern int input,result;
void test_at_t()
{
result= 0;
input = 1;
__asm__
__volatile__ ("addl %1,%0":"=r"(result): "r"(input));
}
對應的匯編代碼為:
movl $0,_result
movl $1,_input
movl _input,%edx /APP
addl %edx,%eax /NO_APP
movl %eax,%edx
movl %edx,_result
input 為輸入型變量,而且需要放在寄存器中,GCC給它分配的寄存器是%edx,在執行addl之前%edx,
的內容已經(jīng)是input的值??梢?jiàn)對于使用"r"限制的輸入型變量或者表達式,在使用之前GCC會(huì )插入
必要的代碼將他們的值讀到寄存器;"m"型變量則不需要這一步。讀入input后執行addl,顯然%eax
的值不對,需要先讀入result的值才行。再往后看:movl %eax,%edx和movl %edx,_result
的作用是將結果存回result,分配給result的寄存器與分配給input的一樣,都是%edx。
綜上可以總結出如下幾點(diǎn):
1. 使用"r"限制的輸入變量,GCC先分配一個(gè)寄存器,然后將值讀入寄存器,最后
用該寄存器替換占位符;
2. 使用"r"限制的輸出變量,GCC會(huì )分配一個(gè)寄存器,然后用該寄存器替換占位符,
但是在使用該寄存器之前并不將變量值先讀入寄存器,GCC認為所有輸出變量以前的
值都沒(méi)有用處,不讀入寄存器(可能是因為AT&T匯編源于CISC架構處理器的匯編語(yǔ)言
,在CISC處理器中大部分指令的輸入輸出明顯分開(kāi),而不像RISC那樣一個(gè)操作數既
做輸入又做輸出,例如add r0,r1,r2,r0,和r1是輸入,r2是輸出,輸入和輸出分開(kāi),
沒(méi)有使用輸入輸出型操作數,這樣我們就可以認為r2對應的操作數原來(lái)的值沒(méi)有用處,
也就沒(méi)有必要先將操作數的值讀入r2,因為這是浪費處理器的CPU周期),最后GCC插入代碼,
將寄存器的值寫(xiě)回變量;
3. 輸入變量使用的寄存器在最后一處使用它的指令之后,就可以挪做其他用處,因為
已經(jīng)不再使用。例如上例中的%edx。在執行完addl之后就作為與result對應的寄存器。
因為第二條,上面的內嵌匯編指令不能奏效,因此需要在執行addl之前把result的值讀入
寄存器,也許再將result放入輸入部分就可以了(因為第一條會(huì )保證將result
先讀入寄存器)。修改后的指令如下(為了更容易說(shuō)明問(wèn)題將input限制符由"r,"改為"m"):
extern int input,result;
void test_at_t()
{
result = 0;
input = 1;
__asm__
__volatile__ ("addl %2,%0":"=r"(result):"r"(result),"m"(input));
}
看上去上面的代碼可以正常工作,因為我們知道%0和%1都和result相關(guān),應該使用同一個(gè)
寄存器,但是GCC并不去判斷%0和%1,是否和同一個(gè)C表達式或變量相關(guān)聯(lián)(這樣易于產(chǎn)生與
內嵌匯編相應的匯編代碼),因此%0和%1使用的寄存器可能不同。我們看一下匯編代碼就知道了。
movl $0,_result
movl $1,_input
movl _result,%edx /APP
addl _input,%eax /NO_APP
movl %eax,%edx
movl %edx,_result
現在在執行addl之前將result的值被讀入了寄存器%edx,但是addl指令的操作數%0
卻成了%eax,而不是%edx,與預料的不同,這是因為GCC給輸出和輸入部分的變量分配了不同
的寄存器,GCC沒(méi)有去判斷兩者是否都與result相關(guān),后面會(huì )講GCC如何翻譯內嵌匯編,看完之后
就不會(huì )驚奇啦。
使用匹配限制符后,GCC知道應將對應的操作數放在同一個(gè)位置(同一個(gè)寄存器或者同一個(gè)
內存變量)。使用匹配限制字符的代碼如下:
extern int input,result;
void test_at_t()
{
result = 0;
input = 1;
__asm__
__volatile__ ("addl %2,%0":"=r"(result):"0"(result),"m"(input));
}
輸入部分中的result用匹配限制符"0"限制,表示%1與%0,代表同一個(gè)變量,
輸入部分說(shuō)明該變量的輸入功能,輸出部分說(shuō)明該變量的輸出功能,兩者結合表示result
是讀寫(xiě)型。因為%0和%1,表示同一個(gè)C變量,所以放在相同的位置,無(wú)論是寄存器還是內存。
相應的匯編代碼為:
movl $0,_result
movl $1,_input
movl _result,%edx
movl %edx,%eax /APP
addl _input,%eax /NO_APP
movl %eax,%edx
movl %edx,_result
可以看到與result相關(guān)的寄存器是%edx,在執行指令addl之前先從%edx將result讀入%eax,
執行之后需要將結果從%eax讀入%edx,最后存入result中。這里我們可以看出GCC
處理內嵌匯編中輸出操作數的一點(diǎn)點(diǎn)信息:addl并沒(méi)有使用%edx,可見(jiàn)它不是簡(jiǎn)單的用result
對應的寄存器%edx去替換%0,而是先分配一個(gè)寄存器,執行運算,最后才將運算結果存入
對應的變量,因此GCC是先看該占位符對應的變量的限制符,發(fā)現是一個(gè)輸出型寄存器變量,
就為它分配一個(gè)寄存器,此時(shí)沒(méi)有去管對應的C變量,最后GCC,知道還要將寄存器的值寫(xiě)回變量,
與此同時(shí),它發(fā)現該變量與%edx關(guān)聯(lián),因此先存入%edx,再存入變量。
至此讀者應該明白了匹配限制符的意義和用法。在新版本的GCC中增加了一個(gè)限制字符"+",
它表示操作數是讀寫(xiě)型的,GCC知道應將變量值先讀入寄存器,然后計算,最后寫(xiě)回變量,而
無(wú)需在輸入部分再去描述該變量。
例;
extern int input,result;
void test_at_t()
{
result = 0;
input = 1;
__asm__
__volatile__ ("addl %1,%0":"+r"(result):"m"(input));
}
此處用"+"替換了"=",而且去掉了輸入部分關(guān)于result的描述,產(chǎn)生的匯編代碼如下:
movl $0,_result
movl $1,_input
movl _result,%eax /APP
addl _input,%eax /NO_APP
movl %eax,_result
L2:
movl %ebp,%esp
處理的比使用匹配限制符的情況還要好,省去了好幾條匯編代碼。
2.3.4.3 "&"限制符
限制符"&"在內核中使用的比較多,它表示輸入和輸出操作數不能使用相同的寄存器,
這樣可以避免很多錯誤。
舉一個(gè)例子,下面代碼的作用是將函數foo的返回值存入變量ret中:
__asm__ ( "call foo;movl %%edx,%1", :"=a"(ret) : "r"(bar) );
我們知道函數的int型返回值存放在%eax中,但是gcc編譯的結果是輸入和輸出同時(shí)使用了
寄存器%eax,如下:
movl bar, %eax
#APP
call foo
movl %ebx,%eax
#NO_APP
movl %eax, ret
結果顯然不對,原因是GCC并不知道%eax中的值是我們所要的。避免這種情況的方法是使用"&"
限定符,這樣bar就不會(huì )再使用%eax寄存器,因為已被ret指定使用。
_asm__ ( "call foo;movl %%edx,%1",:"=&a"(ret) : "r"(bar) );
2.3.5 破壞描述部分
2.3.5.1 寄存器破壞描述符
通常編寫(xiě)程序只使用一種語(yǔ)言:高級語(yǔ)言或者匯編語(yǔ)言。高級語(yǔ)言編譯的步驟大致如下:
l
預處理;
l
編譯
l
匯編
l
鏈接
我們這里只關(guān)心第二步編譯(將C代碼轉換成匯編代碼):因為所有的代碼都是用高級語(yǔ)言編寫(xiě),
編譯器可以識別各種語(yǔ)句的作用,在轉換的過(guò)程中所有的寄存器都由編譯器決定如何分配使用,
它有能力保證寄存器的使用不會(huì )沖突;也可以利用寄存器作為變量的緩沖區,因為寄存器的訪(fǎng)問(wèn)
速度比內存快很多倍。如果全部使用匯編語(yǔ)言則由程序員去控制寄存器的使用,只能靠程序員去
保證寄存器使用的正確性。但是如果兩種語(yǔ)言混用情況就變復雜了,因為內嵌的匯編代碼可以直接
使用寄存器,而編譯器在轉換的時(shí)候并不去檢查內嵌的匯編代碼使用了哪些寄存器(因為很難檢測
匯編指令使用了哪些寄存器,例如有些指令隱式修改寄存器,有時(shí)內嵌的匯編代碼會(huì )調用其他子過(guò)程,
而子過(guò)程也會(huì )修改寄存器),因此需要一種機制通知編譯器我們使用了哪些寄存器(程序員自己知道
內嵌匯編代碼中使用了哪些寄存器),否則對這些寄存器的使用就有可能導致錯誤,修改描述部分
可以起到這種作用。當然內嵌匯編的輸入輸出部分指明的寄存器或者指定為"r","g"型由編譯器
去分配的寄存器就不需要在破壞描述部分去描述,因為編譯器已經(jīng)知道了。
破壞描述符由逗號格開(kāi)的字符串組成,每個(gè)字符串描述一種情況,一般是寄存器名;除寄存器外
還有"memory"。例如:"%eax","%ebx","memory"等。
下面看個(gè)例子就很清楚為什么需要通知GCC內嵌匯編代碼中隱式(稱(chēng)它為隱式是因為GCC并不知道)
使用的寄存器。
在內嵌的匯編指令中可能會(huì )直接引用某些寄存器,我們已經(jīng)知道AT&T格式的匯編語(yǔ)言中,寄存器
名以"%"作為前綴,為了在生成的匯編程序中保留這個(gè)"%"號,在asm語(yǔ)句中對寄存器的
引用必須用"%%"作為寄存器名稱(chēng)的前綴。原因是"%"在asm,內嵌匯編語(yǔ)句中的作用與"\"在C
語(yǔ)言中的作用相同,因此"%%"轉換后代表"%"。
例(沒(méi)有使用修改描述符):
int main(void)
{
int input, output,temp;
input = 1;
__asm__ __volatile__ ("movl $0, %%eax;\n\t
movl %%eax, %1;\n\t
movl %2, %%eax;\n\t
movl %%eax, %0;\n\t"
:"=m"(output),"=m"(temp) /* output */
:"r"(input) /* input */
);
return 0;
}
這段代碼使用%eax作為臨時(shí)寄存器,功能相當于C代碼:"temp = 0;output=input",
對應的匯編代碼如下:
movl $1,-4(%ebp)
movl -4(%ebp),%eax /APP
movl $0, %eax;
movl %eax, -12(%ebp);
movl %eax, %eax;
movl %eax, -8(%ebp); /NO_APP
顯然GCC給input分配的寄存器也是%eax,發(fā)生了沖突,output的值始終為0,而不是input。
使用破壞描述后的代碼:
int main(void)
{
int input, output,temp;
input = 1;
__asm__ __volatile__
( "movl $0, %%eax;\n\t
movl %%eax, %1;\n\t
movl %2, %%eax;\n\t
movl %%eax, %0;\n\t"
:"=m"(output),"=m"(temp) /* output */
:"r"(input) /* input */
:"eax"); /* 描述符 */
return 0;
}
對應的匯編代碼:
movl $1,-4(%ebp)
movl -4(%ebp),%edx /APP
movl $0, %eax;
movl %eax, -12(%ebp);
movl %edx, %eax;
movl %eax, -8(%ebp); /NO_APP
通過(guò)破壞描述部分,GCC得知%eax已被使用,因此給input分配了%edx。在使用內嵌匯編時(shí)請記
住一點(diǎn):盡量告訴GCC盡可能多的信息,以防出錯。
如果你使用的指令會(huì )改變CPU的條件寄存器cc,需要在修改描述部分增加"cc"。
2.3.5.2 memory破壞描述符
"memory"比較特殊,可能是內嵌匯編中最難懂部分。為解釋清楚它,先介紹一下編譯器的
優(yōu)化知識,再看C關(guān)鍵字volatile。最后去看該描述符。
2.3.5.2.1 編譯器優(yōu)化介紹
內存訪(fǎng)問(wèn)速度遠不及CPU處理速度,為提高機器整體性能,在硬件上引入硬件高速緩存Cache,
加速對內存的訪(fǎng)問(wèn)。另外在現代CPU中指令的執行并不一定嚴格按照順序執行,沒(méi)有相關(guān)性
的指令可以亂序執行,以充分利用CPU的指令流水線(xiàn),提高執行速度。以上是硬件級別的優(yōu)化。
再看軟件一級的優(yōu)化:一種是在編寫(xiě)代碼時(shí)由程序員優(yōu)化,另一種是由編譯器進(jìn)行優(yōu)化。編譯器
優(yōu)化常用的方法有:將內存變量緩存到寄存器;調整指令順序充分利用CPU指令流水線(xiàn),常見(jiàn)的
是重新排序讀寫(xiě)指令。
對常規內存進(jìn)行優(yōu)化的時(shí)候,這些優(yōu)化是透明的,而且效率很好。由編譯器優(yōu)化或者硬件重新排序引起的問(wèn)題的解決辦法是在從硬件(或者其他處理器)的角度看必須以特定順序執行的操作之間設置內存屏障(memory barrier),linux提供了一個(gè)宏解決編譯器的執行順序問(wèn)題。
void Barrier(void)
這個(gè)函數通知編譯器插入一個(gè)內存屏障,但對硬件無(wú)效,編譯后的代碼會(huì )把當前CPU
寄存器中的所有修改過(guò)的數值存入內存,需要這些數據的時(shí)候再重新從內存中讀出。
2.3.5.2.2 C 語(yǔ)言關(guān)鍵字volatile
C 語(yǔ)言關(guān)鍵字volatile(注意它是用來(lái)修飾變量而不是上面介紹的__volatile__)表明某個(gè)變量
的值可能在外部被改變,因此對這些變量的存取不能緩存到寄存器,每次使用時(shí)需要重新存取。
該關(guān)鍵字在多線(xiàn)程環(huán)境下經(jīng)常使用,因為在編寫(xiě)多線(xiàn)程的程序時(shí),同一個(gè)變量可能被多個(gè)線(xiàn)程修
改,而程序通過(guò)該變量同步各個(gè)線(xiàn)程,例如:
DWORD __stdcall threadFunc(LPVOID signal)
{
int* intSignal=reinterpret_cast(signal);
*intSignal=2;
while(*intSignal!=1)
sleep(1000);
return 0;
}
該線(xiàn)程啟動(dòng)時(shí)將intSignal置為2,然后循環(huán)等待直到intSignal為1,時(shí)退出。顯然intSignal
的值必須在外部被改變,否則該線(xiàn)程不會(huì )退出。但是實(shí)際運行的時(shí)候該線(xiàn)程卻不會(huì )退出,即使
在外部將它的值改為1,看一下對應的偽匯編代碼就明白了:
mov ax,signal
label:
if(ax!=1)
goto label
對于C編譯器來(lái)說(shuō),它并不知道這個(gè)值會(huì )被其他線(xiàn)程修改。自然就把它c(diǎn)ache在寄存器里面。記住,C
編譯器是沒(méi)有線(xiàn)程概念的!這時(shí)候就需要用到volatile。volatile的本意是指:這個(gè)值可能會(huì )在
當前線(xiàn)程外部被改變。也就是說(shuō),我們要在threadFunc中的intSignal前面加上volatile
關(guān)鍵字,這時(shí)候,編譯器知道該變量的值會(huì )在外部改變,因此每次訪(fǎng)問(wèn)該變量時(shí)會(huì )重新讀取,所作
的循環(huán)變?yōu)槿缦旅鎮未a所示:
label:
mov ax,signal
if(ax!=1)
goto label
2.3.5.2.3 Memory
有了上面的知識就不難理解Memory
修改描述符了,Memory描述符告知GCC:
(1)不要將該段內嵌匯編指令與前面的指令重新排序;也就是在執行內嵌匯編代碼之前,
它前面的指令都執行完畢。
(2)不要將變量緩存到寄存器,因為這段代碼可能會(huì )用到內存變量,而這些內存變量會(huì )
以不可預知的方式發(fā)生改變,因此GCC插入必要的代碼先將緩存到寄存器的變量值寫(xiě)回內存,
如果后面又訪(fǎng)問(wèn)這些變量,需要重新訪(fǎng)問(wèn)內存。
如果匯編指令修改了內存,但是GCC本身卻察覺(jué)不到,因為在輸出部分沒(méi)有描述,
此時(shí)就需要在修改描述部分增加"memory",告訴GCC內存已經(jīng)被修改,GCC得知這個(gè)信息后,
就會(huì )在這段指令之前,插入必要的指令將前面因為優(yōu)化Cache到寄存器中的變量值先寫(xiě)回內存,
如果以后又要使用這些變量再重新讀取。
例:
………..
Char test[100];
char a;
char c;
c = 0;
test[0] = 1;
……..
a = test [0];
……
__asm__(
"cld\n\t"
"rep\n\t"
"stosb"
: /* no output */
: "a" (c),"D" (test),"c" (100)
:
"cx","di","memory");
……….
// 我們知道test[0] 已經(jīng)修改,所以重新讀取
a=test[0];
……
這段代碼中的匯編指令功能與
memset
相當,也就是相當于調用了memset(test,0,100);它使用stosb修改了test
數組的內容,但是沒(méi)有在輸入或輸出部分去描述操作數,因為這兩條指令都不需要
顯式的指定操作數,因此需要增加"memory"通知GCC?,F在假設:GCC在優(yōu)化時(shí)將test[0]
放到了%eax寄存器,那么test[0] = 1對應于%eax=1,a = test [0]被換為a=%eax
,如果在那段匯編指令中不使用"memory",Gcc,不知道現在test[0]
的值已經(jīng)被改變了(如果整段代碼都是我們自己使用匯編編寫(xiě),我們自己當然知道
這些內存的修改情況,我們也可以人為的去優(yōu)化,但是現在除了我們編寫(xiě)的那一小段外,
其他匯編代碼都是GCC
生成的,它并沒(méi)有那么智能,知道這段代碼會(huì )修改test[0]),結果其后的a=test[0]
,轉換為匯編后卻是a=%eax,因為GCC不知道顯式的改變了test數組,結果出錯了。
如果增加了"memory"修飾符,GCC知道:
"這段代碼修改了內存,但是也僅此而已,它并不知道到底修改了哪些變量",
因此他將以前因優(yōu)化而緩存到寄存器的變量值全部寫(xiě)回內存,從內嵌匯編開(kāi)始,如果后面
的代碼又要存取這些變量,則重新存取內存(不會(huì )將讀寫(xiě)操作映射到以前緩存的那個(gè)寄存器)。
這樣上面那段代碼最后一句就不再是%eax=1,而是test[0] = 1。
這兩條對實(shí)現臨界區至關(guān)重要,第一條保證不會(huì )因為指令的重新排序將臨界區內的代碼調
到臨界區之外(如果臨界區內的指令被重排序放到臨界區之外,What will happen?),
第二條保證在臨界區訪(fǎng)問(wèn)的變量的值,肯定是最新的值,而不是緩存在
寄存器中的值,否則就會(huì )導致奇怪的錯誤。例如下面的代碼:
int del_timer(struct timer_list * timer)
{
int
ret = 0;
if
(timer->next) {
unsigned
long flags;
struct
timer_list * next;
save_flags(flags);
cli();
// 臨界區開(kāi)始
if
((next = timer->next) != NULL) {
(next->prev = timer->prev)->next = next;
timer->next = timer->prev = NULL;
ret = 1;
} // 臨界區結束
restore_flags(flags);
}
return
ret;
}
它先判斷timer->next
的值,如果是空直接返回,無(wú)需進(jìn)行下面的操作。如果不是空,則進(jìn)入臨界區進(jìn)行操作,但是cli()
的實(shí)現(見(jiàn)下面)沒(méi)有使用"memory",timer->next的值可能會(huì )被緩存到寄存器中,
后面if ((next =timer->next) != NULL)會(huì )從寄存器中讀取timer->next的值,如果
在if (timer->next)之后,進(jìn)入臨界區之前,timer->next的值可能被在外部改變,
這時(shí)肯定會(huì )出現異常情況,而且這種情況很難Debug。但是如果cli使用"memory",
那么if ((next = timer->next) !=NULL)語(yǔ)句會(huì )重新從內存讀取timer->next的值,而不會(huì )從寄存器
中取,這樣就不會(huì )出現問(wèn)題啦。
2.4 版內核中cli和sti的代碼如下:
#define __cli()
__asm__
__volatile__("cli": : :"memory")
#define __sti()
__asm__
__volatile__("sti": : :"memory")
通過(guò)上面的例子,讀者應該知道,為什么指令沒(méi)有修改內存,但是卻使用"memory
"修改描述符的原因了吧。應從指令的上下文去理解為什么要這樣做。
使用"volatile"也可以達到這個(gè)目的,但是我們在每個(gè)變量前增加該關(guān)鍵字,
不如使用"memory"方便。
2.4 GCC如何編譯內嵌匯編代碼
GCC 編譯內嵌匯編代碼的步驟如下:
1.輸入變量與占位符
根據限定符和破壞描述部分,為輸入和輸出部分的變量分配合適的寄存器,如果限定符指定為立即數
("i"),或內存變量("m"),則不需要該步驟,如果限定符沒(méi)有具體指定輸入操作數的
類(lèi)型(如"g"),GCC會(huì )視需要決定是否將該操作數輸入到某個(gè)寄存器。這樣每個(gè)占位符都與某個(gè)
寄存器、內存變量或立即數形成了一一對應的關(guān)系。對分配了寄存器的輸入變量需要增加代碼
將它的值讀入寄存器。另外還要根據破壞描述符的部分增加額外代碼。
2.指令模板部分
然后根據這種一一對應的關(guān)系,用這些寄存器、內存變量或立即數來(lái)取代匯編代碼中的占位符。
3.變量輸出
按照輸出限定符的指定將寄存器的內容輸出到某個(gè)內存變量中,如果輸出操作數的限定符指定為內存變量("m"),則該步驟被省略。
3 后記
該文檔參照了Web上的許多與GCC內嵌匯編相關(guān)的文章編寫(xiě)而成,在此表示感謝,
如有問(wèn)題請發(fā)Email至:chforest_chang@hotmail.com 一起討論。
聯(lián)系客服