机器语言
由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是一样的
汇编的用途
- 编写驱动程序、操作系统
- 对性能要求极高的程序或者代码片段,可与高级语言混合使用(内联汇编)
- 软件安全
- 病毒分析与防治
- 逆向 \ 加壳 \ 脱壳 \ 破解 \ 外挂 \ 免杀 \ 加密解密 \ 漏洞 \ 黑客
- 理解整个计算机系统的最佳起点和最有效途径
- 为编写高效代码打下基础
- 弄清代码的本质
- 函数的本质究竟是什么?
- ++a + ++a + ++a 底层如何执行的?
- 编译器到底帮我们干了什么?
- DEBUG模式和RELEASE模式有什么关键的地方被我们忽略
- ……
可见了解汇编语言好处良多,尽管它有些晦涩难懂
汇编语言的种类
目前讨论比较多的汇编语言有
- 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与内存相关的
总线
- 每一个CPU芯片都有许多管脚,这些管脚和总线相连,CPU通过总线跟外部器件进行交互
- 总线:一根根导线的集合
- 总线的分类
- 地址总线
- 数据总线
- 控制总线
举个🌰
- 地址总线
- 它的宽度决定了CPU的寻址能力
- 8086的地址总线宽度是20,所以寻址能力是1M( 2^20 )
(1M = 1024KB = 1024 * 1024Byte)
- 数据总线
- 它的宽度决定了CPU的单次数据传送量,也就是数据传送速度
- 8086的数据总线宽度是16,所以单次最大传递2个字节( 1Byte = 8Bit)的数据
- 控制总线
- 它的宽度决定了CPU对其他器件的控制能力、能有多少种控制
算法演练
内存
内存地址空间的大小受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进一
如果十进制由10个符号组成: 0 1 3 2 8 A B E S 7 逢十进一,那这样1 + 1 就等于3,完全取决于定义规则
这样的目的何在?
传统我们定义的十进制和自定义的十进制不一样.那么这10个符号如果我们不告诉别人这个符号表,别人是没办法拿到我们的具体数据的!可用于加密!
十进制由十个符号组成,逢十进一,符号是可以自定义的
进制的运算
八进制加法表
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... |
二进制的简写形式
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> |
拿到内存地址后,可以通过如下方法查看内存地址中的值,也可以直接通过快捷键Commond + Shift + M
,在单步执行后(执行汇编语言时可以在控制台使用ni
命令单步执行),内存地址中的值可能没有立刻改变,点击下一页再回来就可以看到变化
计算机中常见的数据宽度
- 位(Bit): 1个位就是1个二进制位.0或者1
- 字节(Byte): 1个字节由8个Bit组成(8位).内存中的最小单元Byte.
- 字(Word): 1个字由2个字节组成(16位),这2个字节分别称为高字节和低字节.
- 双字(Doubleword): 1个双字由两个字组成(32位)
那么计算机存储数据它会分为有符号数和无符号数.那么关于这个看图就理解了!
1 | 无符号数,直接换算! |
寄存器 (均指的是arm64架构下)
CPU除了有控制器、运算器还有寄存器。其中寄存器的作用就是进行数据的临时存储。
CPU的运算速度是非常快的,为了性能CPU在内部开辟一小块临时存储区域,并在进行运算时先将数据从内存复制到这一小块临时存储区域中,运算时就在这一小快临时存储区域内进行。我们称这一小块临时存储区域为寄存器。
对于arm64系的CPU来说, 如果寄存器以x开头则表明的是一个64位的寄存器,如果以w开头则表明是一个32位的寄存器,在系统中没有提供16位和8位的寄存器供访问和使用。其中32位的寄存器是64位寄存器的低32位,并不是独立存在的。
内部部件之间由总线连接
- 对程序员来说,CPU中最主要部件是寄存器,可以通过改变寄存器的内容来实现对CPU的控制
不同的CPU,寄存器的个数、结构是不相同的
ARM64架构下的寄存器,包括31个64位的通用寄存器 x0 到 x30,和SP, PC, CPSR寄存器及一些其他浮点寄存器等
- w0~w28 这些是x0~x28的低32位, 因为64位CPU可以兼容32位.所以可以只使用64位寄存器的低32位.
- 比如 w0 就是 x0的低32位!
通常,CPU会先将内存中的数据存储到通用寄存器中,然后再对通用寄存器中的数据进行运算
假设内存中有块红色内存空间的值是3,现在想把它的值加1,并将结果存储到蓝色内存空间
- CPU首先会将红色内存空间的值放到X0寄存器中:mov X0,红色内存空间
- 然后让X0寄存器与1相加:add X0,1
- 最后将值赋值给内存空间:mov 蓝色内存空间,X0
- 可以在控制台输入
register read
命令查看所有寄存器
x0~ x30:通用寄存器,通常用来存放一般性的数据,xo~x7用于子程序调用时的参数传递,x0也用于返回值传递
FP(X29): 保存栈帧地址(栈底指针)
LR(x30): 也成为程序连接寄存器,用来保存子程序返回地址
SP:在任意时刻会保存我们栈顶的地址
PC:程序寄存器,总是指向即将要执行的下一条指令的地址
CPSR: 状态寄存器,注意:状态寄存器是32位的
高速缓存
iPhoneX上搭载的ARM处理器A11它的1级缓存的容量是64KB,2级缓存的容量8M.
CPU每执行一条指令前都需要从内存中将指令读取到CPU内并执行。而寄存器的运行速度相比内存读写要快很多,为了性能,CPU还集成了一个高速缓存存储区域
当程序在运行时,先将要执行的指令代码以及数据复制到高速缓存中去(由操作系统完成).CPU直接从高速缓存依次读取指令来执行.
数据地址寄存器
数据地址寄存器通常用来做数据计算的临时存储、做累加、计数、地址保存等功能。定义这些寄存器的作用主要是用于在CPU指令中保存操作数,在CPU中当做一些常规变量来使用。
ARM64中
- 64位: X0-X30, XZR(零寄存器)
- 32位: W0-W30, WZR(零寄存器)
注意:8086汇编中有一种特殊的寄存器段寄存器:CS,DS,SS,ES四个寄存器来保存这些段的基地址,这个属于Intel架构CPU中.在ARM中并没有
浮点和向量寄存器
因为浮点数的存储以及其运算的特殊性,CPU中专门提供浮点数寄存器来处理浮点数
- 浮点寄存器 64位: D0 - D31 32位: S0 - S31
现在的CPU支持向量运算.(向量运算在图形处理相关的领域用得非常的多)为了支持向量计算系统了也提供了众多的向量寄存器.
- 向量寄存器 128位:V0-V31
pc寄存器(program counter)
- 为指令指针寄存器,它指示了CPU当前要读取指令的地址
- 在内存或者磁盘上,指令和数据没有任何区别,都是二进制信息
- CPU在工作的时候把有的信息看做指令,有的信息看做数据,为同样的信息赋予了不同的意义
- 比如 1110 0000 0000 0011 0000 1000 1010 1010
- 可以当做数据 0xE003008AA
- 也可以当做指令 mov x0, x8
CPU根据什么将内存中的信息看做指令?
- CPU将pc指向的内存单元的内容看做指令
- 如果内存中的某段内容曾被CPU执行过,那么它所在的内存单元必然被pc指向过
上面说过,汇编语言单步执行指令是
ni
,PC总是指向将要执行的指令,如果修改PC的值也就可以改变执行顺序,使用命令是register write pc 内存地址
栈
- 栈:是一种具有特殊的访问方式的存储空间(后进先出, Last In Out Firt,LIFO)
SP和FP(x29)寄存器
- sp寄存器在任意时刻会保存我们栈顶的地址.
fp寄存器也称为x29寄存器属于通用寄存器,但是在某些时刻我们利用它保存栈底的地址
注意:ARM64开始,取消32位的 LDM,STM,PUSH,POP指令! 取而代之的是ldr\ldp str\stp
ARM64里面 对栈的操作是16字节对齐的!!
关于内存读写指令
注意:读/写 数据是都是往高地址读/写
str(store register)指令
将数据从寄存器中读出来,存到内存中.
ldr(load register)指令
将数据从内存中读出来,存到寄存器中
此ldr
和 str
的变种ldp
和 stp
还可以操作2个寄存器.
堆栈操作
使用32个字节空间作为这段程序的栈空间,然后利用栈将x0和x1的值进行交换.
sub sp, sp, #0x20 ;拉伸栈空间32个字节
stp x0, x1, [sp, #0x10] ;sp往上加16个字节的地址,存放x0 和 x1
ldp x1, x0, [sp, #0x10] ;将sp偏移16个字节地址的值取出来,放入x1 和 x0
注意:断在bl指令出,使用
s
命令或者点击step into
会直接跳到已经开辟好的栈顶地址处,需要按住control
的同时再点击step into
,这样才会跳到未开辟前的栈顶地址处
bl指令和ret指令
- CPU从何处执行指令是由pc中的内容决定的,我们可以通过改变pc的内容来控制CPU执行目标指令
- ARM64提供了一个mov指令(传送指令),可以用来修改大部分寄存器的值,比如
- mov x0,#10、mov x1,#20
但是,mov指令不能用于设置pc的值,ARM64没有提供这样的功能
ARM64提供了另外的指令来修改PC的值,这些指令统称为转移指令(相当于跳转),最简单的是bl指令
bl
- 将下一条指令的地址放入lr(x30)寄存器
- 转到标号处执行指令
ret
- 默认使用lr(x30)寄存器的值,通过底层指令提示CPU此处作为下条指令地址!
ARM64平台的特色指令,它面向硬件做了优化处理
x30寄存器存放的是函数的返回地址.当ret指令执行时刻,会寻找x30寄存器保存的地址值
注意:在函数嵌套调用的时候.需要将x30入栈,因为嵌套调用时x30寄存器的值会被覆盖,导致死循环
函数的参数和返回值及局部变量
ARM64下,函数调用会开辟一段空间,每个函数调用完毕之后,会将拉伸的栈空间平衡(将sp加回去),函数的参数是存放在X0到X7(W0到W7)这8个寄存器里面的.如果超过8个参数,就会入栈,函数的局部变量放在栈中
函数的返回值是放在X0 寄存器里面的.
int sum(int a,int b) {
return a + b;
}
int main(int argc, char * argv[]) {
sum(16, 32);
}
int sum(int a,int b,int c,int d, int e, int f, int g,int h,int i,int j) {
return a + b + c + d + e + f + g + h + i + j;
}
int main(int argc, char * argv[]) {
sum(16, 16 * 2, 16 * 3, 16 * 4, 16 * 5, 16 * 6, 16 * 7, 16 * 8, 16 * 9,16 * 10);
}
ARM64下部分常用汇编指令
MOV X1,X0 ;将寄存器X0的值传送到寄存器X1
ADD X0,X1,X2 ;寄存器X1和X2的值相加后传送到X0
SUB X0,X1,X2 ;寄存器X1和X2的值相减后传送到X0
AND X0,X0,#0xF ; X0的值与0xF相位与后的值传送到X0
ORR X0,X0,#0x9 ; X0的值与9逻辑或后的值传送到X0
EOR X0,X0,#0xF ; X0的值与0xF相异或后的值传送到X0
LDR X5,[X6,#0x08] ;X6寄存器加0x08的和的地址值内的数据传送到X5
STR X0, [SP, #0x8] ;X0寄存器的数据传送到SP+0x8地址值指向的存储空间
STP x29, x30, [sp, #0x10] ;入栈指令
LDP x29, x30, [sp, #0x10] ;出栈指令
CBZ;比较(Compare),如果结果为零(Zero)就转移(只能跳到后面的指令)
CBNZ;比较,如果结果非零(Non Zero)就转移(只能跳到后面的指令)
CMP;比较指令,相当于SUBS,影响程序状态寄存器
CPSR B/BL ;绝对跳转#imm, 返回地址保存到LR(X30)
RET;子程序返回指令,返回地址默认保存在LR(X30)
状态寄存器(标记寄存器)
CPU内部的寄存器中,有一种特殊的寄存器(对于不同的处理器,个数和结构都可能不同).这种寄存器在ARM中,被称为状态寄存器就是CPSR(current program status register)寄存器
CPSR和其他寄存器不一样,其他寄存器是用来存放数据的,都是整个寄存器具有一个含义.而CPSR寄存器是按位起作用的,也就是说,它的每一位都有专门的含义,记录特定的信息
注意:CPSR寄存器是32位的
- CPSR的低8位(包括I、F、T和M[4:0])称为控制位,程序无法修改,除非CPU运行于特权模式下,程序才能修改控制位!
- N、Z、C、V均为条件码标志位。它们的内容可被算术或逻辑运算的结果所改变,并且可以决定某条指令是否被执行!意义重大!
N(Negative)标志
CPSR的第31位是 N,符号标志位。它记录相关指令执行后,其结果是否为负.如果为负 N = 1,如果是非负数 N = 0.
注意,在ARM64的指令集中,有的指令的执行时影响状态寄存器的,比如add\sub\or等,他们大都是运算指令(进行逻辑或算数运算);
Z(Zero)标志
CPSR的第30位是Z,0标志位。它记录相关指令执行后,其结果是否为0.如果结果为0.那么Z = 1.如果结果不为0,那么Z = 0
对于Z的值,我们可以这样来看,Z标记相关指令的计算结果是否为0,如果为0,则N要记录下”是0”这样的肯定信息.在计算机中1表示逻辑真,表示肯定.所以当结果为0的时候Z = 1,表示”结果是0”.如果结果不为0,则Z要记录下”不是0”这样的否定信息.在计算机中0表示逻辑假,表示否定,所以当结果不为0的时候Z = 0,表示”结果不为0”。
C(Carry)标志
CPSR的第29位是C,进位标志位。一般情况下,进行无符号数的运算
加法运算:当运算结果产生了进位时(无符号数溢出),C=1,否则C=0
减法运算(包括CMP):当运算时产生了借位时(无符号数溢出),C=0,否则C=1
对于位数为N的无符号数来说,其对应的二进制信息的最高位,即第N - 1位,就是它的最高有效位,而假想存在的第N位,就是相对于最高有效位的更高位。如下图所示:
进位
我们知道,当两个数据相加的时候,有可能产生从最高有效位向更高位的进位。比如两个32位数据:0xaaaaaaaa + 0xaaaaaaaa,将产生进位。由于这个进位值在32位中无法保存,我们就只是简单的说这个进位值丢失了。其实CPU在运算的时候,并不丢弃这个进位值,而是记录在一个特殊的寄存器的某一位上。ARM下就用C位来记录这个进位值。比如,下面的指令
1 | mov w0,#0xaaaaaaaa;0xa 的二进制是 1010 |
借位
当两个数据做减法的时候,有可能向更高位借位。再比如,两个32位数据:0x00000000 - 0x000000ff,将产生借位,借位后,相当于计算0x100000000 - 0x000000ff。得到0xffffff01 这个值。由于借了一位,所以C位 用来标记借位。C = 0.比如下面指令:
1 | mov w0,#0x0 |
V(Overflow)溢出标志
CPSR的第28位是V,溢出标志位。在进行有符号数运算的时候,如果超过了机器所能标识的范围,称为溢出。
- 正数 + 正数 为负数 溢出
- 负数 + 负数 为正数 溢出
- 正数 + 负数 不可能溢出
内存分区
代码区: 可读可写
栈区域: 放参数和局部变量,可读可写
堆区域: 动态申请 可读可写
全局: 可读可写
常量区: 只读
adrp指令
adrp
是计算指定的数据地址 到当前PC值的相对偏移
adrp x0, 1
1.将1的值,左移12位 1 0000 0000 0000 == 0x1000
2.将PC寄存器的低12位清零(2^12 = 4kb) 0x1002e6874 ==> 0x1002e6000
3.将1和2的结果相加给X0寄存器
常量获取:基地址+偏移地址
在arm中,ADD加法不带进位,ADDS是带进位的,运算完成要置符号位(最高位作为符号位),SUB和SUBS类似
void funA() {
asm(
"mov w0,#0x7fffffff\n"
"adds w0,w0,#0x2\n"
"mov w0,#0x80000000\n"
"subs w0,w0,#0x2\n"
);
}
void funB() {
int32_t a = 0x80000000;
printf("%d\n",a);
a = a - 2;
printf("%d\n",a);
}
int main(int argc, char * argv[]) {
funA();
funB();
}
如有任何疑问或问题请联系我:fishnewsdream@gmail.com,欢迎交流,共同提高!
Objective-C/Swift技术开发交流群201556264,讨论何种技术并不受限,欢迎各位大牛百家争鸣!
微信公众号OldDriverWeekly
,欢迎关注并提出宝贵意见
老司机iOS周报,欢迎关注或订阅
刚刚在线工作室,欢迎关注或提出建设性意见!
刚刚在线论坛, 欢迎踊跃提问或解答!
如有转载,请注明出处,谢谢!