前言:
逆向开发中,非常重要的一个环节就是静态分析。我们知道,手机上安装App本质是一个二进制文件,而静态分析是建立在分析二进制上面的。所以在学习逆向之前,了解汇编知识非常有必要。
汇编语言的发展
机器语言
由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
高级语言(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
我们的代码在终端设备上是这样的过程:
![](https://img.haomeiwen.com/i1212147/9e122f52058993c1.jpg)
汇编语言与其他语言的对应关系
- 汇编语言与机器语言一一对应,每一条机器指令都有与之对应的汇编指令
- 汇编语言可以通过编译得到机器语言,机器语言可以通过反汇编得到汇编语言
- 高级语言可以通过编译得到汇编语言 \ 机器语言,但汇编语言\机器语言几乎不可能还原成高级语言
汇编语言的特点
- 可以直接访问、控制各种硬件设备,比如存储器、CPU等,能最大限度地发挥硬件的功能
- 能够不受编译器的限制,对生成的二进制代码进行完全的控制
- 目标代码简短,占用内存少,执行速度快
- 汇编指令是机器指令的助记符,同机器指令一一对应。每一种CPU都有自己的机器指令集\汇编指令集,所以汇编语言不具备可移植性
- 知识点过多,开发者需要对CPU等硬件结构有所了解,不易于编写、调试、维护
- 不区分大小写,比如mov和MOV是一样的
汇编语言的用途
任何高级语言最终都会被编译成汇编,了解汇编的相关知识,可以更好的开发、学习探索中帮助我们更好的排查问题、理解底层运行的机制。
- 编写驱动程序、操作系统(比如Linux内核的某些关键部分)
- 对性能要求极高的程序或者代码片段,可与高级语言混合使用(内联汇编)
- 软件安全
1. 病毒分析与防治
2.逆向\加壳\脱壳\破解\外挂\免杀\加密解密\漏洞\黑客 - 理解整个计算机系统的最佳起点和最有效途径
- 为编写高效代码打下基础
- 弄清代码的本质
汇编语言的种类
目前讨论比较多的汇编语言有
- 8086汇编(8086处理器是16bit的CPU)
- Win32汇编
- Win64汇编
- ARM汇编(嵌入式、Mac、iOS)
iPhone里面用到的是ARM汇编,但是不同的设备也有差异.因CPU的架构不同。下面是iPhone中不同架构所对应的设备
![](https://img.haomeiwen.com/i1212147/d9a5c9f54e5f9b1f.png)
汇编常识
- 要想学好汇编,首先需要了解CPU等硬件结构
- APP/程序的执行过程
![](https://img.haomeiwen.com/i1212147/a271af838a8afa57.jpg)
- 可执行文件:程序/App在本地磁盘中即为可执行文件
- image(镜像文件):可执行文件装载到内存即为镜像文件,因为与可执行文件是一模一样,相当于copy过来
- 内存中除了指令,还有数据,但是都是0和1组合,CPU是通过部件PC寄存器来区分
- 硬件相关最为重要是CPU/内存
- 在汇编中,大部分指令都是和CPU与内存相关的
总线
总线是CPU与内存之间的桥梁,下图所示为iPhone X上的A11芯片
![](https://img.haomeiwen.com/i1212147/f4f5092de68a7f2a.jpg)
![](https://img.haomeiwen.com/i1212147/dac5403fc3c16f2f.jpg)
每一个CPU芯片都有许多管脚,这些管脚和总线相连,CPU通过总线跟外部器件进行交互
总线:一根根导线的集合
总线的分类,如下图所示
![](https://img.haomeiwen.com/i1212147/9e5e17e7e7bf9e0c.jpg)
- 地址总线:CPU是通过地址总线来指定存储单元的
- 它的宽度决定了CPU的寻址能力
- 8086的地址总线宽度是20,所以寻址能力是1M( 2^20 )
3.内存地址的单元是 字节byte(简写为B),每个字节里面可以放8位(即bit),内存条的图示
![](https://img.haomeiwen.com/i1212147/eb010f7e2f2189ea.png)
数量单位和容量单位区分
数量单位: 1M=1024k,1k=1024
容量单位: 字节Byte(B) 1024B=1KB,1024KB=1MB,IBM银行的独立系统是以2字节为一个单位,常用的电脑是以1字节为一个单位
网络带宽100M=100Mbpt,传输速率单位(比特位,每秒传输12.5MB/s)
- 数据总线:CPU与内存/其他部件之间的数据传送通道
- 它的宽度决定了CPU的单次数据传送量,也就是数据传送速度
- 8086的数据总线宽度是16,所以单次最大传递2个字节的数据
我们常说的32位(4字节)、64位(8字节)CPU,这里的32、64指的就是数据吞吐量
- 控制总线:CPU通过控制总线对外部器件进行控制
- 它的宽度决定了CPU对其他器件的控制能力、能有多少种控制
- 控制总线是控制线数量之和
![](https://img.haomeiwen.com/i1212147/359d61d54140be7d.png)
CPU从内存的3号单元读取数据
- CPU想操作内存中的数据,首先需要找到内存地址:CPU通过地址总线,将3这个地址传递给内存,即寻址到内存的3号单元
- 需要操作3单元的数据,还需要确定是读还是写:CPU通过控制总线告诉内存需要进行的操作,假设是读
- 内存知道了CPU想要进行的操作:内存将3号单元的数据通过数据线传递给CPU
内存
CPU是通过总线和硬件设备连接的
内存有RAM主存储器、RAM(主存储器)内存条
![](https://img.haomeiwen.com/i1212147/c3422b9033c3f7fc.png)
内存按照物理地址划分为主存储器、显存地址、显卡地址、网卡地址、系统地址
![](https://img.haomeiwen.com/i1212147/abe58a2446b599fd.png)
内存中的低地址是给用户用的,高地址是给系统用的,如下所示
![](https://img.haomeiwen.com/i1212147/9373895f682974b4.png)
- 内存地址空间的大小受CPU地址总线宽度的限制。8086的地址总线宽度为20,可以定位220个不同的内存单元(内存地址范围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 在____情况下等于 3 ?
十进制由10个符号组成: 0 1 3 2 8 A B E S 7 逢十进一
如果这样定义十进制: 1 + 1 = 3
这样的目的何在?
传统我们定义的十进制和自定义的十进制不一样.那么这10个符号如果我们不告诉别人这个符号表,别人是没办法拿到我们的具体数据的!用于加密!
进制的运算
八进制加法表
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
数据的宽度
数学上的数字,是没有大小限制的,可以无限的大。
但在计算机中,由于受硬件的制约,数据都是有长度限制的(我们称为数据宽度),超过最大宽度的数据会被丢弃
以下例子都是真机调试
// 创建demo空工程
#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]));
}
}
打断点调试,可以发现1已经溢出
![](https://img.haomeiwen.com/i1212147/3fb4dbd5dc5de99b.png)
也可以通过获取地址,在Debug-Debug Workflow-ViewMemory中输入地址查看,下图Address 输入地址,Page 也可以翻页
![](https://img.haomeiwen.com/i1212147/950af06e88f608a1.png)
计算机中常见的数据宽度
- 位(Bit):1个位就是1个二进制位,即0或1
- 字节(Byte):1个字节由8个Bit组成,内存中的最小单元Byte
- 字(Word):1个字由两个字节组成(16位),第2个字节分别称为高字节和低字节
- 双字(DoubleWord):1个双字由两个字组成(32位)
计算机存储数据它会分为有符号数和无符号数,看下图
![](https://img.haomeiwen.com/i1212147/fe3d59c7616ddc3b.jpg)
无符号数: 直接换算!
有符号数: 符号放在第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
CPU&寄存器
内部部件之间由总线连接
![](https://img.haomeiwen.com/i1212147/373ec72b6b1254df.jpg)
- CPU除了有控制器、运算器还有寄存器。其中寄存器的作用就是进行数据的临时存储。
- CPU的运算速度是非常快的,为了性能CPU在内部开辟一小块临时存储区域,并在进行运算时先将数据从内存复制到这一小块临时存储区域中,运算时就在这一小快临时存储区域内进行。我们称这一小块临时存储区域为寄存器。
- 对于arm64系的CPU来说, 64位寄存器以x开头,32位寄存器以w开头
- 在系统中没有提供16位和8位的寄存器供访问和使用,其中32位的寄存器是64位寄存器的低32位部分并不是独立存在的。
- 对程序员来说,CPU中最主要部件是寄存器,可以通过改变寄存器的内容来实现对CPU的控制
- 不同的CPU,寄存器的个数、结构是不相同的
浮点和向量寄存器
因为浮点数的存储以及其运算的特殊性,CPU中专门提供浮点数寄存器来处理浮点数
- 浮点寄存器 64位: D0 - D31 32位: S0 - S31
现在的CPU支持向量运算.(向量运算在图形处理相关的领域用得非常的多)为了支持向量计算系统了也提供了众多的向量寄存器. - 向量寄存器 128位:V0-V31
通用寄存器
- 通用寄存器也称数据地址寄存器通常用来做数据计算的临时存储、做累加、计数、地址保存等功能。定义这些寄存器的作用主要是用于在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,并将结果存储到蓝色内存空间
![](https://img.haomeiwen.com/i1212147/49e25a82566f191e.jpg)
- CPU首先会将红色内存空间的值放到X0寄存器中:mov X0,红色内存空间
- 然后让X0寄存器与1相加:add X0,1
- 最后将值赋值给内存空间:mov 蓝色内存空间,X0
pc寄存器(program counter)
- 为指令指针寄存器,它指示了CPU当前要读取指令的地址
- 在内存或者磁盘上,指令和数据没有任何区别,都是二进制信息
- CPU在工作的时候把有的信息看做指令,有的信息看做数据,为同样的信息赋予了不同的意义
比如 1110 0000 0000 0011 0000 1000 1010 1010
可以当做数据 0xE003008AA
也可以当做指令 mov x0, x8 - CPU根据什么将内存中的信息看做指令?
CPU将pc指向的内存单元的内容看做指令
如果内存中的某段内容曾被CPU执行过,那么它所在的内存单元必然被pc指向过
寄存器查看
![](https://img.haomeiwen.com/i1212147/990998af2915d9b8.png)
通用寄存器中fp对应x29,lr对应x30,fp与lr是对寄存器起了别名,如下图
![](https://img.haomeiwen.com/i1212147/f92dd86d79921c85.png)
Xcode查看汇编 Debug-Debug Workflow-Always Show Disassembly
![](https://img.haomeiwen.com/i1212147/bea71e5a0f180c66.png)
![](https://img.haomeiwen.com/i1212147/b1396092294a931b.png)
pc寄存器调试
- 打印pc寄存器现在是c2138,可以看出来内存地址都相同
![](https://img.haomeiwen.com/i1212147/7fcd0b1bde9e7814.png)
- 按住control+Step into,继续打印
![](https://img.haomeiwen.com/i1212147/c71d2464fe6f61ae.png)
- 除了读还可以写
register write pc 0x104c31ecc
register read pc 此时是读不出来的,因为断点断住了,如果step into,此时断点断在哪里?最终通过验证发现,会断在1ecc的下一行
![](https://img.haomeiwen.com/i1212147/10c5ac3bcd80b8bc.png)
![](https://img.haomeiwen.com/i1212147/59534ba4d5c14874.png)
此时pc指向1ecc,会将1ecc中的指令拿出来执行,执行完毕后走到1ecc的下一条指令,而此时d0的指令是还没有执行的
高速缓存
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是两个指令,b就是跳转指令
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
<!--结果-->
流程:
mov x0,#0xa0 -- x0:0xa0
mov x1,#0x00 -- x1:0x00
add x1, x0, #0x14 -- x1:0xa0+0x14=0xb4
mov x0,x1 -- x0:0xb4
add x0, x0, #0x10 -- x0:0xb4+0x10=0xc4
ret -- 回到bl跳转的下一行
mov x0,#0x0 -- x0:0x00
x0的值:0x00
下面编写汇编代码验证 Cmd+n -- empty (other下)-- asm.s(汇编代码文件,会编译成源码)
// asm.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
// ViewController.m 中调用
@implementation ViewController
// 汇编A函数声明
int A();
- (void)viewDidLoad {
[super viewDidLoad];
A();
// Do any additional setup after loading the view.
}
@end
在A()执行处加断点,并执行程序,开启汇编调试
开始调用A函数
![](https://img.haomeiwen.com/i1212147/4a72ae65f60e720b.png)
按住control+Step into,继续执行,跳入汇编A函数内
![](https://img.haomeiwen.com/i1212147/5bb92ff416cbeda1.png)
一步步执行,当执行到0x102925efc <+16>: bl 0x102925f08 ; B,按住control+Step into进入汇编代码B
![](https://img.haomeiwen.com/i1212147/c3d194569c8b60d1.png)
继续一步步执行回到汇编代码A
![](https://img.haomeiwen.com/i1212147/6a2445cfda0abce6.png)
此时我们发现继续执行的时候,一直在汇编代码A的最后两行执行,发生了死循环,退不回ViewController中
0x102925f00 <+20>: mov x0, #0x0
-> 0x102925f04 <+24>: ret
分析原因: 汇编代码A中没有保存回去的路,原因我们下一篇讨论
网友评论