跳到內容

x86 彙編/NASM 語法

來自華夏公益教科書,開放的書籍,開放的世界

Netwide Assembler 是一款 x86 和 x86-64 彙編器,它使用類似於英特爾的語法。它支援各種目標檔案格式,包括

  1. ELF32/64
  2. Linux a.out
  3. NetBSD/FreeBSD a.out
  4. MS-DOS 16 位/32 位目標檔案
  5. Win32/64 目標檔案
  6. COFF
  7. Mach-O 32/64
  8. rdf
  9. 二進位制

NASM 同時執行在 Unix/Linux 和 Windows/DOS 上。

NASM 語法

[編輯 | 編輯原始碼]

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

示例 I/O(Linux 和 BSD)

[編輯 | 編輯原始碼]

要在 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(Linux)

[編輯 | 編輯原始碼]

下面是一個簡單的 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

Hello World(僅使用 Win32 系統呼叫)

[編輯 | 編輯原始碼]

在這個例子中,我們將使用 Win32 系統呼叫來重寫 hello world 示例。有一些主要區別

  1. 中間檔案將是一個 Microsoft Win32 (i386) 物件檔案
  2. 我們將避免使用中斷,因為它們可能不可移植,而且這是 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(使用 C 庫並與 gcc 連結)

[編輯 | 編輯原始碼]

在這個例子中,我們將重寫 Hello World 以使用 printf(3) 來自 C 庫,並使用 gcc 連結。這樣做的好處是,從 Linux 到 Windows 需要的原始碼更改很少,而彙編和連結步驟略有不同。在 Windows 環境中,這樣做還有另外一個好處,即連結步驟在 Windows 命令提示符和 cygwin 中將是相同的。有一些主要變化

  1. "hello, world" 字串現在成為 printf(3) 的格式字串,因此需要以 null 結尾。這也意味著我們不再需要顯式指定它的長度。
  2. gcc 期望執行的入口點為 main
  3. Microsoft 將使用 cdecl 呼叫約定,在函式前面加上下劃線。因此,mainprintf 在 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
華夏公益教科書