清華OS前置知識之彙編

ucore用到的AT&T格式的彙編與Intel格式的彙編在語法層面的不同

* 寄存器命名原則
	AT&T:%eax						Intel:eax
* 源/目的操作數順序
	AT&T:movl %eax, %ebx			Intel:mov ebx, eax
	AT&T:movl $0xff,%ebx			Intel:mov ebx,0ffh
	注:AT&T中表示16進制數用0x
* $表示一個常數/立即數
	AT&T:movl $_value, %ebx			Intel:mov ebx, _value
	para = 0x04 
	movl $para, %ebx 結果是將立即數0x04裝入寄存器%ebx
  例:把地址放入ebx寄存器
	AT&T:movl $0xd00d, %ebx			Intel:mov ebx, 0xd00d
* 操作數長度標識,操作數的字長由操作符的最後一個字母決定 b,w,l
	AT&T:movw %ax, %bx 				Intel:mov bx,ax
* 遠程轉移指令和遠程子調用指令的操作碼,在 AT&T 彙編格式中爲 "ljmp" 和 "lcall"
* 尋址方式
	AT&T:	immed32(basepointer, indexpointer, indexscale)	
    	    =imm32 + basepointer + indexpointer x indexscale
	Intel:	[basepointer + indexpointer x indexscale + imm32]

在ucore中,由於 Linux 工作在保護模式下,用的是 32 位線性地址,所以在計算地址時不用考慮段基址和偏移量,即

immed32(basepointer, indexpointer, indexscale)	
=imm32 + basepointer + indexpointer x indexscale

例子

* 直接尋址
	AT&T:_foo					Intel:[_foo]
	_foo是一個全局變量,表示一個立即數,加上$,$_foo引用_foo本身的值
	不加$,_foo的值作爲一個地址對內存進行尋址,並引用相關地址上存儲着的值
* 寄存器間接尋址
	AT&T:(%eax)					Intel:[eax]
* 變址尋址
	AT&T:_variable(%eax)		Intel:[eax + _variable]
	AT&T:_array( ,%eax,4)		Intel:[eax x 4 + _array]
	AT&T:_array(%ebx,%eax,8)	Intel:[ebx + eax x 8 + _array]

GCC基本內聯彙編

格式:

asm("statements");

例:

asm("nop");
asm("cli");
"asm"和"__asm__"的含義一樣

多行彙編時,每一行都加上"\n\t"(換行符和tab符),是爲了讓gcc把內聯彙編代碼翻譯成一般的彙編代碼時保證換行和留有一定的空格,因爲gcc處理彙編時,要把asm(…)中的內容"打印"到彙編文件中,所以格式控制符是必要的

對於基本asm語句,GCC編譯出來的就是雙引號中的內容,如:

asm( "pushl %eax\n\t"
	 "movl $0,%eax\n\t"
	 "popl %eax"
);

下例中,我們在內聯彙編中改變了edx和ebx的值,但gcc的處理方法特殊:先形成彙編文件,再交給彙編器GAS(GNU ASM,功能是將彙編代碼轉換成二進制形式的目標代碼)去彙編,即編譯器實際上把asm括號中雙引號引起來的語句逐字的插入到編譯後產生的.s彙編文件中,並進行了相應的替換操作

所以GAS並不知道我們已經改變了edx和ebx的值,如果程序的上下文需要用到edx和ebx來暫存數據(程序的上下文是c語言,不能保證不會用到這兩個寄存器),就會產生無法預料的多次賦值,會出現錯誤,變量_boo也有這樣的問題,這就需要擴展GCC內聯彙編

asm( "movl %eax, %ebx");
asm( "xorl %ebx, %edx");
asm( "movl $0, _boo");

GCC擴展內聯彙編

基本格式,共四部分

asm [volatile] ( Assembler Template
	: Output Operands
	: Input Operands
	: Clobbers );

彙編代碼:內聯彙編代碼,語法同基本asm格式。變量用佔位符表示。

輸出位置:彙編代碼中輸出值所在的寄存器和內存單元列表。(執行彙編代碼之後)

輸入操作符:彙編代碼中輸入值所在的寄存器和內存單元的列表。(執行彙編代碼前處理)

