AT&T彙編語言與GCC內嵌彙編簡介

http://lcuc.org.cn/node/7

 

AT&T彙編語言與GCC內嵌彙編簡介


週二, 2006-09-19 11:23 — lostleaf


版本 0.1

時間04/3/30

EMAIL [email protected]


1 AT&T 與INTEL的彙編語言語法的區別

1.1大小寫

1.2操作數賦值方向

1.3前綴

1.4間接尋址語法

1.5後綴

1.6指令


2 GCC內嵌彙編

2.1簡介

2.2內嵌彙編舉例

2.3語法

2.3.1彙編語句模板

2.3.2輸出部分

2.3.3輸入部分

2.3.4限制字符

2.3.5破壞描述部分

2.4GCC如何編譯內嵌彙編代碼


3後記


本節先介紹

AT&T彙編語言語法與INTEL彙編語法的差別,然後介紹GCC內嵌彙編語法。閱讀本節需要讀者具有INTEL彙編語言基礎。


1 AT&T INTEL的彙編語言語法的區別


1.1指令大小寫

INTEL格式的指令使用大寫字母,而AT&T

格式的使用小寫字母。

例:

INTEL AT&T

MOV EAX,EBX movl %ebx,%eax


1.2指令操作數賦值方向


在INTEL語法中,第一個表示目的操作數,第二個表示源操作數,賦值方向從右向左。

AT&T語法第一個爲源操作數,第二個爲目的操作數,方向從左到右,合乎自然。


例:

INTEL AT&T

MOV EAX,EBX movl %ebx,%eax

1.3 指令前綴

在INTEL語法中寄存器和立即數不需要前綴;

AT&T中寄存器需要加前綴"%";立即數需要加前綴"$"。


例:

INTEL AT&T

MOV EAX,1 movl $1,%eax


符號常數直接引用,不需要加前綴,如:

movl value , %ebx

value爲一常數;

在符號前加前綴 $, 表示引用符號地址,

movl $value, %ebx

是將value的地址放到ebx中。


總線鎖定前綴"lock":

總線鎖定操作。"lock"前綴在Linux

核心代碼中使用很多,特別是SMP

代碼中。當總線鎖定後其它CPU

不能存取鎖定地址處的內存單元。


遠程跳轉指令和子過程調用指令的操作碼使用前綴"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 間接尋址語法


INTEL中基地址使用"["、"]",而在AT&T"("、")";

另外處理複雜操作數的語法也不同,

INTEL爲Segreg:[base+index*scale+disp]

,而在AT&T中爲%segreg:disp(base,index,sale),其中segreg

,index,scale,disp都是可選的,在指定index而沒有顯式指定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

語法中大部分指令操作碼的最後一個字母表示操作數大小,"b"表示byte

(一個字節);"w"表示word(2,個字節);"l"表示long(4,個字節)。

INTEL中處理內存操作數時也有類似的語法如:

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彙編指令中,操作數擴展指令有兩個後綴,一個指定源操作數的字長,另一個指定目標操作數的字長。AT&T的符號擴展指令的爲"movs",零擴展指令爲"movz

"(相應的Intel指令爲"movsx"和"movzx")。因此,"movsbl %al,%edx"表示對寄存器al

中的字節數據進行字節到長字的符號擴展,計算結果存放在寄存器edx

中。下面是一些允許的操作數擴展後綴:


l

bl: ,字節>->長字 l

bw: ,字節>->字 l

wl: ,字->長字


跳轉指令標號後的後綴表示跳轉方向,"f"表示向前(forward),

"b,"表示向後(back)。

例:


jmp 1f

jmp 1f


1.6 指令

INTEL彙編與AT&T彙編指令基本相同,差別僅在語法上。關於每條指令的語法可以參考I386Manual。


2 GCC內嵌彙編


2.1 簡介


內核代碼絕大部分使用C

