內嵌匯編


參考1、AT&T匯編語言與GCC內嵌匯編簡介

      2、Professional.Assembly.Language十三章

ARM GCC 內嵌(inline)匯編手冊

 

內嵌匯編語法如下:

__asm__ __volatile__ (

                  匯編語句模板:

                  輸出部分:

                  輸入部分:

                  破壞描述部分

                  );

   匯編語句模板由匯編語句序列組成,語句之間使用“;”、“\n”或“\n\t”分開。指令中的操作數可以使用占位符引用C語言變量,操作數占位符最多10個,名稱如下:%0,%1…,%9。指令中使用占位符表示的操作數,總被視為long型(4個字節),但對其施加的操作根據指令可以是字或者字節,當把操作數當作字或者字節使用時,默認為低字或者低字節。對字節操作可以顯式的指明是低字節還是次字節。方法是在%和序號之間插入一個字母,“b”代表低字節,“h”代表高字節,例如:%h1。

 

“__asm__” 表示后面的代碼為內嵌匯編,“asm”是“__asm__”的別名。

“__volatile__” 表示編譯器不要優化代碼,后面的指令保留原樣,“volatile”是它的別名。括號里面是匯編指令

   C語言關鍵字volatile(注意它是用來修飾變量而不是上面介紹的__volatile__)表明某個變量的值可能在外部被改變,因此對這些變量的存取不能緩存到寄存器,每次使用時需要重新存取。該關鍵字在多線程環境下經常使用,因為在編寫多線程的程序時,同一個變量可能被多個線程修改,而程序通過該變量同步各個線程。

  破壞描述部分內嵌的匯編代碼可以直接使用寄存器,而編譯器在轉換的時候並不去檢查內嵌的匯編代碼使用了哪些寄存器,因此需要一種機制通知編譯器我們使用了哪些寄存器),否則對這些寄存器的使用就有可能導致錯誤,修改描述部分可以起到這種作用。當然內嵌匯編的輸入輸出部分指明的寄存器或者指定為“r”,“g”型由編譯器去分配的寄存器就不需要在破壞描述部分去描述,因為編譯器已經知道了。 

破壞描述部分中的memory描述符告知GCC:

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

 2)不要將變量緩存到寄存器,因為這段代碼可能會用到內存變量,而這些內存變量會以不可預知的方式發生改變,因此GCC插入必要的代碼先將緩存到寄存器的變量值寫回內存,如果后面又訪問這些變量,需要重新訪問內存。如果匯編指令修改了內存,但是GCC 本身卻察覺不到,因為在輸出部分沒有描述,此時就需要在修改描述部分增加“memory”,告訴GCC 內存已經被修改,GCC 得知這個信息后,就會在這段指令之前,插入必要的指令將前面因為優化Cache 到寄存器中的變量值先寫回內存,如果以后又要使用這些變量再重新讀取。第一條保證不會因為指令的重新排序將臨界區內的代碼調到臨界區之外(如果臨界區內的指令被重排序放到臨界區之外,What will happen?),第二條保證在臨界區訪問的變量的值,肯定是最新的值,而不是緩存在寄存器中的值,否則就會導致奇怪的錯誤。

 

1. 使用“r”限制的輸入變量,GCC 先分配一個寄存器,然后將值讀入寄存器,最后用該寄存器替換占位符;

2. 使用“r”限制的輸出變量,GCC會分配一個寄存器,然后用該寄存器替換占位符,但是在使用該寄存器之前並不將變量值先讀入寄存器,GCC認為所有輸出變量以前的值都沒有用處,不讀入寄存器(可能是因為AT&T匯編源於CISC架構處理器的匯編語言,在CISC處理器中大部分指令的輸入輸出明顯分開,而不像RISC那樣一個操作數既做輸入又做輸出,例如add r0,r1,r2,r0 和r1 是輸入,r2 是輸出,輸入和輸出分開,沒有使用輸入輸出型操作數,這樣我們就可以認為r2 對應的操作數原來的值沒有用處,也就沒有必要先將操作數的值讀入r2,因為這是浪費處理器的CPU周期),最后GCC插入代碼,將寄存器的值寫回變量;

3. 輸入變量使用的寄存器在最后一處使用它的指令之后,就可以挪做其他用處,因為已經不再使用。例如上例中的%edx。在執行完addl之后就作為與result對應的寄存器。

 

 

 

  在內嵌的匯編指令中可能會直接引用某些寄存器,我們已經知道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盡可能多的信息,以防出錯。

 

 

&”限制符

  限制符“&”在內核中使用的比較多,它表示輸入和輸出操作數不能使用相同的寄存器,這樣可以避免很多錯誤。舉一個例子,下面代碼的作用是將函數foo的返回值存入變量ret中:

__asm__ ( “call foo;movl %%edx,%1”, :”=a”(ret) : ”r”(bar) );

我們知道函數的int型返回值存放在%eax中,但是gcc編譯的結果是輸入和輸出同時使用了寄存器%eax,如下:

    movl bar, %eax

  #APP

    call foo

    movl %edx,%eax

  #NO_APP

    movl %eax, ret

結果顯然不對,原因是GCC並不知道%eax中的值是我們所要的。避免這種情況的方法是使用“&”限定符,這樣bar就不會再使用%eax寄存器,因為已被ret指定使用。

  __asm__ ( “call foo;movl %%edx,%1”,:”=&a”(ret) : ”r”(bar) );

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM