AT&T彙編與8086彙編區別

在閱讀linux內核源代碼的時候,必須先掌握彙編,大家都知道,內核代碼用的編譯器是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 


l ,w,b是AT&T彙編中用來表示操作屬性的限定符

l是長字(4字節),
w是雙字
b是一個字節

加在指令的後邊
相當於intel中的
dword ptr
word ptr
byte ptr


比如:
subl $8, %esp
leal -792(%ebp), %eax
pushl %eax
movl -796(%ebp), %eax
sall $8, %eax
addl 12(%ebp), %eax
pushl %eax
call _strcpy
addl $16, %esp

在intel 彙編中就相當於:
sub esp,8
lea eax,dword ptr [ebp-792]
push eax
mov eax,dword ptr [ebp- 796]
...

★尋址方式 

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語法
系統調用
本節將介紹linux中彙編語言系統調用的用法。系統調用包括位於/usr/man/man2的手冊裏第二部分所有
的函數。這些函數也在/usr/include/sys/syscall.h中列出來了。一個重要的關於這些函數的列表是
http://www.linuxassembly.org/syscall.html裏。這些函數通過linux中斷服務:int $0x80來被執行
小於六個參數的系統調用
對於所有的系統調用,系統調用號在%eax中。對於小於六個參數的系統調用,參數依次存放
在%ebx,%ecx,%edx,%esi,%edi中,系統調用的返回值保存在%eax中。
系統調用號可以在/usr/include/sys/syscall.h中找到。宏被定義成SYS_的形式,
如SYS_exit, SYS_close等。
例子:(hello world 程序)
參照write(2)的幫助手冊,寫操作被聲明爲ssize_t write(int fd, const void *buf, size_t count);
這樣,fd應存放在%ebx中,buf放在 %ecx, count 放在 %edx , SYS_write 放在 %eax中,緊跟着是
int $0x80語句來執行系統調用。系統調用的返回值保存在%eax中。
$ cat write.s
.include "defines.h"
.data
hello:
    .string "hello world/n"

.globl    main
main:
    movl    $SYS_write,%eax
    movl    $STDOUT,%ebx
    movl    $hello,%ecx
    movl    $12,%edx
    int    $0x80

    ret
$
少於5個參數的系統調用的處理也是這樣的。只是沒有用到的寄存器保持不變罷了。象open或者fcntl這樣
帶有一個可選的額外參數的系統調用也就知道怎麼用了。
大於5個參數的系統調用
參數個數大於五個的系統調用仍然把系統調用號保存在%eax中,但是參數存放在內存中,並且指向第一個
參數的指針保存在%ebx中。
如果你使用棧,參數必須被逆序壓進棧裏,即按最後一個參數到第一個參數的順序。然後將棧的指針拷貝
到%ebx中。或者將參數拷貝到一塊分配的內存區域,然後把第一個參數的地址保存在%ebx中。
例子:(使用mmap作爲系統調用的例子)。在C中使用mmap():
#i nclude
#i nclude
#i nclude
#i nclude
#i nclude

#define STDOUT    1

void main(void) {
    char file[]="mmap.s";
    char *mappedptr;
    int fd,filelen;

    fd=fopen(file, O_RDONLY);
    filelen=lseek(fd,0,SEEK_END);
    mappedptr=mmap(NULL,filelen,PROT_READ,MAP_SHARED,fd,0);
    write(STDOUT, mappedptr, filelen);
    munmap(mappedptr, filelen);
    close(fd);
}
mmap()參數在內存中的排列:
%esp    %esp+4    %esp+8    %esp+12    %esp+16    %esp+20
00000000    filelen    00000001    00000001    fd    00000000
等價的彙編程序:
$ cat mmap.s
.include "defines.h"

.data
file:
    .string "mmap.s"
fd:
    .long     0
filelen:
    .long     0
mappedptr:
    .long     0

.globl main
main:
    push    %ebp
    movl    %esp,%ebp
    subl    $24,%esp

//    open($file, $O_RDONLY);

    movl    $fd,%ebx    // save fd
    movl    %eax,(%ebx)