語言編寫,只有一小部分使用彙編語言編寫,例如與特定體系結構相關的代碼和對性能影響很大的代碼。GCC提供了內嵌彙編的功能,可以在C代碼中直接內嵌彙編語言語句,大大方便了程序設計。


簡單的內嵌彙編很容易理解


例:


__asm__

__volatile__("hlt");


"__asm__"表示後面的代碼爲內嵌彙編,"asm"是"__asm__"的別名。

"__volatile__"表示編譯器不要優化代碼,後面的指令保留原樣,

"volatile"是它的別名。括號裏面是彙編指令。


2.2 內嵌彙編舉例

在內嵌彙編中,可以將C語言表達式指定爲彙編指令的操作數,而且不用去管如何將C

語言表達式的值讀入哪個寄存器,以及如何將計算結果寫回C

變量,你只要告訴程序中C語言表達式與彙編指令操作數之間的對應關係即可, GCC

會自動插入代碼完成必要的操作。


使用內嵌彙編,要先編寫彙編指令模板,然後將C語言表達式與指令的操作數相關聯,並告訴GCC對這些操作有哪些限制條件。例如在下面的彙編語句:


__asm__ __violate__

("movl %1,%0" : "=r" (result) : "m" (input));


"movl %1,%0"是指令模板;"%0"和"%1"代表指令的操作數,稱爲佔位符,內嵌彙編靠它們將C

語言表達式與指令操作數相對應。指令模板後面用小括號括起來的是C

語言表達式,本例中只有兩個:"result"和"input",他們按照出現的順序分別與指令操作

數"%0","%1,"對應;注意對應順序:第一個C表達式對應"%0";第二個表達式對應"%1

",依次類推,操作數至多有10個,分別用"%0","%1"…."%9,"表示。在每個操作數前

面有一個用引號括起來的字符串,字符串的內容是對該操作數的限制或者說要求。"result"前面

的限制字符串是"=r",其中"="表示"result"是輸出操作數,"r

"表示需要將"result"與某個通用寄存器相關聯,先將操作數的值讀入寄存器,然後

在指令中使用相應寄存器,而不是"result"本身,當然指令執行完後需要將寄存器中的值

存入變量"result",從表面上看好像是指令直接對"result"進行操作,實際上GCC

做了隱式處理,這樣我們可以少寫一些指令。"input"前面的"r"表示該表達式需要先放入

某個寄存器,然後在指令中使用該寄存器參加運算。


我們將上面的內嵌代碼放到一個C源文件中,然後使用gcc �c�S得到該C

文件源代碼相對應的彙編代碼,然後查看一下彙編代碼,看看GCC是如何處理的。


C源文件如下內容如下,注意該代碼沒有實際意義,僅僅作爲例子。


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語言語句input = 1;

9 input, %eax

10 #APP GCC插入的註釋,表示內嵌彙編開始

11 movl %eax,%eax 我們的內嵌彙編語句

12 #NO_APP GCC 插入的註釋,表示內嵌彙編結束

13 movl %eax, result 將結果存入result變量

14

18

。。。。。。


從彙編代碼可以看出,第9行和第13行是GCC,自動增加的代碼,GCC

根據限定字符串決定如何處理C表達式,本例兩個表達式都被指定爲"r"型,所以先使用指令:

movl input, %eax


將input讀入寄存器%eax;GCC,也指定一個寄存器與輸出變量result

相關,本例也是%eax,等得到操作結果後再使用指令:


movl %eax, result


將寄存器的值寫回C變量result中。從上面的彙編代碼我們可以看出與result

和input,相關連的寄存器都是%eax,GCC使用%eax,替換內嵌彙編指令模板中的

%0,%1


movl %eax,%eax

顯然這一句可以不要。但是沒有優化,所以這一句沒有被去掉。


由此可見,C表達式或者變量與寄存器的關係由GCC自動處理,我們只需使用限制字符串指導GCC

如何處理即可。限制字符必須與指令對操作數的要求相匹配,否則產生的彙編代碼

