本文解釋兩種最流行的 Linux® 彙編器 —— GNU Assembler(GAS)和 Netwide Assembler(NASM) —— 之間一些比較重要的語法差異和語義差異,包括基本語法、變數和內存訪問、宏處理、函數和外部常式、堆棧處理以及重複執行代碼塊的技術方面的差異。
與其他語言不同,彙編語言要求開發人員了解編程所用機器的處理器體系結構。彙編程序不可移植,維護和理解常常比較麻煩,通常包含大量代碼行。但是,在機器上執行的運行時二進位代碼在速度和大小方面有優勢。
對於在 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 <expression>
nop
%endrep
在 NASM 中還有另一種結構,times 指令。與 %rep 相似,它也在彙編級起作用,後面也是一個表達式。例如,上面的 %rep 結構相當於:
times <expression> 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 語法同樣簡單。
(責任編輯:A6)