內聯彙編
內聯彙編是指在 C/C++ 代碼中嵌入的彙編代碼, 與全部是彙編的彙編源文件不同,它們被嵌入到 C/C++ 的大環境中。
一、gcc 內聯彙編
gcc 內聯彙編的格式如下:
asm ( 彙編語句
: 輸出操作數 // 非必需
: 輸入操作數 // 非必需
: 其他被汙染的寄存器 // 非必需
);
我們通過一個簡單的例子來瞭解一下它的格式(gcc_add.c):
#include <stdio.h>
int main()
{
int a=1, b=2, c=0;
// 蛋疼的 add 操作
asm(
"addl %2, %0" // 1
: "=g"(c) // 2
: "0"(a), "g"(b) // 3
: "memory"); // 4
printf("現在c是:%d\n", c);
return 0;
}
內聯彙編中:
- 第1行是彙編語句,用雙引號引起來, 多條語句用 ; 或者 \n\t 來分隔。
第2行是輸出操作數,都是 "=?"(var) 的形式, var 可以是任意內存變量(輸出結果會存到這個變量中), ? 一般是下面這些標識符 (表示內聯彙編中用什麼來代理這個操作數):
- a,b,c,d,S,D 分別代表 eax,ebx,ecx,edx,esi,edi 寄存器
- r 上面的寄存器的任意一個(誰閒著就用誰)
- m 內存
- i 立即數(常量,只用於輸入操作數)
g 寄存器、內存、立即數 都行(gcc你看著辦)
在彙編中用 %序號 來代表這些輸入/輸出操作數, 序號從 0 開始。為了與操作數區分開來, 寄存器用兩個%引出,如:%%eax
- 第3行是輸入操作數,都是 "?"(var) 的形式, ? 除了可以是上面的那些標識符,還可以是輸出操作數的序號, 表示用 var 來初始化該輸出操作數, 上面的程序中 %0 和 %1 就是一個東西,初始化為 1(a的值)。
- 第4行標出那些在彙編代碼中修改了的、 又沒有在輸入/輸出列表中列出的寄存器, 這樣 gcc 就不會擅自使用這些"危險的"寄存器。 還可以用 "memory" 表示在內聯彙編中修改了內存, 之前緩存在寄存器中的內存變量需要重新讀取。
上面這一段內聯彙編的效果就是, 把a與b的和存入了c。當然這只是一個示例程序, 誰要真這麼用就蛋疼了, 內聯彙編一般在不得不用的情況下才使用。
二、VC 內聯彙編
gcc 內聯彙編被設計得很複雜,初學者看了往往頭大, 而 VC 的內聯彙編就簡單多了:
__asm{
彙編語句
}
一個例子程序如下(vc_add.c):
#include <stdio.h>
int main()
{
int a=1, b=2, c=0;
// 蛋疼的 add 操作
__asm{
push eax // 保護 eax
mov eax, a // eax = a;
add eax, b // eax = eax + b;
mov c, eax // c = eax;
pop eax // 恢復 eax
}
printf("現在c是:%d\n", c);
return 0;
}
VC 的內聯彙編中可以直接以變量名的形式使用局部變量, 這就方便多了。但是, VC 內聯彙編中有些變量名是保留的,比如:size, 使用這些變量名就會報錯(把b改成size, 上面的程序就編譯不通過了)。所以,起名字一定要小心!
因為 VC 沒有輸入/輸出操作數列表, 它也不看你的彙編代碼(直接拿去用), 所以它不知道你修改了哪些寄存器, 這些要修改的寄存器可能保存著重要數據, 所以用 push/pop 來 保護/恢復 要修改的寄存器。 而 gcc 就不需要,它能從輸入/輸出列表中獲得豐富的信息 來調劑各個寄存器的使用, 並進行優化,所以從效率上說 VC 完敗!
三、為什麼用內聯彙編
用內聯彙編的主要目的是為了提高效率: 假設有一個比較文本差異的程序 diff, 它花了 99% 的時間在 strcmp 這個函數上, 如果用內聯彙編實現的一個高效的 strcmp 比用 C 語言實現的快 1 倍,那麼專家花在這個小小函數上的心思就能夠將整個程序的效率 提高差不多 1 倍,這是很值得去做的"斤斤計較"。
還有一個目的就是為了實現 C 語言無法實現的部分, 比如說 IO 操作,還有我們上一篇中提到的自主修改 esp 寄存器 也是必須用匯編才能實現的。
四、memcpy
學 gcc 內聯彙編最好的導師莫過於 linux 內核, 有很多常用的小函數如 memcpy、strlen、strcpy、…… 其中都有短小精悍的內聯彙編版本, 如在 linux 2.6.37 中的 memcpy 函數:
// 位於 /arch/x86/boot/compressed/misc.c
void *memcpy(void *dest, const void *src, size_t n)
{
int d0, d1, d2;
asm volatile(
"rep ; movsl\n\t"
"movl %4,%%ecx\n\t"
"rep ; movsb\n\t"
: "=&c" (d0), "=&D" (d1), "=&S" (d2)
: "0" (n >> 2), "g" (n & 3), "1" (dest), "2" (src)
: "memory");
return dest;
}
與 gcc_add.c 相比,這個函數要複雜不少:
- 關鍵字 volatile 是告訴 gcc 不要嘗試去移動、 刪除這段內聯彙編。
- rep ; movsl 的工作流程如下:
while(ecx) {
movl (%esi), (%edi);
esi += 4;
edi += 4;
ecx--;
}
rep ; movsb 與此類似,只是每次拷貝的不是雙字(4字節), 而是字節。
- "=&D" (d1) 不是想將 edi 的最終值輸出到 d1 中, 而是想告訴 gcc edi的值早就改了, 不要認為它的值還是初始化時的 dest, 避免"吝嗇的" gcc 把修改了的 edi 還當做 dest 來用。 而 d0、d1、d2 在開啟優化後會被 gcc 無視掉 (輸出到它們的值沒有被用過)。
memcpy 先複製一個一個的雙字, 到最後如果還有沒複製完的(少於4個字節), 再一個一個字節地複製。 我最終實現的 d_printf 就模仿了這個函數。
深入研究:
gcc 內聯彙編 HOWTO 文檔
Linux Cross Reference——各版本 linux 內核函數檢索