將會有錯,讀者可以將上例中的兩個"r",都改爲"m"(m,表示操作數放在內存,而不是寄

存器中),編譯後得到的結果是:


movl input, result


很明顯這是一條非法指令,因此限制字符串必須與指令對操作數的要求匹配。例如指令movl

允許寄存器到寄存器,立即數到寄存器等,但是不允許內存到內存的操作,因此兩個操作數

不能同時使用"m"作爲限定字符。


2.3 語法


內嵌彙編語法如下:


__asm__(

彙編語句模板:

輸出部分:

輸入部分:

破壞描述部分)


共四個部分:彙編語句模板,輸出部分,輸入部分,破壞描述部分,各部分使用":"格

開,彙編語句模板必不可少,其他三部分可選,如果使用了後面的部分,而前面部分爲空,

也需要用":"格開,相應部分內容爲空。例如:


__asm__ __volatile__(

"cli":

:

:"memory")


2.3.1 彙編語句模板


彙編語句模板由彙編語句序列組成,語句之間使用";"、"/n"或"/n/t"分開。

指令中的操作數可以使用佔位符引用C語言變量,操作數佔位符最多10個,名稱如下:%0,%1…,%9。

指令中使用佔位符表示的操作數,總被視爲long型(4,個字節),但對其施加的操作

根據指令可以是字或者字節,當把操作數當作字或者字節使用時,默認爲低字或者低字節。

對字節操作可以顯式的指明是低字節還是次字節。方法是在%和序號之間插入一個字母,

"b"代表低字節,"h"代表高字節,例如:%h1。


2.3.2 輸出部分


輸出部分描述輸出操作數,不同的操作數描述符之間用逗號格開,每個操作數描述符由限定字符串和

C語言變量組成。每個輸出操作數的限定字符串必須包含"="表示他是一個輸出操作數。


例:

__asm__ __volatile__("pushfl ; popl %0 ; cli":"=g" (x) )


描述符字符串表示對該變量的限制條件,這樣GCC就可以根據這些條件決定如何

分配寄存器,如何產生必要的代碼處理指令操作數與C表達式或C變量之間的聯繫。


2.3.3 輸入部分


輸入部分描述輸入操作數,不同的操作數描述符之間使用逗號格開,每個操作數描述符由

限定字符串和C語言表達式或者C語言變量組成。


例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。第一個佔位符%0與C,語言變量ADDR

對應,第二個佔位符%1與C,語言變量nr對應。因此上面的彙編語句代碼與下面的僞代碼等價:

btsl nr, ADDR,該指令的兩個操作數不能全是內存變量,因此將nr的限定字符串指定爲"Ir",

將nr,與立即數或者寄存器相關聯,這樣兩個操作數中只有ADDR爲內存變量。


2.3.4 限制字符

2.3.4.1 限制字符列表

限制字符有很多種,有些是與特定體系結構相關,此處僅列出常用的限定字符和i386

中可能用到的一些常用的限定符。它們的作用是指示編譯器如何處理其後的C

語言變量與指令操作數之間的關係,例如是將變量放在寄存器中還是放在內存中等,

下表列出了常用的限定字母。


分類

限定符描述 通用寄存器


"a"將輸入變量放入eax


這裏有一個問題:假設eax已經被使用,那怎麼辦?


其實很簡單:因爲GCC知道eax已經被使用,它在這段彙編代碼的起始處插入一條

語句pushl %eax,將eax內容保存到堆棧,然後在這段代碼結束處再增加一條

語句popl %eax,恢復eax的內容


"b"將輸入變量放入ebx

"c"將輸入變量放入ecx

"d"將輸入變量放入edx

"s"將輸入變量放入esi

"d"將輸入變量放入edi

"q"將輸入變量放入eax,ebx ,ecx ,edx中的一個

"r"將輸入變量放入通用寄存器,也就是eax ,ebx,ecx,edx,esi,edi中的一個

"A"把eax和edx,合成一個64位的寄存器(uselong longs)

"m"內存變量

