x86 彙編/NASM 語法
Netwide Assembler 是一款 x86 和 x86-64 彙編器,它使用類似於英特爾的語法。它支援各種目標檔案格式,包括
- ELF32/64
- Linux a.out
- NetBSD/FreeBSD a.out
- MS-DOS 16 位/32 位目標檔案
- Win32/64 目標檔案
- COFF
- Mach-O 32/64
- rdf
- 二進位制
NASM 同時執行在 Unix/Linux 和 Windows/DOS 上。
Netwide Assembler (NASM) 使用一種 “設計得簡單易懂,類似於英特爾的但更不復雜” 的語法。這意味著運算元順序是 **目標** 然後 **源**,而不是 GNU 彙編器使用的 AT&T 風格。例如,
mov ax, 9
將數字 9 載入到 ax 暫存器中。
對於那些在 nasm 中使用 gdb 的人,你可以透過發出以下命令來設定 gdb 使用英特爾風格的反彙編
set disassembly-flavor intel
單個分號用於註釋,其功能與 C++ 中的雙斜槓相同:編譯器會忽略從分號到下一個換行符的內容。
NASM 具有強大的宏功能,類似於 C 的預處理器。例如,
%define newline 0xA
%define func(a, b) ((a) * (b) + 2)
func (1, 22) ; expands to ((1) * (22) + 2)
%macro print 1 ; macro with one argument
push dword %1 ; %1 means first argument
call printf
add esp, 4
%endmacro
print mystring ; will call printf
要在 Linux 上向核心傳遞簡單的輸入命令,你需要向以下暫存器傳遞值,然後向核心傳送中斷訊號。要從標準輸入(例如,來自使用者鍵盤)讀取單個字元,請執行以下操作
; read a byte from stdin
mov eax, 3 ; 3 is recognized by the system as meaning "read"
mov ebx, 0 ; read from standard input
mov ecx, variable ; address to pass to
mov edx, 1 ; input length (one byte)
int 0x80 ; call the kernel
在 int 0x80 之後,eax 將包含讀取的位元組數。如果這個數字 < 0,則表示發生了某種讀取錯誤。
輸出遵循類似的約定
; print a byte to stdout
mov eax, 4 ; the system interprets 4 as "write"
mov ebx, 1 ; standard output (print to terminal)
mov ecx, variable ; pointer to the value being passed
mov edx, 1 ; length of output (in bytes)
int 0x80 ; call the kernel
BSD 系統(包括 MacOS X)使用類似的系統呼叫,但執行它們的約定不同。在 Linux 上,你將系統呼叫引數傳遞到不同的暫存器中,而在 BSD 系統上,它們被壓入堆疊(除了系統呼叫號,它被放入 eax,與 Linux 中的方式相同)。上述程式碼的 BSD 版本
; read a byte from stdin
mov eax, 3 ; sys_read system call
push dword 1 ; input length
push dword variable ; address to pass to
push dword 0 ; read from standard input
push eax
int 0x80 ; call the kernel
add esp, 16 ; move back the stack pointer
; write a byte to stdout
mov eax, 4 ; sys_write system call
push dword 1 ; output length
push dword variable ; memory address
push dword 1 ; write to standard output
push eax
int 0x80 ; call the kernel
add esp, 16 ; move back the stack pointer
; quit the program
mov eax, 1 ; sys_exit system call
push dword 0 ; program return value
push eax
int 0x80 ; call the kernel
下面是一個簡單的 Hello world 示例,它展示了 nasm 程式的基本結構
global _start
section .data
; Align to the nearest 2 byte boundary, must be a power of two
align 2
; String, which is just a collection of bytes, 0xA is newline
str: db 'Hello, world!',0xA
strLen: equ $-str
section .bss
section .text
_start:
;
; op dst, src
;
;
; Call write(2) syscall:
; ssize_t write(int fd, const void *buf, size_t count)
;
mov edx, strLen ; Arg three: the length of the string
mov ecx, str ; Arg two: the address of the string
mov ebx, 1 ; Arg one: file descriptor, in this case stdout
mov eax, 4 ; Syscall number, in this case the write(2) syscall:
int 0x80 ; Interrupt 0x80
;
; Call exit(3) syscall
; void exit(int status)
;
mov ebx, 0 ; Arg one: the status
mov eax, 1 ; Syscall number:
int 0x80
為了彙編、連結和執行該程式,我們需要執行以下操作
$ nasm -f elf32 -g helloWorld.asm
$ ld -g helloWorld.o
$ ./a.out
在這個例子中,我們將使用 Win32 系統呼叫來重寫 hello world 示例。有一些主要區別
- 中間檔案將是一個 Microsoft Win32 (i386) 物件檔案
- 我們將避免使用中斷,因為它們可能不可移植,而且這是 Windows,而不是 DOS,因此我們需要從 kernel32 DLL 中引入幾個呼叫
global _start
extern _GetStdHandle@4
extern _WriteConsoleA@20
extern _ExitProcess@4
section .data
str: db 'hello, world',0x0D,0x0A
strLen: equ $-str
section .bss
numCharsWritten: resd 1
section .text
_start:
;
; HANDLE WINAPI GetStdHandle( _In_ DWORD nStdHandle ) ;
;
push dword -11 ; Arg1: request handle for standard output
call _GetStdHandle@4 ; Result: in eax
;
; BOOL WINAPI WriteConsole(
; _In_ HANDLE hConsoleOutput,
; _In_ const VOID *lpBuffer,
; _In_ DWORD nNumberOfCharsToWrite,
; _Out_ LPDWORD lpNumberOfCharsWritten,
; _Reserved_ LPVOID lpReserved ) ;
;
push dword 0 ; Arg5: Unused so just use zero
push numCharsWritten ; Arg4: push pointer to numCharsWritten
push dword strLen ; Arg3: push length of output string
push str ; Arg2: push pointer to output string
push eax ; Arg1: push handle returned from _GetStdHandle
call _WriteConsoleA@20
;
; VOID WINAPI ExitProcess( _In_ UINT uExitCode ) ;
;
push dword 0 ; Arg1: push exit code
call _ExitProcess@4
為了彙編、連結和執行該程式,我們需要執行以下操作
$ nasm -f win32 -g helloWorldWin32.asm
$ ld -e _start helloWorldwin32.obj -lkernel32 -o helloWorldWin32.exe
在這個例子中,我們在呼叫 ld 時使用 -e 命令列選項來指定程式執行的入口點。否則,我們將不得不使用 _WinMain@16 作為入口點,而不是 _start。這個例子是在 cygwin 下執行的,在 Windows 命令提示符下,連結步驟會有所不同。最後一點需要注意的是,WriteConsole() 在 cygwin 控制檯中表現不佳,因此為了看到輸出,最終的 exe 應該在 Windows 命令提示符下執行。
在這個例子中,我們將重寫 Hello World 以使用 printf(3) 來自 C 庫,並使用 gcc 連結。這樣做的好處是,從 Linux 到 Windows 需要的原始碼更改很少,而彙編和連結步驟略有不同。在 Windows 環境中,這樣做還有另外一個好處,即連結步驟在 Windows 命令提示符和 cygwin 中將是相同的。有一些主要變化
"hello, world"字串現在成為printf(3)的格式字串,因此需要以 null 結尾。這也意味著我們不再需要顯式指定它的長度。- gcc 期望執行的入口點為 main
- Microsoft 將使用
cdecl呼叫約定,在函式前面加上下劃線。因此,main和printf在 Windows 開發環境中將分別變為_main和_printf。
global main
extern printf
section .data
fmtStr: db 'hello, world',0xA,0
section .text
main:
sub esp, 4 ; Allocate space on the stack for one 4 byte parameter
lea eax, [fmtStr]
mov [esp], eax ; Arg1: pointer to format string
call printf ; Call printf(3):
; int printf(const char *format, ...);
add esp, 4 ; Pop stack once
ret
為了彙編、連結和執行該程式,我們需要執行以下操作。
$ nasm -felf32 helloWorldgcc.asm
$ gcc helloWorldgcc.o -o helloWorldgcc
帶下劃線字首的 Windows 版本
global _main
extern _printf ; Uncomment under Windows
section .data
fmtStr: db 'hello, world',0xA,0
section .text
_main:
sub esp, 4 ; Allocate space on the stack for one 4 byte parameter
lea eax, [fmtStr]
mov [esp], eax ; Arg1: pointer to format string
call _printf ; Call printf(3):
; int printf(const char *format, ...);
add esp, 4 ; Pop stack once
ret
為了彙編、連結和執行該程式,我們需要執行以下操作。
$ nasm -fwin32 helloWorldgcc.asm
$ gcc helloWorldgcc.o -o helloWorldgcc.exe