初识汇编

319 阅读15分钟

初识汇编

静态分析 看汇编 二进制代码 分析出代码的实现逻辑 在逆向iOS系统上面的App。我们知道,一个App安装在手机上面的可执行文件本质上是二进制文件。因为iPhone手机本质上执行的指令是二进制。是有手机CPU执行的。所以静态分析是建立在分析二进制上面。

动态分析 调试应用 例如可以通过xcode调试App的UI

为什么是用二进制? CPU只能读懂0和1。0代表没电,1代表有电。 0和1是最稳定的 如果是用10进制表示,0安到9安表示数据,1.5安怎么表示?最稳定的就两个状态,有电1和没电0。

##汇编语言的发展

###机器语言 有0和1组成的机制指令。

  • 加: 0100 0000
  • 减: 0100 1000
  • 乘: 1111 0111 1110 0000
  • 除: 1111 0111 1111 0000

汇编语言(assembly language)

使用助记符代替机器语言 例如:

  • 加: INC EAX 通过编译器 0100 0000
  • 减: DEC EAX 通过编译器 0100 1000
  • 乘: MUL EAX 通过编译器 1111 0111 1110 0000
  • 除: DIV EAX 通过编译器 1111 0111 1111 0000

PS: 助记符就是汇编语言的前身. PS: 任何语言的产生,都会需要这门语言的编译器。编译器会读懂高级语言,将高级语言转化为0和1的二进制组合, 让CPU可以读懂。后期为了更加高效的编程,就有了高级语言。

高级语言(High-level programming language)

C/C++/Java/OC/Swift, 更接近人类的自然语言。 比如C语言:

  • 加: A+B 通过编译器 0100 0000
  • 减: A-B 通过编译器 0100 1000
  • 乘: A*B 通过编译器 1111 0111 1110 0000
  • 除: A/B 通过编译器 1111 0111 1111 0000

PS: 高级语言也会有弊端。比如 高级语言为了保证高级语言的特性,比较抽象的概念,编译的过程中文件的体积就会更大,不会那么直观。高级语言写出来的文件会比二进制文件更大。

我们的代码在终端设备上是这样的过程:

代码在终端设备上是这样的过程 -1-.png

  • 汇编语言与机器语言一一对应,每一条机器指令都有与之对应的汇编指令
  • 汇编语言可以通过编译得到机器语言,机器语言可以通过反汇编得到汇编语言
  • 高级语言可以通过汇编得到汇编语言/机器语言, 但是汇编语言/机器语言几乎不可能还原为高级语言

PS: 高级语言和汇编语言不是一一对应的,所以不能还原。不同的语言,不同的代码,可以生成同样的汇编指令。同一门语言,不同的代码,可以生成同样的汇编指令。所以只能大致推算出逻辑和算法,不能完全还原。 PS: CPU有不同的型号。不同的型号会有不同的CPU架构。不同的CPU架构对应不同的指令集。

汇编语言的特点

  • 可以直接访问、控制各种硬件设备,比如存储器、CPU等,能最大限度的发挥硬件的功能。
  • 能不受编译器的限制,对生成的二进制代码进行完全的控制
  • 目标代码简短,占用内存少,执行速度快
  • 汇编指令是机器指令的助记符,同机器指令一一对应,每一种CPU都有自己的机器指令集\汇编指令集,汇编语言不具备可移植性
  • 知识点过多,开发者需要对CPU等硬件结构有所了解,不易于编写、调试和维护
  • 不区分大小写,比如mov和MOV是一样的

汇编的种类

目前讨论比较多的汇编语言有:

  • 8086汇编(8086处理器是16bit的CPU)
  • Win32汇编
  • Win64汇编
  • ARM汇编(嵌入式, Mac, iOS)
  • ...

我们iPhone里面用到的ARM汇编,但是不同的设备也有差异。因CPU的架构不同

16191030660368.png

PS: armv6和armv7是32bit的 PS: 32bit和64bit在数据吞吐量上不一样,和总线是有关系的。

几个必要的常识

  • 要想学好汇编,首先需要了解CPU等硬件结构
  • App/程序的执行流程

