Linux 匯編器:對比 GAS 和 NASM
轉自 http://www.ibm.com/developerworks/cn/linux/l-gas-nasm.html#ibm-pcon
與其他語言不同,匯編語言
要求開發人員了解編程所用機器的處理器體系結構。匯編程序不可移植,維護和理解常常比較麻煩,通常包含大量代碼行。但是,在機器上執行的運行時二進制代碼在速度和大小方面有優勢。
對于在 Linux 上進行匯編級編程已經有許多參考資料,本文主要講解語法之間的差異,幫助您更輕松地在匯編形式之間進行轉換。本文源于我自己試圖改進這種轉換的嘗試。
本文使用一系列程序示例。每個程序演示一些特性,然后是對語法的討論和對比。盡管不可能討論 NASM 和 GAS
之間存在的每個差異,但是我試圖討論主要方面,給進一步研究提供一個基礎。那些已經熟悉 NASM 和 GAS
的讀者也可以在這里找到有用的內容,比如宏。
本文假設您至少基本了解匯編的術語,曾經用符合 Intel? 語法的匯編器編寫過程序,可能在 Linux 或 Windows 上使用過 NASM。本文并不講解如何在編輯器中輸入代碼,或者如何進行匯編和鏈接(但是下面的邊欄可以幫助您 快速回憶一下
)。您應該熟悉 Linux 操作系統(任何 Linux 發行版都可以;我使用的是 Red Hat 和 Slackware)和基本的 GNU 工具,比如 gcc 和 ld,還應該在 x86 機器上進行編程。
現在,我描述一下本文討論的范圍。
構建示例
匯編:
GAS:
as –o program.o program.s
NASM:
nasm –f elf –o program.o program.asm
鏈接(對于兩種匯編器通用):
ld –o program program.o
在使用外部 C 庫時的鏈接方法:
ld –-dynamic-linker /lib/ld-linux.so.2 –lc –o program program.o
本文討論:
NASM 和 GAS 之間的基本語法差異
常用的匯編級結構,比如變量、循環、標簽和宏
關于調用外部 C 例程和使用函數的信息
匯編助記符差異和使用方法
內存尋址方法
本文不討論:
處理器指令集
一種匯編器特有的各種宏形式和其他結構
NASM 或 GAS 特有的匯編器指令
不常用的特性,或者只在一種匯編器中出現的特性
更多信息請參考匯編器的官方手冊(參見 參考資料
中的鏈接),因為這些手冊是最完整的信息源。
基本結構
清單 1 給出一個非常簡單的程序,它的作用僅僅是使用退出碼 2 退出。這個小程序展示了 NASM 和 GAS 的匯編程序的基本結構。
清單 1. 一個使用退出碼 2 退出的程序
行號
NASM
GAS
001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
; Text segment begins
section .text
global _start
; Program entry point
_start:
; Put the code number for system call
mov eax, 1
; Return value
mov ebx, 2
; Call the OS
int 80h
# Text segment begins
.section .text
.globl _start
# Program entry point
_start:
# Put the code number for system call
movl $1, %eax
/* Return value */
movl $2, %ebx
# Call the OS
int $0x80
現在解釋一下。
NASM 和 GAS 之間最大的差異之一是語法。GAS 使用 AT&T 語法,這是一種相當老的語法,由 GAS 和一些老式匯編器使用;NASM 使用 Intel 語法,大多數匯編器都支持它,包括 TASM 和 MASM。(GAS 的現代版本支持 .intel_syntax
指令,因此允許在 GAS 中使用 Intel 語法。)
下面是從 GAS 手冊總結出的一些主要差異:
AT&T 和 Intel 語法采用相反的源和目標操作數次序。例如:
Intel:mov eax, 4
AT&T:movl $4, %eax
在 AT&T 語法中,中間操作數前面加 $
;在 Intel 語法中,中間操作數不加前綴。例如:
Intel:push 4
AT&T:pushl $4
在 AT&T 語法中,寄存器操作數前面加 %
。在 Intel 語法中,它們不加前綴。
在 AT&T 語法中,內存操作數的大小由操作碼名稱的最后一個字符決定。操作碼后綴 b
、w
和 l
分別指定字節(8 位)、字(16 位)和長(32 位)內存引用。Intel 語法通過在內存操作數(而不是操作碼本身)前面加 byte ptr
、word ptr
和 dword ptr
來指定大小。所以:
Intel:mov al, byte ptr foo
AT&T:movb foo, %al
在 AT&T 語法中,中間形式長跳轉和調用是 lcall/ljmp $section, $offset
;Intel 語法是 call/jmp far section:offset
。在 AT&T 語法中,遠返回指令是 lret $stack-adjust
,而 Intel 使用 ret far stack-adjust
。
在這兩種匯編器中,寄存器的名稱是一樣的,但是因為尋址模式不同,使用它們的語法是不同的。另外,GAS 中的匯編器指令以 “.” 開頭,但是在 NASM 中不是。
.text
部分是處理器開始執行代碼的地方。global
(或者 GAS 中的 .globl
或 .global
)關鍵字用來讓一個符號對鏈接器可見,可以供其他鏈接對象模塊使用。在清單 1 的 NASM 部分中,global _start
讓 _start
符號成為可見的標識符,這樣鏈接器就知道跳轉到程序中的什么地方并開始執行。與 NASM 一樣,GAS 尋找這個 _start
標簽作為程序的默認進入點。在 GAS 和 NASM 中標簽都以冒號結尾。
中斷是一種通知操作系統需要它的服務的一種方法。第 16 行中的 int
指令執行這個工作。GAS 和 NASM 對中斷使用同樣的助記符。GAS 使用 0x
前綴指定十六進制數字,NASM 使用 h
后綴。因為在 GAS 中中間操作數帶 $
前綴,所以 80 hex 是 $0x80
。
int $0x80
(或 NASM 中的 80h
)用來向 Linux 請求一個服務。服務編碼放在 EAX 寄存器中。EAX 中存儲的值是 1(代表 Linux exit 系統調用),這請求程序退出。EBX 寄存器包含退出碼(在這個示例中是 2),也就是返回給操作系統的一個數字。(可以在命令提示下輸入 echo $?
來檢查這個數字。)
最后討論一下注釋。GAS 支持 C 風格(/* */
)、C++ 風格(//
)和 shell 風格(#
)的注釋。NASM 支持以 “;” 字符開頭的單行注釋。
回頁首
變量和內存訪問
本節首先給出一個示例程序,它尋找三個數字中的最大者。
清單 2. 尋找三個數字中最大者的程序
行號
NASM
GAS
001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
026
027
028
029
030
031
; Data section begins
section .data
var1 dd 40
var2 dd 20
var3 dd 30
section .text
global _start
_start:
; Move the contents of variables
mov ecx, [var1]
cmp ecx, [var2]
jg check_third_var
mov ecx, [var2]
check_third_var:
cmp ecx, [var3]
jg _exit
mov ecx, [var3]
_exit:
mov eax, 1
mov ebx, ecx
int 80h
// Data section begins
.section .data
var1:
.int 40
var2:
.int 20
var3:
.int 30
.section .text
.globl _start
_start:
# move the contents of variables
movl (var1), %ecx
cmpl (var2), %ecx
jg check_third_var
movl (var2), %ecx
check_third_var:
cmpl (var3), %ecx
jg _exit
movl (var3), %ecx
_exit:
movl $1, %eax
movl %ecx, %ebx
int $0x80
在上面的內存變量聲明中可以看到幾點差異。NASM 分別使用 dd
、dw
和 db
指令聲明 32 位、16 位和 8 位數字,而 GAS 分別使用 .long
、.int
和 .byte
。GAS 還有其他指令,比如 .ascii
、.asciz
和 .string
。在 GAS 中,像聲明其他標簽一樣聲明變量(使用冒號),但是在 NASM 中,只需在內存分配指令(dd
、dw
等等)前面輸入變量名,后面加上變量的值。
清單 2 中的第 18 行演示內存直接尋址模式。NASM 使用方括號間接引用一個內存位置指向的地址值:[var1]
。GAS 使用圓括號間接引用同樣的值:(var1)
。本文后面討論其他尋址模式的使用方法。
回頁首
使用宏
清單 3 演示本節討論的概念;它接受用戶名作為輸入并返回一句問候語。
清單 3. 讀取字符串并向用戶顯示問候語的程序
行號
NASM
GAS
001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
026
027
028
029
030
031
032
033
034
035
036
037
038
039
040
041
042
043
044
045
046
047
048
049
050
051
052
053
054
055
056
057
058
059
060
061
062
section .data
prompt_str db 'Enter your name: '
; $ is the location counter
STR_SIZE equ $ - prompt_str
greet_str db 'Hello '
GSTR_SIZE equ $ - greet_str
section .bss
; Reserve 32 bytes of memory
buff resb 32
; A macro with two parameters
; Implements the write system call
%macro write 2
mov eax, 4
mov ebx, 1
mov ecx, %1
mov edx, %2
int 80h
%endmacro
; Implements the read system call
%macro read 2
mov eax, 3
mov ebx, 0
mov ecx, %1
mov edx, %2
int 80h
%endmacro
section .text
global _start
_start:
write prompt_str, STR_SIZE
read buff, 32
; Read returns the length in eax
push eax
; Print the hello text
write greet_str, GSTR_SIZE
pop edx
; edx = length returned by read
write buff, edx
_exit:
mov eax, 1
mov ebx, 0
int 80h
.section .data
prompt_str:
.ascii "Enter Your Name: "
pstr_end:
.set STR_SIZE, pstr_end - prompt_str
greet_str:
.ascii "Hello "
gstr_end:
.set GSTR_SIZE, gstr_end - greet_str
.section .bss
// Reserve 32 bytes of memory
.lcomm buff, 32
// A macro with two parameters
// implements the write system call
.macro write str, str_size
movl $4, %eax
movl $1, %ebx
movl \str, %ecx
movl \str_size, %edx
int $0x80
.endm
// Implements the read system call
.macro read buff, buff_size
movl $3, %eax
movl $0, %ebx
movl \buff, %ecx
movl \buff_size, %edx
int $0x80
.endm
.section .text
.globl _start
_start:
write $prompt_str, $STR_SIZE
read $buff, $32
// Read returns the length in eax
pushl %eax
// Print the hello text
write $greet_str, $GSTR_SIZE
popl %edx
// edx = length returned by read
write $buff, %edx
_exit:
movl $1, %eax
movl $0, %ebx
int $0x80
本節要討論宏以及 NASM 和 GAS 對它們的支持。但是,在討論宏之前,先與其他幾個特性做一下比較。
清單 3 演示了未初始化內存的概念,這是用 .bss
部分指令(第 14
行)定義的。BSS 代表 “block storage segment” (原來是以一個符號開頭的塊),BSS
部分中保留的內存在程序啟動時初始化為零。BSS 部分中的對象只有一個名稱和大小,沒有值。與數據部分中不同,BSS
部分中聲明的變量并不實際占用空間。
NASM 使用 resb
、resw
和 resd
關鍵字在 BSS 部分中分配字節、字和雙字空間。GAS 使用 .lcomm
關鍵字分配字節級空間。請注意在這個程序的兩個版本中聲明變量名的方式。在 NASM 中,變量名前面加 resb
(或 resw
或 resd
)關鍵字,后面是要保留的空間量;在 GAS 中,變量名放在 .lcomm
關鍵字的后面,然后是一個逗號和要保留的空間量。
NASM:varname resb size
GAS:.lcomm varname, size
清單 3 還演示了位置計數器的概念(第 6 行)。
NASM 提供特殊的變量($
和 $$
變量)來操作位置計數器。在 GAS 中,無法操作位置計數器,必須使用標簽計算下一個存儲位置(數據、指令等等)。
例如,為了計算一個字符串的長度,在 NASM 中會使用以下指令:
prompt_str db 'Enter your name: '
STR_SIZE equ $ - prompt_str
; $ is the location counter
$
提供位置計數器的當前值,從這個位置計數器中減去標簽的值(所有變量名都是標簽),就會得出標簽的聲明和當前位置之間的字節數。equ
用來將變量 STR_SIZE 的值設置為后面的表達式。GAS 中使用的相似指令如下:
prompt_str:
.ascii "Enter Your Name: "
pstr_end:
.set STR_SIZE, pstr_end - prompt_str
末尾標簽(pstr_end
)給出下一個位置地址,減去啟始標簽地址就得出大小。還要注意,這里使用 .set
將變量 STR_SIZE 的值設置為逗號后面的表達式。也可以使用對應的 .equ
。在 NASM 中,沒有與 GAS 的 set
指令對應的指令。
正如前面提到的,清單 3 使用了宏(第 21 行)。在 NASM 和 GAS
中存在不同的宏技術,包括單行宏和宏重載,但是這里只關注基本類型。宏在匯編程序中的一個常見用途是提高代碼的清晰度。通過創建可重用的宏,可以避免重復
輸入相同的代碼段;這不但可以避免重復,而且可以減少代碼量,從而提高代碼的可讀性。
NASM 使用 %beginmacro
指令聲明宏,用 %endmacro
指令結束聲明。%beginmacro
指令后面是宏的名稱。宏名稱后面是一個數字,這是這個宏需要的宏參數數量。在 NASM 中,宏參數是從 1 開始連續編號的。也就是說,宏的第一個參數是 %1,第二個是 %2,第三個是 %3,以此類推。例如:
%beginmacro macroname 2
mov eax, %1
mov ebx, %2
%endmacro
這創建一個有兩個參數的宏,第一個參數是 %1
,第二個參數是 %2
。因此,對上面的宏的調用如下所示:
macroname 5, 6
還可以創建沒有參數的宏,在這種情況下不指定任何數字。
現在看看 GAS 如何使用宏。GAS 提供 .macro
和 .endm
指令來創建宏。.macro
指令后面跟著宏名稱,后面可以有參數,也可以沒有參數。在 GAS 中,宏參數是按名稱指定的。例如:
.macro macroname arg1, arg2
movl \arg1, %eax
movl \arg2, %ebx
.endm
當在宏中使用宏參數名稱時,在名稱前面加上一個反斜線。如果不這么做,鏈接器會把名稱當作標簽而不是參數,因此會報告錯誤。
函數、外部例程和堆棧
本節的示例程序在一個整數數組上實現選擇排序。
清單 4. 在整數數組上實現選擇排序
行號
NASM
GAS
001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
026
027
028
029
030
031
032
033
034
035
036
037
038
039
040
041
042
043
044
045
046
047
048
049
050
051
052
053
054
055
056
057
058
059
060
061
062
063
064
065
066
067
068
069
070
071
072
073
074
075
076
077
078
079
080
081
082
083
084
085
086
087
088
089
090
091
092
093
094
095
096
097
098
099
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
section .data
array db
89, 10, 67, 1, 4, 27, 12, 34,
86, 3
ARRAY_SIZE equ $ - array
array_fmt db " %d", 0
usort_str db "unsorted array:", 0
sort_str db "sorted array:", 0
newline db 10, 0
section .text
extern puts
global _start
_start:
push usort_str
call puts
add esp, 4
push ARRAY_SIZE
push array
push array_fmt
call print_array10
add esp, 12
push ARRAY_SIZE
push array
call sort_routine20
; Adjust the stack pointer
add esp, 8
push sort_str
call puts
add esp, 4
push ARRAY_SIZE
push array
push array_fmt
call print_array10
add esp, 12
jmp _exit
extern printf
print_array10:
push ebp
mov ebp, esp
sub esp, 4
mov edx, [ebp + 8]
mov ebx, [ebp + 12]
mov ecx, [ebp + 16]
mov esi, 0
push_loop:
mov [ebp - 4], ecx
mov edx, [ebp + 8]
xor eax, eax
mov al, byte [ebx + esi]
push eax
push edx
call printf
add esp, 8
mov ecx, [ebp - 4]
inc esi
loop push_loop
push newline
call printf
add esp, 4
mov esp, ebp
pop ebp
ret
sort_routine20:
push ebp
mov ebp, esp
; Allocate a word of space in stack
sub esp, 4
; Get the address of the array
mov ebx, [ebp + 8]
; Store array size
mov ecx, [ebp + 12]
dec ecx
; Prepare for outer loop here
xor esi, esi
outer_loop:
; This stores the min index
mov [ebp - 4], esi
mov edi, esi
inc edi
inner_loop:
cmp edi, ARRAY_SIZE
jge swap_vars
xor al, al
mov edx, [ebp - 4]
mov al, byte [ebx + edx]
cmp byte [ebx + edi], al
jge check_next
mov [ebp - 4], edi
check_next:
inc edi
jmp inner_loop
swap_vars:
mov edi, [ebp - 4]
mov dl, byte [ebx + edi]
mov al, byte [ebx + esi]
mov byte [ebx + esi], dl
mov byte [ebx + edi], al
inc esi
loop outer_loop
mov esp, ebp
pop ebp
ret
_exit:
mov eax, 1
mov ebx, 0
int 80h
.section .data
array:
.byte 89, 10, 67, 1, 4, 27, 12,
34, 86, 3
array_end:
.equ ARRAY_SIZE, array_end - array
array_fmt:
.asciz " %d"
usort_str:
.asciz "unsorted array:"
sort_str:
.asciz "sorted array:"
newline:
.asciz "\n"
.section .text
.globl _start
_start:
pushl $usort_str
call puts
addl $4, %esp
pushl $ARRAY_SIZE
pushl $array
pushl $array_fmt
call print_array10
addl $12, %esp
pushl $ARRAY_SIZE
pushl $array
call sort_routine20
# Adjust the stack pointer
addl $8, %esp
pushl $sort_str
call puts
addl $4, %esp
pushl $ARRAY_SIZE
pushl $array
pushl $array_fmt
call print_array10
addl $12, %esp
jmp _exit
print_array10:
pushl %ebp
movl %esp, %ebp
subl $4, %esp
movl 8(%ebp), %edx
movl 12(%ebp), %ebx
movl 16(%ebp), %ecx
movl $0, %esi
push_loop:
movl %ecx, -4(%ebp)
movl 8(%ebp), %edx
xorl %eax, %eax
movb (%ebx, %esi, 1), %al
pushl %eax
pushl %edx
call printf
addl $8, %esp
movl -4(%ebp), %ecx
incl %esi
loop push_loop
pushl $newline
call printf
addl $4, %esp
movl %ebp, %esp
popl %ebp
ret
sort_routine20:
pushl %ebp
movl %esp, %ebp
# Allocate a word of space in stack
subl $4, %esp
# Get the address of the array
movl 8(%ebp), %ebx
# Store array size
movl 12(%ebp), %ecx
decl %ecx
# Prepare for outer loop here
xorl %esi, %esi
outer_loop:
# This stores the min index
movl %esi, -4(%ebp)
movl %esi, %edi
incl %edi
inner_loop:
cmpl $ARRAY_SIZE, %edi
jge swap_vars
xorb %al, %al
movl -4(%ebp), %edx
movb (%ebx, %edx, 1), %al
cmpb %al, (%ebx, %edi, 1)
jge check_next
movl %edi, -4(%ebp)
check_next:
incl %edi
jmp inner_loop
swap_vars:
movl -4(%ebp), %edi
movb (%ebx, %edi, 1), %dl
movb (%ebx, %esi, 1), %al
movb %dl, (%ebx, %esi, 1)
movb %al, (%ebx, %edi, 1)
incl %esi
loop outer_loop
movl %ebp, %esp
popl %ebp
ret
_exit:
movl $1, %eax
movl 0, %ebx
int $0x80
初看起來清單 4 似乎非常復雜,實際上它是非常簡單的。這個清單演示了函數、各種內存尋址方案、堆棧和庫函數的使用方法。這個程序對包含 10 個數字的數組進行排序,并使用外部 C 庫函數 puts
和 printf
輸出未排序數組和已排序數組的完整內容。為了實現模塊化和介紹函數的概念,排序例程本身實現為一個單獨的過程,數組輸出例程也是這樣。我們來逐一分析一下。
在聲明數據之后,這個程序首先執行對 puts
的調用(第 31 行)。puts
函數在控制臺上顯示一個字符串。它惟一的參數是要顯示的字符串的地址,通過將字符串的地址壓入堆棧(第 30 行),將這個參數傳遞給它。
在 NASM 中,任何不屬于我們的程序但是需要在鏈接時解析的標簽都必須預先定義,這就是 extern
關鍵字的作用(第 24 行)。GAS 沒有這樣的要求。在此之后,字符串的地址 usort_str
被壓入堆棧(第 30 行)。在 NASM 中,內存變量(比如 usort_str
)代表內存位置本身,所以 push usort_str
這樣的調用實際上是將地址壓入堆棧的頂部。但是在 GAS 中,變量 usort_str
必須加上前綴 $
,這樣它才會被當作地址。如果不加前綴 $
,那么會將內存變量代表的實際字節壓入堆棧,而不是地址。
因為在堆棧中壓入一個變量會讓堆棧指針移動一個雙字,所以給堆棧指針加 4(雙字的大小)(第 32 行)。
現在將三個參數壓入堆棧,并調用 print_array10
函數(第 37 行)。在 NASM 和 GAS 中聲明函數的方法是相同的。它們僅僅是通過 call
指令調用的標簽。
在調用函數之后,ESP 代表堆棧的頂部。esp + 4
代表返回地址,esp + 8
代表函數的第一個參數。在堆棧指針上加上雙字變量的大小(即 esp + 12
、esp + 16
等等),就可以訪問所有后續參數。
在函數內部,通過將 esp
復制到 ebp
(第 62 行)創建一個局部堆棧框架。和程序中的處理一樣,還可以為局部變量分配空間(第 63 行)。方法是從 esp
中減去所需的字節數。esp – 4
表示為一個局部變量分配 4 字節的空間,只要堆棧中有足夠的空間容納局部變量,就可以繼續分配。
清單 4 演示了基間接尋址模式(第 64 行),也就是首先取得一個基地址,然后在它上面加一個偏移量,從而到達最終的地址。在清單的 NASM 部分中,[ebp + 8]
和 [ebp – 4]
(第 71 行)就是基間接尋址模式的示例。在 GAS 中,尋址方法更簡單一些:4(%ebp)
和 -4(%ebp)
。
在 print_array10
例程中,在 push_loop
標簽后面可以看到另一種尋址模式(第 74 行)。在 NASM 和 GAS 中的表示方法如下:
NASM:mov al, byte [ebx + esi]
GAS:movb (%ebx, %esi, 1), %al
這種尋址模式稱為基索引尋址模式。這里有三項數據:一個是基地址,第二個是索引寄存器,第三個是乘數。因為不可能決定從一
個內存位置開始訪問的字節數,所以需要用一個方法計算訪問的內存量。NASM 使用字節操作符告訴匯編器要移動一個字節的數據。在 GAS
中,用一個乘數和助記符中的 b
、w
或 l
后綴(例如 movb
)來解決這個問題。初看上去 GAS 的語法似乎有點兒復雜。
GAS 中基索引尋址模式的一般形式如下:
%segment:ADDRESS (, index, multiplier)
或
%segment:(offset, index, multiplier)
或
%segment:ADDRESS(base, index, multiplier)
使用這個公式計算最終的地址:
ADDRESS or offset + base + index * multiplier.
因此,要想訪問一個字節,就使用乘數 1;對于字,乘數是 2;對于雙字,乘數是 4。當然,NASM 使用的語法比較簡單。上面的公式在 NASM 中表示為:
Segment:[ADDRESS or offset + index * multiplier]
為了訪問 1、2 或 4 字節的內存,在這個內存地址前面分別加上 byte
、word
或 dword
。
其他方面
清單 5 讀取命令行參數的列表,將它們存儲在內存中,然后輸出它們。
清單 5. 讀取命令行參數,將它們存儲在內存中,然后輸出它們
行號
NASM
GAS
001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
026
027
028
029
030
031
032
033
034
035
036
037
038
039
040
041
042
043
044
045
046
047
048
049
050
051
052
053
054
055
056
057
058
059
060
061
section .data
; Command table to store at most
; 10 command line arguments
cmd_tbl:
%rep 10
dd 0
%endrep
section .text
global _start
_start:
; Set up the stack frame
mov ebp, esp
; Top of stack contains the
; number of command line arguments.
; The default value is 1
mov ecx, [ebp]
; Exit if arguments are more than 10
cmp ecx, 10
jg _exit
mov esi, 1
mov edi, 0
; Store the command line arguments
; in the command table
store_loop:
mov eax, [ebp + esi * 4]
mov [cmd_tbl + edi * 4], eax
inc esi
inc edi
loop store_loop
mov ecx, edi
mov esi, 0
extern puts
print_loop:
; Make some local space
sub esp, 4
; puts function corrupts ecx
mov [ebp - 4], ecx
mov eax, [cmd_tbl + esi * 4]
push eax
call puts
add esp, 4
mov ecx, [ebp - 4]
inc esi
loop print_loop
jmp _exit
_exit:
mov eax, 1
mov ebx, 0
int 80h
.section .data
// Command table to store at most
// 10 command line arguments
cmd_tbl:
.rept 10
.long 0
.endr
.section .text
.globl _start
_start:
// Set up the stack frame
movl %esp, %ebp
// Top of stack contains the
// number of command line arguments.
// The default value is 1
movl (%ebp), %ecx
// Exit if arguments are more than 10
cmpl $10, %ecx
jg _exit
movl $1, %esi
movl $0, %edi
// Store the command line arguments
// in the command table
store_loop:
movl (%ebp, %esi, 4), %eax
movl %eax, cmd_tbl( , %edi, 4)
incl %esi
incl %edi
loop store_loop
movl %edi, %ecx
movl $0, %esi
print_loop:
// Make some local space
subl $4, %esp
// puts functions corrupts ecx
movl %ecx, -4(%ebp)
movl cmd_tbl( , %esi, 4), %eax
pushl %eax
call puts
addl $4, %esp
movl -4(%ebp), %ecx
incl %esi
loop print_loop
jmp _exit
_exit:
movl $1, %eax
movl $0, %ebx
int $0x80
清單 5 演示在匯編程序中重復執行指令的方法。很自然,這種結構稱為重復結構。在 GAS 中,重復結構以 .rept
指令開頭(第 6 行)。用一個 .endr
指令結束這個指令(第 8 行)。.rept
后面是一個數字,它指定 .rept/.endr
結構中表達式重復執行的次數。這個結構中的任何指令都相當于編寫這個指令 count
次,每次重復占據單獨的一行。
例如,如果次數是 3:
.rept 3
movl $2, %eax
.endr
就相當于:
movl $2, %eax
movl $2, %eax
movl $2, %eax
在 NASM 中,在預處理器級使用相似的結構。它以 %rep
指令開頭,以 %endrep
結尾。%rep
指令后面是一個表達式(在 GAS 中 .rept
指令后面是一個數字):
%rep
nop
%endrep
在 NASM 中還有另一種結構,times
指令。與 %rep
相似,它也在匯編級起作用,后面也是一個表達式。例如,上面的 %rep
結構相當于:
times nop
以下代碼:
%rep 3
mov eax, 2
%endrep
相當于:
times 3 mov eax, 2
它們都相當于:
mov eax, 2
mov eax, 2
mov eax, 2
在清單 5 中,使用 .rept
(或 %rep
)指令為 10 個雙字創建內存數據區。然后,從堆棧一個個地訪問命令行參數,并將它們存儲在內存區中,直到命令表填滿。
在這兩種匯編器中,訪問命令行參數的方法是相似的。ESP(堆棧頂部)存儲傳遞給程序的命令行參數數量,默認值是 1(表示沒有命令行參數)。esp + 4
存儲第一個命令行參數,這總是從命令行調用的程序的名稱。esp + 8
、esp + 12
等存儲后續命令行參數。
還要注意清單 5 中從兩邊訪問內存命令表的方法。這里使用內存間接尋址模式(第 31 行)訪問命令表,還使用了 ESI(和 EDI)中的偏移量和一個乘數。因此,NASM 中的 [cmd_tbl + esi * 4]
相當于 GAS 中的 cmd_tbl(, %esi, 4)
。
結束語
盡管在這兩種匯編器之間存在實質性的差異,但是在這兩種形式之間進行轉換并不困難。您最初可能覺得 AT&T 語法難以理解,但是掌握了它之后,它其實和 Intel 語法同樣簡單。