Download:X86汇编语言:实模式到保护模式
因为汇编语言是二进制文件中机器指令的标准表示形式,许多二进制分析都基于反汇编,所以读者必须熟悉x86汇编语言的基础知识,才能从本书中获得最大收获。本附录将为你介绍汇编语言的基础知识。
本附录的目的不是教你如何编写汇编程序(有专门介绍汇编程序的图书),而是向读者展示理解汇编程序所需的基础知识。通过本附录你将了解汇编程序和x86指令的结构以及它们运行时的行为,此外还将看到C/C++程序的通用代码在汇编层面是如何表示的。这里只介绍基本的64位用户模式的x86指令,不包括浮点指令集或者扩展指令集,如SSE或MMX。简单起见,这里将x86的64位版本(x86-64或x64)统称为x86,因为x86才是本书的重点。
A.1 汇编程序的布局
清单A-1显示了一个简单的C程序,清单A-2显示了由GCC v5.4.0对应生成的汇编程序,第1章解释了编译器如何将C程序转换为汇编列表,并最终转换为二进制文件。
在对二进制文件进行反汇编时,反汇编工具会尝试将其转换得与编译器生成的汇编代码尽可能相似。下面我们来看一下汇编程序的布局,但不讨论汇编指令的细节。
清单A-1 C编写“Hello, world!”
#include <stdio.h>
int
❶ main(int argc, char* argv[])
{
❷ printf(❸"Hello, world!\n");
return 0;
}
清单A-2 GCC生成的汇编程序
.file "hello.c"
.intel_syntax noprefix
❹ .section .rodata
.LC0:
❺ .string "Hello, world!"
❻ .text
.globl main
.type main, function
❼ main:
push rbp
mov rbp, rsp
sub rsp, 16
mov DWORD PTR [rbp-4], edi
mov QWORD PTR [rbp-16], rsi
❽ mov edi, OFFSET FLAT:.LC0
❾ call puts
mov eax, 0
leave
ret
.size main, .-main
.ident "GCC: (Ubuntu 5.4.0-6ubuntu1~16.04.9)"
.section .note.GNU-stack,"",progbits
清单 A-1 通过在main函数❶中调用 printf❷输出常量字符串“Hello,World!”❸,在更底层,对应的汇编程序由4种类型的组件组成:指令(instruction)、伪指令(directive)、标号(label)及注释(comment)。
A.1.1 汇编指令、伪指令、标号及注释
表A-1列出了汇编程序的每种组件类型的说明,注意每种组件的语法因汇编或反汇编工具的变化而不同。就本书而言,读者无须非常熟悉任何汇编工具的语法特点,只需读懂和分析反汇编的代码,而无须编写汇编代码。这里推荐GCC使用-masm=intel选项生成的汇编语法。
表A-1 汇编程序的组件
指令是CPU执行的实际操作,伪指令意在告诉汇编工具生成特定数据,并将指令或数据放在指定的节,标号是在汇编工具中引用指令或数据的符号名称,注释是可读注释。在程序被汇编链接成二进制文件后,所有符号名称都被地址所取代。
清单 A-2 中的汇编程序指示汇编工具将“Hello,world!”字符串❺放在.rodata节❹,这是一个用于存储常量数据的节。伪指令.section告诉汇编工具将在哪个节放置后面的内容,.string表示定义ASCII字符串的伪指令。当然还有一些伪指令用于定义其他数据类型,如.byte(1字节)、.word(2字节)、.long(4字节)及.quad(8字节)。
main函数放在.text的代码节中,该节用于存储代码,其中.text伪指令❻是.section .text的简写,另外main❼是main函数的符号标签。
标签后面的就是main函数包含的真实指令,这些指令可以引用先前声明的数据,如.LC0❽(GCC为“Hello,world!”字符串选择的符号名称)。因为程序会输出一个常量字符串(无可变参数),所以GCC用puts❾替换printf,这是一个将指定字符串输出到屏幕的简单函数。
A.1.2 代码与数据分离
可以在清单A-2中观察到一个关键结果,即编译器通常将代码和数据分为不同的节,这在反汇编或分析二进制文件的时候非常方便,因为这样就知道程序中哪些字节被解释为代码,哪些字节被解释为数据。但是,x86架构本质上并没有阻止在同一个节中混合代码和数据,实际上某些编译器或者手写汇编程序也确实将数据和代码混合在同一个节。