04-探究iOS底层原理|ARM64汇编

4,474 阅读16分钟

前言

之前,我们在探索动画及渲染相关原理的时候,我们输出了几篇文章,解答了iOS动画是如何渲染,特效是如何工作的疑惑。我们深感系统设计者在创作这些系统框架的时候,是如此脑洞大开,也 深深意识到了解一门技术的底层原理对于从事该方面工作的重要性。

因此我们决定 进一步探究iOS底层原理的任务 ,本文探索的ARM64汇编属于 探索底层原理的前知识,是iOS系统的真机环境下ARM64硬件架构的相关汇编知识

一、汇编的用途

  • 编写驱动程序、操作系统(比如Linux内核的某些关键部分)
  • 对性能要求极高的程序或者代码片段,可与高级语言混合使用(内联汇编)
  • 软件安全
    • 病毒分析与防治
    • 逆向\加壳\脱壳\破解\外挂\免杀\加密解密\漏洞\黑客
  • 理解整个计算机系统的最佳起点和最有效途径
  • 为编写高效代码打下基础
  • 弄清代码的本质
    • 函数的本质究竟是什么?
    • ++a + ++a + ++a 底层如何执行的?
    • 编译器到底帮我们干了什么?
    • DEBUG模式和RELEASE模式有什么关键的地方被我们忽略
    • ......

总之:

越底层越单纯!真正的程序员都需要了解的一门非常重要的语言,汇编!

二、汇编语言的种类

  • 目前讨论比较多的汇编语言有
    • 8086汇编(8086处理器是16bit的CPU)
    • x86汇编(32bit)
    • x64汇编(64bit)
    • ARM汇编(嵌入式、移动设备)
    • ......
  • x86、x64汇编根据编译器的不同,有2种书写格式
    • Intel:Windows派系
    • AT&T :Unix派系
  • 作为iOS开发工程师,最主要的汇编语言是
    • AT&T汇编 -> iOS模拟器
    • ARM汇编 -> iOS真机设备
  • 我们iPhone里面用到的是ARM汇编,但是不同的设备也有差异.因CPU的架构不同.
架构设备
armv6iPhone, iPhone2, iPhone3G, 第一代、第二代 iPod Touch
armv7iPhone3GS, iPhone4, iPhone4S,iPad, iPad2, iPad3(The New iPad), iPad mini, iPod Touch 3G, iPod Touch4
armv7siPhone5, iPhone5C, iPad4(iPad with Retina Display)
arm64iPhone5S 以后 iPhoneX , iPad Air, iPad mini2以后

三、探究iOS底层原理需要掌握的汇编

iOS汇编语言有很多种。常见的有8086汇编、arm汇编、x86汇编等等。

1. 为什么要学习arm64汇编?

从上表中我们可以得知,现今流行的真机设备(iPhone5s之后的设备)都在是arm64以后的架构,因此我们当下只需要关注ARM64汇编即可

2.arm64汇编入门

想要学习arm64汇编,需要从以下三个方面入手,寄存器汇编指令堆栈

2.1寄存器

有16个常用寄存器,如下:

  • rax、rbx、rcx 、rdx、rsi、rdi、rbp、rsp
  • r8、r9、r10、r11、r12、r13、r14、r15

2.2 寄存器的具体用途

  • rax、rdx常作为函数返回值使用
  • rdi、rsi、rdx、rcx、r8、r9等寄存器常用于存放函数参数
  • rsp、rbp用于栈操作
  • rip作为指令指针
    • 存储着CPU下一条要执行的指令的地址
    • 一旦CPU读取一条指令,rip会自动指向下一条指令(存储下一条指令的地址)

2.3 寄存器的图

image.png

x86汇编(32bit) 32位时代,常用寄存器能存32位,也就是4个字节。
x64汇编(64bit) 64位时代,常用寄存器能存64位,也就是8个字节。

只要是r开头的都是64位寄存器,8字节。e开头的都是32位寄存器,4字节。

