初识汇编
汇编语言(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
我们的代码在终端设备上是这样的过程:
- 汇编语言与机器语言一一对应,每一条机器指令都有与之对应的汇编指令
- 汇编语言可以通过编译得到机器语言,机器语言可以通过反汇编得到汇编语言
- 高级语言可以通过编译得到汇编语言 \ 机器语言,但汇编语言\机器语言几乎不可能还原成高级语言
高级语言有很多,例如C语言、OC、Swift,例如就一句打印语句,高级语言都有很多的写法,最终都会得到唯一的汇编指令,但是汇编是不知道开发者是通过什么语言,什么语法来得到汇编的,所以也就几乎不可能还原成高级语言
汇编语言的特点
可以直接访问、控制各种硬件设备,比如存储器、CPU等,能最大限度地发挥硬件的功能
能够不受编译器的限制,对生成的二进制代码进行完全的控制
目标代码简短,占用内存少,执行速度快
汇编指令是机器指令的助记符,同机器指令一一对应。每一种CPU都有自己的机器指令集\汇编指令集,所以汇编语言不具备可移植性
知识点过多,开发者需要对CPU等硬件结构有所了解,不易于编写、调试、维护
不区分大小写,比如mov和MOV是一样的
汇编的用途
- 编写驱动程序、操作系统
- 对性能要求极高的程序或者代码片段,可与高级语言混合使用(内联汇编)
- 软件安全
- 病毒分析与防治
- 逆向\加壳\脱壳\破解\外挂\免杀\加密解密\漏洞\黑客
- 理解整个计算机系统的最佳起点和最有效途径
- 为编写高效代码打下基础
- 弄清代码的本质
汇编语言的种类
- 目前讨论比较多的汇编语言有
- 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以后 |
APP/程序的执行过程
总线
- 每一个CPU芯片都有许多管脚,这些管脚和总线相连,CPU通过总线跟外部器件进行交互
- 总线:一根根导线的集合
- 总线的分类
- 地址总线
- 数据总线
- 控制总线
举个例子
-
地址总线
- 它的宽度决定了CPU的寻址能力
- 8086的地址总线宽度是20,所以寻址能力是1M(220 )
-
数据总线
- 它的宽度决定了CPU的单次数据传送量,也就是数据传送速度
- 8086的数据总线宽度是16,所以单次最大传递2个字节的数据
-
控制总线
- 它的宽度决定了CPU对其他器件的控制能力、能有多少种控制
内存
- 内存地址空间的大小受CPU地址总线宽度的限制。8086的地址总线宽度为20,可以定位220个不同的内存单元(内存地址范围0x00000~0xFFFFF),所以8086的内存空间大小为1MB
- 0x00000~0x9FFFF:主存储器。可读可写
- 0xA0000~0xBFFFF:向显存中写入数据,这些数据会被显卡输出到显示器。可读可写
- 0xC0000~0xFFFFF:存储各种硬件\系统信息。只读
进制
二进制、八进制、十六进制是完美进制,因为他们之间可以互相转换
八进制加法表
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
数据的宽度
数学上的数字,是没有大小限制的,可以无限的大。但在计算机中,由于受硬件的制约,数据都是有长度限制的(我们称为数据宽度),超过最多宽度的数据会被丢弃。
计算机中常见的数据宽度
- 位(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 C B A 9 8
-1 -2 -3 -4 -5 -6 -7 -8
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位的通用寄存器 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指向过
高速缓存
iPhoneX上搭载的ARM处理器A11它的1级缓存的容量是64KB,2级缓存的容量8M.
CPU每执行一条指令前都需要从内存中将指令读取到CPU内并执行。而寄存器的运行速度相比内存读写要快很多,为了性能,CPU还集成了一个高速缓存存储区域.当程序在运行时,先将要执行的指令代码以及数据复制到高速缓存中去(由操作系统完成).CPU直接从高速缓存依次读取指令来执行.
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此处作为下条指令地址!
x30寄存器
x30寄存器存放的是函数的返回地址.当ret指令执行时刻,会寻找x30寄存器保存的地址值!
bl 和ret是配套使用的
b:标号,转到标号处执行指令,通俗理解就是:要跳转到哪里,函数/方法调用
l:记录回去的路,lr寄存器的值,lr里面存放的就是ret回去的地址,接下来要执行的地址
ret:return,返回,类似函数,方法里面的的return
栈
- 栈:是一种具有特殊的访问方式的存储空间(后进先出, Last In Out Firt,LIFO)
SP和FP寄存器
sp寄存器在任意时刻会保存我们栈顶的地址.
-
fp寄存器也称为x29寄存器属于通用寄存器,但是在某些时刻我们利用它保存栈底的地址!
注意:ARM64开始,取消32位的 LDM,STM,PUSH,POP指令! 取而代之的是ldr\ldp str\stp
ARM64里面 对栈的操作是16字节对齐的!!
函数调用栈
sub sp, sp, #0x40 ; 拉伸0x40(64字节)空间
stp x29, x30, [sp, #0x30] ;x29\x30 寄存器入栈保护
add x29, sp, #0x30 ; x29指向栈帧的底部
...
ldp x29, x30, [sp, #0x30] ;恢复x29/x30 寄存器的值
add sp, sp, #0x40 ; 栈平衡
ret
关于内存读写指令
注意:读/写 数据是都是往高地址读/写
str(store register)指令
将数据从寄存器中读出来,存到内存中.
ldr(load register)指令
将数据从内存中读出来,存到寄存器中
此ldr 和 str 的变种ldp 和 stp 还可以同时操作2个寄存器.
函数的参数和返回值
ARM64下,函数的参数是存放在X0到X7(W0到W7)这8个寄存器里面的.如果超过8个参数,就会入栈.
函数的返回值是放在X0 寄存器里面的.
对象指针等8个字节的地址放在X0~X7里面,X是64位的
基本数据类型,int等4个字节的地址放在W0~W7里面,W是32位的
疑问:oc写方法的时候,方法一般都不超过几个参数呢?
答:6个,因为在消息转发的时候,默认就有两个参数,一个self或者super,一个@selector
总结
ARM64里面 对栈的操作是16字节对齐的!!,不是16字节读取的话会报坏内存访问错误!
死循环/死递归,死循环不一定会程序崩溃,死递归就会崩溃,崩溃的原因:堆栈溢出,递归一直在函数调用,函数调用就一直开辟栈空间,栈空间是高地址往低地址拉伸,数据写入是往高地址写,堆空间是低地址往高地址拉伸,当堆空间和栈空间的栈顶相互碰撞的时候,就发生堆栈溢出,此时,程序崩溃!
arm64系的CPU来说, 寄存器以x开头则表明的是一个64位的寄存器,如果以w开头则表明是一个32位的寄存器,在系统中没有提供16位和8位的寄存器供访问和使用,其中32位的寄存器是64位寄存器的低32位部分并不是独立存在的!!!
栈空间是往低地址拉伸开辟空间,读/写 数据是都是往高地址读/写
-
- str(store register)指令将数据从寄存器中读出来,存到内存中
- ldr(load register)指令将数据从内存中读出来,存到寄存器中
- ldp 和 stp 是ldr和str的变种,可以同时操作2个寄存器
SP指向栈顶,FP是X29寄存器,FP只是X29寄存器的昵称,指向栈底,lr是X30寄存器,只是X30寄存器的昵称,存放函数的返回地址,当ret指令执行时刻,会寻找x30寄存器保存的地址值!