汇编入门

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的寄存器永远指向栈顶。堆和栈的具体区别如下图。


image
扩展段

保存程序其它相关的信息,其段寄存器为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指令实现的。

参考文献

iOS之底层汇编(一)

程序内存空间(代码段、数据段、堆栈段)

8086汇编指令全集

你可能感兴趣的:(汇编入门)