歡迎來到Linux教程網
Linux教程網
Linux教程網
Linux教程網
您现在的位置: Linux教程網 >> UnixLinux >  >> Linux編程 >> Linux編程

GNU C 內聯匯編介紹

簡介

1、很早之前就聽說 C 語言能夠直接內嵌匯編指令。但是之前始終沒有去詳細了解過。最近由於某種需求,看到了相關的 C 語言代碼。也就自然去簡單的學習了一下如何在 C 代碼中內嵌匯編指令。


asm/__asm__ 關鍵字

1、總的來說在 C 代碼中我們通過 asm/__asm__ 關鍵字來告訴編譯器將指定的內容當匯編指令處理。廢話不多說,先看個例子:

#include <stdio.h>

int main(int argc, char *argv[])
{
    int x = 3, y = 4;

    __asm__("addl %%ebx, %%eax"
        : "=a" (y)
        : "b" (x), "a" (y));
    
    printf("x + y = %d\n", y);
    return 0;
}

2、這個例子,求兩數之和。將 x 的值加到 y 中,並輸出 y 值。首先來看一下在 C 代碼中插入匯編指令的框架代碼:

__asm__("匯編指令1\n\t"
        "匯編指令2\n\t"
        "匯編指令3\n\t"
        "匯編指令n"
        : 輸出變量列表
        : 輸入變量列表
        : 被破壞的寄存器列表);

匯編指令

1、在 __asm__(); 的“”中,便是編寫匯編指令的地方。利用 C 語言自動連接雙引號的特性,我們可以像框架那樣每一行只寫一條指令,當然你也可以全部寫在一行,那麼需要用 ';' 將不同的指令分開。

2、\n 用於指令換行,\t使 GCC 編譯的時候產生的匯編指令格式保持規范。
GCC 默認使用 AT&T 格式的匯編語法 它與 intel 的匯編語法之間稍有不同。簡單說兩點不同的地方:

  • AT&T 匯編在操作寄存器時需要在前面加一個 '%' 符號,而 intel 的不用。由於在 C 代碼中嵌入匯編時,寫在字符串中,由於 '%' 在 C 語言中是特殊字符,所以為什麼在第一個例子中寄存器前加了兩個 '%'.
  • AT&T 在操作立即數時,需要在立即數前面加 '$',而 intel 卻是 '#'.
  • AT&T 的源與目的與 intel 相反。例如: intel:mov eax, #1 AT&T:movl $1, %eax.

3、這裡只是提到了本文中會見到的一部分差異,更多具體關於 AT&T 匯編的知識,這裡就不再贅述。可參見相關描述 AT&T 匯編的書籍。


輸出變量列表

1、輸出變量列表是描述,在內嵌的匯編指令中將哪些值輸出到 C 代碼環境中的哪個變量中。比如第一個例子中我們指定在執行完了所寫的匯編指令後將 eax 寄存器的值輸出到變量 y 中。
其中 "=a" 指明使用 eax 寄存器為輸出寄存器,輸出到緊跟的變量 (y) 中。

  • = 代表輸出變量用作輸出,原來的值會被新值替換。
  • + 代表即可用作輸入,也可用作輸出。

2、輸出變量列表可以寫多個變量,每個之間使用逗號隔開。例如:: “=a” (x), "=b" (y), "=r" (z)。其中用到的 a, b 等代表相應的寄存器。如下是一部分對應關系。


代碼含義 a 使用寄存器 eax b 使用寄存器 ebx c 使用寄存器 ecx d 使用寄存器 edx S 使用 esi D 使用 edi q 使用動態分配字節可尋址寄存器 r 使用任意動態分配的寄存器 A 使用寄存器 eax 與 edx 聯合 m 使用內存地址 o 使用內存地址並可以加偏移量 I 使用常數 0-31 J 使用常數 0-63 K 使用常數 0-255 M 使用常數 0-3 N 使用一字節常數 0-255

3、這裡僅僅列出了一部分常用到的代碼,更多詳細請參考 GNU C 的 GCC 使用手冊。
這裡講一下 "=r" 的用法,像 a, b 這些代碼都是指定使用的寄存器。但是 r 是讓編譯器隨機給一個,那麼我怎麼知道是那個呢?
不用擔心,編譯器為使用的隨機寄存器遍了一個號。規則是:從輸出列表開始,一直到輸入列表結束,從左到右,從上到下一次為 %0, %1, %2....所以我們可以這樣改寫第一個代碼例子:

#include <stdio.h>

int main(int argc, char *argv[])
{
    int x = 3, y = 4;

    __asm__("addl %1, %0"
        : "=r" (y)
        : "r" (x), "0" (y));
    
    printf("x + y = %d\n", y);
    return 0;
}

輸入變量列表

1、和輸出變量列表一樣,使用的寄存器代碼依然一樣的含義。只是少了 '=' 而已。注意如果一個變量使用 'r' 代碼時,既做輸出,又做輸入的話,在寫輸入變量對應的寄存器時,就寫它在輸出列表裡對應的編號。如上一個例子中 y 既做輸出又做輸入,那麼剛進入匯編指令時,%0的值便為 y 之前的值 4 ,指令結束後 %0 為 7 , 接著又把 %0 輸出到了 y 。


破壞寄存器列表

1、這一行告訴 GCC 在內聯的匯編代碼中,哪些寄存器可能會被使用到(顯式/隱式)。那麼 GCC 就會在進入內聯匯編之前將這些寄存器保存起來,最後再恢復。避免影響到其他的代碼。
早期的 GCC 要求把輸入、輸出用到的寄存器寫到破壞列表裡面。但是現在的編譯器能夠自動保存、恢復在輸出、輸入列表裡面用到的寄存器。因此上述的例子中由於沒有影響到其他非輸出、非輸入的寄存器,所以可以省略破壞列表。

看個栗子:

#include <stdio.h>

char* strcpy(char *dst, const char *src)
{
    __asm__("cld\n"
        "1:\tlodsb\n\t"
        "stosb\n\t"
        "testb %%al, %%al\n\t"
        "jne 1b"
        :
        :"S" (src), "D" (dst)
        :"ax");
    return dst;
}

int main(int argc, char *argv[])
{
    char buf[512];

    strcpy(buf,"Hello,AT&T!");
    printf("%s\n", buf);
    return 0;
}

// 代碼中隱式的使用到了 ax 寄存器,因此我們特別的指明了 ax 為被破壞的寄存器。

GCC 的一些新特性

1、新的 GCC 允許我們為隨機分配的寄存器命名,這樣極大的方便我們編寫內聯匯編代碼。看個例子:

#include <stdio.h>

int main ( int argc , char *argv[] )
{

    int a = 1;
    int b = 2;

    __asm__("addl %[b], %[a]"
        : [a] "=r"(a)
        : [b] "r"(b), "[a]"(a));

    printf ("a = %d\n" , a);
    return 0;
}   

2、其實一看代碼,你就明白,只需要在指明 "=r" , "r" 的前面加上 [name] 之後,便可以在匯編指令裡面直接通過 %[name] 的方式使用相應分配的寄存器了。
我在閱讀 GCC 的使用手冊時,發現了這個特性十分方便,因此在這裡特別提出。當然還有很多新特性,感興趣的讀者可以自行閱讀 GNU GCC 的開發者手冊,並尋找有用的特性。記得回來分享哦。

好了,這次就到這裡吧!

Copyright © Linux教程網 All Rights Reserved