http://arthurchiao.art/blog/x86-asm-guide-zh/
Translated from CS216, University of Virginia.
一份非常好的 x86 汇编教程,国外 CS 课程所用资料,篇幅简短,逻辑清晰,合适作为入 门参考。以原理为主,有两个例子帮助理解。其开始提到使用 MicroSoft MASM 和 Visual Studio,但非必须, 事实上如果你有 Linux 更好。
本文根据原文内容意译,而非逐词逐句翻译,如需了解更多,推荐阅读原文 .
内容:寄存器, 内存和寻址, 指令, 函数调用约定(Calling Convention)
本文介绍 32bit x86 汇编基础,覆盖其中虽小但很有用的一部分。 有多种汇编语言可以生成 x86 机器代码。我们在 CS216 课程中使用的是 MASM( Microsoft Macro Assembler)。MASM 使用标准 Intel 语法。
整套 x86 指令集庞大而复杂(Intel x86 指令集手册超过 2900 页),本文不会全部覆盖。
Fig 2.1 x86 registers
现代 x86 处理器有 8 个 32 bit 寄存器,如图 1 所示。寄存器名字是早期计算机历史上 流传下来的。例如,EAX 表示 Accumulator,因为它用作算术运算的累加器,ECX 表示 Counter,用来存储循环变量(计数)。大部分寄存器的名字已经失去了原来的意义,但有 两个是例外:栈指针寄存器(Stack Pointer)ESP 和基址寄存器( Base Pointer)EBP。
对于 EAX
, EBX
, ECX
, EDX
四个寄存器,可以再将 32bit 划分成多个子寄存器, 每个子寄存器有专门的名字。例如 EAX
的高 16bit 叫 AX
(去掉 E, E 大概表示 Extended),低 8bit 叫 AL
(Low), 8-16bit 叫 AH
(High)。如图 1 所示。
在汇编语言中,这些寄存器的名字是大小写无关的,既可以用 EAX
,也可以写 eax
。
.DATA
声明静态数据区。
数据类型修饰原语:
DB
: Byte, 1 Byte(DB
的 D
可能表示 Data)DW
: Word, 2 BytesDD
: Double Word, 4 Bytes例子:
.DATA
var DB 64 ; 声明一个 byte 值, referred to as location var, containing the value 64.
var2 DB ? ; 声明一个未初始化 byte 值, referred to as location var2.
DB 10 ; 声明一个没有 label 的 byte 值, containing the value 10. Its location is var2 + 1.
X DW ? ; 声明一个 2-byte 未初始化值, referred to as location X.
Y DD 30000 ; 声明一个 4-byte 值, referred to as location Y, initialized to 30000.
和高级语言不同,在汇编中只有一维数组,只有没有二维和多维数组。一维数组其实就 是内存中的一块连续区域。另外,DUP
和字符串常量也是声明数组的两种方法。
例子:
Z DD 1, 2, 3 ; 声明 3 个 4-byte values, 初始化为 1, 2, and 3. The value of location Z + 8 will be 3.
bytes DB 10 DUP(?) ; 声明 10 个 uninitialized bytes starting at location bytes.
arr DD 100 DUP(0) ; 声明 100 个 4-byte words starting at location arr, all initialized to 0
str DB 'hello',0 ; 声明 6 bytes starting at the address str, 初始化为 hello and the null (0) byte.
有多个指令可以用于内存寻址,我们先看使用 MOV
的例子。MOV
将在内存和寄存器之 间移动数据,接受两个参数:第一个参数是目的地,第二个是源。
合法寻址的例子:
mov eax, [ebx] ; Move the 4 bytes in memory at the address contained in EBX into EAX
mov [var], ebx ; Move the contents of EBX into the 4 bytes at memory address var. (Note, var is a 32-bit constant).
mov eax, [esi-4] ; Move 4 bytes at memory address ESI + (-4) into EAX
mov [esi+eax], cl ; Move the contents of CL into the byte at address ESI+EAX
mov edx, [esi+4*ebx] ; Move the 4 bytes of data at address ESI+4*EBX into EDX
非法寻址的例子:
mov eax, [ebx-ecx] ; 只能对寄存器的值相加,不能相减
mov [eax+esi+edi], ebx ; 最多只能有 2 个寄存器参与地址计算
修饰指针类型:
BYTE PTR
- 1 ByteWORD PTR
- 2 BytesDWORD PTR
- 4 Bytesmov BYTE PTR [ebx], 2 ; Move 2 into the single byte at the address stored in EBX.
mov WORD PTR [ebx], 2 ; Move the 16-bit integer representation of 2 into the 2 bytes starting at the address in EBX.
mov DWORD PTR [ebx], 2 ; Move the 32-bit integer representation of 2 into the 4 bytes starting at the address in EBX.
三大类:
mov
push
pop
lea
- Load Effective Addressadd
, sub
inc
, dec
imul
, idiv
and
, or
, xor
not
neg
shl
, shr
jmp
je
, jne
, jz
, jg
, jl
…cmp
call
, ret
这是最重要的部分。
子过程(函数)调用需要遵守一套共同的调用约定(Calling Convention)。 调用约定是一个协议,规定了如何调用以及如何从过程返回。例如,给定一组 calling convention rules,程序员无需查看子函数的定义就可以确定如何将参数传给它。进一步地 ,给定一组 calling convention rules,高级语言编译器只要遵循这些 rules,就可以使 得汇编函数和高级语言函数互相调用。
Calling conventions 有多种。我们这里介绍使用最广泛的一种:C 语言调用约定(C Language Calling Convention)。遵循这个约定,可以使汇编代码安全地被 C/C++ 调用 ,也可以从汇编代码调用 C 函数库。
C 调用约定:
push
, pop
, call
, ret
指令在大部分处理器上实现的大部分高级过程式语言,都使用与此相似的调用惯例。
调用惯例分为两部分。第一部分用于 调用方(caller),第二部分用于被调 用方(callee)。需要强调的是,错误地使用这些规则将导致栈被破坏,程序 很快出错;因此在你自己的子过程中实现 calling convention 时需要格外仔细。
Fig 5.1 Stack during Subroutine Call
在一个子过程调用之前,调用方应该:
保存应由调用方保存的寄存器(caller-saved registers): EAX
, ECX
, EDX
这几个寄存器可能会被被调用方(callee)修改,所以先保存它们,以便调用结 束后恢复栈的状态。
将需要传给子过程的参数入栈(push onto stack)
参数按逆序 push 入栈(最后一个参数先入栈)。由于栈是向下生长的,第一个参数 会被存储在最低地址(这个特性使得变长参数列表成为可能)。
使用 call
指令,调用子过程(函数)
call
先将返回地址 push 到栈上,然后开始执行子过程代码。子过程代码需要遵 守的 callee rules。
子过程返回后(call
执行结束之后),被调用方会将返回值放到 EAX
寄存器,调用方 可以从中读取。为恢复机器状态,调用方需要做:
从栈上删除传递的参数
栈恢复到准备发起调用之前的状态。
恢复由调用方保存的寄存器(EAX
, ECX
, EDX
)—— 从栈上 pop 出来
调用方可以认为,除这三个之外,其他寄存器的值没有被修改过。
push [var] ; Push last parameter first
push 216 ; Push the second parameter
push eax ; Push first parameter last
call _myFunc ; Call the function (assume C naming)
add esp, 12
将寄存器 EBP
的值入栈,然后 copy ESP
to EBP
push ebp
mov ebp, esp
在栈上为局部变量分配空间
栈自顶向下生长,故随着变量的分配,栈顶指针不断减小。
保存应有被调用方保存(callee-saved
)的寄存器 —— 将他们压入栈。包括 EBX
, EDI
, ESI
以上工作完成,就可以执行子过程的代码了。当子过程返回后,必须做以下工作:
将返回值保存在 EAX
恢复应由被调用方保存的寄存器(EDI
, ESI
) —— 从栈上 pop 出来
释放局部变量
恢复调用方 base pointer EBP
—— 从栈上 pop 出来
最后,执行 ret
,返回给调用方 (caller)
.486
.MODEL FLAT
.CODE
PUBLIC _myFunc
_myFunc PROC
; Subroutine Prologue
push ebp ; Save the old base pointer value.
mov ebp, esp ; Set the new base pointer value.
sub esp, 4 ; Make room for one 4-byte local variable.
push edi ; Save the values of registers that the function
push esi ; will modify. This function uses EDI and ESI.
; (no need to save EBX, EBP, or ESP)
; Subroutine Body
mov eax, [ebp+8] ; Move value of parameter 1 into EAX
mov esi, [ebp+12] ; Move value of parameter 2 into ESI
mov edi, [ebp+16] ; Move value of parameter 3 into EDI
mov [ebp-4], edi ; Move EDI into the local variable
add [ebp-4], esi ; Add ESI into the local variable
add eax, [ebp-4] ; Add the contents of the local variable
; into EAX (final result)
; Subroutine Epilogue
pop esi ; Recover register values
pop edi
mov esp, ebp ; Deallocate local variables
pop ebp ; Restore the caller's base pointer value
ret
_myFunc ENDP
END