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

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__)表明某個變量
的值可能在外部被改變,因此對這些變量的存取不能緩存到寄存器,每次使用時需要重新存取。
該關鍵字在多線程環境下經常使用,因爲在編寫多線程的程序時,同一個變量可能被多個線程修
改,而程序通過該變量同步各個線程。 

FutureChen
2010-01-15 23:50:27 FutureChen (Life With Youtube And Twitter)

操作數類型 

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

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

FutureChen
2010-01-15 23:51:39 FutureChen (Life With Youtube And Twitter)

GCC 內聯彙編 

有時爲了高效,有時爲了直接控制硬件,有些模塊我們不得不直接用彙編語言來編寫,並且對外提供調用的接口,隱藏細節,這其實就是內聯彙編。如何使用內聯彙編?我們就以 GCC 爲例,一窺其中奧祕! 


一、關鍵字 
如何讓 GCC 知道代碼中內嵌的彙編呢? 藉助關鍵字!來看下面的例子: 

__asm__ __volatile__("hlt"); 

__asm__ 表示後面的代碼爲內嵌彙編,asm 是 __asm__ 的別名。__volatile__ 表示編譯器不要優化代碼,後面的指令保留原樣,volatile 是它的別名。括號裏面是彙編指令。 

二、示例分析 
使用內嵌彙編,要先編寫彙編指令模板,然後將 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 自動處理,我們只需使用限制字符串指導 GCC 如何處理即可。限制字符必須與指令對操作數的要求相匹配,否則產生的彙編代碼將會有錯,讀者可以將上例中的兩個 r,都改爲 m (m表示操作數放在內存,而不是寄存器中),編譯後得到的結果是: 

movl input, result 

很明顯這是一條非法指令,因此限制字符串必須與指令對操作數的要求匹配。例如指令 movl 允許寄存器到寄存器,立即數到寄存器等,但是不允許內存到內存的操作,因此兩個操作數不能同時使用 m 作爲限定字符。 
內嵌彙編語法如下: 

__asm__(彙編語句模板: 輸出部分: 輸入部分: 破壞描述部分) 

共四個部分:彙編語句模板,輸出部分,輸入部分,破壞描述部分,各部分使用“:”格開,彙編語句模板必不可少,其他三部分可選,如果使用了後面的部分,而前面部分爲空,也需要用“:”格開,相應部分內容爲空。例如: 

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

具體這幾部分都有什麼限制呢?這得從細處着手! 


三、語法細節 
1、彙編語句模板 
彙編語句模板由彙編語句序列組成,語句之間使用“;”、“\n” 或 “\n\t” 分開。指令中的操作數可以使用佔位符引用 C 語言變量,操作數佔位符最多10 個,名稱如下:%0,%1,…,%9。指令中使用佔位符表示的操作數,總被視爲 long 型(4個字節),但對其施加的操作根據指令可以是字或者字節,當把操作數當作字或者字節使用時,默認爲低字或者低字節。對字節操作可以顯式的指明是低字節還是次字節。方法是在 % 和序號之間插入一個字母,b 代表低字節,h 代表高字節,例如:%h1。 

2、輸出部分 
輸出部分描述輸出操作數,不同的操作數描述符之間用逗號格開,每個操作數描述符由限定字符串和 C 語言變量組成。每個輸出操作數的限定字符串必須包含“=”表示他是一個輸出操作數。 例如: 

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

描述符字符串表示對該變量的限制條件,這樣 GCC 就可以根據這些條件決定如何分配寄存器,如何產生必要的代碼處理指令操作數與 C 表達式或 C 變量之間的聯繫。 

3、輸入部分 
輸入部分描述輸入操作數,不同的操作數描述符之間使用逗號格開,每個操作數描述符由限定字符串和 C 語言表達式或者 C 語言變量組成。 示例如下: 

例 1 : 
__asm__ __volatile__ ("lidt %0" : : "m" (real_mode_idt)); 

例 2: 
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 爲內存變量。 

4、限制字符 
限制字符有很多種,有些是與特定體系結構相關,此處僅列出常用的限定字符和i386中可能用到的一些常用的限定符。它們的作用是指示編譯器如何處理其後的 C 語言變量與指令操作數之間的關係。 



分類 
限定符 
描述 

通用寄存器 
“a” 
將輸入變量放入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 位的寄存器(use long 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” ... 
也即該操作數就是指定的那個操作數,例如“0” 

“9” 
去描述“%1”操作數,那麼“%1”引用的其實就是“%0”操作數,注意作爲限定符字母的0-9 與指令中的“%0”-“%9”的區別,前者描述操作數, 後者代表操作數。 


該輸出操作數不能使用過和輸入操作數相同的寄存器 

操作數類型 
“=” 
操作數在指令中是隻寫的(輸出操作數) 

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

浮點數 
“f” 
浮點寄存器 

“t” 
第一個浮點寄存器 

“u” 
第二個浮點寄存器 

“G” 
標準的80387浮點常數 


該操作數可以和下一個操作數交換位置,例如addl的兩個操作數可以交換順序(當然兩個操作數都不能是立即數) 


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


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





5、破壞描述部分 
破壞描述符用於通知編譯器我們使用了哪些寄存器或內存,由逗號格開的字符串組成,每個字符串描述一種情況,一般是寄存器名;除寄存器外還有 “memory”。例如:“%eax”,“%ebx”,“memory” 等。

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