Intel格式和AT&T格式匯編區別


一、AT&T 格式Linux 匯編語法格式

  1. AT&T 匯編格式中,寄存器名要加上 '%' 作為前綴;而在 Intel 匯編格式中,寄存器名不需要加前綴。例如:

AT&T 格式

Intel 格式

pushl %eax

push eax

2.         AT&T 匯編格式中,用 '$' 前綴表示一個立即操作數;而在 Intel 匯編格式中,立即數的表示不用帶任何前綴。例如:

AT&T 格式

Intel 格式

pushl $1

push 1

3.         AT&T Intel 格式中的源操作數和目標操作數的位置正好相反。在 Intel 匯編格式中,目標操作數在源操作數的左邊;而在 AT&T 匯編格式中,目標操作數在源操作數的右邊。例如:

AT&T 格式

Intel 格式

addl $1, %eax

add eax, 1

4.         AT&T 匯編格式中,操作數的字長由操作符的最后一個字母決定,后綴'b''w''l'分別表示操作數為字節(byte8 比特)、字(word16 比特)和長字(long32比特);而在 Intel 匯編格式中,操作數的字長是用 "byte ptr" "word ptr" 等前綴來表示的。例如:

AT&T 格式

Intel 格式

movb val, %al

mov al, byte ptr val

5.         AT&T 匯編格式中,絕對轉移和調用指令(jump/call)的操作數前要加上'*'作為前綴,而在 Intel 格式中則不需要。

6.         遠程轉移指令和遠程子調用指令的操作碼,在 AT&T 匯編格式中為 "ljump" "lcall",而在 Intel 匯編格式中則為 "jmp far" "call far",即:

AT&T 格式

Intel 格式

ljump $section, $offset

jmp far section:offset

lcall $section, $offset

call far section:offset

7.         與之相應的遠程返回指令則為:

AT&T 格式

Intel 格式

lret $stack_adjust

ret far stack_adjust

8.         AT&T 匯編格式中,內存操作數的尋址方式是

section:disp(base, index, scale)

9.         而在 Intel 匯編格式中,內存操作數的尋址方式為:

section:[base + index*scale + disp]

10.     由於 Linux 工作在保護模式下,用的是 32 位線性地址,所以在計算地址時不用考慮段基址和偏移量,而是采用如下的地址計算方法:

disp + base + index * scale

11.     下面是一些內存操作數的例子:

AT&T 格式

Intel 格式

movl -4(%ebp), %eax

mov eax, [ebp - 4]

movl array(, %eax, 4), %eax

mov eax, [eax*4 + array]

movw array(%ebx, %eax, 4), %cx

mov cx, [ebx + 4*eax + array]

movb $4, %fs:(%eax)

mov fs:eax, 4

二、Hello World!

既然所有程序設計語言的第一個例子都是在屏幕上打印一個字符串 "Hello World!",那我們也以這種方式來開始介紹 Linux 下的匯編語言程序設計。

Linux 操作系統中,你有很多辦法可以實現在屏幕上顯示一個字符串,但最簡潔的方式是使用 Linux 內核提供的系統調用。使用這種方法最大的好處是可以直接和操作系統的內核進行通訊,不需要鏈接諸如 libc 這樣的函數庫,也不需要使用 ELF 解釋器,因而代碼尺寸小且執行速度快。

Linux 是一個運行在保護模式下的 32 位操作系統,采用 flat memory 模式,目前最常用到的是 ELF 格式的二進制代碼。一個 ELF 格式的可執行程序通常划分為如下幾個部分:.text.data .bss,其中 .text 是只讀的代碼區,.data 是可讀可寫的數據區,而 .bss 則是可讀可寫且沒有初始化的數據區。代碼區和數據區在 ELF 中統稱為 section,根據實際需要你可以使用其它標准的 section,也可以添加自定義 section,但一個 ELF 可執行程序至少應該有一個 .text 部分。下面給出我們的第一個匯編程序,用的是 AT&T 匯編語言格式:

1. AT&T 格式

#hello.s

.data                    # 數據段聲明

        msg : .string "Hello, world!\\n" # 要輸出的字符串

        len = . - msg                   # 字串長度

.text                    # 代碼段聲明

.global _start           # 指定入口函數

_start:                  # 在屏幕上顯示一個字符串

        movl $len, %edx  # 參數三:字符串長度

        movl $msg, %ecx  # 參數二:要顯示的字符串

        movl $1, %ebx    # 參數一:文件描述符(stdout)

        movl $4, %eax    # 系統調用號(sys_write)

        int  $0x80       # 調用內核功能

                         # 退出程序

        movl $0,%ebx     # 參數一:退出代碼

        movl $1,%eax     # 系統調用號(sys_exit)

        int  $0x80       # 調用內核功能

初次接觸到 AT&T 格式的匯編代碼時,很多程序員都認為太晦澀難懂了,沒有關系,在 Linux 平台上你同樣可以使用 Intel 格式來編寫匯編程序:

2. Intel 格式

; hello.asm

section .data            ; 數據段聲明

        msg db "Hello, world!", 0xA     ; 要輸出的字符串

        len equ $ - msg                 ; 字串長度

section .text            ; 代碼段聲明

global _start            ; 指定入口函數

_start:                  ; 在屏幕上顯示一個字符串

        mov edx, len     ; 參數三:字符串長度

        mov ecx, msg     ; 參數二:要顯示的字符串

        mov ebx, 1       ; 參數一:文件描述符(stdout)

        mov eax, 4       ; 系統調用號(sys_write)

        int 0x80         ; 調用內核功能

                         ; 退出程序

        mov ebx, 0       ; 參數一:退出代碼

        mov eax, 1       ; 系統調用號(sys_exit)

        int 0x80         ; 調用內核功能

上面兩個匯編程序采用的語法雖然完全不同,但功能卻都是調用 Linux 內核提供的 sys_write 來顯示一個字符串,然后再調用 sys_exit 退出程序。在 Linux 內核源文件 include/asm-i386/unistd.h 中,可以找到所有系統調用的定義。

四、系統調用

即便是最簡單的匯編程序,也難免要用到諸如輸入、輸出以及退出等操作,而要進行這些操作則需要調用操作系統所提供的服務,也就是系統調用。除非你的程序只完成加減乘除等數學運算,否則將很難避免使用系統調用,事實上除了系統調用不同之外,各種操作系統的匯編編程往往都是很類似的。

Linux 平台下有兩種方式來使用系統調用:利用封裝后的 C 庫(libc)或者通過匯編直接調用。其中通過匯編語言來直接調用系統調用,是最高效地使用 Linux 內核服務的方法,因為最終生成的程序不需要與任何庫進行鏈接,而是直接和內核通信。

DOS 一樣,Linux 下的系統調用也是通過中斷(int 0x80)來實現的。在執行 int 80 指令時,寄存器 eax 中存放的是系統調用的功能號,而傳給系統調用的參數則必須按順序放到寄存器 ebxecxedxesiedi 中,當系統調用完成之后,返回值可以在寄存器 eax 中獲得。

所有的系統調用功能號都可以在文件 /usr/include/bits/syscall.h 中找到,為了便於使用,它們是用 SYS_<name> 這樣的宏來定義的,如 SYS_writeSYS_exit 等。例如,經常用到的 write 函數是如下定義的:

ssize_t write(int fd, const void *buf, size_t count);

該函數的功能最終是通過 SYS_write 這一系統調用來實現的。根據上面的約定,參數 fbbuf count 分別存在寄存器 ebxecx edx 中,而系統調用號 SYS_write 則放在寄存器 eax 中,當 int 0x80 指令執行完畢后,返回值可以從寄存器 eax 中獲得。