"o"操作數爲內存變量,但是其尋址方式是偏移量類型,也即是基址尋址,或者是基址加變址尋址

"V"操作數爲內存變量,但尋址方式不是偏移量類型

"," 操作數爲內存變量,但尋址方式爲自動增量

"p"操作數是一個合法的內存地址(指針)


寄存器或內存


"g" 將輸入變量放入eax,ebx,ecx ,edx中的一個或者作爲內存變量

"X"操作數可以是任何類型


立即數

"I" 0-31 之間的立即數(用於32位移位指令)

"J" 0-63 之間的立即數(用於64 位移位指令)

"N" 0-255 ,之間的立即數(用於out 指令)

"i" 立即數

"n" 立即數,有些系統不支持除字以外的立即數,這些系統應該使用"n"而不是"i"


匹配


"0","1 ,"... "9 "


表示用它限制的操作數與某個指定的操作數匹配,也即該操作數就是指定的那個操作數,

例如用"0 "去描述"%1"操作數,那麼"%1"引用的其實就是"%0"操作數,注意作爲

限定符字母的0-9 ,與指令中的"%0"-"%9"的區別,前者描述操作數,後者代表操作數。


後面有詳細描述 & 該輸出操作數不能使用過和輸入操作數相同的寄存器


後面有詳細描述


操作數類型

"=" 操作數在指令中是隻寫的(輸出操作數)

"+" 操作數在指令中是讀寫類型的(輸入輸出操作數)


浮點數

"f"


浮點寄存器

"t"第一個浮點寄存器

"u"第二個浮點寄存器

"G"標準的80387


浮點常數

% 該操作數可以和下一個操作數交換位置


例如addl的兩個操作數可以交換順序(當然兩個操作數都不能是立即數)


# 部分註釋,從該字符到其後的逗號之間所有字母被忽略


* 表示如果選用寄存器,則其後的字母被忽略


現在繼續看上面的例子,

"=m" (ADDR)表示ADDR爲內存變量("m"),而且是輸出變量("=");"Ir" (nr)表示nr,爲

0-31之間的立即數("I")或者一個寄存器操作數("r")。


2.3.4.2

匹配限制符


I386

指令集中許多指令的操作數是讀寫型的(讀寫型操作數指先讀取原來的值然後參加運算,最後

將結果寫回操作數),例如addl %1,%0,它的作用是將操作數%0與操作數%1的和存入操作數%0,

因此操作數%0是讀寫型操作數。老版本的GCC對這種類型操作數的支持不是很好,它將操作數嚴格

分爲輸入和輸出兩種,分別放在輸入部分和輸出部分,而沒有一個單獨部分描述讀寫型操作數,

因此在GCC中讀寫型的操作數需要在輸入和輸出部分分別描述,靠匹配限制符將兩者關聯到一起

注意僅在輸入和輸出部分使用相同的C變量,但是不用匹配限制符,產生的代碼很可能不對,後

面會分析原因。


匹配限制符是一位數字:"0"、"1"……"9,",分別表示它限制的C表達式分別與

佔位符%0,%1,……%9對應的C變量匹配。例如使用"0"作爲%1,的限制字符,那麼

%0和%1表示同一個C,變量。


看一下下面的代碼就知道爲什麼要將讀寫型操作數,分別在輸入和輸出部分加以描述。


該例功能是求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,

的內容已經是input的值。可見對於使用"r"限制的輸入型變量或者表達式,在使用之前GCC會插入

必要的代碼將他們的值讀到寄存器;"m"型變量則不需要這一步。讀入input後執行addl,顯然%eax

的值不對,需要先讀入result的值才行。再往後看:movl %eax,%edx和movl %edx,_result

的作用是將結果存回result,分配給result的寄存器與分配給input的一樣,都是%edx。


綜上可以總結出如下幾點:


1. 使用"r"限制的輸入變量,GCC先分配一個寄存器,然後將值讀入寄存器,最後

用該寄存器替換佔位符;


