我们大都是被高级语言惯坏了的一代,源源不断的新特性正在逐步添加到各类高级语言之中,汇编作为最接近机器指令的低级语言,已经很少被直接拿来写程序了,不过我还真的遇到了一个,那是之前的一个同事,因为在写代码时遇到了成员函数权限及可见性的问题,导致他无法正确调用想执行的函数,结果他就开始在 C++
代码里嵌入汇编了,绕过了种种限制终于如愿以偿,但是读代码的我们傻眼了…
因为项目是跨平台的,代码推送的 Linux 上编译的时候他才发现,汇编代码的语法在 Linux 和 Windows 上居然是不一样的,结果他又用一个判断平台的宏定义“完美”的解决了,最终这些代码肯定是重写了啊,因为可读性太差了,最近在学习左值、右值、左引用和右引用的时候,总是有人用程序编译生成的中间汇编代码来解释问题,看得我迷迷糊糊,所以决定熟悉一下简单的汇编指令,边学习边记录,方便今后忘记了可以直接拿来复习。
汇编语言是最接近机器语言的编程语言,引用百科中的一段话解释为:
汇编语言(assembly language)是一种用于电子计算机、微处理器、微控制器或其他可编程器件的低级语言,亦称为符号语言。在汇编语言中,用助记符代替机器指令的操作码,用地址符号或标号代替指令或操作数的地址。汇编语言又被称为第二代计算机语言。
对于绝大多数人来说,二进制程序是不可读的,当然有能人可以读,比如第一代程序员,但这类人快灭绝了,直接看二进制不容易看出来究竟做了什么事情,比如最简单的加法指令二进制表示为 00000011
,如果它混在一大串01字符串中就很难把它找出来,所以汇编语言主要就是为了解决二进制编码的可读性问题。
换句话来说,汇编语言就是把给机器看的二进制编码翻译成人话,汇编指令是机器指令的助记符,与机器指令是一一对应的关系,是一种便于阅读和记忆的书写格式。有效地解决了机器指令编写程序难度大的问题,并且使用编译器,可以很方便的把汇编程序转译成机器指令程序,比如之前提到的 00000011
加法指令,对应的汇编指令是 ADD
,在调用汇编器时就会把 ADD
翻译成 00000011
。
说到汇编指令不得不提到寄存器,寄存器本身是用来存数据的,因为 CPU
本身只负责逻辑运算,数据需要单独储存在其他的地方,但是对于不熟悉寄存器的人来说会有疑惑,数据不是存在硬盘上吗?或者说数据不是存在内存中吗?这些想法都没错,那么寄存器是用来做什么的呢?
其实硬盘、内存都是用来存储数据的,但是 CPU
的运算速度远高于内存的读写速度,更不用说从硬盘上取数据了,所以为了避免被拖慢速度影响效率,CPU
都自带一级缓存和二级缓存,一些 CPU
甚至增加了三级缓存,从这些缓存中读写数据要比内存快很多,但是还是无法使用飞速运转的 CPU
,所以才会有寄存器的存在。
寄存器不是后来增加的,在最初的计算中就已经设计出来,相比而言,多级缓存出现的更晚一些,通常那些最频繁读写的数据都会被放在寄存器里面,CPU
优先读写寄存器,再通过寄存器、缓存跟内存来交换数据,达到缓冲的目的,因为可以通过名称访问寄存器,这样访问速度是最快的,因此也被称为零级缓存。
通过上面的叙述我们可以知道存取速度从高到低分别是: 寄存器 > 1级缓存 > 2级缓存 > 3级缓存 > 内存 > 硬盘
,关于它们的存取速度,举个例子很容易就能明白了,比如我们做菜(CPU工作)时,取手中(寄存器)正拿着的肉和蔬菜肯定是最快的,如果没有就需要把案板上(1级缓存)处理好的菜拿过来,如果案板上没有就在更远一点的洗菜池(2级缓存)中找一找,还没找到的话就要到冰箱(3级缓存)中看一看了,这时发现家里真没有,那去楼下的菜店(内存)去买点吧,转了一圈发现没有想要的,最后还是开车去农贸市场(硬盘)买吧。
通过上面这个例子应该能明白它们的速度关系了,既然缓存这么快,为什么不用缓存代替内存,或者将2、3级缓存都换成1级缓存呢?这里边有一个成本问题,速度越快对应着价格越高,如果你买过机械硬盘和固态硬盘应该很容易就理解了。
常用的 x86 CPU
寄存器有8个:EAX
、EBX
、ECX
、EDX
、EDI
、ESI
、EBP
、ESP
,据说现在寄存器总数已经超过100个了,等我找到相关资料再来补充,上面这几个寄存器是最常用的,这些名字也常常出现在汇编的代码中。
我们常说的32位、64位 CPU
是指数据总线的宽度或根数,而寄存器是暂存数据和中间结果的单元,因此寄存器的位数也就是处理数据的长度与数据总线的根数是相同的,所以32位 CPU
对应的寄存器也应该是32位的。
上面提到大8个寄存器都有其特定的用途,我们以32位 CPU
为例简单说明下这些寄存器的作用,整理如下表:
寄存器 | 含义 | 用途 | 包含寄存器 |
---|---|---|---|
EAX | 累加(Accumulator)寄存器 | 常用于乘、除法和函数返回值 | AX(AH、AL) |
EBX | 基址(Base)寄存器 | 常做内存数据的指针, 或者说常以它为基址来访问内存. | BX(BH、BL) |
ECX | 计数器(Counter)寄存器 | 常做字符串和循环操作中的计数器 | CX(CH、CL) |
EDX | 数据(Data)寄存器 | 常用于乘、除法和 I/O 指针 | DX(DH、DL) |
ESI | 来源索引(Source Index)寄存器 | 常做内存数据指针和源字符串指针 | SI |
EDI | 目的索引(Destination Index)寄存器 | 常做内存数据指针和目的字符串指针 | DI |
ESP | 堆栈指针(Stack Point)寄存器 | 只做堆栈的栈顶指针; 不能用于算术运算与数据传送 | SP |
EBP | 基址指针(Base Point)寄存器 | 只做堆栈指针, 可以访问堆栈内任意地址, 经常用于中转 ESP 中的数据, 也常以它为基址来访问堆栈; 不能用于算术运算与数据传送 | BP |
在上面的图标中每个常用寄存器后面还有其他的名字,它们是同一个寄存器不同用法下的不同名字,比如在32位 CPU
上,EAX是32位的寄存器,而AX是EAX的低16位,AH是AX的高8位,而AL是AX的低8位,它们的对照关系如下:
00000000 00000000 00000000 00000000
|===============EAX===============|---4个字节
|======AX=======|---2个字节
|==AH===|-----------1个字节
|===AL==|---1个字节
终于说到汇编常用指令了,因为 linux
和 windows
下的汇编语法是有些不同的,所以下面我们先通过 windows
下的汇编指令来简单学习一下,后续再来比较两者的不同。
指令 | 名称 | 示例 | 备注 |
---|---|---|---|
MOV | 传送指令 | MOV dest, src | 将数据从src移动到dest |
PUSH | 进栈指令 | PUSH src | 把源操作数src压入堆栈 |
POP | 出栈指令 | POP desc | 从栈顶弹出字数据到dest |
指令 | 名称 | 示例 | 备注 |
---|---|---|---|
ADD | 加法指令 | ADD dest, src | 在dest基础上加src |
SUB | 减法指令 | SUB dest, src | 在dest基础上减src |
INC | 加1指令 | INC dest | 在dest基础上加1 |
DEC | 减1指令 | DEC dest | 在dest基础上减1 |
指令 | 名称 | 示例 | 备注 |
---|---|---|---|
NOT | 取反运算指令 | NOT dest | 把操作数dest按位取反 |
AND | 与运算指令 | AND dest, src | 把dest和src进行与运算之后送回dest |
OR | 或运算指令 | OR dest, src | 把dest和src进行或运算之后送回dest |
XOR | 异或运算 | XOR dest, src | 把dest和src进行异或运算之后送回dest |
指令 | 名称 | 示例 | 备注 |
---|---|---|---|
LOOP | 计数循环指令 | LOOP label | 使ECX的值减1,当ECX的值不为0的时候跳转至label,否则执行LOOP之后的语句 |
指令 | 名称 | 示例 | 备注 |
---|---|---|---|
JMP | 无条件转移指令 | JMP lable | 无条件地转移到标号为label的位置 |
CALL | 过程调用指令 | CALL labal | 直接调用label |
JE | 条件转移指令 | JE lable | zf =1 时跳转到标号为label的位置 |
JNE | 条件转移指令 | JNE lable | zf=0 时跳转到标号为label的位置 |
前面说到 linux
和 windows
下的汇编语法是不同的,其实两种语法的不同和系统不同没有绝对的关系,一般在 linux
上会使用 gcc/g++
编译器,而在 windows
上会使用微软的 cl
也就是 MSBUILD
,所以产生不同的代码是因为编译器不同,gcc
下采用的是AT&T的汇编语法格式,MSBUILD
采用的是Intel汇编语法格式。
差异 | Intel | AT&T |
---|---|---|
引用寄存器名字 | eax | %eax |
赋值操作数顺序 | mov dest, src | movl src, dest |
寄存器、立即数指令前缀 | mov ebx, 0xd00d | movl $0xd00d, %ebx |
寄存器间接寻址 | [eax] | (%eax) |
数据类型大小 | 操作码后加后缀字母,“l” 32位,“w” 16位,“b” 8位(mov dx, word ptr [eax]) | 操作数前面加dword ptr, word ptr,byte ptr的格式 (movb %bl %al) |
EAX
、EBX
、ECX
、EDX
、EDI
、ESI
、EBP
、ESP
寄存器 > 1级缓存 > 2级缓存 > 3级缓存 > 内存 > 硬盘
mov
、je
、jmp
、call
、add
、sub
、inc
、dec
、and
、or
如今的每分每秒都是人生,不要总想着将自然发生的事情拖到预定的时刻才进行~