对X86汇编的理解与入门
本文描述基本的32位X86汇编语言的一个子集,其中涉及汇编语言的最核心部分,包括寄存器结构,数据表示,基本的操作指令(包括数据传送指令、逻辑计算指令、算数运算指令),以及函数的调用规则。个人认为:在理解了本文后,基本可以无障碍地阅读绝大部分标准X86汇编程序。当然,更复杂的指令请参阅Intel相关文档。
1 寄存器.
主要寄存器如下图所示:
X86处理器中有8个32位的通用寄存器。由于历史的原因,EAX通常用于计算,ECX通常用于循环变量计数。ESP和EBP有专门用途,ESP指示栈指针(用于指示栈顶位置),而EBP则是基址指针(用于指示子程序或函数调用的基址指针)。如图中所示,EAX、EBX、ECX和EDX的前两个高位字节和后两个低位字节可以独立使用,其中两位低字节又被独立分为H和L部分,这样做的原因主要是考虑兼容16位的程序,具体兼容匹配细节请查阅相关文献。
应用寄存器时,其名称大小写是不敏感的,如EAX和eax没有区别。
2 内存和寻址模式
2.1声明静态数据区
可以在X86汇编语言中用汇编指令.DATA声明静态数据区(类似于全局变量),数据以单字节、双字节、或双字(4字节)的方式存放,分别用DB,DW, DD指令表示声明内存的长度。在汇编语言中,相邻定义的标签在内存中是连续存放的。
.DATA | |||
var | DB 64 | ;声明一个字节,并将数值64放入此字节中 | |
var2 | DB ? | ; 声明一个为初始化的字节. | |
DB 10 | ; 声明一个没有label的字节,其值为10. | ||
X | DW ? | ; 声明一个双字节,未初始化. | |
Y | DD 30000 | ; 声明一个4字节,其值为30000. |
还可以声明连续的数据和数组,声明数组时使用DUP关键字
Z | DD 1, 2, 3 | ; Declare three 4-byte values, initialized to 1, 2, and 3. The value of location Z + 8 will be 3. |
bytes | DB 10 DUP(?) | ; Declare 10 uninitialized bytes starting at location bytes. |
arr | DD 100 DUP(0) | ; Declare 100 4-byte words starting at location arr, all initialized to 0 |
str | DB 'hello',0 | ; Declare 6 bytes starting at the address str, initialized to the ASCII character values for hello and the null (0) byte. |
2.2 寻址模式
现代X86处理器具有232字节的寻址空间。在上面的例子中,我们用标签(label)表示内存区域,这些标签在实际汇编时,均被32位的实际地址代替。除了支持这种直接的内存区域描述,X86还提供了一种灵活的内存寻址方式,即利用最多两个32位的寄存器和一个32位的有符号常数相加计算一个内存地址,其中一个寄存器可以左移1、2或3位以表述更大的空间。下面例子是汇编程序中常见的方式
mov eax, [ebx] ; 将ebx值指示的内存地址中的4个字节传送到eax中 mov [var], ebx ; 将ebx的内容传送到var的值指示的内存地址中. mov eax, [esi-4] ; 将esi-4值指示的内存地址中的4个字节传送到eax中 mov [esi+eax], cl ; 将cl的值传送到esi+eax的值指示的内存地址中 mov edx, [esi+4*ebx] ; 将esi+4*ebx值指示的内存中的4个字节传送到edx
下面是违反规则的例子:
mov eax, [ebx-ecx] | ; 只能用加法 |
mov [eax+esi+edi], ebx | ; 最多只能有两个寄存器参与运算 |
2.3 长度规定
在声明内存大小时,在汇编语言中,一般用DB,DW,DD均可声明的内存空间大小,这种现实声明能够很好地指导汇编器分配内存空间,但是,对于
mov [ebx], 2
如果没有特殊的标识,则不确定常数2是单字节、双字节,还是双字。对于这种情况,X86提供了三个指示规则标记,分别为BYTE PTR, WORD PTR, and DWORD PTR,如上面例子写成:mov BYTE PTR [ebx], 2, mov WORD PTR [ebx], 2, mov DWORD PTR [ebx], 2,则意思非常清晰。
3 汇编指令
汇编指令通常可以分为数据传送指令、逻辑计算指令和控制流指令。本节将讲述其中最重要的指令,以下标记分别表示寄存器、内存和常数。
<reg32> | 32位寄存器 (EAX, EBX, ECX, EDX, ESI, EDI, ESP, or EBP) |
<reg16> | 16位寄存器 (AX, BX, CX, or DX) |
<reg8> | 8位寄存器(AH, BH, CH, DH, AL, BL, CL, or DL) |
<reg> | 任何寄存器 |
<mem> | 内存地址 (e.g., [eax], [var + 4], or dword ptr [eax+ebx]) |
<con32> | 32为常数 |
<con16> | 16位常数 |
<con8> | 8位常数 |
<con> | 任何8位、16位或32位常数 |
3.1 数据传送指令
mov — Move (Opcodes: 88, 89, 8A, 8B, 8C, 8E, ...)
mov指令将第二个操作数(可以是寄存器的内容、内存中的内容或值)复制到第一个操作数(寄存器或内存)。mov不能用于直接从内存复制到内存,其语法如下所示:
mov <reg>,<reg>
mov <reg>,<mem>
mov <mem>,<reg>
mov <reg>,<const>
mov <mem>,<const>
Examples
mov eax, ebx — 将ebx的值拷贝到eax
mov byte ptr [var], 5 — 将5保存找var指示内存中的一个字节中
push— Push stack (Opcodes: FF, 89, 8A, 8B, 8C, 8E, ...)
push指令将操作数压入内存的栈中,栈是程序设计中一种非常重要的数据结构,其主要用于函数调用过程中,其中ESP只是栈顶。在压栈前,首先将ESP值减4(X86栈增长方向与内存地址编号增长方向相反),然后将操作数内容压入ESP指示的位置。其语法如下所示:
push <reg32>
push <mem>
push <con32>
Examples
push eax — 将eax内容压栈
push [var] — 将var指示的4直接内容压栈
pop— Pop stack
pop指令与push指令相反,它执行的是出栈的工作。它首先将ESP指示的地址中的内容出栈,然后将ESP值加4. 其语法如下所示:
pop <reg32>
pop <mem>
Examples
pop edi — pop the top element of the stack into EDI.
pop [ebx] — pop the top element of the
stack into memory at the four bytes starting at location EBX.
lea— Load effective address
lea实际上是一个载入有效地址指令,将第二个操作数表示的地址载入到第一个操作数(寄存器)中。其语法如下所示:
Syntax
lea <reg32>,<mem>
Examples
lea eax, [var] — var指示的地址载入eax中.
lea edi, [ebx+4*esi] — ebx+4*esi表示的地址载入到edi中,这实际是上面所说的寻址模式的一种表示方式.
3.2 算术和逻辑指令
add— Integer Addition
add指令将两个操作数相加,且将相加后的结果保存到第一个操作数中。其语法如下所示:
add <reg>,<reg>
add <reg>,<mem>
add <mem>,<reg>
add <reg>,<con>
add <mem>,<con>
Examples
add eax, 10 — EAX ← EAX + 10
add BYTE PTR [var], 10 — 10与var指示的内存中的一个byte的值相加,并将结果保存在var指示的内存中
sub— Integer Subtraction
sub指令指示第一个操作数减去第二个操作数,并将相减后的值保存在第一个操作数,其语法如下所示:
sub <reg>,<reg>
sub <reg>,<mem>
sub <mem>,<reg>
sub <reg>,<con>
sub <mem>,<con>
Examples
sub al, ah — AL ← AL - AH
sub eax, 216 — eax中的值减26,并将计算值保存在eax中
inc, dec— Increment, Decrement
inc,dec分别表示将操作数自加1,自减1,其语法如下所示:
inc <reg>
inc <mem>
dec <reg>
dec <mem>
Examples
dec eax — eax中的值自减1.
inc DWORD PTR [var] — var指示内存中的一个4-byte值自加1
imul— Integer Multiplication
整数相乘指令,它有两种指令格式,一种为两个操作数,将两个操作数的值相乘,并将结果保存在第一个操作数中,第一个操作数必须为寄存器;第二种格式为三个操作数,其语义为:将第二个和第三个操作数相乘,并将结果保存在第一个操作数中,第一个操作数必须为寄存器。其语法如下所示:
imul <reg32>,<reg32>
imul <reg32>,<mem>
imul <reg32>,<reg32>,<con>
imul <reg32>,<mem>,<con>
Examples
idiv— Integer Division
idiv指令完成整数除法操作,idiv只有一个操作数,此操作数为除数,而被除数则为EDX:EAX中的内容(一个64位的整数),操作的结果有两部分:商和余数,其中商放在eax寄存器中,而余数则放在edx寄存器中。其语法如下所示:
Syntax
idiv <reg32>
idiv <mem>
Examples
and <reg>,<mem>
and <mem>,<reg>
and <reg>,<con>
and <mem>,<con>
or <reg>,<reg>
or <reg>,<mem>
or <mem>,<reg>
or <reg>,<con>
or <mem>,<con>
xor <reg>,<reg>
xor <reg>,<mem>
xor <mem>,<reg>
xor <reg>,<con>
xor <mem>,<con>
Examples
and eax, 0fH — 将eax中的钱28位全部置为0,最后4位保持不变.
xor edx, edx — 设置edx中的内容为0.
not— Bitwise Logical Not
位翻转指令,将操作数中的每一位翻转,即0->1, 1->0。其语法如下所示:
not <reg>
not <mem>
Example
not BYTE PTR [var] — 将var指示的一个字节中的所有位翻转.
neg— Negate
取负指令。语法为:
neg <reg>
neg <mem>
Example
neg eax — EAX → - EAX
shl, shr— Shift Left, Shift Right
位移指令,有两个操作数,第一个操作数表示被操作数,第二个操作数指示位移的数量。其语法如下所示:
shl <reg>,<con8>
shl <mem>,<con8>
shl <reg>,<cl>
shl <mem>,<cl>
shr <reg>,<con8>
shr <mem>,<con8>
shr <reg>,<cl>
shr <mem>,<cl>
Examples
by 2 (if the most significant bit is 0),左移1位,相当于乘以2
by 2n where n is the value in CL.
mov esi, [ebp+8]
begin: xor ecx, ecx
mov eax, [esi]
如第二条指令用begin指示,这种标签的方法在某种程度上简化了汇编程序设计,控制流指令通过标签实现程序指令跳转。
jmp — Jump
控制转移到label所指示的地址,(从label中取出执行执行),如下所示:
jmp <label>
Example
jmp begin — Jump to the instruction
labeled begin.
jcondition— Conditional Jump
条件转移指令,条件转移指令依据机器状态字中的一些列条件状态转移。机器状态字中包括指示最后一个算数运算结果是否为0,运算结果是否为负数等。机器状态字具体解释请见微机原理、计算机组成等课程。语法如下所示:
je <label> (jump when equal)
jne <label> (jump when not equal)
jz <label> (jump when last result was zero)
jg <label> (jump when greater than)
jge <label> (jump when greater than or equal to)
jl <label> (jump when less than)
jle <label>(jump when less than or equal to)
Example
cmp eax, ebx
jle done , 如果eax中的值小于ebx中的值,跳转到done指示的区域执行,否则,执行下一条指令。
cmp <reg>,<mem>
cmp <mem>,<reg>
cmp <reg>,<con>
Example
cmp DWORD PTR [var], 10
jeq loop,
在X86中,栈增长方向与内存编号增长方向相反。
Caller Rules
调用者规则包括一系列操作,描述如下:
1)在调用子程序之前,调用者应该保存一系列被设计为调用者保存的寄存器的值。调用者保存寄存器有eax,ecx,edx。由于被调用的子程序会修改这些寄存器,所以为了在调用子程序完成之后能正确执行,调用者必须在调用子程序之前将这些寄存器的值入栈。
2)在调用子程序之前,将参数入栈。参数入栈的顺序应该是从最后一个参数开始,如上图中parameter3先入栈。
3)利用call指令调用子程序。这条指令将返回地址放置在参数的上面,并进入子程序的指令执行。(子程序的执行将按照被调用者的规则执行)
当子程序返回时,调用者期望找到子程序保存在eax中的返回地址。为了恢复调用子程序执行之前的状态,调用者应该执行以下操作:
1)清除栈中的参数;
2)将栈中保存的eax值、ecx值以及edx值出栈,恢复eax、ecx、edx的值(当然,如果其它寄存器在调用之前需要保存,也需要完成类似入栈和出栈操作)
Example
如下代码展示了一个调用子程序的调用者应该执行的操作。此汇编程序调用一个具有三个参数的函数_myFunc,其中第一个参数为eax,第二个参数为常数216,第三个参数为var指示的内存中的值。
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
在调用返回时,调用者必须清除栈中的相应内容,在上例中,参数占有12个字节,为了消除这些参数,只需将ESP加12即可。
_myFunc的值保存在eax中,ecx和edx中的值也许已经被改变,调用者还必须在调用之前保存在栈中,并在调用结束之后,出栈恢复ecx和edx的值。
被调用者规则
被调用者应该遵循如下规则:
1)将ebp入栈,并将esp中的值拷贝到ebp中,其汇编代码如下:
push ebp
mov ebp, esp
上述代码的目的是保存调用子程序之前的基址指针,基址指针用于寻找栈上的参数和局部变量。当一个子程序开始执行时,基址指针保存栈指针指示子程序的执行。为了在子程序完成之后调用者能正确定位调用者的参数和局部变量,ebp的值需要返回。
2)在栈上为局部变量分配空间。
3)保存callee-saved寄存器的值,callee-saved寄存器包括ebx,edi和esi,将ebx,edi和esi压栈。
4)在上述三个步骤完成之后,子程序开始执行,当子程序返回时,必须完成如下工作:
4.1)将返回的执行结果保存在eax中
4.2)弹出栈中保存的callee-saved寄存器值,恢复callee-saved寄存器的值(ESI和EDI)
4.3)收回局部变量的内存空间。实际处理时,通过改变EBP的值即可:mov esp, ebp。
4.4)通过弹出栈中保存的ebp值恢复调用者的基址寄存器值。
4.5)执行ret指令返回到调用者程序。
After these three actions are performed, the body of the subroutine may proceed. When the subroutine is returns, it must follow these steps:
- Leave the return value in EAX.
Example
.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
子程序首先通过入栈的手段保存ebp,分配局部变量,保存寄存器的值。
在子程序体中,参数和局部变量均是通过ebp进行计算。由于参数传递在子程序被调用之前,所以参数总是在ebp指示的地址的下方(在栈中),因此,上例中的第一个参数的地址是ebp+8,第二个参数的地址是ebp+12,第三个参数的地址是ebp+16;而局部变量在ebp指示的地址的上方,所有第一个局部变量的地址是ebp-4,而第二个这是ebp-8.
对X86汇编的理解与入门的更多相关文章
- 寄存器理解 及 X86汇编入门
本文整理自多材料源,感谢原址分享,请查看末尾Url I, 汇编语言分类: 汇编语言和CPU息息相关,但是不能把汇编语言完全等同于CPU的机器指令.不同架构的CPU指令并不相同,如x86,powerpc ...
- X86汇编概要
来自:https://www.cnblogs.com/jiftle/p/8453106.html 本文翻译自:http://www.cs.virginia.edu/~evans/cs216/guide ...
- x86汇编反编译到c语言之——(1)表达式求值及赋值语句
一. 反编译一种可能的实现方式 我们的目的是将多种平台的汇编如x86,ARM,6502反编译为c语言,所以实现时先将多种汇编转化为 特定虚拟机汇编语言,然后只需要将虚拟机汇编语言反编译为c语言.其中多 ...
- C# inline-asm / 嵌入x86汇编
C#可不可以嵌入汇编 可以 在我眼中C#作为一个介于中上层语言是不可能不可以 置入汇编代码的 为什么会被我认为中上层语言呢 从C#保留指针就可以看出 我知 道有很多人一定不会相信C#可以使用汇编代码 ...
- 为什么X86汇编中的mov指令不支持内存到内存的寻址?
在X86汇编中,MOV [0012H], [0016H]这种指令是不允许的,至少得有一个操作数是寄存器.当然,这种问题在用高级语言的时候看不到,感觉好像基本上都是从内存到内存啊,为毛到了汇编就不行了? ...
- 通过汇编一个简单的C程序,分析汇编代码理解计算机是如何工作的
秦鼎涛 <Linux内核分析>MOOC课程http://mooc.study.163.com/course/USTC-1000029000 实验一 通过汇编一个简单的C程序,分析汇编代码 ...
- 第一周:通过汇编一个简单的C程序,分析汇编代码理解计算机是如何工作的
姓名:吕松鸿 学号:20135229 ( *原创作品转载请注明出处*) ( 学习课程:<Linux内核分析>MOOC课程http://mooc.study.163.com/course/U ...
- 通过反汇编一个简单的C程序,分析汇编代码理解计算机是如何工作的
实验一:通过反汇编一个简单的C程序,分析汇编代码理解计算机是如何工作的 学号:20135114 姓名:王朝宪 注: 原创作品转载请注明出处 <Linux内核分析>MOOC课程http: ...
- x86汇编之十(使用字符串)
x86汇编之十(使用字符串) 转自网络,出处不详 一.传送字符串 Intel提供了完整的字符串传送指令,就像是MOV指令一样. 1.MOVS指令 1)movs指令格式 把字符串从一个位内存位置传送到另 ...
随机推荐
- 使用c3p0与DBCP连接池,造成的MySql 8小时问题解决方案
转自:http://blog.csdn.net/wangfayinn/article/details/24623575 c3p0数据库连接池mysql8小时 目录(?)[-] 基本问题解决 项目环境 ...
- vue+vux+axios+vuex+vue-router的项目的理解
本文主要是讲解项目前期的工作,后期考虑再详细说明. 作为一个技术团队如果你们团队选择了上面的技术栈,这说明你们的技术团体对于vue有很熟练的掌握了.在这里我想说明的是前期架构的重要.这里有一遍博客写的 ...
- MyBatis_Generator插件的安装以及简单使用
MyBatis_Generator_1.3.1.zip 1 下载安装包 安装包名称:MyBatis_Generator_1.3.1.zip 2 在Eclipse上进行安装 l ...
- Java-面向对象总结
下面是学习面向对象的知识点和总结: 面向对象思想: 遇到需求,首先去找是否有现成的类来实现此功能,创建对象来调用,以此来组合成新的类实现自己的需求. 在java中是以类为单位,一个类包括成员变量.成员 ...
- SpringMVC——数据校验
数据校验在web应用里是非常重要的功能,尤其是在表单输入中.在这里采用Hibernate-Validator进行校验,该方法实现了JSR-303验证框架支持注解风格的验证. 一.导入jar包 若要实现 ...
- Zepto源码分析-callbacks模块
// Zepto.js // (c) 2010-2015 Thomas Fuchs // Zepto.js may be freely distributed under the MIT licens ...
- Javascript加载执行顺序
本文主要内容 一.不同位置的script标签执行顺序 二.document.ready和window.onload的区别 一.不同位置的script标签执行顺序 整个加载的过程从解析头部开始,比如ht ...
- awk内引用shell变量【自己手动加精】
题目 [root@localhost ~]# cat 1.txt iii sss ddd 执行命令 [root@localhost ~]# A=0 [root@localhost ~]# awk '{ ...
- 动态分配数组(new)和用随机数赋值(rand)
#include <iostream>#include <ctime>#include <cstdlib>using namespace std; int main ...
- Thread初探
Thread初探 前言 以前大家写的都是单线程的程序,全是在main函数中调用方法,可以清楚的看到它的效率是特别低的,就像python中使用单线程取爬一个网站,可以说能让你等的吐血,因为数据量实在太大 ...