2. 使用"r"限制的輸出變量,GCC會分配一個寄存器,然後用該寄存器替換佔位符,

但是在使用該寄存器之前並不將變量值先讀入寄存器,GCC認爲所有輸出變量以前的

值都沒有用處,不讀入寄存器(可能是因爲AT&T彙編源於CISC架構處理器的彙編語言

,在CISC處理器中大部分指令的輸入輸出明顯分開,而不像RISC那樣一個操作數既

做輸入又做輸出,例如add r0,r1,r2,r0,和r1是輸入,r2是輸出,輸入和輸出分開,

沒有使用輸入輸出型操作數,這樣我們就可以認爲r2對應的操作數原來的值沒有用處,

也就沒有必要先將操作數的值讀入r2,因爲這是浪費處理器的CPU週期),最後GCC插入代碼,

將寄存器的值寫回變量;


3. 輸入變量使用的寄存器在最後一處使用它的指令之後,就可以挪做其他用處,因爲

已經不再使用。例如上例中的%edx。在執行完addl之後就作爲與result對應的寄存器。


因爲第二條,上面的內嵌彙編指令不能奏效,因此需要在執行addl之前把result的值讀入

寄存器,也許再將result放入輸入部分就可以了(因爲第一條會保證將result

先讀入寄存器)。修改後的指令如下(爲了更容易說明問題將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相關,應該使用同一個

寄存器,但是GCC並不去判斷%0和%1,是否和同一個C表達式或變量相關聯(這樣易於產生與

內嵌彙編相應的彙編代碼),因此%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沒有去判斷兩者是否都與result相關,後面會講GCC如何翻譯內嵌彙編,看完之後

就不會驚奇啦。


使用匹配限制符後,GCC知道應將對應的操作數放在同一個位置(同一個寄存器或者同一個

內存變量)。使用匹配限制字符的代碼如下:


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,代表同一個變量,

輸入部分說明該變量的輸入功能,輸出部分說明該變量的輸出功能,兩者結合表示result

是讀寫型。因爲%0和%1,表示同一個C變量,所以放在相同的位置,無論是寄存器還是內存。


相應的彙編代碼爲:


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相關的寄存器是%edx,在執行指令addl之前先從%edx將result讀入%eax,

執行之後需要將結果從%eax讀入%edx,最後存入result中。這裏我們可以看出GCC

處理內嵌彙編中輸出操作數的一點點信息:addl並沒有使用%edx,可見它不是簡單的用result

對應的寄存器%edx去替換%0,而是先分配一個寄存器,執行運算,最後纔將運算結果存入

對應的變量,因此GCC是先看該佔位符對應的變量的限制符,發現是一個輸出型寄存器變量,

就爲它分配一個寄存器,此時沒有去管對應的C變量,最後GCC,知道還要將寄存器的值寫回變量,

與此同時,它發現該變量與%edx關聯,因此先存入%edx,再存入變量。


至此讀者應該明白了匹配限制符的意義和用法。在新版本的GCC中增加了一個限制字符"+",

它表示操作數是讀寫型的,GCC知道應將變量值先讀入寄存器,然後計算,最後寫回變量,而

無需在輸入部分再去描述該變量。


例;

extern int input,result;


void test_at_t()

{


result = 0;

input = 1;

__asm__

__volatile__ ("addl %1,%0":"+r"(result):"m"(input));


}


此處用"+"替換了"=",而且去掉了輸入部分關於result的描述,產生的彙編代碼如下:

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 "&"限制符


限制符"&"在內核中使用的比較多,它表示輸入和輸出操作數不能使用相同的寄存器,

這樣可以避免很多錯誤。


舉一個例子,下面代碼的作用是將函數foo的返回值存入變量ret中:


__asm__ ( "call foo;movl %%edx,%1", :"=a"(ret) : "r"(bar) );


我們知道函數的int型返回值存放在%eax中,但是gcc編譯的結果是輸入和輸出同時使用了

寄存器%eax,如下:


movl bar, %eax

#APP

call foo

movl %ebx,%eax


#NO_APP

movl %eax, ret


結果顯然不對,原因是GCC並不知道%eax中的值是我們所要的。避免這種情況的方法是使用"&"

限定符,這樣bar就不會再使用%eax寄存器,因爲已被ret指定使用。


_asm__ ( "call foo;movl %%edx,%1",:"=&a"(ret) : "r"(bar) );


2.3.5 破壞描述部分


2.3.5.1 寄存器破壞描述符


通常編寫程序只使用一種語言:高級語言或者彙編語言。高級語言編譯的步驟大致如下:

l

預處理;

l

編譯

l

彙編

l

鏈接


我們這裏只關心第二步編譯(將C代碼轉換成彙編代碼):因爲所有的代碼都是用高級語言編寫,

編譯器可以識別各種語句的作用,在轉換的過程中所有的寄存器都由編譯器決定如何分配使用,

它有能力保證寄存器的使用不會衝突;也可以利用寄存器作爲變量的緩衝區,因爲寄存器的訪問

速度比內存快很多倍。如果全部使用彙編語言則由程序員去控制寄存器的使用,只能靠程序員去

保證寄存器使用的正確性。但是如果兩種語言混用情況就變複雜了,因爲內嵌的彙編代碼可以直接

使用寄存器,而編譯器在轉換的時候並不去檢查內嵌的彙編代碼使用了哪些寄存器(因爲很難檢測

彙編指令使用了哪些寄存器,例如有些指令隱式修改寄存器,有時內嵌的彙編代碼會調用其他子過程,

而子過程也會修改寄存器),因此需要一種機制通知編譯器我們使用了哪些寄存器(程序員自己知道

內嵌彙編代碼中使用了哪些寄存器),否則對這些寄存器的使用就有可能導致錯誤,修改描述部分

可以起到這種作用。當然內嵌彙編的輸入輸出部分指明的寄存器或者指定爲"r","g"型由編譯器

去分配的寄存器就不需要在破壞描述部分去描述,因爲編譯器已經知道了。


破壞描述符由逗號格開的字符串組成,每個字符串描述一種情況,一般是寄存器名;除寄存器外

還有"memory"。例如:"%eax","%ebx","memory"等。


下面看個例子就很清楚爲什麼需要通知GCC內嵌彙編代碼中隱式(稱它爲隱式是因爲GCC並不知道)

使用的寄存器。


在內嵌的彙編指令中可能會直接引用某些寄存器,我們已經知道AT&T格式的彙編語言中,寄存器

名以"%"作爲前綴,爲了在生成的彙編程序中保留這個"%"號,在asm語句中對寄存器的

引用必須用"%%"作爲寄存器名稱的前綴。原因是"%"在asm,內嵌彙編語句中的作用與"/"在C

語言中的作用相同,因此"%%"轉換後代表"%"。


例(沒有使用修改描述符):


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作爲臨時寄存器,功能相當於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,發生了衝突,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


通過破壞描述部分,GCC得知%eax已被使用,因此給input分配了%edx。在使用內嵌彙編時請記

住一點:儘量告訴GCC儘可能多的信息,以防出錯。


如果你使用的指令會改變CPU的條件寄存器cc,需要在修改描述部分增加"cc"。


2.3.5.2 memory破壞描述符


"memory"比較特殊,可能是內嵌彙編中最難懂部分。爲解釋清楚它,先介紹一下編譯器的優化知識,再看C關鍵字volatile。最後去看該描述符。


2.3.5.2.1 編譯器優化介紹


內存訪問速度遠不及CPU處理速度,爲提高機器整體性能,在硬件上引入硬件高速緩存Cache,

加速對內存的訪問。另外在現代CPU中指令的執行並不一定嚴格按照順序執行,沒有相關性的指令可以亂序執行,以充分利用CPU的指令流水線,提高執行速度。以上是硬件級別的優化。

再看軟件一級的優化:一種是在編寫代碼時由程序員優化,另一種是由編譯器進行優化。編譯器優化常用的方法有:將內存變量緩存到寄存器;調整指令順序充分利用CPU指令流水線,常見的是重新排序讀寫指令。


對常規內存進行優化的時候,這些優化是透明的,而且效率很好。由編譯器優化或者硬件重新排序引起的問題的解決辦法是在從硬件(或者其他處理器)的角度看必須以特定順序執行的操作之間設置內存屏障(memory barrier),linux提供了一個宏解決編譯器的執行順序問題。


void Barrier(void)


這個函數通知編譯器插入一個內存屏障,但對硬件無效,編譯後的代碼會把當前CPU寄存器中的所有修改過的數值存入內存,需要這些數據的時候再重新從內存中讀出。


2.3.5.2.2 C 語言關鍵字volatile


C 語言關鍵字volatile(注意它是用來修飾變量而不是上面介紹的__volatile__)表明某個變量的值可能在外部被改變,因此對這些變量的存取不能緩存到寄存器,每次使用時需要重新存取。

該關鍵字在多線程環境下經常使用,因爲在編寫多線程的程序時,同一個變量可能被多個線程修改,而程序通過該變量同步各個線程,例如:


DWORD __stdcall threadFunc(LPVOID signal)

{


int* intSignal=reinterpret_cast(signal);


*intSignal=2;


while(*intSignal!=1)

sleep(1000);


return 0;


}


該線程啓動時將intSignal置爲2,然後循環等待直到intSignal爲1,時退出。顯然intSignal的值必須在外部被改變,否則該線程不會退出。但是實際運行的時候該線程卻不會退出,即使在外部將它的值改爲1,看一下對應的僞彙編代碼就明白了:


mov ax,signal

label:

if(ax!=1)

goto label


對於C編譯器來說,它並不知道這個值會被其他線程修改。自然就把它cache在寄存器裏面。記住,C

編譯器是沒有線程概念的!這時候就需要用到volatile。volatile的本意是指:這個值可能會在

當前線程外部被改變。也就是說,我們要在threadFunc中的intSignal前面加上volatile

關鍵字,這時候,編譯器知道該變量的值會在外部改變,因此每次訪問該變量時會重新讀取,所作

的循環變爲如下面僞碼所示:


label:

mov ax,signal

if(ax!=1)

goto label


2.3.5.2.3 Memory


有了上面的知識就不難理解Memory

修改描述符了,Memory描述符告知GCC:

(1)不要將該段內嵌彙編指令與前面的指令重新排序;也就是在執行內嵌彙編代碼之前,

它前面的指令都執行完畢。


(2)不要將變量緩存到寄存器,因爲這段代碼可能會用到內存變量,而這些內存變量會

以不可預知的方式發生改變,因此GCC插入必要的代碼先將緩存到寄存器的變量值寫回內存,

如果後面又訪問這些變量,需要重新訪問內存。


如果彙編指令修改了內存,但是GCC本身卻察覺不到,因爲在輸出部分沒有描述,

此時就需要在修改描述部分增加"memory",告訴GCC內存已經被修改,GCC得知這個信息後,

就會在這段指令之前,插入必要的指令將前面因爲優化Cache到寄存器中的變量值先寫回內存,

如果以後又要使用這些變量再重新讀取。


例:

………..

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] 已經修改,所以重新讀取

