GCC內嵌彙編
刺蝟@http://blog.csdn.net/littlehedgehog
AT&T手冊裡面的,我整理了下,方便閱讀
核心代碼絕大部分使用C 語言編寫,只有一小部分使用組合語言編寫,例如與特定體繫結構相關的代碼和對效能影響很大的代碼。GCC提供了內嵌彙編的功能,可以在C代碼中直接內嵌組合語言語句,大大方便了程式設計。
簡單的內嵌彙編很容易理解
例如:
__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”表示該運算式需要先放入某個寄存器,然後在指令中使用該寄存器參加運算。
下面來看看一個內嵌彙編的小例子:
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 movl input, %eax
10 #APP GCC插入的注釋,表示內嵌彙編開始
11 movl %eax,%eax 我們的內嵌彙編語句
12 #NO_APP GCC 插入的注釋,表示內嵌彙編結束
13 movl %eax, result 將結果存入result變數
從彙編代碼可以看出,第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”作為限定字元。
由此我們可以總結出來內嵌彙編的格式:
__asm__(
彙編語句模板:
輸出部分:
輸入部分:
破壞描述部分)
共四個部分:彙編語句模板,輸出部分,輸入部分,破壞描述部分,各部分使用“:”格開,彙編語句模板必不可少,其他三部分可選,如果使用了後面的部分,而前面部分為空白,也需要用“:”格開,相應部分內容為空白。例如:
__asm__ __volatile__(
"cli":
:
:"memory")
我們來分別說明:
輸出部分
輸出部分描述輸出運算元,不同的運算元描述符之間用逗號格開,每個運算元描述符由限定字串和C語言變數組成。每個輸出運算元的限定字串必須包含“=”表示他是一個輸出運算元。
例:
__asm__ __volatile__("pushfl ; popl %0 ; cli":"=g" (x) )
描述符字串表示對該變數的限制條件,這樣GCC就可以根據這些條件決定如何分配寄存器,如何產生必要的代碼處理指示運算元與C運算式或C變數之間的聯絡.
輸入部分
輸入部分描述輸入運算元,不同的運算元描述符之間使用逗號格開,每個運算元描述符由限定字串和C語言運算式或者C語言變數組成。
例如:
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的限定字串指定"lr"(下文會有解釋),與立即數或者寄存器相關聯,這樣兩個運算元中只有ADDR為記憶體變數。
限制字元
限制字元有很多種,有些是與特定體繫結構相關,此處僅列出常用的限定字元和i386中可能用到的一些常用的限定符。它們的作用是指示編譯器如何處理其後的C語言變數與指令運算元之間的關係,例如是將變數放在寄存器中還是放在記憶體中等,下表列出了常用的限定字母。
“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
現在繼續看上面的例子:
"=m" (ADDR)表示ADDR為記憶體變數(“m”),而且是輸出變數(“=”);"Ir" (nr)表示nr,為 0-31之間的立即數(“I”)或者一個寄存器運算元(“r”)。
匹配限制符是一位元字 "0"、"1" ....."9" , 分別表示它限制的C運算式分別與預留位置%0,%1,……%9對應的C變數匹配。例如使用“0”作為%1,的限制字元,那麼 %0和%1表示同一個C變數。
還是來看個例子吧:
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變數,所以放在相同的位置,無論是寄存器還是記憶體。
寄存器破壞描述符
通常編寫程式只使用一種語言:進階語言或者組合語言。進階語言編譯的步驟大致如下:
預先處理;
l
編譯
l
彙編
l
連結
我們這裡只關心第二步編譯(將C代碼轉換成彙編代碼):因為所有的代碼都是用進階語言編寫,編譯器可以識別各種語句的作用,在轉換的過程中所有的寄存器都由編譯器決定如何分配使用,它有能力保證寄存器的使用不會衝突;也可以利用寄存器作為變數的緩衝區,因為寄存器的訪問速度比記憶體快很多倍。如果全部使用組合語言則由程式員去控制寄存器的使用,只能靠程式員去保證寄存器使用的正確性。但是如果兩種語言混用情況就變複雜了,因為內嵌的彙編代碼可以直接使用寄存器,而編譯器在轉換的時候並不去檢查內嵌的彙編代碼使用了哪些寄存器(因為很難檢測彙編指令使用了哪些寄存器,例如有些指令隱式修改寄存器,有時內嵌的彙編代碼會調用其他子過程,而子過程也會修改寄存器),因此需要一種機制通知編譯器我們使用了哪些寄存器(程式員自己知道內嵌彙編代碼中使用了哪些寄存器),否則對這些寄存器的使用就有可能導致錯誤,修改描述部分可以起到這種作用。當然內嵌彙編的輸入輸出部分指明的寄存器或者指定為“r”,“g”型由編譯器去分配的寄存器就不需要在破壞描述部分去描述,因為編譯器已經知道了。
下面看個例子就很清楚為什麼需要通知GCC內嵌彙編代碼中隱式(稱它為隱式是因為GCC並不知道)使用的寄存器。
在內嵌的彙編指令中可能會直接引用某些寄存器,我們已經知道AT&T格式的組合語言中,寄存器名以“%”作為首碼,為了在產生的組譯工具中保留這個“%”號,在asm語句中對寄存器的引用必須用“%%”作為寄存器名稱的首碼。原因是“%”在asm,內嵌彙編語句中的作用與“/”在C語言中的作用相同,因此“%%”轉換後代表“%”。
int main(void)
...{
int input, output,temp;
input = 1;
__asm__ __volatile__ ("movl $0, %%eax;
movl %%eax, %1;
movl %2, %%eax;
movl %%eax, %0; "
:"=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;
movl %%eax, %1;
movl %2, %%eax;
movl %%eax, %0; "
:"=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儘可能多的資訊,以防出錯。
本文來自CSDN部落格,轉載請標明出處:http://blog.csdn.net/littlehedgehog/archive/2008/04/08/2259665.aspx