GCC內嵌彙編(一)

 由於工作的需要,所以花了幾天時間從網上找了不少資料學習了一下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的值,而不會從寄存器中取,這樣就不會出現問題啦。

 

 

發佈了36 篇原創文章 · 獲贊 37 · 訪問量 40萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章