2010年1月19日 星期二

AT&T 組合語言指令詳解

AT&T 組合語言指令詳解
以下資料皆為網路搜尋的;
在閱讀linux/unix內核源代碼的時候,必須先掌握彙編,大家都知道,內核代碼用的編譯器是gcc,而gcc採用的是AT&T的彙編格式,與MS的intel有些區別。
一 AT&T的基本語法
語法上主要有以下幾個不同.
★ 寄存器命名原則
AT&T: %eax Intel: eax
★ 源/目的操作數順序
AT&T: movl %eax,%ebx Intel: mov ebx,eax
★ 常數/立即數的格式
AT&T: movl $_value,%ebx Intel: mov eax,_value
把_value的位址放入eax寄存器
AT&T: movl $0xd00d,%ebx Intel: mov ebx,0xd00d
★ 操作數長度標識
AT&T: movw %ax,%bx Intel: mov bx,ax
★尋址方式
AT&T: immed32(basepointer,indexpointer,indexscale)
Intel: [basepointer + indexpointer*indexscale + imm32)
Linux工作於保護模式下,用的是32位線性位址,所以在計算位址時
不用考慮segment-offset的問題.上式中的地址應為:
imm32 + basepointer + indexpointer*indexscale
下面是一些例子:
★直接尋址
AT&T: _booga ; _booga是一個全局的C變量
注意加上$是表示位址引用,不加是表示值引用.
註:對於局部變量,可以通過堆棧指針引用.
Intel: [_booga]
★寄存器間接尋址
AT&T: (%eax)
Intel: [eax]
★變址尋址
AT&T: _variable(%eax)
Intel: [eax + _variable]
AT&T: _array(,%eax,4)
Intel: [eax*4 + _array]
AT&T: _array(%ebx,%eax,8)
Intel: [ebx + eax*8 + _array]

二 基本的行內彙編

