注册

汇编-基本概念



在逆向开发中,非常重要的一个环节就是静态分析。对于逆向iOS app来说,一个APP安装在手机上面的可执行文件本质上是二进制文件。因为iPhone手机本质上执行的指令是二进制。是由手机上的CPU执行的,静态分析是建立在分析二进制上面。


汇编语言的发展

机器语言

01组成的机器指令。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,更加接近人类的自然语言。
比如C语言:

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

代码在终端设备上的过程:


61dccd612839118c44921933b97b0f6c.png


  • 汇编语言机器语言一一对应,每一条机器指令都有与之对应的汇编指令
  • 汇编语言可以通过编译得到机器语言机器语言可以通过反汇编得到汇编语言
  • 高级语言可以通过编译得到汇编语言 \ 机器语言,但汇编语言\机器语言几乎不可能还原成高级语言(不是一一对应关系,反推出是不准确的,只能大致。)

汇编语言的特点


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

汇编的用途

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

汇编语言的种类

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

  • 8086汇编(8086处理器是16bitCPU
  • Win32汇编
  • Win64汇编
  • ARM汇编(嵌入式、MaciOS
  • ......

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

位数架构设备
32armv6iPhone, iPhone2, iPhone3G, 第一代、第二代 iPod Touch
32armv7iPhone3GS, iPhone4, iPhone4S,iPad, iPad2, iPad3(The New iPad), iPad mini, iPod Touch 3G, iPod Touch4
32armv7siPhone5, iPhone5C, iPad4(iPad with Retina Display)
64arm64iPhone5s,iPhone6、7、8,iPhone6、7、8 Plus,iPhone X,iPad Air,iPad mini2(iPad mini with Retina Display)
64arm64eXS/XS Max/XR/ iPhone 11, iPhone 11 pro 及以后
64x86_64模拟器64位处理器 (intel)
32i386模拟器32位处理器(intel)

⚠️:苹果A7处理器支持两个不同的指令集:32ARM指令集(armv6|armv7|armv7s)和64ARM指令集(arm64

汇编相关的学习需要了解CPU等硬件结构,最为重要的是CPU/内存。在汇编中,大部分指令都是和CPU与内存相关的。
APP/程序的执行过程:


11e6c308df9089545461747bc2e4a3e4.png


执行过程:
1.地址总线先去内存地址。
2.控制读取发送读/写命令。
3.数据总线写数据->内存/ 内存发送数据->数据总线

地址总线

  • 它的宽度决定了CPU的寻址能力(也就是寻址范围)
  • 8086的地址总线宽度是20,所以寻址能力是1M( 220)(这里的M是大小,数量单位)

10f90c9ede56c1c2bb46279b11c53fea.png


内存中的MB是容量单位。如果内存很大, 地址总线宽度不够怎么处理?以前的cpu是通过2次寻址相加得到一个最终的值来访问内存,现在的cpu没有寻址能力的问题。
数量单位:M,K。1M = 1024K,1K= 1024。比如:10,100
容量单位:字节Byte。 1024B = 1KB,1024KB = 1MB。比如:10个,100只。(大部分计算机都是以1个字节为单位。银行系统的IBM电脑例外是2个字节为单位。)
对于100M 宽带,这里的100M是100Mbps(每秒钟传递多少二进制位,bit位。所以100M带宽理论下载速度12.5MB/s)。

数据总线

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

我们现在常说的32位,64位cpu说的就是它的数据吞吐量。1次放电分别4字节,8字节数据。

控制总线

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

案例:
1.一个CPU 的寻址能力为8KB,那么它的地址总线的宽度为____
答案:8KB对应 8192, 213 = 8192 所以为13。

  1. 8080,8088,80286,80386 的地址总线宽度分别为16根,20根,24根,32根。那么他们的寻址能力分别为多少____KB, ____MB,____MB,____GB?
    答案:1kb = 210 = 1024
    1kb * 26 = 64kb
    1kb * 1kb = 1mb
    1mb * 24 = 16mb
    1kb * 1kb * 1kb * 22 = 4gb

  2. 8080,8088,8086,80286,80386 的数据总线宽度分别为8根,8根,16根,16根,32根.那么它们一次可以传输的数据为:____B,____B,____B,____B,____B
    答案:1 、1、2、2、4

4.从内存中读取1024字节的数据,8086至少要读____次,80386至少要读取____次.
答案:8086 数据总线宽度为16。8086一次读2个字节,那么需要512次,80286数据总线宽度为32,一次4个字节,需要256次。

内存

17794b9311eb57d3304e77e0a5a98a96.pngee22f556c4f679ea021801f494591fd6.png4f638b1dd887fe97d5d56f849d064de5.png


内存地址空间的大小受CPU地址总线宽度的限制。8086的地址总线宽度为20,可以定位220个不同的内存单元(内存地址范围0x00000~0xFFFFF),所以8086的内存空间大小为1MB0x00000~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 + 1 在____情况下等于 3 ?
    除了算错的情况下。在十进制由10个符号组成,假如由: 0 1 3 2 8 A B E S 7组成逢十进一,那么在这种情况下1+1=3

传统定义的十进制和自定义的十进制不一样。那么这10个符号如果我们不告诉别人这个符号表,别人是没办法拿到我们的具体数据的,可以用于加密!
⚠️:十进制由十个符号组成,逢十进一,符号是可以自定义的!!!

  1. 八进制运算:
  • 2 + 3 = __ , 2 * 3 = __ ,4 + 5 = __ ,4 * 5 = __.
    答案:5,6,11,24
  • 277 + 333 = __ , 276 * 54 = __ , 237 - 54 = __ , 234 / 4 = __ .
    答案:632, 20250, 163,47


八进制加法表
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

二进制的简写形式

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

数据的宽度

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


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

- (void)viewDidLoad {
[super viewDidLoad];
NSLog(@"%x",test());
}
输出:

ffffffff
数据溢出了。刚开始cTemp默认值1,溢出后变为-1第一位符号位,1代表负数,0代表正数。往后逐位取反,末尾加1。)。

(lldb) p cTemp
(int) $0 = 1
(lldb) p cTemp
(int) $1 = -1
(lldb) p &cTemp
(int *) $2 = 0x000000016b3a9b1c
(lldb) x 0x000000016b3a9b1c
0x16b3a9b1c: ff ff ff ff 10 00 00 00 00 00 00 00 ef 98 3a 6b ..............:k
0x16b3a9b2c: 01 00 00 00 70 a9 f0 59 01 00 00 00 50 d4 a5 04 ....p..Y....P...
(lldb) p (uint)cTemp
(uint) $3 = 4294967295

Debug -> Debug Workflow -> View Memory中也可以查看(这里查看内容更新后需要翻页刷新然后切换回来才能显示新值):

346c8108f131aa23dc95badd256ece77.png


再看下汇编代码(Debug -> Debug Workflow -> Always Show Disassembly):


TestDemo`test:
0x104a59ec8 <+0>: sub sp, sp, #0x10 ; =0x10
0x104a59ecc <+4>: mov w8, #-0x1
0x104a59ed0 <+8>: str w8, [sp, #0xc]
-> 0x104a59ed4 <+12>: ldr w0, [sp, #0xc]
0x104a59ed8 <+16>: add sp, sp, #0x10 ; =0x10
0x104a59edc <+20>: ret

可以看到直接将-1给力w8。指令在内存中占用4字节。

计算机中常见的数据宽度

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

计算机存储数据会分为有符号数和无符号数(对于数据本身内容没有变化,取决于你怎么看):


无符号数,直接换算!
有符号数:
正数: 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 = ____

十进制:
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 = 1                 
9+1 = 7 1+1 = 6
9+7 = 6 1+7 = 5 7+7 = 4
9+6 = 5 1+6 = 4 7+6 = 8 6+6 = 3
9+5 = 4 1+5 = 8 7+5 = 3 6+5 = A 5+5 = 92
9+4 = 8 1+4 = 3 7+4 = a 6+4 = 92 5+4 = 99 4+4 = 91
9+8 = 3 1+8 = A 7+8 = 92 6+8 = 99 5+8 = 91 4+8 = 97 8+8 = 96
9+3 = A 1+3 = 92 7+3 = 99 6+3 = 91 5+3 = 97 4+3 = 96 8+3 = 95 3+3 = 94
9+A = 92 1+A = 99 7+A = 91 6+A = 97 5+A = 96 4+A = 95 8+A = 94 3+A = 98 A+A = 93

123 + 234 = 1A6

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

十进制:
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

转换后加法表:

9+9 = 1                 
9+1 = 7 1+1 = 6
9+7 = 6 1+7 = 5 7+7 = 4
9+6 = 5 1+6 = 4 7+6 = 8 6+6 = 3
9+5 = 4 1+5 = 8 7+5 = 3 6+5 = 92 5+5 = 99
9+4 = 8 1+4 = 3 7+4 = 92 6+4 = 99 5+4 = 91 4+4 = 97
9+8 = 3 1+8 = 92 7+8 = 99 6+8 = 91 5+8 = 97 4+8 = 96 8+8 = 95
9+3 = 92 1+3 = 99 7+3 = 91 6+3 = 97 5+3 = 96 4+3 = 95 8+3 = 94 3+3 = 98

123 + 234 = 725

CPU&寄存器

内部部件之间由总线连接


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

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

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

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

浮点寄存器

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

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

向量寄存器

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

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

通用寄存器

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

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

在"Xcode"中我们可以查看具体寄存器的内容:

02f62eb1338015149316aa42cbd2d4e8.png034b8df13ab1776a7d9dd2633ceb2878.png


分别看一下x0w0的值:
x0  unsigned long   0x0000000159f0a970
w0 unsigned int 0x59f0a970

验证了w0x0的低32位。

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


91818fd332f2c4765ba4e3c3108f807c.png


pc寄存器

单步执行汇编代码(pc始终指向下一条指令):

8518efaafc548ffef6ba11ca37bd5498.png


为指令指针寄存器,它指示了CPU当前要读取指令的地址(指向下一条即将执行的指令)在内存或者磁盘上,指令和数据没有任何区别,都是二进制信息CPU在工作的时候把有的信息看做指令,有的信息看做数据,为同样的信息赋予了不同的意义

比如 1110 0000 0000 0011 0000 1000 1010 1010,
可以当做数据 0xE003008AA。
也可以当做指令 mov x0, x8

  • CPU根据什么将内存中的信息看做指令?

CPU将pc指向的内存单元的内容看做指令
如果内存中的某段内容曾被CPU执行过,那么它所在的内存单元必然被pc指向过。

高速缓存

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

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

bl指令

bl分位bl:
b:跳转。
l:lr寄存器。

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

分析:


Xcode中创建Empty文件命名为asm.s.s汇编代码会被Xcode自动识别编译)。


//asm.s
.text // 告诉是代码
.global _A, _B //.global 是标号

_A:
mov x0,#0xa0 //a0 给 x0 x0 = 0xa0
mov x1,#0x00 //00 给x1 x1 = 0x00
add x1, x0, #0x14 //x0 + 0x14 给 x1 x1 = 0xb4
mov x0,x1 //x1 的值给 x0 x0 = 0xb4
bl _B //跳转B
mov x0,#0x0 //0x0 给 x0 x0 = 0x0
ret //return 上层调用的地方

_B:
add x0, x0, #0x10 //x0 + 0x10 给 x0 x0 = 0xc4
ret //return A

oc调用汇编:

//ViewController.m
int A();

- (void)viewDidLoad {
[super viewDidLoad];
A();
}
swift调用汇编:

//声明方法A。Swift中C和汇编都可以这么暴露。
@_silgen_name("A")
func A()

class ViewController: UIViewController {

override func viewDidLoad() {
super.viewDidLoad()
A();
}

}
答案:0x00

536b623047c3cb249ca3e8ecafba07a6.png

断点验证了x0最终值为0x00。这里有个问题是发生死循环了。(bl跳转指令导致的,lr寄存器在跳转后需要保护现场还原。)


总结

  • 汇编概述:
    • 使用助记符代替集齐指令的一种编程语言。
    • 汇编和及其指令是一一对应的关系,拿到二进制就可以反汇编。
    • 由于汇编和CPU指令集是对应的,所以汇编不具备移植性。
  • 总线:是一堆导线的集合
    • 地址总线:地址总线的宽度决定了寻址能力
    • 数据总线:数据总线的宽度决定了CPU的吞吐量
  • 进制
    • 任意进制都是由对应个数的符号组成的。符号可以自定义。
    • 2/8/16是相对完美的集智,他们之间的关系
      • 3个2进制使用一个8进制标识
      • 4个2进制使用一个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\WZR 30个32位寄存器
      • 32位寄存器并不是独立存在的,比如W0是X0的低32位
    • PC寄存器:指令指针寄存器
      • PC寄存器里面的值保存的就是CPU接下来需要执行的指令地址!
      • 改变PC的值可以改变程序的执行流程!


作者:HotPotCat
链接:https://www.jianshu.com/p/e8ea78cb10f0



0 个评论

要回复文章请先登录注册