那么寄存器是如何兼容的呢?
如下图,看第一行:
最外面是rax寄存器占用8字节(64位),它会拿出自己最低4字节(32位)当作eax寄存器使用,eax也拿出自己最低2字节(16位)当作ax寄存器使用,ax寄存器又砍成两半,分别将高8位和低8位当作ah和al寄存器来使用(其实ah中的h是high的意思,al中的l是low的意思)。

总结:
r开头:64bit,8字节
e开头:32bit,4字节
ax、bx、cx等:16bit,2字节
ah、al、bh、bl等:8bit,1字节

3. ARM64的汇编指令

常见ARM汇编指令

1、移动指令:MOV,寄存器和寄存器之间传值

mov    x2, x16             ;把x16的值传递给寄存器x2

2、算术运算指令: 加(ADD)、减(SUB)

add x1, x2, x3             ;把x2+x3的值传递给寄存器x1
sub x1, x2, x3             ;把x2-x3的值传递给x1

3、逻辑运算指令:与(AND)、或(ORR)、异或(EOR)

and    x1,x1,#0xf           ;把x1中的值与0xf按位与后传递给x1
orr    x1,x1,#6               ;把x1中的值与6按位与后传递给x1
eor    x1,x1,#0xf           ;把x1中的值与0xf按位异或后传递给x1

4、桶形移位器操作指令:LSL、LSR、ASR、ROR

LSL:逻辑左移 
LSR:逻辑右移
ASR:算术右移
ROR:循环右移

5、存/取数据指令:STR(寄存器加载到内存中)、取数据LDR (把内存中的数据传递给寄存器)

