CPU在运行的时候实际上是读取指令并一条一条的执行,而这些指令是二进制的,也就是机器码,但是由于二进制的语言对人类的可读性不好,因此便出现了汇编语言,一般而言,汇编语言可以看作是机器码的文本格式,它们间可以相互转换,还原成机器码后便可以被CPU执行。其特点有:
- 可直接访问、控制各种硬件设备。比如存储器、CPU等,能最大限度地发挥硬件的功能
- 能够不受编译器的限制,对生成的二进制代码进行完全的控制
- 目标代码简短,占用内存少,执行速度快
- 汇编指令是机器指令的助记符,同机器指令一一对应。每种CPU都有自己的机器指令集/汇编指令集,所以* 汇编语言不具备可移植性
- 汇编语言知识点过多,开发者需要对CPU等硬件结构有所了解,不宜于编写、调试、维护
- 不区分大小写,比如mov和MOV是一样的
对于一个使用高级语言的程序员来说,汇编语言不需要自己编写,但是至少需要看得懂,知道其中的原理,才能更好的排查问题。
总的来说,汇编代码是被CPU一条一条地取出执行的,CPU会通过这些指令来对寄存器和内存等进行操作。因此学好汇编,需要对寄存器和内存有一定的了解
寄存器
由于CPU的运算速度通常都比内存读写速度要快,而CPU要运算的时候都要从内存中读取数据。为了节省时间,一般CPU都自带缓存,除此之外,还自带了寄存器,用来存储频繁使用的数据,此外CPU还会使用寄存器与内存交换数据。
不同CPU里面的寄存器名字和数量都不一致,寄存器的用法基本都是想通的,但是大致上可以分成以下种:
通用寄存器
主要是AX,BX,CX,DX等等
指令寄存器
也就是IP,用于记录现在程序执行到哪
指针寄存器
主要是栈指针寄存器SP和栈基指针寄存器BP,每当有数据入栈/出栈的时候,都会导致SP改变
段寄存器
标志段的开始,主要有CS,DS,SS,ES等等
当然还有许多别的种类的寄存器没有提及,比如索引寄存器(SI,DI),而且,上述的通用寄存器部分又有各自的用途,比如AX用于累加和中断,CX用于计数、循环等等。实际上,如果仅是看代码的话,大部分寄存器在通常情况下都可以看作通用寄存器。因为它们存放的数值被汇编代码的指令控制的。我们只需要知道的特殊的寄存器(比如IP,SP)即可。
内存分段
在程序运行的时候,系统会给程序分配一定的内存空间,这些内存空间会分成好几段:
代码段
存放汇编指令,其段寄存器为CS,此外还有指令寄存器IP,CPU就是从这个段读取指令
数据段
存放全局变量,可以根据这些变量有没有被初始化进一步划分,其段寄存器为DS
堆栈段
用来存放程序运行期间产生的变量,又分成堆和栈,其中栈用于存放函数中的局部变量,而堆用来存放动态分配的变量,堆栈段的寄存器为SS,另外有个SP的寄存器永远指向栈顶。堆和栈的具体区别如下图。
扩展段
保存程序其它相关的信息,其段寄存器为ES
常用汇编指令
每一个处理器汇编指令都不太一样,但是其基本功能都是相通的,这里仅以8086处理器的指令集为例:
mov
传送指令,其用法为mov a,b
,指的是将b的值赋值给a
add
加法,其用法为add a,b
,将b的值加上a的值赋值给a,即a = a + b
sub
减法,其用法为sub a, b
,与上面类似,相当于a = a - b
cmp
比较,其用法为cmp a, b
,比较a和b的大小,其比较的结果存储在标志寄存器中。
jmp
无条件转移指令,通过修改IP和CS寄存器,使程序跳到目标地址运行
jcc
条件转移指令,jcc包含一系列的指令,通过判断标志寄存器的状态决定是否跳转
call
调用函数,程序会调到函数入口执行
ret
函数返回
高级语言程序结构对应的汇编语言
下面看一下程序的顺序结构、选择结构、循环结构下的汇编语言是怎么样的,本文所使用的语言为C++,处理器为x86_64,其指令集跟上面的不太一样,而且,其用法跟8086是相反的。
顺序结构
在main函数里面写上如下代码:
int a = 5;
int b = 1;
a++;
b += a;
其对应的汇编语言是这样的
;a = 5
0x100000f94 <+20>: movl $0x5, -0x14(%rbp) ;将5赋值给偏移量为-0x14的内存区域,也就是说-0x14代表a
;b = 1
0x100000f9b <+27>: movl $0x1, -0x18(%rbp) ;将1赋值给偏移量为-0x18的内存区域,也就是说-0x18代表b
;a++
0x100000fa2 <+34>: movl -0x14(%rbp), %edi ;将a赋值给edi寄存器
0x100000fa5 <+37>: addl $0x1, %edi ; edi寄存器加1
0x100000fa8 <+40>: movl %edi, -0x14(%rbp) ;将edi寄存器赋值给a,这个时候完成了a++的操作
;b += a
0x100000fab <+43>: movl -0x14(%rbp), %edi ;将a赋值给edi寄存器
0x100000fae <+46>: addl -0x18(%rbp), %edi ;edi加上b的值
0x100000fb1 <+49>: movl %edi, -0x18(%rbp) ;将edi寄存器赋值给a,这个时候完成了b += a的操作
选择结构
同样,在main函数中写一个简单的选择结构
int a = 5;
if (a>3) {
a++;
}else {
a--;
}
对应的汇编代码如下:
0x100000f82 <+18>: movl $0x5, -0x14(%rbp) ;a = 5
0x100000f89 <+25>: cmpl $0x3, -0x14(%rbp) ;a 和 3比较
0x100000f8d <+29>: jle 0x100000fa1 ;如果小于等于,就跳到0x100000fa1执行
;下面是a++的实现
0x100000f93 <+35>: movl -0x14(%rbp), %eax
0x100000f96 <+38>: addl $0x1, %eax
0x100000f99 <+41>: movl %eax, -0x14(%rbp)
0x100000f9c <+44>: jmp 0x100000faa ; 实现后程序跳转到0x100000faa执行
;下面是a--
0x100000fa1 <+49>: movl -0x14(%rbp), %eax
0x100000fa4 <+52>: addl $-0x1, %eax
0x100000fa7 <+55>: movl %eax, -0x14(%rbp)
0x100000faa .....
显然,汇编语言是使用cmp、jmp和jcc指令实现选择结构的。
循环机构
在main函数中写一个简单的循环:
int i = 0;
while(1){
i++;
if(i>5){
break;
}
}
这里使用了while做循环,实际上跟使用for循环的汇编代码是差不多的。
0x100000f82 <+18>: movl $0x0, -0x14(%rbp) ;i = 0
0x100000f89 <+25>: movl -0x14(%rbp), %eax ;这里开始循环
0x100000f8c <+28>: addl $0x1, %eax
0x100000f8f <+31>: movl %eax, -0x14(%rbp)
0x100000f92 <+34>: cmpl $0x5, -0x14(%rbp)
0x100000f96 <+38>: jle 0x100000fa1
0x100000f9c <+44>: jmp 0x100000fa6 ;如果i>5,则跳出循环
0x100000fa1 <+49>: jmp 0x100000f89 ;跳回0x100000f89,继续循环
0x100000fa6 ....
可以看出,循环结构也是通过cmp、jmp和jcc指令实现的。