基本的行內彙編很簡單,一般是按照下面的格式
asm("statements");
例如:asm("nop"); asm("cli");
asm 和 __asm__是完全一樣的.
如果有多行彙編,則每一行都要加上 "\n\t"
例如:
asm( "pushl %eax\n\t"
"movl $0,%eax\n\t"
"popl %eax");
實際上gcc在處理彙編時,是要把asm(...)的內容"列印"到彙編
檔中,所以格式控制字元是必要的.
再例如:
asm("movl %eax,%ebx");
asm("xorl %ebx,%edx");
asm("movl $0,_booga);
在上面的例子中,由於我們在行內彙編中改變了edx和ebx的值,但是
由於gcc的特殊的處理方法,即先形成彙編檔,再交給GAS去彙編,
所以GAS並不知道我們已經改變了edx和ebx的值,如果程式的上下文
需要edx或ebx作暫存,這樣就會引起嚴重的後果.對於變量_booga也
存在一樣的問題.為瞭解決這個問題,就要用到擴展的行內彙編語法.

三 擴展的行內彙編

擴展的行內彙編類似於Watcom.
基本的格式是:
asm ( "statements" : output_regs : input_regs : clobbered_regs);
clobbered_regs指的是會被改變的寄存器.
下面是一個例子(為方便起見,我使用全局變量):
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[0])
: "%ecx","%edi" );
}
得到的主要彙編代碼為:
movl count,%ecx
movl value,%eax
movl buf,%edi
#APP
cld
rep
stosl
#NO_APP
cld,rep,stos就不用多解釋了.
這幾條語句的功能是向buf中寫上count個value值.
冒號後的語句指明輸入,輸出和被改變的寄存器.
通過冒號以後的語句,編譯器就知道你的指令需要和改變哪些寄存器,
從而可以優化寄存器的分配.
其中符號"c"(count)指示要把count的值放入ecx寄存器
類似的還有:
a eax
b ebx
c ecx
d edx
S esi
D edi
I 常數值,(0 - 31)
q,r 動態分配的寄存器
g eax,ebx,ecx,edx或內存變量
A 把eax和edx合成一個64位的寄存器(use long longs)
我們也可以讓gcc自己選擇合適的寄存器.
如下面的例子:
asm("leal (%1,%1,4),%0"
: "=r" (x)
: "0" (x) );
這段代碼實現5*x的快速乘法.
得到的主要彙編代碼為:
movl x,%eax
#APP
leal (%eax,%eax,4),%eax
#NO_APP
movl %eax,x
幾點說明:
1.使用q指示編譯器從eax,ebx,ecx,edx分配寄存器.
使用r指示編譯器從eax,ebx,ecx,edx,esi,edi分配寄存器.
2.我們不必把編譯器分配的寄存器放入改變的寄存器列表,因為寄存器
已經記住了它們.
3."="是標示輸出寄存器,必須這樣用.
4.數字%n的用法:
數字表示的寄存器是按照出現和從左到右的順序映射到用"r"或"q"請求
的寄存器.如果我們要重用"r"或"q"請求的寄存器的話,就可以使用它們.
5.如果強制使用固定的寄存器的話,如不用%1,而用ebx,則
asm("leal (%%ebx,%%ebx,4),%0"
: "=r" (x)
: "0" (x) );
注意要使用兩個%,因為一個%的語法已經被%n用掉了.
下面可以來解釋letter 4854-4855的問題:
1、變量加下劃線和雙下劃線有什麼特殊含義嗎?
加下劃線是指全局變量,但我的gcc中加不加都無所謂.
2、以上定義用如下調用時展開會是什麼意思?
#define _syscall1(type,name,type1,arg1) \
type name(type1 arg1) \
{ \
long __res; \
/* __res應該是一個全局變量 */
__asm__ volatile ("int $0x80" \
/* volatile 的意思是不允許優化,使編譯器嚴格按照你的彙編代碼彙編*/
: "=a" (__res) \
/* 產生代碼 movl %eax, __res */
: "0" (__NR_##name),"b" ((long)(arg1))); \
/* 如果我沒記錯的話,這裡##指的是兩次宏展開.
  即用實際的系統調用名字代替"name",然後再把__NR_...展開.
  接著把展開的常數放入eax,把arg1放入ebx */
if (__res >= 0) \
return (type) __res; \
errno = -__res; \
return -1; \
}

四.AT&T彙編與Intel彙編的比較

Intel和AT&T語法的區別

Intel和AT&T彙編語言的語法表面上各不相同,這將導致剛剛學會INTEL彙編的人第一次見到AT&T彙編時
會感到困惑,或者反之。因此讓我們從基礎的東西開始。
前綴
在Intel彙編中沒有寄存器前綴或者立即數前綴。而在AT&T彙編中寄存器有一個「%」前綴,立即數有
一個「$」前綴。Intel語句中十六進制和二進制數據分別帶有「h」和「b」後綴,並且如果十六進制
數字的第一位是字母的話,那麼數值的前面要加一個「0」前綴。
例如,
Intex Syntax
mov eax,1
mov ebx,0ffh
int 80h
AT&T Syntax
movl $1,%eax
movl $0xff,%ebx
int $0x80
就像你看到的,AT&T非常難懂。[base+index*scale+disp] 看起來比disp(base,index,scale)更好理解。
操作數的用法
intel語句中操作數的用法和AT&T中的用法相反。在Intel語句中,第一個操作數表示目的,第二個
操作數表示源。然而在AT&T語句中第一個操作數表示源而第二個操作數表示目的。在這種情形下AT&T語法
的好處是顯而易見的。我們從左向右讀,也從左向右寫,這樣比較自然。
例如,
Intex Syntax
instr dest,source
mov eax,[ecx]
AT&T Syntax
instr source,dest
movl (%ecx),%eax
存儲器操作數
如同上面所看到的,存儲器操作數的用法也不相同。在Intel語句中基址寄存器用「[」和「]」括起來
而在AT&T語句中是用「(」和「)」括起來的。
例如,
Intex Syntax
mov eax,[ebx]
mov eax,[ebx+3]
AT&T Syntax
movl (%ebx),%eax
movl 3(%ebx),%eax
AT&T語法中用來處理複雜的操作的指令的形式和Intel語法中的形式比較起來要難懂得多。在Intel語句
中這樣的形式是segreg:[base+index*scale+disp]。在AT&T語句中這樣的形式是
%segreg:disp(base,index,scale)。
Index/scale/disp/segreg 都是可選並且可以去掉的。Scale在本身沒有說明而index已指定的情況下
缺省值為1。segreg的確定依賴於指令本身以及程式運行在實模式還是pmode。在實模式下它依賴於
指令本身而pmode模式下它是不需要的。在AT&T語句中用作scale/disp的立即數不要加「$」前綴。
例如
Intel Syntax
instr foo,segreg:[base+index*scale+disp]
mov eax,[ebx+20h]
add eax,[ebx+ecx*2h]
lea eax,[ebx+ecx]
sub eax,[ebx+ecx*4h-20h]
AT&T Syntax
instr %segreg:disp(base,index,scale),foo
movl 0x20(%ebx),%eax
addl (%ebx,%ecx,0x2),%eax
leal (%ebx,%ecx),%eax
subl -0x20(%ebx,%ecx,0x4),%eax

後綴

就像你已經注意到的,AT&T語法中有一個後綴,它的意義是表示操作數的大小。「l」代表long,
「w」代表word,「b」代表byte。Intel語法中在處理存儲器操作數時也有類似的表示,
如byte ptr, word ptr, dword ptr。"dword" 顯然對應於「long」。這有點類似於C語言中定義的
類型,但是既然使用的寄存器的大小對應著假定的數據類型,這樣就顯得不必要了。
例子:
Intel Syntax
mov al,bl
mov ax,bx
mov eax,ebx
mov eax, dword ptr [ebx]
AT&T Syntax
movb %bl,%al
movw %bx,%ax
movl %ebx,%eax
movl (%ebx),%eax
注意:從此開始所有的例子都使用AT&T語法

AT&T組合語言的相關知識

在Linux源代碼中,以.S為副檔名的文件是“純”組合語言的文件。這裏,我們結合具體的例子再介紹一些AT&T組合語言的相關知識。
1.GNU組合語言程式GAS GNU Assembly和連接程式
當你編寫了一個程式後,就需要對其進行彙編(assembly)和連接。在Linux下有兩種方式,一種是使用組合語言程式GAS和連接程式ld,一種是使用gcc。我們先來看一下GAS和ld:
GAS把組合語言原始檔案(.o)轉換為目標檔(.o),其基本語法如下:

as filename.s -o filename.o

一旦創建了一個目標檔,就需要把它連接並執行,連接一個目標檔的基本語法為:
ld filename.o -o filename
這裏 filename.o是目標檔案名,而filename 是輸出(可執行) 檔。

GAS使用的是AT&T的語法而不是Intel的語法,這就再次說明了AT&T語法是Unix世界的標準,你必須熟悉它。

如果要使用GNC的C編譯器gcc,就可以一步完成彙編和連接,例如:

gcc -o example example.S

這裏,example.S是你的組合語言程式,輸出檔(可執行檔)名為example。其中,副檔名必須為大寫的S,這是因為,大寫的S可以使gcc自動識別組合語言程式中的C預處理命令,像#include、#define、#ifdef、 #endif等,也就是說,使用gcc進行編譯,你可以在組合語言程式中使用C的預處理命令。

2. AT&T中的節(Section)
在AT&T的語法中,一個節由.section關鍵字來標識,當你編寫組合語言程式時,至少需要有以下三種節:
.section .data: 這種節包含程式已初始化的資料,也就是說,包含具有初值的那些變數,例如:
hello : .string "Hello world!\n"

hello_len : .long 13

.section .bss:這個節包含程式還未初始化的資料,也就是說,包含沒有初值的那些變數。當操作

系統裝入這個程式時將把這些變數都置為0,例如:

name : .fill 30 # 用來請求用戶輸入名字

name_len : .long 0 # 名字的長度 (尚未定義)

當這個程式被裝入時,name 和 name_len都被置為0。如果你在.bss節不小心給一個變數賦了初值,這個值也會丟失,並且變數的值仍為0。

使用.bss比使用.data的優勢在於,.bss節不佔用磁片的空間。在磁片上,一個長整數就足以存放.bss節。當程式被裝入到記憶體時,作業系統也只分配給這個節4個位元組的記憶體大小。

注意:編譯程序把.data和.bss在4位元組上對齊(align),例如,.data總共有34位元組,那麼編譯程序把它對其在36位元組上,也就是說,實際給它36位元組的空間。

.section .text :這個節包含程式的代碼,它是唯讀節,而.data 和.bss是讀/寫節。

3.組合語言程式指令(Assembler Directive)

上面介紹的.section就是組合語言程式指令的一種,GNU組合語言程式提供了很多這樣的指令(directiv),這種指令都是以句點(.)為開頭,後跟指令名(小寫字母),在此,我們只介紹在內核源代碼中出現的幾個指令(以arch/i386/kernel/head.S中的代碼為例)。

(1)ascii "string"...

.ascii 表示零個或多個(用逗號隔開)字串,並把每個字串(結尾不自動加“0“位元組)中的字元放在連續的位址單元。

還有一個與.ascii類似的.asciz,z代表“0“,即每個字串結尾自動加一個”0“位元組,例如:

int_msg:

.asciz "Unknown interrupt\n"

(2).byte 運算式

.byte表示零或多個運算式(用逗號隔開),每個運算式被放在下一個位元組單元。

(3).fill 運算式

形式:.fill repeat , size , value

其中,repeat、size 和value都是常量運算式。Fill的含義是反復拷貝size個位元組。Repeat可以大於等於0。size也可以大於等於0,但不能超過8,如果超過8,也只取8。把repeat個位元組以8個為一組,每組的最高4個位元組內容為0,最低4位元組內容置為value。

Size和 value為可選項。如果第二個逗號和value值不存在,則假定value為0。如果第一個逗號和size不存在,則假定size為1。

例如,在Linux初始化的過程中,對全局描述符表GDT進行設置的最後一句為:

.fill NR_CPUS*4,8,0 /* space for TSS's and LDT's */

因為每個描述符正好占8個位元組,因此,.fill給每個CPU留有存放4個描述符的位置。

(4).globl symbol

.globl使得連接程式(ld)能夠看到symbl。如果你的局部程式中定義了symbl,那麼,與這個局部程式連接的其他局部程式也能存取symbl,例如:

.globl SYMBOL_NAME(idt)

.globl SYMBOL_NAME(gdt)

定義idt和gdt為全局符號。


(5)quad bignums

.quad表示零個或多個bignums(用逗號分隔),對於每個bignum,其缺省值是8位元組整數。如果bignum超過8位元組,則列印一個警告資訊;並只取bignum最低8位元組。

例如,對全局描述符表的填充就用到這個指令:

.quad 0x00cf9a000000ffff /* 0x10 kernel 4GB code at 0x00000000 */

.quad 0x00cf92000000ffff /* 0x18 kernel 4GB data at 0x00000000 */

.quad 0x00cffa000000ffff /* 0x23 user 4GB code at 0x00000000 */

.quad 0x00cff2000000ffff /* 0x2b user 4GB data at 0x00000000 */


(6)rept count

把.rept指令與.endr指令之間的行重複count次,例如

.rept 3

.long 0

.endr

相當於

.long 0

.long 0

.long 0

(7)space size , fill


這個指令保留size個位元組的空間,每個位元組的值為fill。size 和fill都是常量運算式。如果逗號和fill被省略,則假定fill為0,例如在arch/i386/bootl/setup.S中有一句:

.space 1024

表示保留1024位元組的空間,並且每個位元組的值為0。

(8).word expressions

這個運算式表示任意一節中的一個或多個運算式(用逗號分開),運算式的值占兩個位元組,例如:

gdt_descr:

.word GDT_ENTRIES*8-1

表示變數gdt_descr的置為GDT_ENTRIES*8-1

(9).long expressions

這與.word類似

(10).org new-lc , fill

把當前節的位置計數器提前到new-lc(new location counter)。new-lc或者是一個常量運算式,或者是一個與當前子節處於同一節的運算式。也就是說,你不能用.org橫跨節:如果new-lc是個錯誤的值,則.org被忽略。.org只能增加位置計數器的值,或者讓其保持不變;但絕不能用.org來讓位置計數器倒退。

注意,位置計數器的起始值是相對於一個節的開始的,而不是子節的開始。當位置計數器被提升後,中間位置的位元組被填充值fill(這也是一個常量運算式)。如果逗號和fill都省略,則fill的缺省值為0。

例如:.org 0x2000

ENTRY(pg0)

表示把位置計數器置為0x2000,這個位置存放的就是臨時頁表pg0。
本文來自CSDN博客,轉載請標明出處:http://blog.csdn.net/kpgood/archive/2009/04/05/4049495.aspx

沒有留言:

張貼留言