01-汇编基础(1)

发布于 2022年 02月 12日 23:33

前言

从本篇文章开始,即将给大家分享关于iOS逆向安全攻防等相关的知识点,在分析逆向之前,我们必须掌握关于汇编的相关的知识点,作为逆向学习的一个准备。这篇文章首先给大家讲解一下汇编的一些基础知识点,希望大家能够掌握。

一、初识汇编

1.1汇编发展史

我们先来看看汇编语言的发展史👇

机器语言

机器语言 👉 由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

那什么是助记符呢?你可以这么理解,助记符就是帮助我们(程序猿)将上面的加减乘除等指令翻译成机器语言0和1的一些符号。

高级语言

接下来就是我们日常开发中使用的高级语言了,称作High-level programming language。例如C\C++\Java\OC\Swift,它们是更加接近于人类的自然语言。👇

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

综上所述,语言的一个发展过程大致就是👇

机器语言0和1 -->助记符-->编译器(负责读取助记符)产生汇编-->高级语言(接近人类的自然语言)

####补充:代码执行的过程 代码执行的过程如下图👇

上图可知:

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

1.2汇编语言的特点

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

再来安利一下,学习汇编语言能干啥😂👇

  1. 编写驱动程序、操作系统(比如Linux内核的某些关键部分)
  2. 对性能要求极高的程序或者代码片段,可与高级语言混合使用(内联汇编)
  3. 软件安全
    • 病毒分析与防治
    • 逆向\加壳\脱壳\破解\外挂\免杀\加密解密\漏洞\黑客
  4. 理解整个计算机系统的最佳起点和最有效途径
  5. 为编写高效代码打下基础
  6. 弄清代码的本质

最后来句装13的话

越底层越单纯!真正的程序员都需要了解的一门非常重要的语言,汇编!

###1.3汇编语言的种类 目前讨论比较多的汇编语言有

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

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

架构设备
armv6iPhone, iPhone2, iPhone3G, 第一代、第二代 iPod Touch
armv7iPhone3GS, iPhone4, iPhone4S,iPad, iPad2, iPad3(The New iPad), iPad mini, iPod Touch 3G, iPod Touch4
armv7siPhone5, iPhone5C, iPad4(iPad with Retina Display)
arm64iPhone5S 以后 iPhoneX , iPad Air, iPad mini2以后

二、必要常识点

在学好汇编之前,首先需要了解CPU等硬件结构

上图是程序(APP)的执行过程

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

我们知道,在磁盘中我们的应用程序被称作可执行文件,例如pc端的exe,iOS端的exc等,而这个可执行文件被加载到内存中就是镜像文件了。镜像文件其实和可执行文件一模一样的,因为是从磁盘copy到内存中,所以称作镜像

2.1 总线

总线是什么?先看下图👇

上图是苹果A11的CPU芯片,每一个CPU芯片都有很多管脚,这些管脚总线相连,CPU通过总线外部器件进行交互,所以总线是CPU与内存之间的桥梁

总线:是一根根导线的集合。

总线的分类

总线主要分为三类,如下图所示👇

  • 地址总线:CPU是通过地址总线来指定存储单元的
  • 数据总线:CPU与内存/其他部件之间的数据传送通道
  • 控制总线:CPU通过控制总线对外部器件进行控制
举例说明

上图是CPU从内存的3号单元读取数据,大致过程是这样的👇

  1. CPU首先要找到内存地址,才能读写内存中的数据。CPU通过地址总线,将3这个地址传递给内存,即寻址到内存的3号单元
  2. 需要操作3单元的数据,还需要确定是还是。CPU通过控制总线告诉内存需要进行的操作,例如示例中的是
  3. 内存接收了CPU想要进行的操作,将3号单元的数据通过数据总线传递给CPU。

至此,整个CPU和内存交互的过程结束。

2.1.1地址总线

地址总线的宽度决定了寻址的能力。 例如:8086的地址总线宽度是20,那么寻址能力就是2的20次方 = 1M(1048576),这是数量单位。

数量单位和数值单位的区别:

1M和1MB

  • 1M是数量单位,大小就是上面说的1048576。
  • 1MB是数值单位,例如 👉 内存的单位是B(byte字节),市面上卖的内存条512MB,就是512x1024x1024字节,每个字节占8bit大小的空间,即8位。

