译者:https://github.com/ArthurChiao
来源:https://github.com/ArthurChiao/arthurchiao.github.io
译者序
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 页),本文不会全部覆盖。
1. 参考资料
- Guide to Using Assembly in Visual Studio — a tutorial on building and debugging assembly code in Visual Studio
- Intel x86 Instruction Set Reference
- Intel’s Pentium Manuals (the full gory details)
2. 寄存器

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。
3. 内存和寻址模式
3.1 声明静态数据区
.DATA 声明静态数据区。
数据类型修饰原语:
DB: Byte, 1 Byte(DB的D可能表示 Data)DW: Word, 2 BytesDD: Double Word, 4 Bytes
例子:
| |
和高级语言不同,在汇编中只有一维数组,只有没有二维和多维数组。一维数组其实就
是内存中的一块连续区域。另外,DUP 和字符串常量也是声明数组的两种方法。
例子:
| |
3.2 内存寻址 (Addressing Memory)
有多个指令可以用于内存寻址,我们先看使用 MOV 的例子。MOV 将在内存和寄存器之
间移动数据,接受两个参数:第一个参数是目的地,第二个是源。
合法寻址的例子:
| |
非法寻址的例子:
3.3 数据类型(大小)原语(Size Directives)
修饰指针类型:
BYTE PTR- 1 ByteWORD PTR- 2 BytesDWORD PTR- 4 Bytes
| |
4. 指令
三大类:
- 数据移动
movpushpoplea- Load Effective Address
- 算术/逻辑运算
add,subinc,decimul,idivand,or,xornotnegshl,shr
- 控制流
jmpje,jne,jz,jg,jl…cmpcall,ret
5. 调用约定
这是最重要的部分。
子过程(函数)调用需要遵守一套共同的调用约定(Calling Convention)。 调用约定是一个协议,规定了如何调用以及如何从过程返回。例如,给定一组 calling convention rules,程序员无需查看子函数的定义就可以确定如何将参数传给它。进一步地 ,给定一组 calling convention rules,高级语言编译器只要遵循这些 rules,就可以使 得汇编函数和高级语言函数互相调用。
Calling conventions 有多种。我们这里介绍使用最广泛的一种:C 语言调用约定(C Language Calling Convention)。遵循这个约定,可以使汇编代码安全地被 C/C++ 调用 ,也可以从汇编代码调用 C 函数库。
C 调用约定:
- 强烈依赖硬件栈的支持 (hardwared-supported stack)
- 基于
push,pop,call,ret指令 - 子过程参数通过栈传递: 寄存器保存在栈上,子过程用到的局部变量也放在栈上
在大部分处理器上实现的大部分高级过程式语言,都使用与此相似的调用惯例。
调用惯例分为两部分。第一部分用于 调用方(caller),第二部分用于被调 用方(callee)。需要强调的是,错误地使用这些规则将导致栈被破坏,程序 很快出错;因此在你自己的子过程中实现 calling convention 时需要格外仔细。

Fig 5.1 Stack during Subroutine Call
5.1 调用方规则 (Caller Rules)
在一个子过程调用之前,调用方应该:
保存应由调用方保存的寄存器(caller-saved registers):
EAX,ECX,EDX这几个寄存器可能会被被调用方(callee)修改,所以先保存它们,以便调用结 束后恢复栈的状态。
将需要传给子过程的参数入栈(push onto stack)
参数按逆序 push 入栈(最后一个参数先入栈)。由于栈是向下生长的,第一个参数 会被存储在最低地址(这个特性使得变长参数列表成为可能)。
使用
call指令,调用子过程(函数)call先将返回地址 push 到栈上,然后开始执行子过程代码。子过程代码需要遵 守的 callee rules。
子过程返回后(call 执行结束之后),被调用方会将返回值放到 EAX 寄存器,调用方
可以从中读取。为恢复机器状态,调用方需要做:
从栈上删除传递的参数
栈恢复到准备发起调用之前的状态。
恢复由调用方保存的寄存器(
EAX,ECX,EDX)—— 从栈上 pop 出来调用方可以认为,除这三个之外,其他寄存器的值没有被修改过。
例子
5.2 被调用方规则 (Callee Rules)
将寄存器
EBP的值入栈,然后 copyESPtoEBP在栈上为局部变量分配空间
栈自顶向下生长,故随着变量的分配,栈顶指针不断减小。
保存应有被调用方保存(
callee-saved)的寄存器 —— 将他们压入栈。包括EBX,EDI,ESI
以上工作完成,就可以执行子过程的代码了。当子过程返回后,必须做以下工作:
将返回值保存在
EAX恢复应由被调用方保存的寄存器(
EDI,ESI) —— 从栈上 pop 出来释放局部变量
恢复调用方 base pointer
EBP—— 从栈上 pop 出来最后,执行
ret,返回给调用方 (caller)
例子
| |