内容来自:x86/x64体系探索及编程 (豆瓣) (douban.com) 第二章 - x86/x64编程基础
1 选择编译器
nasm、fasm、yasm 是免费开源的汇编编译器,总体上来讲都使用 Intel 的语法。yasm 是在 nasm 的基础上开发的,与 nasm 同宗。由于使用了相同的语法,因此 nasm 的代码可以直接用 yasm 来编译。
2 机器指令
一条机器指令由相应的二进制数标识,直接能被机器识别。
C 语言中的 c = a + b
在机器语言中应该怎样表达?
首先用相应的汇编语言表达出来。
asm
mov eax, [ebp-4] ; 变量 a 的值放到 eax 寄存器中,变量 a 是局部变量
add eax, [ebp-8] ; 执行 a + b,变量 b 也是局部变量
mov [0x0000001c], eax ; 放到 c 中,变量 c 可能是外部变量
在 x86 机器中,如果两个内存操作数要进行加法运算,不能直接相加,其中一方必须是寄存器。
上面的汇编语言译成机器语言为:
asm
8b 45 fc ; 对应于 mov eax, [ebp-4]
03 45 f8 ; 对应于 add eax, [ebp-8]
a3 1c 00 00 00 ; 对应于 mov [0x0000001c]
eax
3 Hello world
"Hello, World" 程序的汇编版。
asm
main: ; 这是模块代码的入口点。
mov si, caller_message
call puts ; 打印信息
mov si, current_eip
mov di, caller_address
current_eip:
call get_hex_string ; 转换为 hex
mov si, caller_address
call puts
mov si, 13 ; 打印回车
call putc
mov si, 10 ; 打印换行
call putc
call say_hello ; 打印信息
jmp $
caller_message db 'Now:I am the caller,address is 0x'
caller_address dq 0
hello_message db 13,10,'hello,world!',13,10 db 'This is my first assembly program...',13,10,13,10,0
callee_message db "Now:I'm callee - say_hello(),address is 0x"
callee_address dq 0
这段汇编代码相当于下面的几条 C 语言语句。
c
int main()
{
print("new: I am the caller, address is 0x%x", get_hex_string(current_eip));
printf("\n");
say_hello(); // 调用 say_hello()
}
下面是 say_hello() 的汇编代码。
asm
say_hello:
mov si, hello_message
call puts
mov si, callee_message
call puts
mov si, say_hello
mov di, callee_address
call get_hex_string
mov si, callee_address
call puts
ret
这个 say_hello() 相当于以下几条 C 语言。
c
void say_hello()
{
printf("hello, workd\nThis is my first assembly progra...");
printf("Now: I'm callee -say_hello(), address is 0x%x", get_hex_string(say_hello);
}
3.1 使用寄存器传递参数
在汇编程序里尽量采用寄存器传递参数,使用寄存器传递参数获得更高的效率。
asm
mov si, hello_message ; 使用 si 寄存器传递参数
call puts
3.2 调用过程
call 指令用来调用一个过程,可以直接出一个目标地址值作为操作数。
asm
e8 c2 00 ; call puts
3.3 定义变量
在 nasm 的汇编源程序里,可以使用 db 系列伪指令来定义初始化的变量。
例如,我们可以这样使用 db 伪指令。
asm
hello_message db 13, 10, 'hello, world!', 13, 10
这里为 hello_message 定义了一个字符串变量,相当于如下 c 语句。
c
hello_message[] = "\nhello, world!\n";
十进制数字13是 ASCII 码中的回车符,10是换行符,当然也可以使用十六进制数0x0d和0x0a来赋初值。
4 16位编程、32位编程,以及64位编程
在 nasm 中可以在同一个源代码文件里同时指出16位代码、32位代码,以及64位代码。
asm
bits 16
... ... ; 以下是 16位代码
bits 32
... ... ; 以下是 32位代码
bits 64
... ... ; 以下是 64位代码
4.1 通用寄存器
在16位和32位编程里,可以使用的通用寄存器是一样的。
4.2 操作数大小
在16位编程和32位编程下,寄存器没有使用上的不便,32位的操作数依旧可以在16位编程里使用,而16位的操作数也可以在32位编程下使用。
4.3 64位模式下的内存地址
在64位编程里可以使用宽达64位的地址值。
4.4 内存寻址模式
在16位和32位编程里,16位和32位的寻址模式都可以使用。在64位下,32位的寻址模式被扩展为64位,而且不能使用16位的寻址模式。
5 编程基础
一行有效的汇编代码主体是 instruction expression(指令表达式),label(标签)定义了一个地址,汇编语言的 comment(注释)以";"号开始,以行结束为止。
5.1 操作数寻址
5.1.1 寄存器寻址
用户编程中几乎只使用 GPR(通用寄存器),sp/esp/rsp 寄存器被用作 stack top pointer(栈顶指针),bp/ebp/rbp 寄存器通常被用作维护过程的 stack frame 结构。可是它们都可以被用户代码直接读/写,维护 stack 结构的正确性和完整性,职责在于程序员。
5.1.2 内存操作数寻址
内存操作数由一对[]括号进行标识。
直接寻址是 memory 的地址值明确提供的,是个绝对地址。
asm
mov eas, [0x00400000] ; 明确提供一个地址值
间接寻址 memory 的地址存放在寄存器里,或者需要进行求值。
asm
mov eax, [ebx] ; 地址值放在 ebx 寄存器里
mov eax, [base_address + ecx * 2] ; 通过求值得到地址值
5.1.3 立即数寻址
立即数无需进行额外的寻址,immediate 值将从机器指令中获取。
asm
b8 01 00 00 00 ; 对应 mov eax, 1
5.1.4 I/O 端口寻址
x86/x64 体系实现了独立的 64K I/O 地址空间(从 0000H 到 FFFFH),IN 和 OUT 指令用来访问这个 I/O 地址。
in 指令读取外部端口数据,out 指令往外部端口写数据。
asm
in al,20H ; 从端口20H里读取一个 byte
5.2 传送数据指令
x86提供了非常多的 data-transfer 指令,在这些传送操作中包括了:load(加载),store(存储),move(移动)。其中,mov 指令是最常用的。
5.3 位操作指令
x86 也提供了几类位操作指令,包括:逻辑指令,位指令,位查询指令,位移指令。
5.4 算术指令
- 加法运算:ADD,ADC,以及INC指令。
- 减法运算:SUB,SBB,以及DEC指令。
- 乘法运算:MUL和IMUL指令。
- 除法运算:DIV和IDIV指令。
- 取反运算:NEG指令。
5.5 CALL 与 RET 指令
CALL 调用子过程,在汇编语言里,它的操作数可以是地址(立即数)、寄存器或内存操作数。call 指令的目的是转入目标代码的 IP(Instruction Pointer)值。
为了返回到调用者, call 指令会在 stack 中压入返回地址,ret 指令返回时从 stack 里取出返回值重新装载到 EIP 里然后返回到调用者。
5.6 跳转指令
jmp 系列指令与 call 指令最大的区别是:jmp 指令并不需要返回,因此不需要进行压 stack 操作。