1. 程式人生 > >C語言內聯彙編

C語言內聯彙編

在閱讀linux 原始碼的時候,我們會看到很多C語言內聯彙編的程式碼。下面我們集中看看C語言內聯彙編是怎麼樣的。

首先,我們得想想為什麼會有在C語言裡面內聯彙編的需求。
主要有兩個,一個是我們覺得在被頻繁呼叫的函式,如果使用C寫出來的程式碼,可能執行效率達不到我們的預期,於是我們就使用匯編語言來把這個函式的邏輯實現出來,例如qsort函式;
另一個是我們需要使用某些只能通過彙編指令才能實現的功能。可能有人會問,還有C語言無法實現的功能?這還真的有,例如開中斷和關中斷

#define sti() __asm__ ("sti"::)
#define cli() __asm__ ("cli"::)

顯然就只能使用匯編指令來開中斷和關中斷了。

現在我們來看看內聯彙編的一些規則。
內聯彙編的一般格式如下:

asm [volatile] ( AssemblerTemplate 
                 : [部分1OutputOperands] 
                 [ : 部分2InputOperands
                 [ : 部分3Clobbers ] ])
                 ;

首先! 內聯彙編是一個statement ,也就是一條語句! 因此,一條內聯彙編程式碼後邊,需要跟著一個分號。

asm是關鍵字,告訴編譯器之後緊挨著的第一個小括號內部的就是內聯的彙編程式碼,一般也可以把asm

寫作__asm__
volatile 也是關鍵字,如果寫上volatile表示關閉編譯器對這段彙編程式碼的優化。我們看看gcc官網對 volatile 的解釋:

GCC’s optimizers sometimes discard asm statements if they determine there is no need for the output variables. Also, the optimizers may move code out of loops if they believe that the code will always return the same result (i.e. none of its input values change between calls). Using the volatile qualifier disables these optimizations. asm statements that have no output operands, including asm goto statements, are implicitly volatile.

也就是說,如何gcc編譯器發現一段內聯彙編程式碼的輸出不被使用到,或者它發現在一個迴圈裡面這段程式碼一直返回同一個值,那麼它就會把這段內聯彙編程式碼直接discard. 顯然,對於用一個問題,有千千萬萬種寫法,編譯器只能做一些淺層的優化。當我們的程式碼寫的比較複雜時,它將對我們的程式碼進行錯誤的優化,這是我們不想看到的,因此一般我們會加上這個volatile引數。

接下來是一個 AssemblerTemplate ,這個彙編模板要求是一個包含彙編指令的字串,裡面可以含有一些指向輸入、輸出運算元的佔位符。gcc編譯器通過一定的規則,將模板裡面所有的佔位符替換掉,並將替換後的結果輸入到彙編器中。

因為彙編模板要求是一個字串,那麼如果我們有多條彙編指令,那怎麼辦呢?一個方法是將所有的彙編指令寫在一行,這種方式當然可以,但是程式碼不美觀。另外一種方法是利用C語言裡面相鄰字串可以直接拼接成一個長的字串這條規則,我們可以每一行寫一條彙編指令,然後使用\ 將不同行的彙編指令字串合併起來就可以了。例如一個合法的彙編模板可以是這樣子的:

"mov %0,%1\n\t " \
"mov %1,%2\n\t" 

它其實就是等價於:mov %0,%1\n\t mov %1 ,%2
其中以%開始的%1,%2···加做佔位符,它由下面的輸入輸出運算元來決定。既然% 開始的都是佔位符,那麼我想輸出%怎麼辦呢?比如我寫了這麼一條彙編模板:

"mov %eax,%ebx\n\t"

這是會報錯的,因為gcc把%eax中的eax當做一個佔位符了。此時,我需要使用%%來轉義出%符號
ok,到目前為止,我們可能會對%0,%1,%2···產生疑問,這些佔位符是如何與具體的某個資料產生關聯的?
此時就需要介紹輸出運算元和輸入操作數了。
輸出運算元緊跟著彙編模板,之間隔著一個:號。
輸出運算元的格式為:

[asmSymbolicName1] constraint (cvariablename1),[asmSymbolicName1] constraint (cvariablename1)···

第一部分[asmSymbolicName1]叫做asm符號別名,就是相當於給後面的C語言變數cvariablename1設定一個彙編裡面使用的別名。在彙編指令裡面使用%[別名]來訪問這個變數。
這部分可以省略。另外編譯器預設為內聯彙編的每個輸出、輸入運算元設定一個0,1,2,3,4···的數字別名。按各個操作數出現的次序,依次給這些運算元設定對應序號的數字別名。這些數字別名,在彙編模板裡面使用%數字來訪問。
舉個例子:

int sum(int a,int b)
{
        int rst = 0;
__asm__ volatile("addl %1,%2\n\t"\
        "addl %3,%2\n\t"\
        "mov %2,%[rst]\n\t"\
        "mov %%eax,%2\n\t"
        :[rst]"+r"(rst)
        :"a" (a),
         "b"(b),
        "c"(123456)
        :);
        return rst;
}

輸出運算元有:[rst]"+r"(rst),因此,在彙編模板中%[rst]%0都是與輸出變數rst繫結。
第二部分是一個約束字串。約束字串給出了程式設計師對編譯器在轉換匯編模板時候的一些建議。注意,只是建議。常見約束有https://gcc.gnu.org/onlinedocs/gcc/Simple-Constraints.html#Simple-Constraints
最後一個(cvariablename1) 通過一個括號將所指向的C語言變數指示出來。
整個輸出運算元的描述可以沒有。
下一部分的輸入運算元的原理同輸出部分。
最後一個所謂的破壞域宣告,這個一般可不填,也只是給編譯器提供的建議而已。

例項分析

#define _set_gate(gate_addr,type,dpl,addr) \
__asm__ (
    "movw %%dx,%%ax\n\t" \
	"movw %0,%%dx\n\t" \
	"movl %%eax,%1\n\t" \
	"movl %%edx,%2" \
	: \
	: "i" ((short) (0x8000+(dpl<<13)+(type<<8))), \
	"o" (*((char *) (gate_addr))), \
	"o" (*(4+(char *) (gate_addr))), \
	"d" ((char *) (addr)),"a" (0x00080000))

輸入運算元描述部分:

:
	"i" ((short) (0x8000+(dpl<<13)+(type<<8))), \
	"o" (*((char *) (gate_addr))), \
	"o" (*(4+(char *) (gate_addr))), \
	"d" ((char *) (addr)),"a" (0x00080000))

說明:%0 指向表示式((short) (0x8000+(dpl<<13)+(type<<8))),其約束i表明它是一個立即數;
%1指向表示式*((char *) (gate_addr)),其約束o表示這個表示式的值是一個記憶體地址===>就是會翻譯成一個地址而不是立即數$xxxxxx
%2指向表示式*(4+(char *) (gate_addr)),其約束o表示這個表示式的值也是一個記憶體地址。
%3指向表示式((char *) (addr)),其約束d表示彙編指令的在執行前先將(char*)(addr)的值給edx;
%4指向表示式(0x00080000)), 其約束a表示彙編指令在執行前會先將(4+(char*)(addr))的值給eax;

注意:在AT&T彙編中,立即數和直接記憶體訪問時不同的。
例如:
head.S中

	movl $0x10,%eax		# reload all the segment registers
	mov %ax,%ds		# after changing gdt. CS was already
	mov %ax,%es		# reloaded in 'setup_gdt'
	mov %ax,%fs
	mov %ax,%gs
	lss stack_start,%esp
	xorl %eax,%eax
1:	incl %eax		# check that A20 really IS enabled
	movl %eax,0x000000	# loop forever if it isn't

movl $0x10,%eax中的0x10是立即數,因為前面有個$
movl %eax,0x000000中的0x0000000卻是記憶體偏移量,表示 ds指示的段基址+0x0000000 所指向的記憶體區域。