str x1, [sp, #0x4]                  ;把x1寄存器的数据传递给sp+0x4地址值指向的内存空间
ldr x1, [sp, #0x4]                  ;把sp+0x4地址值内的数据传递给寄存器x1

6、栈操作指令:STP(入栈)、LDP(出栈)

stp x0, x1,  [sp, #0x4] 
ldp x0, x1,  [sp, #0x4] 

7、比较指令:CMP,CBZ,CBNZ,TBZ,TBNZ

cmp x0,x1                        ;把x0的内容和x1的内容进行比较,根据结果更新条件标志,并丢弃结果,相当于subs xzr x0, x1
cbz x0, LGetImpMiss1                    ;如果x0等于0就跳转到LGetImpMiss1,不影响条件标志
cbnz x0, LGetImpMiss1               ;如果x0不等于0就跳转到LGetImpMiss1,不影响条件标志

tbz x0, #20, LGetImpMiss1                 ;寄存器中指定位某个值是否为零,如果x0中的第20位(x0[20])等于0就跳转到LGetImpMiss1,不影响条件标志

tbnz x0, #20, LGetImpMiss1                 ;寄存器中指定位比较,如果x0中的第20位(x0[20])不等于0就跳转到LGetImpMiss1,不影响条件标志

8、跳转指令: B:无返回跳转,配合CMP使用 BL:带返回的跳转,会将返回地址存储到寄存器x30,说明这是一个子程序调用

示例1:
b    LLookupExample          ;直接跳转到LLookupExample

示例2:配合cmp使用
cmp x0,#6                    
b.eq LReturnZeroExample     ;如果x0等于6,则跳转LReturnZeroExample

条件代码:
b.eq                ;等于
b.ne                ;不等于
b.le                 ;有符号的小于或等于
b.ge                ;有符号的大于或等于
b.lt                ;有符号小于
b.gt                ;有符号大于

示例3:
bl lookUpFindExample

9、子程序返回指令:RET(返回地址存储在x30)

LTestExample:
    mov x2, #0
    ret

10、寻址指令:ADRP(将PC相对地址形成4KB页面,即:取指定label的基地址,存储到指定寄存器中)

adrp    x1, __example_handle@PAGE       ;获取__example_handle所在页的基地址存储到x1寄存器中

11、无符号位域选取指令:UBFX(提取指定位)

ubfx    x11, x0, #60, #4            ;从源寄存器x0中提取4位,位置从60开始。即将x0中的60-63位复制到目标寄存器x11的最低有效位,并将该x11上的其他高位设置为零

四. LLDB指令

我们在阅读汇编时,常常会用lldb指令在调试窗口 对寄存器 进行一些打印 或调试。因此我们也需要去了解LLDB相关的知识: LLDB【命令结构、查询命令、断点设置、流程控制、模块查询、内存读写、chisel插件】

五. 内存地址的规律

我们在阅读汇编的时候,常常会看到一些内存地址。通过长期阅读汇编,总结了内存地址的一些规律:

  • 内存地址格式为:0x4bdc(%rip),一般是全局变量,全局区(数据段)
  • 内存地址格式为:-0x78(%rbp),一般是局部变量,栈空间
  • 内存地址格式为:0x10(%rax),一般是堆空间

六. 了解相关知识

编程语言的发展

机器语言

由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

我们的代码在终端设备上是这样的过程:

15193669666308.jpg

  • 汇编语言机器语言一一对应,每一条机器指令都有与之对应的汇编指令
  • 汇编语言可以通过编译得到机器语言机器语言可以通过反汇编得到汇编语言
  • 高级语言可以通过编译得到汇编语言 \ 机器语言,但汇编语言\机器语言几乎不可能还原成高级语言

汇编语言的特点

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

几个必要的常识

  • 要想学好汇编,首先需要了解CPU等硬件结构
  • APP/程序的执行过程

15193672391363.jpg

  • 硬件相关最为重要是CPU/内存
  • 在汇编中,大部分指令都是和CPU与内存相关的

总线

15193692496345.jpg 15193692648349.jpg

  • 每一个CPU芯片都有许多管脚,这些管脚和总线相连,CPU通过总线跟外部器件进行交互

  • 总线:一根根导线的集合

  • 总线的分类

    • 地址总线
    • 数据总线
    • 控制总线

15193693448725.jpg

举个例子

6f6c97846f844e85baaf2ba03df82df9~tplv-k3u1fbpfcp-zoom-1.image.png

  • 地址总线

    • 它的宽度决定了CPU的_寻址能力_
    • 8086的地址总线宽度是_20_,所以寻址能力是_1M_( 2^20 )

  • 数据总线

    • 它的宽度决定了CPU的单次数据传送量,也就是数据_传送速度_
    • 8086的数据总线宽度是_16_,所以单次最大传递_2个字节_的数据
  • 控制总线

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

内存

790f5d65626e4b3085141dc2a0d128e2~tplv-k3u1fbpfcp-zoom-1.image.png

790f5d65626e4b3085141dc2a0d128e2~tplv-k3u1fbpfcp-zoom-1.image.png

ff6ca34daf454a7c9c5e8eef2f4a0f2c~tplv-k3u1fbpfcp-zoom-1.image.png

  • 内存地址空间的大小受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进一

数据的宽度

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

#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]));
    }
}

计算机中常见的数据宽度

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

那么计算机存储数据它会分为有符号数和无符号数.那么关于这个看图就理解了!

15178439312380.jpg

无符号数,直接换算!
有符号数:
正数:  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&寄存器

内部部件之间由总线连接

15193738988252.jpg

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

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

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

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

专题系列文章

1.前知识

2. 基于OC语言探索iOS底层原理

3. 基于Swift语言探索iOS底层原理

关于函数枚举可选项结构体闭包属性方法swift多态原理StringArrayDictionary引用计数MetaData等Swift基本语法和相关的底层原理文章有如下几篇:

其它底层原理专题

1.底层原理相关专题

2.iOS相关专题

3.webApp相关专题

4.跨平台开发方案相关专题

5.阶段性总结:Native、WebApp、跨平台开发三种方案性能比较

6.Android、HarmonyOS页面渲染专题

7.小程序页面渲染专题