16191032752882.png

  • 硬件相关最为重要的是CPU/内存
  • 在汇编中,大部分指令都是和CPU与内存相关的

PS:可执行文件->加载到内存之后->image(镜像文件) PS: CPU读取内存的时候,如何区分指令和数据。 PC寄存器

总线

16191040073616.png

  • 每个CPU芯片都有许多管脚,这些管脚和总线相连,CPU通过总线跟 外部的器件进行交互
  • 总线:一个个导线的集合
  • 总线的分类
    • 地址总线 确定地址到内存寻找数据
    • 数据总线 数据的查找和读写,传递数据
    • 控制总线

16191041171198.png

举个例子:CPU从内存的3单元读取数据 16191042816415.png

  1. CPU 通过地址总线通知内存,要操作3位置的数据
  2. CPU 通过控制总线通知内存,要读取数据
  3. 通过数据总线,将内存中3位置的数据返回给CPU

PS:《汇编语言》 王爽

地址总线

  • 它的宽度决定了CPU的寻址能力
  • 8086的总线宽度为20,所以寻址能力为1M(2^^20)

PS: 有多少根地址总线就有多宽。比如有10个,每一个表达的能力是0和1,10根线,就有10个0和1的组合。和表示的数据的大小有关。地址总线越大,读到的数值就越大,那么寻址能力愈强。

PS:1M 是数量单位 1MB 是内存的容量 内存的地址单元是字节Byte,内存最大是1MB

16191050304021.png

PS: 数量单位 1M = 1024K 1K = 1024 PS: 容量单位 1024B = 1KB 1024KB = 1MB 内存的单位是1个字节 PS: IBM 银行的单独系统不是字节为单位 是两个字节为一个单位 PS: 宽度 100M 是100Mbps 每秒钟传输多少个二进制位(bit位)是传输速率。理论的下载速度 100/8 = 12.5MB/s

数据总线

  • 它的宽度决定了CPU的单次数据传输量,也就输数据的传输速度
  • 8086的数据总线的宽度是16,所以单次最大传递2个字节的数据

PS: 数据的吞吐量 平时说的32位 64位就是数据的吞吐量 32位 32 / 8 = 4字节 64位 64 / 8 = 8字节。64位,一个指针是8个字节。 PS: CPU上数据单元的大小

控制总线

  • 它的宽度决定了CPU对其他器件的控制能力,能有多少种控制

内存

16191068942074.png

16191069346332.png

16191069944134.png

  • 内存地址空间的大小受CPU地址总线宽度的限制。8086的地址总线宽度为20,可以定位2^^20 个不同的内存单元【内存地址范围0x00000~0xFFFFF】,所以8086的内存空间的大小为1MB
  • 0x00000~0x9ffff: 主存储器。可读可写
  • 0xA0000~0xBFFFF: 向显存中写入数据,这些数据会被显卡输出到显示器。可读可写
  • 0xC0000~0xfffff: 存储各种硬件/系统信息。只读

进制

学习进制的障碍 很多人学不好进制,原因是总以十进制为依托去考虑其他进制,需要原酸的时候也总是先转换成是进制,这种学习方法是错误的。

PS 进制本身是符号

进制的定义

  • 8进制有8个字符组成 0 1 2 3 4 5 6 7 逢八进一
  • 10进制有10个字符组成 0 1 2 3 4 5 6 7 8 9 逢十进一
  • N进制就是有N个符号组成 逢N进一

练习 1 + 1 什么时候等于3

十进制是有10个符号组成: 0 1 3 2 8 A B E S 7 逢十进一 如果这样定义十进制 1+1=3 就对了

这个的目的何在? 传统我们定义十进制和自定义十进制不一样。那么这10个符号如果我们不告诉别人这个符号表,别人就没有办法拿到我们具体的数据。可以用户加密。 PS: 十进制有十个符号组成,逢十进一,符号就是自定义的

进制的运算

16191339574480.png

16191340096667.png

二进制的简写形式

PS: 半斤八两 古称的1斤就是16两

