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