请看内存条图示👇

2.1.2数据总线

数据总线的宽度决定了CPU的单次数据传送量,也就是数据传送速度

  • 每条数据线一次只能传输一位二进制数据,例如 8根数据线一次可传送一个8位二进制数据(即1个字节的数据)
  • 数据总线是数据线数量之和

例如:8086的数据总线宽度是16,所以单次最大传递2个字节的数据。

吞吐量 还有一个名词叫吞吐量,其实就是CPU的单次数据传送的总量,和宽度是一个意思。

2.1.3控制总线
  • 控制总线的宽度决定了CPU对其他器件的控制能力,能有多少种控制,即CPU对外部器件的控制能力
  • 控制总线是控制线数量之和

2.2 内存

上面分析总线的时候,提到了内存,那么内存究竟是如何与CPU以及其它设备进行交互的呢?我们先来看看下面这张内存物理结构分布图👇

上图可知👇

  1. CPU是通过总线和其它硬件设备连接的
  2. 内存有RAM主存储器RAM主存储器(内存条)

下图是按照物理地址划分的内存,有主存储器、显存地址、显卡地址、网卡地址

其中内存中的低地址是给用户用的,高地址是给系统用的👇

内存地址空间的大小受CPU地址总线宽度的限制。例如:8086的地址总线宽度为20,可以定位2^20个不同的内存单元(即内存地址范围0x00000~0xFFFFF),所以8086的内存空间大小为1MB

  • 0x00000~0x9FFFF:主存储器,可读可写
  • 0xA0000~0xBFFFF:向显存中写入数据,这些数据会被显卡输出到显示器,可读可写
  • 0xC0000~0xFFFFF:存储各种硬件/系统信息,只读

2.3 进制

我们最熟悉的进制就是十进制,接触编程后,又知道了二进制、八进制、十六进制等,具体的意思👇

  • 八进制由8个符号组成:0 1 2 3 4 5 6 7 逢八进一
  • 十进制由10个符号组成:0 1 2 3 4 5 6 7 8 9 逢十进一
  • 以此类推:N进制就是由N个符号组成: 逢N进一

2.3.1学习进制的障碍

很多人学不好进制,原因是总以十进制为依托去考虑其他进制,需要运算的时候也总是先转换成十进制,这种学习方法是错误的! 为什么一定要转换十进制呢?仅仅是因为我们对十进制最熟悉,所以才转换。

每一种进制都是完美的,想学好进制首先要忘掉十进制,也要忘掉进制间的转换!

#####练习:1 + 1 在____情况下等于 3 ? 当然有人会回答 👉 在算错的情况下等于3!哈哈! 我们抛开约定俗成的十进制规则,重新定义10个符号,例如👇

0 1 3 2 8 A B E S 7 逢十进一

那么,此时1+1=3吗?肯定YES!那么,这么做的目的何在呢? 传统定义的十进制和自定义的十进制不一样。如果我们不告诉别人这10个符号的表,别人是没办法拿到我们的具体数据的,所以这样我们可以用自定义的符号表加密

综上所述👇

十进制十个符号组成,逢十进一,符号是可以自定义的!!

2.3.2进制的运算规则

做个练习 👉 八进制运算

2 + 3 = __ , 2 * 3 = __ ,4 + 5 = __ ,4 * 5 = __.
277 + 333 = __ , 276 * 54 = __ , 237 - 54 = __ , 234 / 4 = __ .

八进制加法表

 0  1  2  3  4  5  6  7 
10 11 12 13 14 15 16 17
20 21 22 23 24 25 26 27
...

1+1 = 2                     
1+2 = 3   2+2 = 4               
1+3 = 4   2+3 = 5   3+3 = 6
1+4 = 5   2+4 = 6   3+4 = 7   4+4 = 10  
1+5 = 6   2+5 = 7   3+5 = 10  4+5 = 11  5+5 = 12
1+6 = 7   2+6 = 10  3+6 = 11  4+6 = 12  5+6 = 13  6+6 = 14
1+7 = 10  2+7 = 11  3+7 = 12  4+7 = 13  5+7 = 14  6+7 = 15  7+7 = 16