或許你已經發現,在進行系統調用時至多只有 5 個寄存器能夠用來保存參數,難道所有系統調用的參數個數都不超過 5 嗎?當然不是,例如 mmap 函數就有 6 個參數,這些參數最后都需要傳遞給系統調用 SYS_mmap

void * mmap(void *start, size_t length, int prot , int flags, int fd, off_t offset);c

當一個系統調用所需的參數個數大於 5 時,執行int 0x80 指令時仍需將系統調用功能號保存在寄存器 eax 中,所不同的只是全部參數應該依次放在一塊連續的內存區域里,同時在寄存器 ebx 中保存指向該內存區域的指針。系統調用完成之后,返回值仍將保存在寄存器 eax 中。

由於只是需要一塊連續的內存區域來保存系統調用的參數,因此完全可以像普通的函數調用一樣使用棧(stack)來傳遞系統調用所需的參數。但要注意一點, Linux 采用的是 C 語言的調用模式,這就意味着所有參數必須以相反的順序進棧,即最后一個參數先入棧,而第一個參數則最后入棧。如果采用棧來傳遞系統調用所需的參數,在執行 int 0x80 指令時還應該將棧指針的當前值復制到寄存器 ebx中。

五、命令行參數

Linux 操作系統中,當一個可執行程序通過命令行啟動時,其所需的參數將被保存到棧中:首先是 argc,然后是指向各個命令行參數的指針數組 argv,最后是指向環境變量的指針數據 envp。在編寫匯編語言程序時,很多時候需要對這些參數進行處理,下面的代碼示范了如何在匯編代碼中進行命令行參數的處理:

3. 處理命令行參數

# args.s

.text

.globl _start

_start:

popl %ecx # argc

vnext:

popl %ecx # argv

test %ecx, %ecx # 空指針表明結束

jz exit

movl %ecx, %ebx

xorl %edx, %edx

strlen:

movb (%ebx), %al

inc %edx

inc %ebx

test %al, %al

jnz strlen

movb $10, -1(%ebx)

movl $4, %eax # 系統調用號(sys_write)

movl $1, %ebx # 文件描述符(stdout)

int $0x80

jmp vnext

exit: movl $1,%eax # 系統調用號(sys_exit)

xorl %ebx, %ebx # 退出代碼

int $0x80

ret

 

六、GCC 內聯匯編

用匯編編寫的程序雖然運行速度快,但開發速度非常慢,效率也很低。如果只是想對關鍵代碼段進行優化,或許更好的辦法是將匯編指令嵌入到 C 語言程序中,從而充分利用高級語言和匯編語言各自的特點。但一般來講,在 C 代碼中嵌入匯編語句要比"純粹"的匯編語言代碼復雜得多,因為需要解決如何分配寄存器,以及如何與C代碼中的變量相結合等問題。

GCC 提供了很好的內聯匯編支持,最基本的格式是:

__asm__("asm statements");

例如:

__asm__("nop");

如果需要同時執行多條匯編語句,則應該用"\\n\\t"將各個語句分隔開,例如:

__asm__( "pushl %%eax \\n\\t"

"movl $0, %%eax \\n\\t"

"popl %eax");

通常嵌入到 C 代碼中的匯編語句很難做到與其它部分沒有任何關系,因此更多時候需要用到完整的內聯匯編格式:

__asm__("asm statements" : outputs : inputs : registers-modified);

插入到 C 代碼中的匯編語句是以":"分隔的四個部分,其中第一部分就是匯編代碼本身,通常稱為指令部,其格式和在匯編語言中使用的格式基本相同。指令部分是必須的,而其它部分則可以根據實際情況而省略。

在將匯編語句嵌入到C代碼中時,操作數如何與C代碼中的變量相結合是個很大的問題。GCC采用如下方法來解決這個問題:程序員提供具體的指令,而對寄存器的使用則只需給出"樣板"和約束條件就可以了,具體如何將寄存器與變量結合起來完全由GCCGAS來負責。

