Intel x86 Assembly Language & Microarchitecture英特尔x86汇编语言和微体系结构入门


备注

本节概述了x86是什么,以及开发人员可能想要使用它的原因。

它还应该提到x86中的任何大型主题,并链接到相关主题。由于x86的Documentation是新的,因此您可能需要创建这些相关主题的初始版本。

x86汇编语言

x86汇编语言系列代表了原始Intel 8086架构数十年的进步。除了基于所使用的汇编程序的几种不同的方言之外,多年来还增加了额外的处理器指令,寄存器和其他功能,同时仍然向后兼容20世纪80年代使用的16位汇编。

使用x86程序集的第一步是确定目标是什么。例如,如果您要在操作系统中编写代码,则需要另外确定是选择使用独立汇编程序还是使用更高级语言(如C)的内置内联汇编功能。如果您希望在没有操作系统的情况下编写“裸机”代码,您只需安装您选择的汇编程序,并了解如何创建可以转换为闪存,可引导映像或以其他方式加载到内存中的二进制代码。适当的位置开始执行。

在许多平台上得到很好支持的非常受欢迎的汇编程序是NASM(Netwide Assembler),它可以从http://nasm.us/获得。在NASM站点上,您可以继续为您的平台下载最新版本。

视窗

32位和64位版本的NASM都可用于Windows。 NASM附带了一个方便的安装程序,可以在Windows主机上使用以自动安装汇编程序。

Linux的

很可能NASM已经安装在您的Linux版本上。要检查,执行:

nasm -v
 

如果找不到该命令,则需要执行安装。除非您正在做一些需要前沿NASM功能的事情,否则最好的方法是使用内置的软件包管理工具来安装NASM。例如,在Debian派生的系统(如Ubuntu等)下,从命令提示符执行以下命令:

sudo apt-get install nasm
 

对于基于RPM的系统,您可以尝试:

sudo yum install nasm
 

Mac OS X.

OS X的最新版本(包括Yosemite和El Capitan)预装了旧版本的NASM。例如,El Capitan已安装版本0.98.40。虽然这可能适用于几乎所有正常目的,但它实际上已经很老了。在撰写本文时,NASM版本2.11发布,2.12有许多候选版本可用。

您可以从上面的链接获取NASM源代码,但除非您特别需要从源代码安装,否则从OS X版本目录下载二进制包并将其解压缩要简单得多。

解压缩后,强烈建议您不要覆盖系统安装的NASM版本。相反,您可以将其安装到/ usr / local中:

 $ sudo su
 <user's password entered to become root>
 # cd /usr/local/bin
 # cp <path/to/unzipped/nasm/files/nasm> ./
 # exit
 

此时,NASM位于/usr/local/bin ,但它不在您的路径中。您现在应该将以下行添加到配置文件的末尾:

 $ echo 'export PATH=/usr/local/bin:$PATH' >> ~/.bash_profile
 

这将在/usr/local/bin 到您的路径中。现在,在命令提示符下执行nasm -v 应显示正确的较新版本。

x86 Linux Hello World示例

这是32位x86 Linux的NASM程序集中的基本Hello World程序,直接使用系统调用(没有任何libc函数调用)。这需要很多,但随着时间的推移它会变得可以理解。以分号( ; )开头的行是注释。

如果您还不熟悉低级Unix系统编程,您可能只想在asm中编写函数并从C或C ++程序中调用它们。然后你可以担心学习如何处理寄存器和内存,而不用学习POSIX系统调用API和ABI来使用它。


这会产生两个系统调用: write(2) _exit(2) (不是exit(3) libc包装器,用于刷新stdio缓冲区等)。 (从技术上讲, _exit() 调用sys_exit_group,而不是sys_exit,但这只在多线程进程中很重要 。)另请参阅syscalls(2) 以获取有关系统调用的文档,以及直接使用libc和使用libc之间的区别包装函数。

总之,系统调用是通过将args放在适当的寄存器中,系统调用号放在eax ,然后运行int 0x80 指令来完成的。另请参见Assembly中系统调用的返回值是什么?有关如何使用C语法记录asm系统调用接口的更多说明。

对于32位ABI系统调用呼叫号码是在/usr/include/i386-linux-gnu/asm/unistd_32.h (相同内容在/usr/include/x86_64-linux-gnu/asm/unistd_32.h )。

