本文的立意仅仅是讨论基本的x86汇编语法,。所以我要重申这里的汇编教程并不是深入研究汇编,本人也没有那么大的能耐。学习基础的汇编主要达到以下的程度即可。
- 读懂常用的运算符指令
- 读懂存储和加载指令
- 了解x86和x86_64常用的寄存器的用途。
- 了解基本的寻址模型。
了解基本的汇编以后,有什么用?
- 加深你对C/C++的编译器行为和大部分内部操作大有益处
- 让你了解到用C/C++实现的高层算法在汇编层面实现细节。
- 让你更深入地了解函数栈调用的细节,能够写出高效的代码。
其实我这里可以提一个问题?你知道 以下简单的语句它们在计算机底层做了写什么操作吗?
int *a=123; 或
double b=456;
double *pp=b;
如果你心中没有明确的答案,请不要蛮目自信地欺骗自己C/C++的功底是如何如何地扎实.....,我自己都没自信心敢这么说?如果你打算专职用C/C++写算法实现的,那么基本的汇编功底是必须的。
之前我也写过一篇关于x64的汇编教程提交到寄存器和系统调用的基本关系《汇编语言基础:寄存器和系统调用》
上面的图例是一个比较完整的示意图,通常在IA32或x86_64中主要集中讨论CPU中的寄存器与主内存的数据交换,而其他的外部寄存器不在本文的讨论范围之内。
加载与存储
在汇编中mov指令主要用于在主内存(RAM)和寄存器之间传输数据,按照传递的方向可以分为两类
- 加载(Load)指令:从主内存(RAM)到寄存器传输数据,以一个给定的内存地址作为参数,他从内存中获取该地址位置存储的数据,并将该数据放入寄存器
- 存储(Store)指令:即从寄存器中缓存的内存地址中所指向的目标位置,并将另外一个寄存器中缓存的数据保存到该目标位置,我们可以把内存看作已经编了号的数组,而通过索引就是内存地址,通过指定的内存地址可以在特定位置中的修改内存中的数据。
IA32中的寄存器
在IA32架构中有八个寄存器,其中有6个是通用的寄存器,另外两个是esp和ebp有特殊的用途,如你对栈有所了解的话,就知道esp是始终指向栈顶的,ebp始终指向栈底。对于EAX,EBX,ECX和EDX寄存器,也会通常使用小字节的数据类型。 例如,EAX的最低有效2字节可被视为称为AX的16位寄存器。 AX的最低有效字节可以用作单个8位寄存器,称为AL,而AX的最高有效字节可以用作单个8位寄存器,称为AH。 这些名称指的是相同的物理寄存器。 当将两个字节的数量放入DX中时,更新将影响DH,DL和EDX的值。 这些子寄存器主要是较旧的16位版本指令集的保留。 但是,在处理小于32位(例如1字节ASCII字符)的数据时,它们有时很方便。
mov指令的长度分类
根据操作数的字长可以分为三个版本的mov指令
- movl Source Dest : L表示可以移动4个字节
- movw Source Dest: W表示可以移动2个字节
- movb Source Dest: B表示可以移动1个字节
mov指令的操作数
并且在移动指令中会用到两个操作数,Source表示移动的数据源,Dest表示移动的数据最后到达的位置。
通常在x86用的比较频繁的mov指令版本是32位的movl,这里就以 movl Source Dest为例子
mov指令的操作数通常分为三类
- 立即数(Immediate) 可以将其视为常数,和C语言的常量类似,但是以“$”为前缀,例如-500
- 寄存器(Register)可以是以上8个整数寄存器中的其中一个,例如:“%eax”,“%edx”,当我们执行类似“movl %eax %edx”这种情况下,eax寄存器就成为源参数,而edx寄存器就成为目标参数,其含义是获取eax中的内容并存储在edx中。
- 内存(Memory):而内存作为操作数通常是由寄存器缓存给定的内存地址来间接去操作的,该地址占用4个字节,例如(%eax),当寄存器在一个括号中,我们就说eax持有一个指向RAM中的某个位置的地址,你可以类比为类似C/C++中的指针变量,的那么 movl (%eax) %ebx 表示的是什么意思呢?你可以考虑一下表示什么意思?
mov的操作数组合
movl指令通过不同类型的操作数组合,能够表达出不同类型的指令类型的。
- 例如源参数是立即数,而目标参数是的寄存器,即“movl $123,%eax ”表示将常量保存在寄存器eax中,等价于C/C++声明并初始化一个变量例如
其实在C编译器对每个声明的基本数据类型的变量会映射到一个寄存器中,例如一个int类型4字节,会选择一个32位的寄存器类装载int整数等待CPU中的运算单元(AU)处理。int a=123;
- 源参数是立即数,而目标参数是一个主内存中的地址,即“movl $123,%(eax) ”表示将常量保存到eax寄存器中的存储的内存位置所指向的内存位置,同理等价的C/C++语句如下:
int *a=123;
- 源参数是寄存器,而目标参数也是寄存器,例如“movl %edx,%eax”其实就是在寄存器之间拷贝数据。
- 源参数是寄存器,而目标参数是内存。其实就是一种存储类型,例如“movl %eax,(%edx)”,表示当前寄存器eax中的数据写回主内存中的某个位置,而这个位置是由寄存器edx保存的地址所指向的。
- 源参数是内存,而目标参数是寄存器,例如“movl (%edx),%eax”这是从内存中加载数据到eax寄存器当中。因此表示加载指令的其中一种。
可能你会想到最后一种,有从“内存”到“内存”传递的指令类型吗?单一的一条指令是不可能的。只能通过两步实现
- 首先“内存”到“寄存器”例如:“movl (%edx),%eax”
- 然后“寄存器”到“内存” 例如:“movl %eax,(%ecx)”
基本的内存寻址模型
间接访问
就是“(R)”这种格式,R表示一个寄存器名称。寄存器中保存着一个内存地址,我们用mov指令操作主内存时,只能通过使用该寄存器中的内容去实现对该地址指向的RAM中位置进行读/写操作。对于操作系统来说,内存其实就是一个连续的字节数组,每个字节都有对应的编号,而这个编号就是内存地址,也可以理解为该字节数组的索引。
我们用“RAM”表示主内存,那么 (R) 其实等价于 RAM[Reg[R]],
那么“movl (%edx),%eax”类似这样的指令的含义到这里应该不用多说了吧!
移位寻址
英文名称叫“Displacement”,这个其实就是在间接寻址表达式的基础是加上一个有符号的整数N。在汇编中抽象的移位寻址表达式“N(R)”
我们用“RAM”表示主内存,那么 N(R) 其实等价于 RAM[Reg[R]+N],
那么具体的例子就是
- 例如 “-4(%ebp)”:表示ebp寄存器中缓存的内存地址向低地址方向移动了4个字节
- 例如“8(%ebp)”:表示ebp寄存器中缓存的内存地址向高地址方向移动8个字节。
- 那么像这种“movl 4(%esp),%ecx”其实就是寄存器esp的指针向高地址方向偏移4个字节后的个新的地址,从该新地址指向的内存位置获取数据并保存到寄存器ecx。