八进制乘法表

0 1 2 3 4 5 6 7 10 11 12 13 14 15 16 17 20 21 22 23 24 25 26 27...
1*1 = 1                     
1*2 = 2   2*2 = 4               
1*3 = 3   2*3 = 6   3*3 = 11    
1*4 = 4   2*4 = 10  3*4 = 14  4*4 = 20
1*5 = 5   2*5 = 12  3*5 = 17  4*5 = 24  5*5 = 31
1*6 = 6   2*6 = 14  3*6 = 22  4*6 = 30  5*6 = 36  6*6 = 44
1*7 = 7   2*7 = 16  3*7 = 25  4*7 = 34  5*7 = 43  6*7 = 52  7*7 = 61

实战:四则运算

   277         236         276         234
+  333       -  54       *  54       /   4
--------    --------    --------    --------    

请大家算算!

2.3.3 二进制的简写形式

       二进制: 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 这就是十六进制了

2.4 数据的宽度

数学上的数字,是没有大小限制的,可以无限的大。但在计算机中,由于受硬件的制约,数据都是有长度限制的(我们称为数据宽度),超过最多宽度的数据会被丢弃。 示例👇

#import <UIKit/UIKit.h>
#import "AppDelegate.h"

int test(){
    int cTemp = 0x1FFFFFFFF;
    return cTemp;
}

int main(int argc, char * argv[]) {
    printf("%x\n",test());
    @autoreleasepool {
        return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
    }
}

断点调试结果可见,cTemp溢出了。

也可以通过获取的地址,然后在菜单栏选择Debug --> Debug Workflow --> ViewMemory中输入地址查看👇

2.4.1计算机中常见的数据宽度

  • 位(Bit):1个位就是1个二进制位,即0或1
  • 字节(Byte):1个字节由8个Bit组成,内存中的最小单元Byte
  • 字(Word):1个字由两个字节组成(16位),第2个字节分别称为高字节和低字节
  • 双字(DoubleWord):1个双字由两个字组成(32位)

那么计算机存储数据,它会分为有符号数无符号数。那么关于这个看下图就理解了!👇

  • 无符号数,直接换算
  • 有符号数,符号放在第1位,第1位是0即正数,为1即负数:
正数:0 1 2 3 4 5 6 7
负数:F E D B C A 9 8
表示:-1 -2 -3 -4 -5 -6 -7 -8
练习
  1. 现在有10进制数 10个符号分别是:2,9,1,7,6,5,4, 8,3 , A 逢10进1 那么: 123 + 234 = ____AA6

我们可以把自定义的十进制写出来,然后查表,逢十进一,👇

十进制:    0  1  2  3  4  5  6  7  8  9
自定义:    2  9  1  7  6  5  4  8  3  A
         92 99 91 97 96 95 94 98 93 9A
         12 19 11 17 16 15 14 18 13 1A
         72 79 71 77 76 75 74 78 73 7A
         62 69 61 67 66 65 64 68 63 6A
         52 59 51 57 56 55 54 58 53 5A
         42 49 41 47 46 45 44 48 43 4A
         82 89 81 87 86 85 84 88 83 8A
         32 39 31 37 36 35 34 38 33 3A
         922

对照着常规的十进制,做一个转换,即可得出答案。

  1. 现在有9进制数 9个符号分别是:2,9,1,7,6,5,4, 8,3 逢9进1 那么: 123 + 234 = ____9926

同理👇

十进制:    0  1  2  3  4  5  6  7  8  
自定义:    2  9  1  7  6  5  4  8  3  
         92 99 91 97 96 95 94 98 93 
         12 19 11 17 16 15 14 18 13 
         72 79 71 77 76 75 74 78 73 
         62 69 61 67 66 65 64 68 63 
         52 59 51 57 56 55 54 58 53 
         42 49 41 47 46 45 44 48 43 
         82 89 81 87 86 85 84 88 83 
         32 39 31 37 36 35 34 38 33 
         922

2.5 CPU&寄存器

内部部件之间由总线连接👇

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

什么是寄存器?它的作用是什么?👇

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

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

  1. 对程序员来说,CPU中最主要部件是寄存器,可以通过改变寄存器的内容来实现对CPU的控制
  2. 不同的CPU,寄存器的个数、结构是不相同的

