C語言的本質(32)——C語言與彙編之C語言內聯彙編

尹成發表於2014-07-24


用C寫程式比直接用匯編寫程式更簡潔,可讀性更好,但效率可能不如彙編程式,因為C程式畢竟要經由編譯器生成彙編程式碼,儘管現代編譯器的優化已經做得很好了,但還是不如手寫的彙編程式碼。另外,有些平臺相關的指令必須手寫,在C語言中沒有等價的語法,因為C語言的語法和概念是對各種平臺的抽象,而各種平臺特有的一些東西就不會在C語言中出現了,例如x86是埠I/O,而C語言就沒有這個概念,所以in/out指令必須用匯編來寫。

 

C語言簡潔易讀,容易組織規模較大的程式碼,而彙編效率高,而且寫一些特殊指令必須用匯編,為了把這兩方面的好處都佔全了,gcc提供了一種擴充套件語法可以在C程式碼中使用內聯彙編(Inline Assembly)。最簡單的格式是

__asm__("assembly code");

例如

__asm__("nop");

nop 這條指令什麼都不做,只是讓CPU空轉一個指令執行週期。如果需要執行多條彙編指令,則應該用\n\t將各條指令分隔開,例如:

 

__asm__("movl $1, %eax\n\t"
         "movl$4, %ebx\n\t"
         "int$0x80");

通常 C 程式碼中的內聯彙編需要和C的變數建立關聯,需要用到完整的內聯彙編格式:

 

__asm__(assembler template
         :output operands                  /*optional */
         :input operands                   /*optional */
         :list of clobbered registers      /*optional */
         );

 

這種格式由四部分組成,第一部分是彙編指令,和上面的例子一樣,第二部分和第三部分是約束條件,第二部分指示彙編指令的運算結果要輸出到哪些C運算元中,C運算元應該是左值表示式,第三部分指示彙編指令需要從哪些C運算元獲得輸入,第四部分是在彙編指令中被修改過的暫存器列表,指示編譯器哪些暫存器的值在執行這條__asm__語句時會改變。後三個部分都是可選的,如果有就填寫,沒有就空著只寫個:號。例如:

 

#include <stdio.h>
 
int main(void)
{
       int a = 10, b;
 
         __asm__("movl%1, %%eax\n\t"
                   "movl%%eax, %0\n\t"
                   :"=r"(b)        /* output */
                   :"r"(a)         /* input */
                   :"%eax"         /* clobbered register */
                   );
         printf("Result:%d, %d\n", a, b);
         return0;
}

這個程式將變數a的值賦給b。"r"(a)指示編譯器分配一個暫存器儲存變數a的值,作為彙編指令的輸入,也就是指令中的%1(按照約束條件的順序,b對應%0,a對應1%),至於%1究竟代表哪個暫存器則由編譯器自己決定。彙編指令首先把%1所代表的暫存器的值傳給eax(為了和%1這種佔位符區分,eax前面要求加兩個%號),然後把eax的值再傳給%0所代表的暫存器。"=r"(b)就表示把%0所代表的暫存器的值輸出給變數b。在執行這兩條指令的過程中,暫存器eax的值被改變了,所以把"%eax"寫在第四部分,告訴編譯器在執行這條__asm__語句時eax要被改寫,所以在此期間不要用eax儲存其它值。

  

上面的程式完成將變數a的值賦予變數b,有幾點需要說明:

 

1、變數b是輸出運算元,通過%0來引用,而變數a是輸入運算元,通過%1來引用。

2、輸入運算元和輸出運算元都使用r進行約束,表示將變數a和變數b儲存在暫存器中。輸入約束和輸出約束的不同點在於輸出約束多一個約束脩飾符'='。

3、在內聯彙編語句中使用暫存器eax時,暫存器名前應該加兩個'%',即%%eax。內聯彙編中使用%0、%1等來標識變數,任何只帶一個'%'的識別符號都看成是運算元,而不是暫存器。

內聯彙編語句的最後一個部分告訴GCC它將改變暫存器eax中的值,GCC在處理時不應使用該暫存器來儲存任何其它的值。

4、由於變數b被指定成輸出運算元,當內聯彙編語句執行完畢後,它所儲存的值將被更新。

5、在內聯彙編中用到的運算元從輸出部的第一個約束開始編號,序號從0開始,每個約束記數一次,指令部要引用這些運算元時,只需在序號前加上'%'作為字首就可以了。

 

 

我們看一下這個程式的反彙編結果:

 

       __asm__("movl %1, %%eax\n\t"
 80483dc:      8b 55 f8                mov    -0x8(%ebp),%edx
 80483df:      89 d0                   mov    %edx,%eax
 80483e1:      89 c2                   mov    %eax,%edx
 80483e3:      89 55 f4                mov    %edx,-0xc(%ebp)
                "movl %%eax, %0\n\t"
                :"=r"(b)        /* output */
                :"r"(a)         /* input */
                :"%eax"         /* clobbered register */
                );

 

可見%0和%1都代表edx暫存器,首先把變數a(位於ebp-8的位置)的值傳給edx然後執行內聯彙編的兩條指令,然後把edx的值傳給b(位於ebp-12的位置)。

有關內聯彙編,通常情況下只需要瞭解這些就足夠了。

 

 

 

 

 

相關文章