汇编语言(assembly language):是任何一种用于电子计算机、微处理器、微控制器,或其他可编程器件的低级语言。在不同的设备中,汇编语言对应着不同的机器语言指令集。一种汇编语言专用于某种计算机系统结构,而不像许多高级语言,可以在不同系统平台之间移植。
使用汇编语言编写的源代码,然后通过相应的汇编程序将它们转换成可执行的机器代码。这一过程被称为汇编过程。
逆向开发中,有一个非常重要的环节就是静态分析。首先我们是逆向iOS系统上面的APP,一个APP安装在手机上面的可执行文件本质上是二进制文件。因为iPhone手机上执行的指令是二进制,由手机上的CPU执行的,所以静态分析是建立在分析二进制上面,对汇编语言的掌握是必不可少的。
汇编语言的发展
机器语言
由
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
代码在终端设备上的过程:
![]()
汇编语言与机器语言一一对应,每一条机器指令都有与之对应的汇编指令汇编语言可以通过编译得到机器语言,机器语言可以通过反汇编得到汇编语言高级语言可以通过编译得到汇编语言\机器语言,但汇编语言\机器语言几乎不可能还原成高级语言
汇编语言的特点
- 可以直接访问、控制各种硬件设备,比如存储器、
CPU等,能最大限度地发挥硬件的功能- 能够不受编译器的限制,对生成的二进制代码进行完全的控制
- 目标代码简短,占用内存少,执行速度快
- 汇编指令是机器指令的助记符,同机器指令一一对应。每一种
CPU都有自己的机器指令集\汇编指令集,所以汇编语言不具备可移植性- 知识点过多,开发者需要对
CPU等硬件结构有所了解,不易于编写、调试、维护- 不区分大小写,比如
mov和MOV是一样的
汇编的用途
- 编写驱动程序、操作系统(比如
Linux内核的某些关键部分)- 对性能要求极高的程序或者代码片段,可与高级语言混合使用(内联汇编)
- 软件安全
病毒分析与防治
逆向\加壳\脱壳\破解\外挂\免杀\加密解密\漏洞\黑客- 理解整个计算机系统的最佳起点和最有效途径
- 为编写高效代码打下基础
- 弄清代码的本质
函数的本质究竟是什么?
++a + ++a + ++a底层如何执行的?
编译器到底帮我们干了什么?
DEBUG模式和RELEASE模式有什么关键的地方被我们忽略
......越底层越单纯!真正的程序员都需要了解的一门非常重要的语言,汇编!
汇编语言的种类
目前讨论比较多的汇编语言有:
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 Touch4armv7siPhone5,iPhone5C,iPad4(iPad with Retina Display)arm64iPhone5S以后,iPhoneX,iPad Air,iPad mini2以后
必要常识
要想学好汇编,首先需要了解
CPU等硬件结构
APP/程序的执行过程:
![]()
- 硬件相关最为重要是
CPU/内存- 在汇编中,大部分指令都是和
CPU与内存相关的
总线
![]()
- 每一个
CPU芯片都有许多管脚,这些管脚和总线相连,CPU通过总线跟外部器件进行交互- 总线:一根根导线的集合
- 总线的分类
地址总线
数据总线
控制总线![]()
例如:
CPU从内存的3号单元读取数据
![]()
数量单位 & 容量单位
- 地址单元:字节
- 字节:
Byte(B)1个字节等于8个二进制位- 数量单位:
1M = 1024K,1K = 1024- 容量单位:
1MB = 1024KB,1KB = 1024B例如:
100M的带宽,运营商所指100Mbps。Mbps是一种传输速率单位,指每秒传输的位(bit)数量。所以100M的带宽理论下载速度是100M/8=12.5MB/s
地址总线
- 它的宽度决定了
CPU的寻址能力8086的地址总线宽度是20,所以寻址能力是1M(2 ^ 20)![]()
数据总线
- 它的宽度决定了
CPU的单次数据传送量,也就是数据传送速度8086的数据总线宽度是16,所以单次最大传递2个字节的数据日常所说
CPU的32位、64位,就是指数据吞吐量。32位一次可传送4字节,64位可传送8字节
控制总线
- 它的宽度决定了
CPU对其他器件的控制能力、能有多少种控制
案例:
一个
CPU的寻址能力为8KB,那么它的地址总线的宽度为13
8*1024=2^13
8080,8088,80286,80386的地址总线宽度分别为16根,20根,24根,32根。那么他们的寻址能力分别为64KB,1MB,16MB,4GB
2^16=65536=64KB2^20=1048576=1MB2^24=16777216=16MB2^32=4294967296=4GB
8080,8088,8086,80286,80386的数据总线宽度分别为8根,8根,16根,16根,32根。那么它们一次可以传输的数据为1B,1B,2B,2B,4B
8/8=1B8/8=1B16/8=2B16/8=2B32/8=4B从内存中读取
1024字节的数据,8086至少要读512次,80386至少要读取256次
1024*8/16=5121024*8/32=256
内存
![]()
![]()
![]()
- 内存地址空间的大小受
CPU地址总线宽度的限制。8086的地址总线宽度为20,可以定位2 ^ 20个不同的内存单元(内存地址范围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在什么情况下等于3?
十进制由10个符号组成:0 1 3 2 8 A B E S 7,逢十进一- 如果这样定义
十进制:1 + 1 = 3
这样的目的何在?
- 传统定义的
十进制和自定义十进制不一样。这10个符号构成的符号表如果保密,别人是没办法拿到具体的数据,可以用于加密
进制的运算
八进制加法表
![]()
八进制乘法表
![]()
二进制的简写形式
二进制: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,这就是十六进制
自定义进制符号
现在有
十进制数10个符号分别是:2,9,1,7,6,5,4, 8,3 , A,逢十进一十进制: 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...
123 + 234 = 1A6
上述案例,可以转化常规
十进制运算,然后查表。但如果是其他进制,就不能转换,要直接学会查表现在有
九进制数9个符号分别是:2,9,1,7,6,5,4, 8,3,逢九进一十进制: 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...
123 + 234 = 725
数据的宽度
数学上的数字,是没有大小限制的,可以无限的大。但在计算机中,由于受硬件的制约,数据都是有长度限制的(我们称为
数据宽度),超过最多宽度的数据会被丢弃
计算机中常见的数据宽度
- 位(
Bit):1个位就是1个二进制位,0或者1- 字节(
Byte):1个字节由8个Bit组成(8位),内存中的最小单元Byte- 字(
Word):1个字由2个字节组成(16位),这2个字节分别称为高字节和低字节- 双字(
Doubleword):1个双字由两个字组成(32位)
计算机存储数据,它会分为
有符号数和无符号数
![]()
- 无符号数,直接换算。例如:
F表示15- 有符号数:
正数:0 1 2 3 4 5 6 7 负数:F E D B C A 9 8 -1 -2 -3 -4 -5 -6 -7 -8
案例:
搭建
Demo项目
![]()
打开
ViewController.m文件,写入以下代码:#import "ViewController.h" @implementation ViewController int test(){ int cTemp = 0x1FFFFFFFF; return cTemp; } - (void)viewDidLoad { [super viewDidLoad]; printf("%x\n",test()); } @end在
test函数return时设置断点,使用真机运行项目
![]()
cTemp的值为-1- 使用
p &cTemp获取cTemp地址- 使用
x 0x000000016d4f382c读取内存地址,int类型占4字节,所以只保留FFFFFFFF,前面的01溢出- 寄存器对于溢出会有单独的记录
iOS为小端模式,内存从右往左读另一种查看方式,选择
Debug->Debug Workflow->View Memory
![]()
在
Address中输入查看的地址:0x16b389b6c
![]()
int类型是有符号整型,占4字节,也就是32个二进制位。此刻全部为1。第一位的符号位为1,所以是负数
![]()
有符号数和无符号数是读取时区分的,对于数据本身的内容没有任何变化。分别使用
%d和%u进行打印:
![]()
CPU & 寄存器
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中当做一些常规变量来使用
![]()
- 当真机调试时,读取的是手机设备的寄存器
- 列出的是
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
pc寄存器(program counter)
为指令指针寄存器,它指示了
CPU当前要读取指令的地址
![]()
在内存或者磁盘上,指令和数据没有任何区别,都是二进制信息
CPU在工作的时候,把有的信息看做指令,有的信息看做数据,为同样的信息赋予了不同的意义
- 例如:
1110 0000 0000 0011 0000 1000 1010 1010- 可以当做数据
0xE003008AA- 也可以当做指令
mov x0, x8
CPU根据什么将内存中的信息看做指令?
CPU将pc指向的内存单元的内容看做指令- 如果内存中的某段内容曾被
CPU执行过,那么它所在的内存单元必然被pc指向过
案例:
延用上述
Demo案例在
test函数上设置断点,查看汇编代码
![]()
- 断点所在位置,是
CPU即将执行的指令- 而
pc寄存器中,存储的就是CPU即将执行的指令对
pc寄存器的值进行修改,写入0x104ec5ea0
![]()
断点继续执行,
CPU会直接将pc寄存器中存储的0x100b31ea0指令读取出并执行,然后pc寄存器指向下一条即将执行的0x100b31ea4指令
![]()
- 此时
0x100b31ea0是当前执行的指令- 执行后,断点将断到
0x100b31ea4指令pc寄存器存储的也是0x100b31ea0的下一条指令地址:0x100b31ea4
高速缓存
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指令
案例:
延用上述
Demo案例创建
Empty文件
![]()
命名
asm.s
![]()
创建
.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文件,写入以下代码:#import "ViewController.h" @implementation ViewController int A(); - (void)viewDidLoad { [super viewDidLoad]; A(); } @end
.text:声明为代码段.global:暴露全局函数在
viewDidLoad方法调用A函数时设置断点,使用真机运行项目
![]()
查看汇编代码,选择
Debug->Debug Workflow->Always Show Disassembly
![]()
查看寄存器,点击调试区底部
Auto,菜单中选择All Variables, Registers, Globals and Statics
![]()
汇编代码应该如何查看?一共划分
4列:
![]()
- 第
1列:内存地址- 第
2列:偏移地址,0,4,8,12...,由此可见每条指令占4字节- 第
3列:指令- 第
4列:注释,以;开头按住
control,点击Setp info,进行单步调试。此时进入A函数
![]()
mov x0, #0xa0:将#0xa0赋值给x0寄存器mov x1, #0x0:将#0x0赋值给x1寄存器add x1, x0, #0x14:x0寄存器+ #0x14,赋值给x1寄存器- 此时
x1寄存器保存的是#0xb4- 将
x1寄存器赋值给x0寄存器bl 0x100009f14:通过bl指令跳转到B函数,跳转前lr寄存器保存lb指令的下一条指令地址通过
bl指令进入B函数,x0、x1寄存器都为#0xb4
![]()
add x0, x0, #0x10:将x0寄存器+ #0x10,赋值给x0寄存器- 此时
x0寄存器保存的是#0xc4ret:通过ret指令返回,跳转到lr寄存器保存的指令地址通过
ret指令回到A函数,x0寄存器为#0xc4,x1寄存器为#0xb4
![]()
mov x0, #0x0:将#0x0赋值给x0寄存器ret:通过ret指令返回,跳转到lr寄存器保存的指令地址案例中,由于没有对
lr寄存器进行保护,继续执行会形成递归。此问题待解决,先看一下x0、x1寄存器的值
![]()
- 最终
x0寄存器为#0x0,x1寄存器为#0xb4
总结
汇编概述:
- 使用助记符代替机器指令的一种编程语言
- 汇编和机器指令是一一对应的关系,拿到二进制就可以反汇编
- 由于汇编和
CPU的指令集是对应的,所以汇编不具备移植性总线:
- 总线:一堆导线的集合
- 地址总线的宽度决定了寻址能力
- 数据总线的宽度决定了
CPU数据的吞吐量进制
- 任意进制,都是由对应个数的符号组成的,符号可以自定义
2\8\16是相对完美的进制,它们之间的关系:
3个2进制位,使用一个8进制标识
4个2进制位,使用一个16进制标识
2个16进制位,可以标识一个字节- 数量单位
1024 = 1K;1024K = 1M;1024M = 1G- 容量单位
1024B = 1KB;1024KB = 1MB;1024MB = 1GB
B:字节(Byte);1B = 8bit
bit:比特;一个2进制位- 数据宽度
计算机中的数据是有宽度的,超过了就会溢出寄存器:
- 寄存器:
CPU为了性能,在内存开屏了一小块临时存储区域- 浮点向量寄存器
- 异常状态寄存器
- 通用寄存器
通用寄存器:除了存放数据有时候也有特殊的用途
ARM64拥有32个64位的通用寄存器x0 - x30,以及XZR(零寄存器)
为了兼容32位,所以ARM64拥有w0 - w28\WZR,30个32位寄存器
32位寄存器并不是独立存在的,比如w0是x0的低32位pc寄存器
pc寄存器:指令指针寄存器
pc寄存器里面的值,保存的是CPU接下来需要执行的指令地址
改变pc的值可以改变程序的执行流程










网友评论