AT&T彙編格式
絕大多數 Linux 程式設計師以前只接觸過DOS/Windows 下的組合語言,這些彙編程式碼都是 Intel 風格的。但在 Unix 和 Linux 系統中,更多采用的還是 AT&T 格式,兩者在語法格式上有著很大的不同:
在 AT&T 彙編格式中,暫存器名要加上 ' %' 作為字首;而在 Intel 彙編格式中,暫存器名不需要加字首。例如:
AT&T 格式 |
Intel 格式 |
pushl �x |
push eax |
在 AT&T 彙編格式中,用 '$' 字首表示一個立即運算元;而在 Intel 彙編格式中,立即數的表示不用帶任何字首。例如:
AT&T 格式 |
Intel 格式 |
pushl $1 |
push 1 |
AT&T 和 Intel 格式中的源運算元和目標運算元的位置正好相反。在 Intel 彙編格式中,目標運算元在源運算元的左邊;而在 AT&T 彙編格式中,目標運算元在源運算元的右邊。例如:
AT&T 格式 |
Intel 格式 |
addl $1, �x |
add eax, 1 |
在 AT&T 彙編格式中,運算元的字長由操作符的最後一個字母決定,字尾'b'、'w'、'l'分別表示運算元為位元組(byte,8 位元)、字(word,16 位元)和長字(
AT&T 格式 |
Intel 格式 |
movb val, %al |
mov al, byte ptr val |
在 AT&T 彙編格式中,絕對轉移和呼叫指令(jump/call)的運算元前要加上'*'作為字首,而在 Intel 格式中則不需要。
遠端轉移指令和遠端子呼叫指令的操作碼,在 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 |
與之相應的遠端返回指令則為:
AT&T 格式 |
Intel 格式 |
lret $stack_adjust |
ret far stack_adjust |
在 AT&T 彙編格式中,記憶體運算元的定址方式是
section:disp(base, index, scale) |
而在 Intel 彙編格式中,記憶體運算元的定址方式為:
section:[base + index*scale + disp] |
由於 Linux 工作在保護模式下,用的是 32 位線性地址,所以在計算地址時不用考慮段基址和偏移量,而是採用如下的地址計算方法:
disp + base + index * scale |
下面是一些記憶體運算元的例子:
AT&T 格式 |
Intel 格式 |
movl -4(�p), �x |
mov eax, [ebp - 4] |
movl array(, �x, 4), �x |
mov eax, [eax*4 + array] |
movw array(�x, �x, 4), %cx |
mov cx, [ebx + 4*eax + array] |
movb $4, %fs:(�x) |
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, �x # 引數三:字串長度 movl $msg, �x # 引數二:要顯示的字串 movl $1, �x # 引數一:檔案描述符(stdout) movl $4, �x # 系統呼叫號(sys_write) int $0x80 # 呼叫核心功能 # 退出程式 movl $0,�x # 引數一:退出程式碼 movl $1,�x # 系統呼叫號(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 中,可以找到所有系統呼叫的定義。