Contents

汇编入门

汇编语言概述

  • 汇编语言是什么?
    • 对于人类来说,组成机器指令的01代码是不可读的,为了解决可读性问题以及偶尔的编辑需求,诞生了二进制指令的文本形式——汇编语言
    • 汇编语言与指令是一一对应的关系
  • 来历
    • 手写二进制指令按动开关模拟01——>纸袋打孔——>八进制表示——>文字标识(操作码用代号表示,地址码用标签表示)
    • 把assembly language翻译成二进制的步骤就成为assembling,完成这个步骤的程序叫做assembler
  • 每一种CPU的指令集都是不一样的,本文介绍以intel的x86指令集架构为例

寄存器

  • 寄存器不依靠地址区分数据,而依靠名称定位,因此不需要像Cache和内存一样寻址,再加上本身的静态RAM结构,速度是非常快的,可以戏称为0级缓存
  • 寄存器种类:
    • 早期的x86指令集架构CPU只有8个寄存器,每个寄存器的功能都不同(目前已经有100多个了,并且几乎都是通用寄存器)
    • EAX,EBX,ECX,EDX,EDI,ESI,EBP,ESP:前7个是通用寄存器,ESP是保存当前Stack地址的特定寄存器
    • xx位CPU指的就是CPU内每个寄存器的位数大小

内存模型

  • 程序运行的时候,操作系统会给程序分配一段内存用来存储程序和运行产生的数据
  • 由于用户动态主动请求而划分出来的内存区域(一个进程的所有线程共享),叫做Heap堆,Heap里的数据不会自动消失,必须手动释放,或者由垃圾回收机制来回收。从起始地址开始,从低位向高位增长
  • 由于程序中的函数运行而临时占用的内存区域(每个线程独享)叫做Stack栈,栈的每一层叫做一个帧。从内存区域的结束地址开始,从高位地址向地位地址分配

CPU指令

  • C高级语言代码和其对应的汇编程序:
    1
    2
    3
    4
    5
    6
    7
    
      int add_a_and_b(int a, int b) {
      return a + b;
      }
    
      int main() {
      return add_a_and_b(2, 3);
      }
    
    通过gcc将程序转为汇编语言gcc -S example.c
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    
    _add_a_and_b:
     push   %ebx
     mov    %eax, [%esp+8] 
     mov    %ebx, [%esp+12]
     add    %eax, %ebx 
     pop    %ebx 
     ret  
    
    _main:
      push   3
      push   2
      call   _add_a_and_b 
      add    %esp, 8
      ret
    
  • 程序从_main标签开始执行,这时会在 Stack 上为main建立一个帧,并将Stack 所指向的地址,写入 ESP 寄存器。后面如果有数据要写入main这个帧,就会写在 ESP 寄存器所保存的地址。 然后,开始执行第一行代码:
  • push指令:
    • push 3:将运算子放入main函数的stack,即将3写入main的帧。看上去很简单,push指令有一个前置操作。它会先取出 ESP 寄存器里面的地址,将其减去4个字节,然后将新地址写入 ESP 寄存器。使用减法是因为 Stack 从高位向低位发展,4个字节则是因为3的类型是int,占用4个字节。得到新地址以后, 3 就会写入这个地址开始的四个字节。
    • push 2:push指令将2写入main这个帧,位置紧贴着前面写入的3。这时,ESP 寄存器会再减去 4个字节(累计减去8)
  • call指令:
    • call _add_a_and_b:调用add_a_and_b函数。这时,程序就会去找_add_a_and_b标签,并为该函数建立一个新的帧
    • push %ebx:将 EBX 寄存器里面的值,写入_add_a_and_b这个帧。这是因为后面要用到这个寄存器,就先把里面的值取出来,用完后再写回去。这时,push指令会再将 ESP 寄存器里面的地址减去4个字节(累计减去12)。
  • mov指令:用于将一个值写入某个寄存器
    • mov %eax, [%esp+8] :先将 ESP 寄存器里面的地址加上8个字节,得到一个新的地址,然后按照这个地址在 Stack 取出数据。根据前面的步骤,可以推算出这里取出的是2,再将2写入 EAX 寄存器。
    • mov %ebx, [%esp+12] :将 ESP 寄存器的值加12个字节,再按照这个地址在 Stack 取出数据,这次取出的是3,将其写入 EBX 寄存器。
  • add指令:将两个运算子相加,并将结果写入第一个运算子
    • add %eax, %ebx:EAX 寄存器的值(即2)加上 EBX 寄存器的值(即3),得到结果5,再将这个结果写入第一个运算子 EAX 寄存器。
  • pop指令:取出 Stack 最近一个写入的值(即最低位地址的值),并将这个值写入运算子指定的位置。
    • pop %ebx:取出 Stack 最近写入的值(即 EBX 寄存器的原始值),再将这个值写回 EBX 寄存器(因为加法已经做完了,EBX 寄存器用不到了)。注意,pop指令还会将 ESP 寄存器里面的地址加4,即回收4个字节。
  • ret指令:用于终止当前函数的执行,将运行权交还给上层函数。也就是,当前函数的帧将被回收。
    • ret:该指令没有运算子
    • add %esp, 8 :将 ESP 寄存器里面的地址,手动加上8个字节,再写回 ESP 寄存器。这是因为 ESP 寄存器的是 Stack 的写入开始地址,前面的pop操作已经回收了4个字节,这里再回收8个字节,等于全部回收。
    • ret:最后,main函数运行结束,ret指令退出程序执行。