#include <sys/syscall.h> 最终将包含正确的文件,因此您可以运行echo '#include <sys/syscall.h>' | gcc -E - -dM | less 看到宏defs( 有关在C头中查找asm的常量的更多信息,请参阅此答案


section .text             ; Executable code goes in the .text section
global _start             ; The linker looks for this symbol to set the process entry point, so execution start here
;;;a name followed by a colon defines a symbol.  The global _start directive modifies it so it's a global symbol, not just one that we can CALL or JMP to from inside the asm.
;;; note that _start isn't really a "function".  You can't return from it, and the kernel passes argc, argv, and env differently than main() would expect.
 _start:
    ;;; write(1, msg, len);
    ; Start by moving the arguments into registers, where the kernel will look for them
    mov     edx,len       ; 3rd arg goes in edx: buffer length
    mov     ecx,msg       ; 2nd arg goes in ecx: pointer to the buffer
    ;Set output to stdout (goes to your terminal, or wherever you redirect or pipe)
    mov     ebx,1         ; 1st arg goes in ebx: Unix file descriptor. 1 = stdout, which is normally connected to the terminal.

    mov     eax,4         ; system call number (from SYS_write / __NR_write from unistd_32.h).
    int     0x80          ; generate an interrupt, activating the kernel's system-call handling code.  64-bit code uses a different instruction, different registers, and different call numbers.
    ;; eax = return value, all other registers unchanged.

    ;;;Second, exit the process.  There's nothing to return to, so we can't use a ret instruction (like we could if this was main() or any function with a caller)
    ;;; If we don't exit, execution continues into whatever bytes are next in the memory page,
    ;;; typically leading to a segmentation fault because the padding 00 00 decodes to  add [eax],al.

    ;;; _exit(0);
    xor     ebx,ebx       ; first arg = exit status = 0.  (will be truncated to 8 bits).  Zeroing registers is a special case on x86, and mov ebx,0 would be less efficient.
                      ;; leaving out the zeroing of ebx would mean we exit(1), i.e. with an error status, since ebx still holds 1 from earlier.
    mov     eax,1         ; put __NR_exit into eax
    int     0x80          ;Execute the Linux function

section     .rodata       ; Section for read-only constants

             ;; msg is a label, and in this context doesn't need to be msg:.  It could be on a separate line.
             ;; db = Data Bytes: assemble some literal bytes into the output file.
msg     db  'Hello, world!',0xa     ; ASCII string constant plus a newline (0x10)

             ;;  No terminating zero byte is needed, because we're using write(), which takes a buffer + length instead of an implicit-length string.
             ;; To make this a C string that we could pass to puts or strlen, we'd need a terminating 0 byte. (e.g. "...", 0x10, 0)

len     equ $ - msg       ; Define an assemble-time constant (not stored by itself in the output file, but will appear as an immediate operand in insns that use it)
                          ; Calculate len = string length.  subtract the address of the start
                          ; of the string from the current position ($)
  ;; equivalently, we could have put a str_end: label after the string and done   len equ str_end - str
 

在Linux上,您可以将此文件另存为Hello.asm 并使用以下命令从中构建32位可执行文件:

nasm -felf32 Hello.asm                  # assemble as 32-bit code.  Add -Worphan-labels -g -Fdwarf  for debug symbols and warnings
gcc -nostdlib -m32 Hello.o -o Hello     # link without CRT startup code or libc, making a static binary
 

有关程序集构建为32或64位静态或动态链接的Linux可执行文件,NASM / YASM语法或GNU AT&T语法以及GNU as 指令的更多详细信息,请参阅此答案 。 (关键点:确保在64位主机上构建32位代码时使用-m32 或等效代码,否则在运行时会遇到令人困惑的问题。)

您可以使用strace 跟踪它的执行情况,以查看它所进行的系统调用:

$ strace ./Hello 
execve("./Hello", ["./Hello"], [/* 72 vars */]) = 0
[ Process PID=4019 runs in 32 bit mode. ]
write(1, "Hello, world!\n", 14Hello, world!
)         = 14
_exit(0)                                = ?
+++ exited with 0 +++
 

stderr上的跟踪和stdout上的常规输出都到达终端,因此它们会干扰write 系统调用。如果您愿意,可以重定向或跟踪到文件。请注意,这样可以让我们轻松查看syscall返回值,而无需添加代码来打印它们,实际上比使用常规调试器(如gdb)更容易。

该程序的x86-64版本非常相似,将相同的args传递给相同的系统调用,只是在不同的寄存器中。并使用syscall 指令而不是int 0x80