//    lseek($fd,0,$SEEK_END);

    movl    $filelen,%ebx    // save file length
    movl    %eax,(%ebx)

    xorl    %edx,%edx

//    mmap(NULL,$filelen,PROT_READ,MAP_SHARED,$fd,0);
    movl    %edx,(%esp)
    movl    %eax,4(%esp)    // file length still in %eax
    movl    $PROT_READ,8(%esp)
    movl    $MAP_SHARED,12(%esp)
    movl    $fd,%ebx    // load file descriptor
    movl    (%ebx),%eax
    movl    %eax,16(%esp)
    movl    %edx,20(%esp)
    movl    $SYS_mmap,%eax
    movl    %esp,%ebx
    int    $0x80

    movl    $mappedptr,%ebx    // save ptr
    movl    %eax,(%ebx)
        
//     write($stdout, $mappedptr, $filelen);
//    munmap($mappedptr, $filelen);
//    close($fd);
    
    movl    %ebp,%esp
    popl    %ebp

    ret
$
注意:上面所列出的源代碼和本文結束部分的例子的源代碼不同。上面列出的代碼中沒有說明其它的
系統調用,因爲這不是本節的重點,上面列出的源代碼僅僅打開mmap.s文件,而例子的源代碼要讀
命令行的參數。這個mmap的例子還用到lseek來獲取文件大小。
Socket系統調用
Socket系統調用使用唯一的系統調用號:SYS_socketcall,它保存在%eax中。Socket函數是通過位於
/usr/include/linux/net.h的一個子函數號來確定的,並且它們被保存在%ebx中。指向系統調用參數
的一個指針存放在%ecx中。Socket系統調用也是通過int $0x80來執行的。
$ cat socket.s
.include "defines.h"

.globl    _start
_start:
    pushl    %ebp
    movl    %esp,%ebp
    sub    $12,%esp

//    socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);
    movl    $AF_INET,(%esp)
    movl    $SOCK_STREAM,4(%esp)
    movl    $IPPROTO_TCP,8(%esp)

    movl    $SYS_socketcall,%eax
    movl    $SYS_socketcall_socket,%ebx
    movl    %esp,%ecx
    int    $0x80

    movl     $SYS_exit,%eax
    xorl     %ebx,%ebx
    int     $0x80

    movl    %ebp,%esp
    popl    %ebp
    ret
$

命令行參數
在linux中執行的時候命令行參數是放在棧上的。先是argc,跟着是一個由指向命令行中各字符串的
指針組成的數組(**argv)並以空指針結束。接下來是一個由指向環境變量的指針組成的
數組(**envp)。這些東西在asm中都可以很容易的獲得,並且在例子代碼(args.s)中有示範。

 
GCC內聯彙編
本節中GCC內聯彙編僅涉及x86的應用程序。操作數約束會和其它處理器上的有所不同。關於這部分
的說明放在本文的最後。
gcc中基本的內聯彙編非常易懂,如
__asm__("movl    %esp,%eax");    // look familiar ?