二进制1 0 1 1 1 0 1 1 1 1 0 0
三个二进制一组101 110 111 100
八进制5   6    7  4
四个二进制一组1011 1011 1100
十六进制b    b    c

二进制:从0写到1111 0000 0001 0010 0011 0100 0101 0110 0111 1000 1001 1010 1011 1100 1101 1110 1111 这种二进制使用起来太麻烦,改成简单一点的符号 0 1 2 3 4 5 6 7 8 9 A B C D E F 就是十六进制了

计算机中常用的数据宽度

  • 位(bit): 1位就是一个二进制位。0或者1
  • 字节(Byte): 1个字节是有8个bit组成(8位), 内存中的最小单元Byte
  • 字(Word): 1个字有两个字节组成(16位),这两个字节分别称为高字节和低字节
  • 双字(doubleword): 1个双字节由两个字组成(32位)

那么计算机存储数据它就会分为有符号数和无符号数字

16191358692988.png

无符号数,直接换算 有符号数: 正数: 0 1 2 3 4 5 6 7 负数: F E D B C A 9 8 -1 -2 -3 -4 -5 -6 -7 -8

16191365341161.png

PS: 符号放在第一位

自定义进制符号

16191366727394.png

16191366958376.png

CPU和寄存器

内部部件之间由总线链接

16191368220991.png

CPU除了由控制器、运算器,还有寄存器。其中寄存器的作用就是进行数据的临时存储。

PS: CPU的运算速度是非常快的,为了性能,CPU在那边开辟了一小块临时存储区域,并在进行运算时先将数据从内存复制到这一小块存储区域中,运算时就在这一小块临时存储区域进行。我们称这一小块临时存储区域位寄存器。

对于arm64系的CPU来说,如果寄存器以x开头,则表明是一个64位的寄存器,如果以w开头,则说明是一个32位的寄存器,在系统中没有提供16位和8位的寄存器访问和使用。其中32位寄存器是64位寄存器的低32位部分,不是独立存在的。

  • 对于程序员来说,CPU中最重要的部件是寄存器,可以通过改变寄存器的内容事件对CPU的控制
  • 不同的CPU,寄存器的个数、结构不相同

浮点和向量寄存器

16191376778403.png

截屏2021-04-23 下午11.05.01.png

截屏2021-04-23 下午11.06.50.png

截屏2021-04-23 下午11.08.45.png

通用寄存器

  • 通用寄存器也称数据地址寄存器,通常用来做数据计算的临时存储、累加、计数、地址保存等功能。定义这个寄存器的作用,主要是在CPU指令中保存操作数,在CPU中当作一些常规变量来使用
  • ARM64拥有32个64位的通用寄存器x0到x30,以及XZR(零寄存器)。这个寄存器有时也有特定的用途。
    • w0到w28这些是32位的,应为64位的CPU可以兼容32位。所以可以只使用64位寄存器的低32位
    • 比如w0就是x0的低32位

16191385344997.png

16191385582504.png

  • 通常,CPU会先将内存中的数据存储到通用寄存器中,然后再对通用寄存器中的数据进行运算

  • 假设,内存中有块红色内存空间值位3,现在想把它的值加1,并将结果存储到蓝色的内存空间 16191387505242.png

    • CPU首先会将红色内存空间的值放到x0寄存器中,mov X0, 红色内存空间
    • 然后让x0寄存器与1相加, add x0, 1
    • 最后将值给内存空间: mov 蓝色内存空间, x0

    PS: 一条汇编指令占用4的字节

    通用寄存器

截屏2021-04-23 下午11.10.51.png

pc寄存器(program counter)

  • 为指令指针寄存器,它只是了CPU当前要读取指令的地址
  • 在内存或者磁盘上,指令和数据是没有区别的,都是二进制信息
  • CPU在工作的时候把有的信息看作指令,有的信息看作数据,为同样的信息赋予了不同的意义
    • 比如1110 0000 0000 0011 0000 1000 1010 1010
    • 可以当作数据0xE003008AA
    • 也可以当作指令mov x0, x8
  • CPU是根据什么将内存中的信息看作指令?
    • CPU将pc指向的内存单元的内容看作指针
    • 如果内存中的某段内容曾被CPU执行过,那么它所在的内存单元必然被pc指向过

