絕大多數 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 比特)和長字(long,32比特);而在 Intel 彙編格式中,操作數的字長是用 "byte ptr" 和 "word ptr" 等前綴來表示的。例如:
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 中,可以找到所有系統調用的定義。