或者是
__asm__("
            movl    $1,%eax        // SYS_exit
            xor    %ebx,%ebx
            int    $0x80
    ");
如果指定了用作asm的輸入、輸出數據並指出哪一個寄存器會被修改,會使程序的執行效率提高。
input/output/modify都不是必需的。格式如下:
__asm__("" : output : input : modify);
output和input中必須包含一個操作數約束字符串,並緊跟一個用圓括號括起來的C語言表達式。
輸出操作數約束的前面必須有一個“=”,表示這是一個輸出。可能會有多個輸出,多個輸入和
多個修改過的寄存器。每個“入口”應該用“,”分隔開,並且入口的總數不多有10個。
操作數約束字符串可以是包含整個寄存器的名稱也可以是簡寫。
Abbrev Table
Abbrev    Register
a    %eax/%ax/%al
b    %ebx/%bx/%bl
c    %ecx/%cx/%cl
d    %edx/%dx/%dl
S    %esi/%si
D    %edi/%di
m    memory
例如:

    __asm__("test    %%eax,%%eax", : /* no output */ : "a"(foo));


或者是

    __asm__("test    %%eax,%%eax", : /* no output */ : "eax"(foo));
你可以在__asm__後使用關鍵字__volatile__:“你可以利用在__asm__後使用關鍵字__volatile__的
方法防止一條‘asm’指令被刪除、移動或者被重新組合。”(出自gcc的info文件中"Assembler
Instructions with C Expression Operands" 部分)
$ cat inline1.c
#i nclude

int main(void) {
    int foo=10,bar=15;
    
    __asm__ __volatile__ ("addl     %%ebxx,%%eax"
        : "=eax"(foo)         // ouput
        : "eax"(foo), "ebx"(bar)// input
        : "eax"            // modify
    );
    printf("foo+bar=%d/n", foo);
    return 0;
}
$
你可能已經注意到現在寄存器使用“%%”前綴而不是“%”。這在使用output/input/modify域時是必要的,
這是因爲此時基於其它域的寄存器的別名的使用。我馬上來討論這個問題。
你可以很簡單的指定“a”而不是寫“eax”或者強制使用一個特殊寄存器如"eax"、"ax"、"al",
這同樣適用於其它一般用途的寄存器(在Abbrev表中列出的)。當你在當前的代碼中使用特殊的寄存器
時這好像毫無用處,因此gcc提供了寄存器別名。最多有10個別名(%0—%9),這也是爲什麼只允許10個
輸入/輸出的原因。
$ cat inline2.c
int main(void) {
    long eax;
    short bx;
    char cl;

    __asm__("nop;nop;nop"); // to separate inline asm from the rest of
                // the code
    __volatile__ __asm__("
        test    %0,%0
        test    %1,%1
        test    %2,%2"
        : /* no outputs */
        : "a"((long)eax), "b"((short)bx), "c"((char)cl)
    );
    __asm__("nop;nop;nop");
    return 0;
}
$ gcc -o inline2 inline2.c
$ gdb ./inline2
GNU gdb 4.18
Copyright 1998 Free Software Foundation, Inc.
GDB is free software, covered by the GNU General Public License, and you are
welcome to change it and/or distribute copies of it under certain conditions.
Type "show copying" to see the conditions.
There is absolutely no warranty for GDB.  Type "show warranty" for details.
This GDB was configured as "i686-pc-linux-gnulibc1"...
(no debugging symbols found)...
(gdb) disassemble main
Dump of assembler code for function main:
... start: inline asm ...
0x8048427 : nop
0x8048428 : nop
0x8048429 : nop
0x804842a : mov 0xfffffffc(%ebp),%eax
0x804842d : mov 0xfffffffa(%ebp),%bx
0x8048431 : mov 0xfffffff9(%ebp),%cl
0x8048434 : test %eax,%eax
0x8048436 : test %bx,%bx
0x8048439 : test %cl,%cl
0x804843b : nop
0x804843c : nop
0x804843d : nop
... end: inline asm ...
End of assembler dump.
$
就像你看到的,由內聯彙編生成的代碼將變量的值放入它們在input域中指定的寄存器中,然後繼續
執行當前的代碼。編譯器自動根據變量的大小來偵測操作數的大小,這樣相應的寄存器就被
別名%0, %1 和 %2代替了(當使用寄存器別名時在存儲器裏指定操作數的大小回導致編譯時發生錯誤)
在操作數約束裏也可以使用別名。這不允許你在輸入/輸出域中指定多於10個的入口。我能想到的這樣
做的唯一用法是在你指定操作數約束爲“q”以便讓編譯器在a,b,c,d寄存器之間進行選擇的時候。
當這個寄存器被修改時,我們不會知道選中了那個寄存器,因而不能在modify域中指定它。
這種情況下你只需指定""。
例子:
$ cat inline3.c
#i nclude

int main(void) {
    long eax=1,ebx=2;

    __asm__ __volatile__ ("add %0,%2"
        : "=b"((long)ebx)
        : "a"((long)eax), "q"(ebx)
        : "2"
    );
    printf("ebx=%x/n", ebx);
    return 0;
}

轉自:http://blog.csdn.net/cxjnet/archive/2008/12/25/3601559.aspx

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