由於工作的需要,所以花了幾天時間從網上找了不少資料學習了一下GCC內嵌彙編,在此把我所認為比較重要的部分跟大家分享下,同時也在此感謝那些發表GCC內嵌彙編相關文章的作者!在此也希望我整理的資料對需要學習GCC內嵌彙編的朋友有所協助。因為內容比較多,所以我特地把它分為幾個章節來講。
內嵌彙編文法: __asm__(彙編語句模板: 輸出部分: 輸入部分: 破壞描述部分)
共四個部分所組成:彙編語句模板,輸出部分,輸入部分,破壞描述部分,各部分使用“:”格開,彙編語句模板必不可少, 其他三部分可選,如果使用了後面的部分,而前面部分為空白,也需要用“:”格開,相應部分內容為空白。例如:
__asm__ __volatile__("cli": : :"memory")
彙編語句模板
彙編語句模板由彙編語句序列組成,語句之間使用“;” 、“\n”或“\n\t”分開。指令中的運算元可以使用預留位置引用C語言變數, 運算元預留位置最多10個, 名稱如下: %0, %1, …,%9。指令中使用預留位置表示的運算元,總被視為long型(4個位元組) ,但對其施加的操作根據指令可以是字或者位元組,當把運算元當作字或者位元組使用時,預設為低字或者低位元組。對位元組操作可以顯式的指明是低位元組還是次位元組。方法是在%和序號之間插入一個字母, “b”代表低位元組, “h”代表高位元組,例如:%h1。
輸出部分
輸出部分描述輸出運算元,不同的運算元描述符之間用逗號格開,每個運算元描述符由限定字串和 C 語言變數組成。每個輸出運算元的限定字串必須包含“=”表示他是一個輸出運算元。
例:
__asm__ __volatile__("pushfl ; popl %0 ; cli":"=g" (x) )
描述符字串表示對該變數的限制條件, 這樣 GCC 就可以根據這些條件決定如何分配寄存器,如何產生必要的代碼處理指示運算元與C運算式或 C變數之間的聯絡。
輸入部分
輸入部分描述輸入運算元,不同的運算元描述符之間使用逗號格開,每個運算元描述符由限定字串和 C語言運算式或者 C語言變數組成。。
例一:
__asm__ __volatile__ ("lidt %0" : : "m" (real_mode_idt));
例二 :
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為記憶體變數。
破壞描述部分
寄存器破壞描述符
如果代碼用進階語言編寫,編譯器可以識別各種語句的作用,在轉換的過程中所有的寄存器都由編譯器決定如何分配使用, 它有能力保證寄存器的使用不會衝突; 也可以利用寄存器作為變數的緩衝區,因為寄存器的訪問速度比記憶體快很多倍。如果全部使用組合語言則由程式員去控制寄存器的使用,只能靠程式員去保證寄存器使用的正確性。但是如果兩種語言混用情況就變複雜了,因為內嵌的彙編代碼可以直接使用寄存器, 而編譯器在轉換的時候並不去檢查內嵌的彙編代碼使用了哪些寄存器(因為很難檢測彙編指令使用了哪些寄存器,例如有些指令隱式修改寄存器,有時內嵌的彙編代碼會調用其他子過程,而子過程也會修改寄存器) ,因此需要一種機制通知編譯器我們使用了哪些寄存器(程式員自己知道內嵌彙編代碼中使用了哪些寄存器) ,否則對這些寄存器的使用就有可能導致錯誤,修改描述部分可以起到這種作用。當然內嵌彙編的輸入輸出部分指明的寄存器或者指定為“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” 。
memory 破壞描述符
“memory”比較特殊,可能是內嵌彙編中最難懂部分。為解釋清楚它,先介紹一下編譯器的最佳化知識,再看C關鍵字volatile。最後去看該描述符。
編譯器最佳化介紹
記憶體訪問速度遠不及CPU處理速度,為提高機器整體效能,在硬體上引入硬體快取Cache,加速對記憶體的訪問。另外在現代 CPU中指令的執行並不一定嚴格按照順序執行,沒有相關性的指令可以亂序執行,以充分利用 CPU的指令流水線,提高執行速度。以上是硬體層級的最佳化。再看軟體一級的最佳化:一種是在編寫代碼時由程式員最佳化,另一種是由編譯器進行最佳化。編譯器最佳化常用的方法有:將記憶體變數緩衝到寄存器;調整指令順序充分利用CPU指令流水線,常見的是重新排序讀寫指令。對常規記憶體進行最佳化的時候,這些最佳化是透明的,而且效率很好。由編譯器最佳化或者硬體重新排序引起的問題的解決辦法是在從硬體 (或者其他處理器)的角度看必須以特定順序執行的操作之間設定記憶體屏障(memory barrier) ,linux 提供了一個宏解決編譯器的執行順序問題。void Barrier(void) 這個函數通知編譯器插入一個記憶體屏障,但對硬體無效, 編譯後的代碼會把當前 CPU寄存器中的所有修改過的數值存入記憶體,需要這些資料的時候再重新從記憶體中讀出。
C 語言關鍵字volatile
C語言關鍵字volatile(注意它是用來修飾變數而不是上面介紹的__volatile__)表明某個變數的值可能在外部被改變,因此對這些變數的存取不能緩衝到寄存器,每次使用時需要重新存取。該關鍵字在多線程環境下經常使用,因為在編寫多線程的程式時,同一個變數可能被多個線程修改,而程式通過該變數同步各個線程,例如: DWORD __stdcall threadFunc(LPVOID signal)
{
int* intSignal=reinterpret_cast<int*>(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
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的值,而不會從寄存器中取,這樣就不會出現問題啦。