跳到内容

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
华夏公益教科书