改變的寄存器:內聯彙編代碼中改變的寄存器(用於提醒編譯器對這些寄存器進行保護操作)

舉例:

#define read_cr0() ({ 
	unsigned int __dummy; 
	__asm__( 
		"movl %%cr0,%0\n\t" 
		:"=r" (__dummy)); 
	__dummy; 
})

__asm__表示彙編代碼的開始,其後可以跟__volatile__(這是可選項)其含義是避免“asm”指令被刪除、移動或組合,指向代碼時,如果不希望彙編語句被gcc優化而改變位置。就添加該關鍵詞,如下

asm volatile(…)
或者更詳細的
__asm__ __volatile__(…)

(……)中的部分就是具體的內聯彙編指令代碼,"……"中的爲彙編指令部分

彙編指令中數字前加前綴%(稱爲佔位符),如%1,%2,用來表示輸出輸入列表中的變量,表示使用寄存器的樣板操作數,可以使用的操作數總數取決於具體CPU中通用寄存器的數量,如Intel有8個,指令中有幾個操作數,說明有幾個變量需要與寄存器結合,gcc編譯時根據後面輸出部分和輸入部分的約束條件進行相應處理,由於樣板操作數的前綴使用了%,所以在用到具體寄存器時前面加兩個%,如%%cr0

佔位符%n的用法:%0 %1 %2表示從輸出部分開始出現的第1、2、3個變量

舉例:

#include <stdio.h>
int main(void){
    int xa=6;
    int xb=2;
    int result;
   	// 使用佔位符,由r表示,編譯器自主選擇使用哪些寄存器,%0,%1。。。表示第1、2。。。個變量
    // %0對應變量result
    // %1對應變量xa
    // %2對應變量xb
    asm volatile(
   			"add %1,%2\n\t" 
    		"movl %2,%0"
     :"=r"(result)
     :"r"(xa),"r"(xb)
    );    
    
    printf("%d\n",result);
    return 0;
}

下圖中%0對應變量_out,變量_out指定的寄存器爲%eax,所以編譯器會將佔位符替換爲%eax

031414093142799

引用佔位符

#include <stdio.h>
int main(void){
    int xa=6;
    int xb=2;
    
    asm volatile(
    		"add %1,%0\n\t" 
     :"=r"(xb)
     :"r"(xa),"0"(xb)	//"0"(xb)爲引用佔位符,表示使用第一個命令的寄存器存放xb輸出值
    );
    
    printf("%d\n",xb);
    return 0;
}

輸出部分用來規定對輸出變量(目標操作數)如何與寄存器結合的約束,可以有多個約束,以逗號分開,每個約束以"="開頭,接着一個字母表示操作數的類型,然後時變量結合的約束,如

"=r"表示相應的目標操作數可以使用任何一個通用寄存器
:"=r" (__dummy) 表示變量__dummy可以使用任何一個通用寄存器

如果是

"=m"表示相應的目標操作數是存放在內存單元中
:"=m"(__dummy) 表示變量__dummy使用內存單元來保存值

各個約束條件字母及其含義

字母
m,v,o 內存單元
R 任何通用寄存器
Q 寄存器eax,ebx,ecx,edx之一
l,h 直接操作數
E,F 浮點數
G 任意
a,b,c,d 寄存器eax/ax/al,ebx/bx/bl,ecx/cx/cl,edx/dx/dl
S,D 寄存器esi或edi
I 常數(0~31)

輸入部分與輸出部分相似,但沒有"=",如果輸入部分一個操作數所要求使用的寄存器與前面輸出部分某個約束所要求的是同一個寄存器,就把對應操作數的編號(如"1",“2”)放在約束條件中

修改部分(也稱亂碼列表)如果想通知gcc當前內嵌彙編語句可能會對某些寄存器或內存進行修改,希望gcc在編譯時能夠考慮到,那麼我們就在修改部分聲明這些寄存器或內存

若要修改內存,用"memory"聲明,表示操作完成後內存中的內容已有改變,如果原來某個寄存器的內容來自內存,那麼現在內存中這個單元的內容已經改變,亂碼列表通知編譯器,有些寄存器或內存因內聯彙編塊造成亂碼,可隱式地破壞了條件寄存器的某些位

