天天看點

[轉貼]gcc内嵌彙編簡介收藏

在内嵌彙編中,可以将C語言表達式指定為彙編指令的操作數,而且不用去管如何将C語言表達式的值讀入哪個寄存器,以及如何将計算結果寫回C 變量,你隻要告訴程式中C語言表達式與彙編指令操作數之間的對應關系即可, GCC會自動插入代碼完成必要的操作。 

1、簡單的内嵌彙編 

例: 

__asm__ __volatile__("hlt"); "__asm__"表示後面的代碼為内嵌彙編,"asm"是"__asm__"的别名。"__volatile__"表示編譯器不要優化代碼,後面的指令 保留原樣,"volatile"是它的别名。括号裡面是彙編指令。 

2、内嵌彙編舉例 

使用内嵌彙編,要先編寫彙編指令模闆,然後将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)); 

例二(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為記憶體變量。 

4、限制字元 

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 位的寄存器(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"等。 

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

1、編譯器優化介紹 

内 存通路速度遠不及CPU處理速度,為提高機器整體性能,在硬體上引入硬體高速緩存Cache,加速對記憶體的通路。另外在現代CPU中指令的執行并不一定嚴 格按照順序執行,沒有相關性的指令可以亂序執行,以充分利用CPU的指令流水線,提高執行速度。以上是硬體級别的優化。再看軟體一級的優化:一種是在編寫 代碼時由程式員優化,另一種是由編譯器進行優化。編譯器優化常用的方法有:将記憶體變量緩存到寄存器;調整指令順序充分利用CPU指令流水線,常見的是重新 排序讀寫指令。對正常記憶體進行優化的時候,這些優化是透明的,而且效率很好。由編譯器優化或者硬體重新排序引起的問題的解決辦法是在從硬體(或者其他處理 器)的角度看必須以特定順序執行的操作之間設定記憶體屏障(memory barrier),linux 提供了一個宏解決編譯器的執行順序問題。 

void Barrier(void) 

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

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 

3、Memory 

有了上面的知識就不難了解Memory修改描述符了,Memory描述符告知GCC: 

1)不要将該段内嵌彙編指令與前面的指令重新排序;也就是在執行内嵌彙編代碼之前,它前面的指令都執行完畢 

2)不要将變量緩存到寄存器,因為這段代碼可能會用到記憶體變量,而這些記憶體變量會以不可預知的方式發生改變,是以GCC插入必要的代碼先将緩存到寄存器的變量值寫回記憶體,如果後面又通路這些變量,需要重新通路記憶體。 

如 果彙編指令修改了記憶體,但是GCC 本身卻察覺不到,因為在輸出部分沒有描述,此時就需要在修改描述部分增加"memory",告訴GCC 記憶體已經被修改,GCC 得知這個資訊後,就會在這段指令之前,插入必要的指令将前面因為優化Cache 到寄存器中的變量值先寫回記憶體,如果以後又要使用這些變量再重新讀取。 

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

繼續閱讀