1. 程式人生 > >GCC彙編指令格式

GCC彙編指令格式

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語法
系統呼叫
本節將介紹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():
#include
#include
#include
#include
#include
#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
#include
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
#include
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;
}