a=test[0];


……


這段代碼中的彙編指令功能與

memset

相當,也就是相當於調用了memset(test,0,100);它使用stosb修改了test

數組的內容,但是沒有在輸入或輸出部分去描述操作數,因爲這兩條指令都不需要

顯式的指定操作數,因此需要增加"memory"通知GCC。現在假設:GCC在優化時將test[0]

放到了%eax寄存器,那麼test[0] = 1對應於%eax=1,a = test [0]被換爲a=%eax

,如果在那段彙編指令中不使用"memory",Gcc,不知道現在test[0]

的值已經被改變了(如果整段代碼都是我們自己使用匯編編寫,我們自己當然知道

這些內存的修改情況,我們也可以人爲的去優化,但是現在除了我們編寫的那一小段外,

其他彙編代碼都是GCC

生成的,它並沒有那麼智能,知道這段代碼會修改test[0]),結果其後的a=test[0]

,轉換爲彙編後卻是a=%eax,因爲GCC不知道顯式的改變了test數組,結果出錯了。

如果增加了"memory"修飾符,GCC知道:

"這段代碼修改了內存,但是也僅此而已,它並不知道到底修改了哪些變量",

因此他將以前因優化而緩存到寄存器的變量值全部寫回內存,從內嵌彙編開始,如果後面

