前言
从本篇文章开始,即将给大家分享关于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汇编语言的特点
- 可以
直接访问、控制
各种硬件设备,比如存储器、CPU等,能最大限度
地发挥硬件
的功能 - 能够
不受
编译器的限制,对生成的二进制代码
进行完全的控制
- 目标代码
简短,占用内存少,执行速度快
- 汇编指令是机器指令的
助记符
,同机器指令一一对应。每一种CPU都有自己的机器指令集\汇编指令集,所以汇编语言不具备可移植性
- 知识点
过多
,开发者需要对CPU等硬件
结构有所了解,不易于编写、调试、维护
-
不区分
大小写,比如mov和MOV是一样的
用途
再来安利一下,学习汇编语言能干啥
- 编写驱动程序、操作系统(比如Linux内核的某些关键部分)
- 对性能要求极高的程序或者代码片段,可与高级语言混合使用(内联汇编)
- 软件安全
- 病毒分析与防治
- 逆向\加壳\脱壳\破解\外挂\免杀\加密解密\漏洞\黑客
- 理解整个计算机系统的最佳起点和最有效途径
- 为编写高效代码打下基础
- 弄清代码的本质
最后来句装13的话
越底层越单纯!真正的程序员都需要了解的一门非常重要的语言,汇编!
1.3汇编语言的种类
目前讨论比较多的汇编语言有
- 8086汇编(8086处理器是16bit的CPU)
- Win32汇编
- Win64汇编
- ARM汇编(嵌入式、Mac、iOS)
我们iPhone里面用到的是ARM汇编,但是不同的设备也有差异,因CPU的架构不同。
架构 | 设备 |
---|---|
armv6 | iPhone, iPhone2, iPhone3G, 第一代、第二代 iPod Touch |
armv7 | iPhone3GS, iPhone4, iPhone4S,iPad, iPad2, iPad3(The New iPad), iPad mini, iPod Touch 3G, iPod Touch4 |
armv7s | iPhone5, iPhone5C, iPad4(iPad with Retina Display) |
arm64 | iPhone5S 以后 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号单元读取数据,大致过程是这样的
- CPU首先要找到内存地址,才能读写内存中的数据。CPU通过
地址总线
,将3
这个地址传递给内存,即寻址
到内存的3号单元
; - 需要操作3单元的数据,还需要确定是
读
还是写
。CPU通过控制总线
告诉内存需要进行的操作,例如示例中的是读
; - 内存接收了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以及其它设备进行交互的呢?我们先来看看下面这张内存物理结构分布图
上图可知
- CPU是通过总线和其它硬件设备连接的
- 内存有
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
#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
练习
- 现在有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
对照着常规的十进制,做一个转换,即可得出答案。
- 现在有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点
- 对程序员来说,CPU中最主要部件是寄存器,可以通过改变寄存器的内容来实现对CPU的控制
- 不同的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,并将结果存储到蓝色内存空间
- CPU首先会将红色内存空间的值放到X0寄存器中:
mov X0 红色内存空间
- 然后让X0寄存器与1相加:
add X0,1
- 最后将值赋值给内存空间:
mov 蓝色内存空间,X0
2.5.3pc寄存器(program counter)
- pc寄存器也称作
指令指针
寄存器,它指示了CPU当前要读取指令的地址
- 在内存或者磁盘上,指令和数据
没有
任何区别,都是二进制信息
- CPU在工作的时候把有的信息看做指令,有的信息看做数据,为同样的信息赋予了不同的意义
- 比如 1110 0000 0000 0011 0000 1000 1010 1010
- 可以当做数据 0xE003008AA
- 也可以当做指令 mov x0, x8
- CPU根据什么将内存中的信息看做指令?
- CPU将pc指向的
内存单元的内容
看做指令 - 如果内存中的某段内容曾
被CPU执行
过,那么它所在的内存单元必然被pc指向
过
- CPU将pc指向的
案例演示
下面通过一个例子来演示下pc寄存器的读和写,还是上面的溢出的例子
注意:真机联调!
#import
#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
接着执行 如何让汇编代码跑起来呢?
- 在你需要调用的地方,先声明函数(例如VC中)
cmd+b编译,能成功!
- 在A()执行处加断点,并执行程序,开启汇编调试
上图可见,进入的汇编就是我们之前写的_A函数汇编代码!
调试汇编
接下来开始lldb调试,下面是一步步到0x0
的过程
首先执行下一步,查看寄存器的值
接着往下走
再接下来走
接着进入函数B(按住control键step into到B函数)
接着往下执行指令
继续往下执行,回到了A函数
再接着往下走,会发现在最后2条指令之间死循环了!why?请看下一篇!
总结
本篇文章开始带大家了解了下汇编的发展史和特点
,然后探讨了cpu和内存及其他硬件,是通信总线
进行数据的交换处理,最后重点示例分析了寄存器
,带大家通过lldb指令
对pc寄存器
进行读和写
,以及bl指令的调试
,希望大家能够动手实操一遍,加深印象,感谢阅读!