2.5.1浮点和向量寄存器

因为浮点数的存储以及其运算的特殊性,CPU中专门提供浮点数寄存器来处理浮点数

  • 浮点寄存器 64位: D0 - D31 32位: S0 - S31

现在的CPU支持向量运算.(向量运算在图形处理相关的领域用得非常的多)为了支持向量计算系统了也提供了众多的向量寄存器.

  • 向量寄存器 128位:V0-V31

2.5.2通用寄存器

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

注意: 了解过8086汇编的同学应该知道,有一种特殊的寄存器段寄存器:CS,DS,SS,ES四个寄存器来保存这些段的基地址,这个属于Intel架构CPU中,在ARM中并没有

通常,CPU会先将内存中的数据存储到通用寄存器中,然后再对通用寄存器中的数据进行运算。看下面示例👇 假设内存中有块红色内存空间的值是3,现在想把它的值加1,并将结果存储到蓝色内存空间👇

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

2.5.3 pc寄存器(program counter)

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

下面通过一个例子来演示下pc寄存器的读和写,还是上面的溢出的例子👇

注意:真机联调!

#import <UIKit/UIKit.h>
#import "AppDelegate.h"

int test(){
    int cTemp = 0x1FFFFFFFF;
    return cTemp;
}

int main(int argc, char * argv[]) {
    printf("%x\n",test());
    @autoreleasepool {
        return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
    }
}

运行,demo中寄存器种类如下所示👇

然后我们来看看汇编代码👇

pc寄存器调试

接下来我们尝试调试下pc寄存器。首先在控制台打印pc寄存器地址,指令👇

register read pc

当前pc寄存器的内存地址是0x0000000100ac9520。按住control+Step into,走到下一步指令,继续打印

pc寄存器的内存地址是0x0000000100ac9524,再继续👇

pc寄存器的内存地址是0x0000000100ac9528

所以,一条指令在内存中占用4字节大小的空间。

除了pc寄存器地址外,当然还可以

首先断点端在第一行👇

接着输入写的指令👇

register write pc 0x10260151c

上图中,register read pc 此时是读不出来的,因为断点断住了,如果step into,此时断点断在哪里?👇

最终通过验证发现,会断在0x10260151c下一行,说明pc寄存器中执行完成了0x10260151c这个地址对应的指令,然后走到下一条指令,所以0x102601520中的指令是没有执行的。

高速缓存

iPhoneX上搭载的ARM处理器A11它的1级缓存的容量是64KB,2级缓存的容量8M.

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

2.5.4 bl指令

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

现在有两段代码!假设程序先执行A,请写出指令执行顺序.最终寄存器x0的值是多少?

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

_B:
    add x0, x0, #0x10
    ret

我们直接上机实操。 首先将上面的汇编代码写到工程之中 👉 com+n --> empty --> asm.s(.s代表是汇编代码文件)👇

接着写入汇编代码👇

.text
.global _A,_B

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

_B:
    add x0, x0, #0x10
    ret

注意:需要声明下面2行 .text .global _A,_B

接着执行 👉 如何让汇编代码跑起来呢?

  1. 在你需要调用的地方,先声明函数👇(例如VC中)

cmd+b编译,能成功!

  1. 在A()执行处加断点,并执行程序,开启汇编调试👇

上图可见,进入的汇编就是我们之前写的_A函数汇编代码!👏👏👏

调试汇编

接下来开始lldb调试,下面是一步步到0x0的过程👇

首先执行下一步,查看寄存器的值👇

接着往下走👇

再接下来走👇

接着进入函数B👇(按住control键step into到B函数)

接着往下执行指令👇

继续往下执行,回到了A函数👇

再接着往下走,会发现在最后2条指令之间死循环了!why?请看下一篇!

总结

本篇文章开始带大家了解了下汇编的发展史和特点,然后探讨了cpu和内存及其他硬件,是通信总线进行数据的交换处理,最后重点示例分析了寄存器,带大家通过lldb指令pc寄存器进行读和写,以及bl指令的调试,希望大家能够动手实操一遍,加深印象,感谢阅读!

推荐文章