若要修改寄存器,只需要將寄存器的名字用雙引號括起來就可以了;如果要聲明多個寄存器,則相鄰兩個寄存器名字之間用逗號隔開

另外,因爲你在輸入/輸出部分的操作表達式中指定寄存器,或當你爲一些輸入/輸出操作表達式使用“r”/“g”約束,讓GCC爲你選擇一個寄存器時,GCC對這些寄存器的狀態是非常清楚的,它知道這些寄存器是被修改的,你根本不需要在修改部分聲明它們

注:指令部分必須有輸入部分,輸出部分、修改部分爲可選,輸入部分存在而輸出部分不存在時,冒號“:”要保留,當"memory"存在時,三個冒號都要保留,如

#define __cli() __asm__ __volatile__("cli": : :"memory")
int count=1
int value=1
int buf[10]
void main()
{
    asm(
    	"cld \n\t"
    	"rep \n\t"
    	"stosl"
    :
    : "c" (count), "a" (value), "D" (buf)
    );
}

得到的主要彙編代碼爲

# 根據寄存器約束爲變量選擇合適的寄存器
movl count,%ecx	
movl value,%eax
movl buf,%edi
#APP
cld
rep
stosl
#NO_APP

cld,rep,stos這幾條的功能是向buff中寫上count個value值,通過冒號後的語句指明輸入,輸出和被改變的寄存器,編譯器就知道你的指令需要和改變哪些寄存器,從而優化寄存器的分配

符號"c"(count)指示把count的值放入ecx寄存器,系統會根據變量的寬度分配8/16/32位的寄存器,類似還有

a eax
b ebx
c ecx
d edx
S esi
D edi
I 常數值(0-31)
q,r 動態分配的寄存器
	q指示編譯器從eax、ebx、ecx、edx分配寄存器
	r指示編譯器從eax、ebx、ecx、edx、esi、edi分配寄存器
g eax,ebx,ecx,edx或內存變量
A 把eax和edx合成一個64位的寄存器

也可以讓gcc自己選擇合適的寄存器

asm("leal (%1,%1,4),%0"
	: "=r" (x)
	: "0" (x)	# 輸入與輸出使用同一個寄存器
);

得到的主要彙編代碼爲

movl x,%eax
#APP
leal (%eax,%eax,4),%eax
#NO_APP
movl %eax,x	# 最終系統爲輸出選擇的寄存器中的內容會輸出到變量中

如果強制使用固定的寄存器,如:不用佔位符%1而用%ebx,則可以寫爲

asm("leal (%%ebx,%%ebx,4),%0"
	: "=r" (x)
	: "0" (x)	# 儘管這裏不需要輸入,但是輸入部分是必須存在的
);

來一個例子熟悉一下

#include <stdio.h>

int main() 
{
        int a = 10, b;

	__asm__(
        	"movl %1, %%eax\n\t"
			"movl %%eax, %0\n\t"
		:"=r"(b)        		/* output */
		:"r"(a)         		/* input */
		:"%eax"         		/* clobbered register */
		);
    
	printf("Result: %d, %d/n", a, b);
	return 0;
}

該程序實現把a的值賦給b

%0對應變量b,b是輸出,系統自動爲b指定一個寄存器

%1對應變量a,a是輸入 ,系統自動爲a指定一個寄存器

因爲內嵌的彙編指令中用到了%eax寄存器,所以在修改部分進行聲明,告訴編譯器eax要被改寫,在此期間不要用eax保存其它值

首先將變量a中的值賦給指定的寄存器,指定的寄存器通過%eax寄存器將值傳遞給變量b指定的那個寄存器,最後該寄存器將值賦給b


再來一個例子

#include <stdio.h>
int main(){
    int data1 = 10;
    int data2 = 20;
    int result;
    
    asm (
        	"imull %%edx, %%ecx\n\t"
        	"movl %%ecx, %%eax"
        : "=a"(result)
        : "d"(data1), "c"(data2)
        );
    printf("The result is %d\n",result);
    return 0;
}

根據上文中的寄存器約束,這個例子中變量data1放在%edx中,data2放在%ecx中。輸出結果會被放到%eax中,然後送到result變量中。

這裏雖然顯式的用到了各個寄存器名,但並不需要在修改部分中聲明

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章