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, #1AT&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 的開發者手冊,並尋找有用的特性。記得回來分享哦。
好了,這次就到這里吧!
// 本文屬於博主原創,歡迎使用任何形式的轉載。
// 但是必須注明出處,否則必究相關責任。
