內聯彙編

  內聯彙編是指在 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. 第1行是彙編語句,用雙引號引起來, 多條語句用 ; 或者 \n\t 來分隔。

  2. 第2行是輸出操作數,都是 "=?"(var) 的形式, var 可以是任意內存變量(輸出結果會存到這個變量中), ? 一般是下面這些標識符 (表示內聯彙編中用什麼來代理這個操作數):

    • a,b,c,d,S,D 分別代表 eax,ebx,ecx,edx,esi,edi 寄存器
    • r 上面的寄存器的任意一個(誰閒著就用誰)
    • m 內存
    • i 立即數(常量,只用於輸入操作數)
    • g 寄存器、內存、立即數 都行(gcc你看著辦)

    在彙編中用 %序號 來代表這些輸入/輸出操作數, 序號從 0 開始。為了與操作數區分開來, 寄存器用兩個%引出,如:%%eax

  3. 第3行是輸入操作數,都是 "?"(var) 的形式, ? 除了可以是上面的那些標識符,還可以是輸出操作數的序號, 表示用 var 來初始化該輸出操作數, 上面的程序中 %0 和 %1 就是一個東西,初始化為 1(a的值)。

  4. 第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 內核函數檢索


书籍推荐