GCC在C語言中內嵌彙編-轉載

在內嵌彙編中,能夠將C語言表達式指定爲彙編指令的操做數,並且不用去管如何將C語言表達式的值讀入哪一個寄存器,以及如何將計算結果寫回C 變量,你只要告訴程序中C語言表達式與彙編指令操做數之間的對應關係便可, GCC會自動插入代碼完成必要的操做。

一、簡單的內嵌彙編
例:html

       __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"表示該表達式須要先放入某個寄存器,而後在指令中使用該寄存器參加運算。
   C表達式或者變量與寄存器的關係由GCC自動處理,咱們只需使用限制字符串指導GCC如何處理便可。限制字符必須與指令對操做數的要求相匹配,不然產生的 彙編代碼將會有錯,讀者能夠將上例中的兩個"r",都改成"m"(m表示操做數放在內存,而不是寄存器中),編譯後獲得的結果是:
             movl input, result
很明顯這是一條非法指令,所以限制字符串必須與指令對操做數的要求匹配。例如指令movl容許寄存器到寄存器,當即數到寄存器等,可是不容許內存到內存的操做,所以兩個操做數不能同時使用"m"做爲限定字符。

內嵌彙編語法以下:
       __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語言變量組成。
例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.一、限制字符列表
   限制字符有不少種,有些是與特定體系結構相關,此處僅列出經常使用的限定字符和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的兩個操做數能夠交換順序
                                      (固然兩個操做數都不能是當即數)
                       #                   部分註釋,從該字符到其後的逗號之間全部字母被忽略
                       *                     表示若是選用寄存器,則其後的字母被忽略

五、破壞描述部分
   破壞描述符用於通知編譯器咱們使用了哪些寄存器或內存,由逗號格開的字符串組成,每一個字符串描述一種狀況,通常是寄存器名;除寄存器外還有"memory"。例如:"%eax","%ebx","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 到寄存器中的變量值先寫回內存,若是之後又要使用這些變量再從新讀取。

   使用"volatile"也能夠達到這個目的,可是咱們在每一個變量前增長該關鍵字,不如使用"memory"方便linux

 

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

http://www.diybl.com/course/3_program/c++/cppjs/2008331/107755.htmlc++

 

最近開始學習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++拒絕了。緩存

相關文章
相關標籤/搜索