1. 程式人生 > >GCC在C語言中內嵌彙編 asm __volatile__

GCC在C語言中內嵌彙編 asm __volatile__

在內嵌彙編中,可以將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<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

3、Memory
     有了上面的知識就不難理解Memory修改描述符了,Memory描述符告知GCC:
     1)不要將該段內嵌彙編指令與前面的指令重新排序;也就是在執行內嵌彙編程式碼之前,它前面的指令都執行完畢
     2)不要將變數快取到暫存器,因為這段程式碼可能會用到記憶體變數,而這些記憶體變數會以不可預知的方式發生改變,因此GCC插入必要的程式碼先將快取到暫存器的變數值寫回記憶體,如果後面又訪問這些變數,需要重新訪問記憶體。

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

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

關於編譯器優化的兩個型別限定詞:volatile和restrict

最近開始學習C語言,想把學習過程中的一些心得記錄下來,權當自己學習經歷中的筆記吧。如果你無意中看到這些文章,能幫我指出其中一些理解不正確的地方,在這裡小弟將萬分感謝。呵呵。
       volatile和restrict這兩個型別限定詞的運用與編譯器的優化存在著一定的關係。volatile這個關鍵字用在變數型別定義上,指明這個變數的值存在不確定因素。也就是說這個變數不光會被我們編寫的程式改變值,也可能會被某個外部代理改變(比如:某個硬體中斷、外部程式等)。這樣就不能保證如果程式沒有改變這個變數值,而又存在多次呼叫後進入暫存器中的值就一定正確。
       從編譯器的優化角度,舉個例子:
       int x=5;
       int a, b;
       a = x;
       b = x;
       由於程式沒有更改X的值,但又存在多次呼叫,編譯器為了優化執行速度,會給a賦值後,把X的值5從記憶體放入到暫存器中。當給b賦值時,不是再次讀取X記憶體地址中的值,而是直接把暫存器中的5賦給b。這一優化對於普通變數沒有問題。但如果定義成 volatile int x;則表明x可以被程式程式碼外的其他代理改變值。如果編譯器也採用這樣的優化,很可能在給b賦值時,x的值已經被程式外部的某個硬體中斷改變了。這樣從暫存器獲取到的值肯定是不正確的。
       因此當給變數加上volatile關鍵字,除了表示這一變數可以被其他代理改變值,也明確說明編譯器不能為此變數進行上面那種方式的優化:每次呼叫這一變數,都從變數的地址中獲取值,而不是暫存器(此變數使用的硬體記憶體地址是與其他並行執行的程式共享資料的,因此不管是程式自身改變變數值,還是其他代理改變變數值,都是改變記憶體地址中的資料)。
      看個有趣的例子:
      int square(volatile int *a)
      {
           return (*a * *a);
      }
      函式的目的本來是計算平方根,但由於a指標用了volatile關鍵字,兩次獲取a指標地址中的值不能完全保證一樣,所以計算出來的結果也未必就是我們需要的。考慮修改成這樣:
      int square(volatile int *a)
      {
           int temp = *a;
           return (temp * temp);
      }

      restrict關鍵字只能用來修飾指標,表示被定義的指標是訪問指標中資料的唯一途徑。這一目的是告訴編譯器可以進行一些優化。看個例子:
      int x = 2;
      int *a = (int *) malloc(sizeof(int));
      *a = 2;
      int *b = &x;
      *a += 2;
      *b += 2;
      x *= 3;
      *a += 3;
      *b += 3; 
     編譯器進行優化時可以用一條語句代替:*a += 5;這對於a來說是正確的,但如果用*b += 5來優化b是不正確的。因為其他變數影響了結果。因此,當編譯器不確定某些因素時,會放棄尋找某個途徑進行優化。如果在變數前加上restrict關鍵字。則告訴編譯器可以“放心大膽”的進行優化。但編譯器並不會驗證你定義為restrict的指標,是否真正是某個資料的唯一訪問途徑;就像陣列的下標越界一樣,如果你不遵守規則,編譯器並不會指出錯誤,但後果由你自己負責:)
     同樣看個有趣的類子:
    void change_array(restrict int *array, const restrict int *value,const int size)
    {
           for(int i=0;i<size;i++)
           {
                  array[i] += *value;
           }
    }

    int main(void)
    {
           int *array[SIZE]  = {1,2,3};

          change_array(array,&array[0],SIZE);

          for(int i=0;i<SIZE;i++)
          {
                printf("%d \n",array[i]);
         }
     }
     如果編譯器支援優化,執行後的結果是:2   3   4   而不是實際正確的結果:2   4   5 。這是在定義函式時,指明兩個指標為restrict,因此編譯器進行優化了:在程式呼叫函式時,將value指標的變數值在暫存器中生成了一個副本。後面的執行都是獲取暫存器上的value值。同時可以看出,當你沒有遵守restrict定義的指標指向的變數只能通過該指標修改的規則時(函式中 value指標指向的資料,在main呼叫時,array指標也進行了修改),編譯器不會檢查。
    對於優化來說,volatile是強制性,而restrict是建議性。也就是加了volatile則強制不進行優化,而加入restrict編譯器也不一定肯定優化。大部分情況下restrict和什麼都不加編譯結果相同,restrict只是告訴編譯器可以自由地做一些相關優化的假定。同時也告訴呼叫者僅使用滿足restrict定義條件的引數,如果你不遵守,嘿嘿。。。

     restrict這個關鍵字是C99標準加入,在C++中不支援,因此我在VC++中加入restrict關鍵字編譯不了:(
     關於restrict的加入,在網上還找到一段小故事:
     為了提高 Cray機器上的效率, ANSI C委員會提出過一種稱為noalias的機制來解決這個問題,用它來說明某個C指標可以認為是沒有別名, 只是這種機制不成熟,這件事激怒了Dennis Ritchie,拿他對C的標準化過程做了唯一的一次干預。他寫了一封公開信說“noalias必須靠邊站,這一點是不能協商的。”  

      後來Cray的Mike Holly又抓起了這個難題,向數值C語言擴充工作組和C++委員會提出了一種改進的反別名建議。所建議的想法是允許程式設計師說明一個指標可以認為是沒有別名的,採用的方式是將它說明為restrict。  這個建議C99採納了,但標準C++拒絕了。