OC逆向01:初始汇编
前言
在逆向开发中,非常重要的一个环节就是 静态分析
,众所周知,手机上安装 APP
其本质是一个 二进制文件
,因为iPhone手机本质上执行的指令是二进制,是由手机上的 CPU
执行的;而静态分析是建立在分析二进制上面的。所以在学习逆向之前,需要提前了解相关的汇编知识
汇编语言的发展
机器语言
是由 0和1
组成的机器指令,表示特定的功能,如下所示:
1 | 加:0100 0000 |
汇编语言(assembly language)
由于使用机器语言表示时不方便记忆,于是开始使用 助记符
来代替机器语言,例如下面使用助记符表示的 加减乘除
1 | 加:INC EAX 通过编译器 0100 0000 |
高级语言(High-level programming language)
在后期,为了更加高效的编程,在汇编语言的基础上有了更高级的语言,例如:C/C++/Java/OC/Swift
等,这些语言更加接近人类的自然语言,例如C语言表示加减乘除
1 | 加:A+B 通过编译器 0100 0000 |
最终的代码在终端设备上显示的过程如下:
【说明】:
汇编语言
与机器语言
是--对应
的关系,每一条机器指令都有与之对应的汇编指令汇编语言
可以通过编译
得到机器语言
,机器语言
可以通过反编译
得到汇编语言
高级语言
可以通过编译
得到汇编语言/机器语言
,但汇编汇演/机器语言
几乎不可能还原成高级语言- 不同的
高级语言
可以得到同样的
汇编语言,但是同一个汇编语言
无法推断出用什么高级语言 - 不同的
CPU架构
,而CPU架构对应不同的指令集
- 不同的
汇编语言的特点
汇编语言主要有以下几个特点:
- 可以
直接访问、控制
各种硬件设备,比如存储器、CPU等
,能最大限度地发挥硬件的功能 - 能够不受编译器的限制,对生成的二进制代码进行完全的控制
- 目标代码简短,占用内存少,执行速度快
- 汇编指令是机器指令的
助记符
,同机器指令一一对应,每一种CPU都有自己的机器指令/汇编指令集 - 知识点过多,要求过高,需要开发者对CPU等硬件结构有所了解,不易于编写、调式,以及维护
- 不区分大小写,例如mov和MOV是一样的
汇编语言的用途(哥 我学了能干啥?)
按理说,汇编这么难,为什么还要学呢?以及学了能干什么?
汇编学习的目的
任何高级语言最终都会被编译汇编,学习了汇编的相关知识,可以更好的日常开发,学习探索中帮助我们更好的排查问,理解底层运行的机制,大致分为以下几点:
是理解整个计算机系统的最佳起点和最有效途径
为编写高效代码打下基础
理解代码的本质,例如:
- 函数的本质是什么?
- ++a底层是如何执行的?
- 编译器在底层到底帮我们做了哪些工作?
- DEBUG模式和RELEASE模式到底有哪些地方是不同的?以及被我们忽略的?
汇编使用的场景
可以
编写驱动程序、操作系统
,例如Linux内核的某些关键字对性能要求极高的程序或代码片段,可
与高级语言混合使用(称为内联汇编)
用于
软件安全方面
- 病毒的分析与防治
- 逆向、加壳、脱壳、破解、外挂、免杀、加解密、黑客等
所以综上所述,汇编时所有👨🏻💻都需要了解的一门非常重要的语言,这也是为什么大学生中计算机相关专业学生的必修课,就好比修房子,地基稳了,高楼才能平地起
汇编语言的种类
目前讨论比较多的汇编语言有:
- 8086汇编(8086处理器是16bit的CPU)
- Win32汇编
- Win64汇编
- ARM汇编(嵌入式、Mac、iOS)
- …
我们iPhone里面用到的是 ARM汇编
,但是不同的设备也有差异,因CPU的架构不同
必备常识
想要学好汇编,需要有以下几个常识:
1、首先需要了解CPU等硬件的架构
2、APP/程序的执行过程
可执行文件:程序/APP在本地磁盘的文件
image(镜像文件):可执行文件被装载到内存的文件(在早起,其本质与可执行文件是一模一样的)
内存中除了
指令
,还有数据
,但都是0和1
组合,CPU是如何区分的?是通过CPU上的部件CPU寄存器来区分3、硬件相关最为重要的是CPU/内存
4、在汇编中,大部分指令都是和CPU和内存相关的
总线
总线
是CPU和内存之间的桥梁,如下图所示,是iPhoneX上的A11(CPU芯片)
从图中可以看出:每一个CPU芯片都有很多管脚,这些管脚和总线相连,CPU通过总线跟外部件进行交互
总线:是一根根导线的集合
总线的分类
总线主要分为三类,如下图所示:
- 1、地址总线:CPU是通过地址总线来指定存储单元的
- 2、数据总线:CPU与内存/其他部件之间的数据传送通道
- 3、控制总线:CPU通过控制总线对外部器件进行控制
举例:CPU从内存的3号单元读取数据
1、CPU想操作内存中的数据,首先需要找到内存地址:CPU通过地址总线,将3这个地址传递给内存你,即寻址到内存的3号单元
2、需要操作3号单元的数据,还需要确定是读还是写:CPU通过控制总线告诉内存需要进行的操作,假设是读
3、内存知道了CPU想要进行的操作:内存将3号单元的数据通过数据线传递给CPU
地址总线
它的宽度决定了
CPU的寻址能力
,即地址总线决定了CPU所能访问的最大内存空间的大小,例如:10根地址线
能访问的最大内存是2^10 = 1024
位二进制数据(即1KB
)地址总线是地址线数量之和
8086
的地址总线
宽度是20
,所以寻址能力是1M(即2^20)
内存地址的单元是
字节byte(简写B)
,每个字节里面可以放8位(即bit)
,以下是内存条的图示:
注意区分 数量单位
和 容量单位
- 数量单位:
1M = 1024K,1K = 1024
- 容量单位:字节byte(B)
1024B = 1KB,1024KB = 1MB
,IBM银行的独立系统是以2字节为一个单位,常用的电脑是以1字节为一个单位 - 网络宽带
100M=100Mbps
(比特位,每秒传输100/8 = 12.5MB/s
)
数据总线
它的宽度决定了
CPU的单次数据传送量(即吞吐量)
,也就是数据传送速度
即CPU和外界数据的传送速度每条数据线一次只能传输一位二进制数据,例如:
8根
数据线一次
可传送一个8位
二进制数据(即一个字节的数据)数据总线是数据线数量之和
8086
的数据总线
总宽度是16
,所以单次最大传送2个字节
的数据
我们常说的32位(4字节)、64位(8字节)CPU,这里的32、64指的就是数据吞吐量
控制总线
它的宽度决定了
CPU对其他器件的控制能力
,能有多少中控制,即CPU对外部器件的控制能力控制总线是控制线数量之和
小练习
- 一个CPU的寻址能力为8KB,那么它的地址总线的宽度为 2^13
答:我们知道 10根 -> 2^0 -> 1KB,所以,8KB -> 2^3 -> 13根
内存
CPU是通过
总线
和硬件设备连接的内存有RAM主存储器、RAM主存储器(内存条)
- 下图是按照物理地址划分的内存,有
主存储器、显存地址、显卡地址、网卡地址、系统地址
其中内存中的 低地址
是给 用户用的
,高地址
是给 系统用的
,如下所示:
内存地址空间的大小受CPU地址总线宽度的限制,8086的地址总线宽度为20,可以定位2^20个不同的内存单元(内存地址范围0x00000~0xFFFFF),所以8086的内存空间大小为1MB
0x00000~0x9FFFF
:主存储器,可读可写
0xA0000~0xBFFFF
:向显存中写入数据,这些数据会被显卡输出到显示器,可读可写
0xC0000~0xFFFFF
:存储各种硬件/系统信息,只读
进制
很多人学不好进制,原因是总以十进制为依托去考虑其他进制,需要运算的时候也总是先转换为十进制,在转换为其他进制,其实这种方式是错的
我们为什么一定要转换成十进制呢?仅仅是因为我们对十进制最熟悉,所以才转换
每一种进制都是完美的,想学号进制首先要忘掉十进制,也要忘掉进制间的转换
进制的定义
- 八进制由8个符号组成:0 1 2 3 4 5 6 7
逢八进一
- 十进制由10个符号组成:0 1 2 3 4 5 6 7 8 9
逢十进一
- N进制就是由N个符号组成:
逢N进一
1 | <!--练习--> |
进制的运算
八进制
八进制加法表
1 | 0 1 2 3 4 5 6 7 |
八进制乘法表
1 | 0 1 2 3 4 5 6 7 10 11 12 13 14 15 16 17 20 21 22 23 24 25 26 27... |
做个练习
- 2 + 3 = ___,2 * 3 = ___,4 + 5 = ___,4 * 5 = ___
- 277 + 333 = ___,276 * 54 = ___, 237 - 54 = ___ ,234/4 = ___
第一排的计算,我们可以很好的得出结论
但是第二排涉及数据较大,如何计算呢?下面以 277 + 333
为例,计算过程如下:
1 | 实际上是以加法表为基本的每个位置相加 |
二进制
二进制的简写形式
1 | 二进制: 1 0 1 1 1 0 1 1 1 1 0 0 |
使用二进制从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 这就是十六进制了
数据的宽度
数学上的数字是没有大小限制的,可以无限大
但是在计算机中,由于硬件的制约,数据都是有长度限制的(称为 数据宽度
),超过最多宽度的数据会被丢弃
1 | #import <UIKit/UIKit.h> |
打断点在 int cTemp
处和 return cTemp
处,调试结果发现 0
溢出了:
也可以此处通过获取的地址,然后在 Debug-Debug Workflow-ViewMemory
中输入地址(0x000000016d82f70c) 查看
计算机中常见的数据宽度
位(bit)
:1个位
就是1个二进制
,即0或1
字节(byte)
:1个字节
由8个bit
组成,内存中的最小单元byte字(Word)
:1个字
由两个字节
组成(16位),第2个字节分别称为高字节和低字节
双字(DoubleWork)
:1个双字由两个字组成(32位)
那么计算机存储数据它分为 有符号数和无符号数
,如下图所示:
无符号数,直接换算
有符号数,符号放在第1位,第1位是0即
正数
,为1即负数
:- 正数:0 1 2 3 4 5 6 7
- 负数:F E D C B 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)
1 | 十进制: 0 1 2 3 4 5 6 7 8 9 |
刚才通过10进制运算可以转换,然后查表,但是如果是其他进制,就不能转换,要学会直接查表。
【练习2】
现在有9进制数9个符号,分别是:2,9,1,7,6,5,4,8,3 逢9进1
,那么:123 + 234 = (9926)
1 | 十进制: 0 1 2 3 4 5 6 7 8 |
CPU和寄存器
内部部件之间是由 总线
连接,如下图所示:
CPU除了有
控制器、运算器
,还有寄存器
,其中寄存器的作用就是运行数据的临时存储
CPU的运算速度是非常快的,为了性能
CPU
在内部开辟了一小块临时存储区域
,并在进行运算时先将数据从内存中复制
到这一小块临时区域中,运算就在这一小块临时存储区进行,称这一小块临时存储区域为寄存器
【针对arm64的CPU来说】
如果寄存器以
x
开头,则表明是一个64位(84/8=8字节)的寄存器z
。如果寄存器以
w
开头,则表明是一个32位(32/8=4字节) 的寄存器
在系统中没有提供16位和32位的寄存器供访问和使用,其中 32位的寄存器是64位寄存器的低32位的部分,并不是独立存在的
对于程序来说,CPU中最主要的部件是
寄存器
,可以通过改变寄存器的内容来实现对CPU的控制不同CPU,寄存器的个数和结构是不相同的
查看寄存器
默认情况下是不显示的,如下图:
选中后,会出现三种寄存器,如下图:
浮点和向量寄存器
因为浮点数的存储以及其运算的特殊性,CPU中专门提供 浮点寄存器
来处理浮点数
浮点寄存器
64位:D0-D31
32位:S0-S31
现在 CPU支持向量运算
(向量运算在图形处理相关的领域用的非常多),为了支持向量计算,系统也提供了众多的向量寄存器
- 向量寄存器
128
位:V0 - V31
通用寄存器
- 通用寄存器也称为
数据地址寄存器
。通常用来做数据计算的临时存储、累加、计数、地址保存等功能
。定义这些寄存器的作用主要是用于在CPU指令中保存操作数,在CPU中当做一些常规变量来使用
ARM64中
64位:x0-x30,XZR(寄存器)
32位:W0-W30,WZR(零寄存器)
arm64拥有
32个64位的通用寄存器X0-X30
,以及XZR(零寄存器)。这些通用寄存器有时也有特定用途- 那么
w0-w28
这些32位的,因为64位CPU可以兼容32位,所以可以只使用64位寄存器的低32位 - 例如 w0就是x0的低32位
- 那么
1 | 注意:了解过8086汇编的一定知道,还有一种特殊的寄存器段寄存器:CS、DS、SS 、ES。这四个寄存器主要用来保存这些段的基地址,是属于Intel架构的CPU的,在arm中并没有 |
- 通常,
CPU会先将内存中的数据存储到通用寄存器中,然后再对寄存器中的数据进行运算
例子:假设内存中有块红色内存空间的值是3,现在想把它的值加1,并将结果存储到蓝色内存空间,如何做呢?
CPU首先会将红色内存空间的值放到X0寄存器中:
mov X0,红色内存空间
然后让X0寄存器与1相加:
add X0,1
最后将值赋值给内存空间:
mov 蓝色内存空间,X0
PC寄存器
为
指令指针寄存器
,它指示了CPU当前要读取指令的地址
在内存/磁盘中,指令和数据没有任何区别,都是二进制信息
CPU在工作时,将有的信息看做指令,有的信息看做数据,为同样的信息赋予了不同的意义
- 例如 1110 0000 0000 0011 0000 1000 1010 1010
- 可以当做数据 0xE003008AA
- 也可以当做指令 mov x0,x8
CPU根据什么将内存中的信息看作指令呢?
- CPU将pc指向的内存单元的内容看作指令
- 如果内存中的某段内容曾经被CPU执行过,那么它所在的内存单元必然被pc指向过
寄存器的案例分析
下面通过以下代码的例子来分析
1 | #import <UIKit/UIKit.h> |
- 以下是demo的汇编图示
pc寄存器调式
- 打印
pc
寄存器,现在是d613c
- 按住
control+Setp into
,继续打印
除了读还可以写:
register write pc
0x1025d6134register read pc
此时是读不出来的,因为断点断住了,如果step into ,此时断点断在哪里?
高速缓存
iPhoneX上搭载的arm处理器 A11
,它的1级缓存的容量是64Kb,2级缓存的容量是8M
CPU每执行一条指令前都需要从内存中将指令读取到CPU内存并执行,而寄存器的运行速度相比内存读写要快很多,为了性能,CPU还集成了一个 高速缓存区域
。当程序运行时,先将要执行的指令代码以及数据复制到高速缓存中(由操作系统完成),然后CPU直接从高速缓存依次读取指令来执行
bl指令
- CPU从何处执行指令,是由pc中的内容决定的,可以
通过改变pc的内容来控制CPU执行目标指令
- arm64提供了一个
mov指令(传送指令)
,可以用来改变大部分寄存器的值,例如 mov x0,#10、mov x1,#20 - 但是,
mov指令并不能用于设置pc的值
,arm64没有提供这样的功能 - arm64提供了另外的指令来
修改pc的值
,这些指令统一称为转移指令
,其中最简单的是bl指令
bl指令练习
现在有两段代码,假设程序先执行A,请写出指令执行顺序,最终寄存器x0的值是多少?
1 | _A: |
汇编代码验证
下面来写一段汇编代码验证(command + n -> empty -> asm.s
)(汇编代码文件,会编译成源码)
如何执行呢?
- 在VC中定义函数的声明
- 在
mov x0,#0xa0
执行处加断点,并执行程序,开启汇编调试
- 按住
control+step into
,进入A的具体汇编
- 继续
control+step into
,到bl _B
处
- 按住
control+step into
,进入汇编代码B
- 执行到A的ret
疑问: 发现走到这里死循环了,这是为什么呢?预知后事如何,请看下一篇
总结
1、汇编概述
使用
助记符代替机器指令
的一种编程语言汇编和机器指令是
一一对应
的关系,拿到二进制就可以反汇编由于汇编和CPU的指令集是对应的,所以汇编不具备移植性
2、总线:是由一堆导线的集合
地址总线
:其宽度决定了寻址能力数据总线
:其宽度决定了CPU数据的吞吐量控制总线
:其宽度决定了CPU对其他器件的控制能力
3、进制
任意进制都是由对应个数的符号组成的,符号可以自定义
2/8/16是相对完美的进制,他们之间的关系
3
个二进制使用一个8进制
标识4
个二进制使用一个16进制
标识两个16进制可以标识一个字节,即8位
数量单位
1024 = 1k,1024k = 1M,1024M = 1G
容量单位
1024 = 1KB,1024KB = 1MB,1024MB = 1GB
数据的宽度
计算机中的数据是有宽度的,超过了就会溢出
4、
寄存器
:CPU为了性能,自内部开辟了一小块临时存储区域浮点向量寄存器
:用于浮点数/向量的存储及运算异常状态寄存器
通用寄存器
:除了存放数据有时也有特殊的用途ARM64拥有32个64位的通用寄存器
X0-X30
以及XZR(零寄存器)为了兼容32位,所以arm64位拥有
W0-W28
以及WZR 30个32位寄存器32位寄存器并不是独立存在的,例如 W0是X0的低32位
PC寄存器
:指令指针寄存器PC寄存器里面的值保存的就是CPU接下来需要执行的指令地址
改变PC的值可以改变程序的执行流程
mov指令不能更改PC寄存器的值,需要通过bl跳转指令来改变PC寄存器的值
- Post title:OC逆向01:初始汇编
- Post author:张建
- Create time:2022-04-20 15:50:53
- Post link:https://redefine.ohevan.com/2022/04/20/OC逆向/OC逆向01:初始汇编/
- Copyright Notice:All articles in this blog are licensed under BY-NC-SA unless stating additionally.