PS: ps是CPU即将执行的指令

WeChatd394cf6316e4a3640d3a2ee27a862742.png

WeChatbc302c6cb4407aca9439663b5aa6430c.png

控制pc寄存器的指向的例子

WeChatdcf277837b24844534c5c476f0f8be3a.png WeChat42d0f39a03bd1dd62b4ce8ed328c3d5d.png WeChat50049b08d27b72e15a5666fccae35aba.png

高速缓冲

iPhoneX上搭载的ARM处理器A11它的1级缓存的容量为64KB,二级缓存的容量为8MB

PS: CPU每执行一条指令前都需要从内存中将指令读取到CPU内执行。而寄存器的运行速度相比内存读取要快得多,为了性能,CPU还集成了一个高速缓存存储区域。当程序运行时,先将要执行的指令代码以及数据复制到高速缓存中去(由操作系统完成)。CPU直接从高速缓存依次读取指令来执行。

PS: 操作系统会建立高速缓存和虚拟内存的映射关系 和 pagefault的策略很像

bl指令

  • CPU从何处执行指令是由pc中的内容决定,我们可以通过改变pc的内容来控制CPU执行目标指令
  • ARM64提供了一个mov指令(传送指令),可以用来修改大部分寄存器的值,比如 * mov x0,#10 mov x1, #20
  • 但是,mov指令不能设置pc的值,ARM64没有提供这样的功能
  • ARM64提供了另外的指令来修改PC的值,这些指令统称为转移指令,最简单的事bl指令

创建一个汇编文件asm.s,代码如下

.text
.global _A, _B

_A:
    mov x0,#0xa0
    mov x1,#0x00
    add x1,x0,#0x14
    mov x0,x1
    bl _B
    mov x0,#0x00
    ret

_B:
    add x0,x0,#0x10
    ret

在原文件中声明A函数,并调用

#import "ViewController.h"

@interface ViewController ()

@end

@implementation ViewController

int A();

void test() {
    int a = 10;
//    printf("%d", a);
}

- (void)viewDidLoad {
    [super viewDidLoad];
//    test();
    A();
}


@end

总结

  • 汇编的概述
    • 使用助记符代替机器指令的一种编程语言
    • 汇编和机器指令是一一对应的关系,拿到二进制就可以反汇编
    • 由于汇编和CPU的指令集是对应的,所以汇编不具备移植性
  • 总线 是有一堆导线的集合
    • 地址总线
      • 地址总线的宽度决定了寻址能力
    • 数据总线
      • 数据总线的宽度决定了CPU数据的吞吐量
  • 进制 其实是符号
    • 任意进制,都是有对应个数的符号组成,符号可以自定义
    • 2\8\16是相对完美的进制,他们之间的关系
      • 3个二进制位 使用一个8进制标识
      • 4个二进制位 使用一个16进制标识
      • 两个16进制位可以标识一个字节
    • 数量单位
      • 1024 = 1K; 1024K = 1M; 1024M = 1G
    • 容量单位
      • 1024B = 1KB; 1024KB = 1MB; 1024MB = 1GB
      • B:byte(字节) 1B = 8bit
      • bit:(比特) 一个二进制位
    • 数据的宽度
      • 计算机中的数据是有宽度的,超过了就会溢出
  • 寄存器 CPU为了性能,在内部开辟了一小块临时存储区域
    • 浮点向量寄存器
    • 异常状态寄存器
    • 通用寄存器 除了存放数据有时候也有个数的用途
      • ARM64拥有32个64位的通用寄存器x0~x30 以及XZR(零寄存器)
      • 为了兼容32位,所以ARM64拥有w0~w28\WZR30个32位寄存器
      • 32位寄存器并不是独立存在的,比如w0时x0的低32位
    • PC寄存器:指令指针寄存器
      • pc寄存器里面保存的就是CPU接下来需要执行的指令的地址
      • 改变pc的值可以改变程序的执行流程