的代碼又要存取這些變量,則重新存取內存(不會將讀寫操作映射到以前緩存的那個寄存器)。

這樣上面那段代碼最後一句就不再是%eax=1,而是test[0] = 1。


這兩條對實現臨界區至關重要,第一條保證不會因爲指令的重新排序將臨界區內的代碼調

到臨界區之外(如果臨界區內的指令被重排序放到臨界區之外,What will happen?),

第二條保證在臨界區訪問的變量的值,肯定是最新的值,而不是緩存在

寄存器中的值,否則就會導致奇怪的錯誤。例如下面的代碼:


int del_timer(struct timer_list * timer)

{

int

ret = 0;

if

(timer->next) {

unsigned

long flags;

struct

timer_list * next;

save_flags(flags);

cli();


// 臨界區開始

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

的值,如果是空直接返回,無需進行下面的操作。如果不是空,則進入臨界區進行操作,但是cli()

的實現(見下面)沒有使用"memory",timer->next的值可能會被緩存到寄存器中,

後面if ((next =timer->next) != NULL)會從寄存器中讀取timer->next的值,如果在if (timer->next)之後,進入臨界區之前,timer->next的值可能被在外部改變,

這時肯定會出現異常情況,而且這種情況很難Debug。但是如果cli使用"memory",那麼if ((next = timer->next) !=NULL)語句會重新從內存讀取timer->next的值,而不會從寄存器中取,這樣就不會出現問題啦。


2.4 版內核中cli和sti的代碼如下:

#define __cli()

__asm__

__volatile__("cli": : :"memory")

#define __sti()

__asm__

__volatile__("sti": : :"memory")


通過上面的例子,讀者應該知道,爲什麼指令沒有修改內存,但是卻使用"memory

"修改描述符的原因了吧。應從指令的上下文去理解爲什麼要這樣做。


使用"volatile"也可以達到這個目的,但是我們在每個變量前增加該關鍵字,

不如使用"memory"方便。


2.4 GCC如何編譯內嵌彙編代碼


GCC 編譯內嵌彙編代碼的步驟如下:


1.輸入變量與佔位符


根據限定符和破壞描述部分,爲輸入和輸出部分的變量分配合適的寄存器,如果限定符指定爲立即數

("i"),或內存變量("m"),則不需要該步驟,如果限定符沒有具體指定輸入操作數的

類型(如"g"),GCC會視需要決定是否將該操作數輸入到某個寄存器。這樣每個佔位符都與某個

寄存器、內存變量或立即數形成了一一對應的關係。對分配了寄存器的輸入變量需要增加代碼

將它的值讀入寄存器。另外還要根據破壞描述符的部分增加額外代碼。


2.指令模板部分

然後根據這種一一對應的關係,用這些寄存器、內存變量或立即數來取代彙編代碼中的佔位符。


3.變量輸出


按照輸出限定符的指定將寄存器的內容輸出到某個內存變量中,如果輸出操作數的限定符指定爲內存變量("m"),則該步驟被省略。


3 後記


該文檔參照了Web上的許多與GCC內嵌彙編相關的文章編寫而成,在此表示感謝,

如有問題請發Email至:[email protected] 一起討論。


感謝作者!

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