GCC 內聯匯編語句的指令部中,加上前綴''%''的數字(%0%1)表示的就是需要使用寄存器的"樣板"操作數。指令部中使用了幾個樣板操作數,就表明有幾個變量需要與寄存器相結合,這樣GCCGAS在編譯和匯編時會根據后面給定的約束條件進行恰當的處理。由於樣板操作數也使用'' %''作為前綴,因此在涉及到具體的寄存器時,寄存器名前面應該加上兩個''%'',以免產生混淆。

緊跟在指令部后面的是輸出部,是規定輸出變量如何與樣板操作數進行結合的條件,每個條件稱為一個"約束",必要時可以包含多個約束,相互之間用逗號分隔開就可以了。每個輸出約束都以''=''號開始,然后緊跟一個對操作數類型進行說明的字后,最后是如何與變量相結合的約束。凡是與輸出部中說明的操作數相結合的寄存器或操作數本身,在執行完嵌入的匯編代碼后均不保留執行之前的內容,這是GCC在調度寄存器時所使用的依據。

輸出部后面是輸入部,輸入約束的格式和輸出約束相似,但不帶''=''號。如果一個輸入約束要求使用寄存器,則GCC在預處理時就會為之分配一個寄存器,並插入必要的指令將操作數裝入該寄存器。與輸入部中說明的操作數結合的寄存器或操作數本身,在執行完嵌入的匯編代碼后也不保留執行之前的內容。

有時在進行某些操作時,除了要用到進行數據輸入和輸出的寄存器外,還要使用多個寄存器來保存中間計算結果,這樣就難免會破壞原有寄存器的內容。在GCC內聯匯編格式中的最后一個部分中,可以對將產生副作用的寄存器進行說明,以便GCC能夠采用相應的措施。

下面是一個內聯匯編的簡單例子:

4.內聯匯編

 

int main()

{

int a = 10, b = 0;

__asm__ __volatile__("movl %1, %%eax;\\n\\r"

"movl %%eax, %0;"

:"=r"(b)

:"r"(a)

:"%eax");

printf("Result: %d, %d\\n", a, b);

}

上面的程序完成將變量a的值賦予變量b,有幾點需要說明:

  • 變量b是輸出操作數,通過%0來引用,而變量a是輸入操作數,通過%1來引用。
  • 輸入操作數和輸出操作數都使用r進行約束,表示將變量a和變量b存儲在寄存器中。輸入約束和輸出約束的不同點在於輸出約束多一個約束修飾符''=''
  • 在內聯匯編語句中使用寄存器eax時,寄存器名前應該加兩個''%'',即%%eax。內聯匯編中使用%0%1等來標識變量,任何只帶一個''%''的標識符都看成是操作數,而不是寄存器。
  • 內聯匯編語句的最后一個部分告訴GCC它將改變寄存器eax中的值,GCC在處理時不應使用該寄存器來存儲任何其它的值。
  • 由於變量b被指定成輸出操作數,當內聯匯編語句執行完畢后,它所保存的值將被更新。

在內聯匯編中用到的操作數從輸出部的第一個約束開始編號,序號從0開始,每個約束記數一次,指令部要引用這些操作數時,只需在序號前加上''%''作為前綴就可以了。需要注意的是,內聯匯編語句的指令部在引用一個操作數時總是將其作為32位的長字使用,但實際情況可能需要的是字或字節,因此應該在約束中指明正確的限定符:

限定符

意義

"m""v""o"

內存單元

"r"

任何寄存器

"q"

寄存器eaxebxecxedx之一

"i""h"

直接操作數

"E""F"

浮點數

"g"

任意

"a""b""c""d"

分別表示寄存器eaxebxecxedx

"S""D"

寄存器esiedi

"I"

常數(031


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM