一只代码狗的随笔

知其然知其所以然!

0%

UML基础用法

类关系

  1. 泛化(Generalization)
    表现为继承
  2. 实现(Realization)
    表现为实现接口
  3. 关联(Association)
    表现为成员变量,一种强依赖的关系
  4. 聚合(Aggregation)
    一种强关联,关联关系双方是平级的,是个体和个体的关系,聚合关系双方不是平级的,是整体和部分的关系。
  5. 组合(Compostion)
    组合关系是一种强聚合的关系,组合关系与聚合关系的区别在于:聚合关系中部分离开整体仍可存活,组合关系中部分离开整体没有意义
  6. 依赖(Dependency)
    一种使用关系,依赖方可以是参数,也可以是返回值。

各种关系的强弱顺序:泛化 = 实现 > 组合 > 聚合 > 关联 > 依赖

UML

Defining Relationship

对应的是上述类之间的关系,UML中区分的很细致。

Type Description
<|– Inheritance
*– Composition
o– Aggregation
–> Association
Link (Solid)
..> Dependency
..|> Realization
.. Link (Dashed)

Defining a class

UML中使用框形式表示一个类。如下:

  • 第一格表示类/接口名称
    名称,声明的是接口则标注关键字*<>, 抽象类标注<>*,枚举类型<>,service class<>

  • 第二格表示声明的成员变量
    第一个字符表示属性,例如公开,私有。随后是成员类型,最后是名称

  • 第三格表示声明的方法
    方法和成员相似,不同的是有个括号,括号内是参数名称。成员类型字段表示returnType。当没有返回值时returntype可以省略。

Defining member of a class

Type Examples keyword
Public +String owner
+String foo()
+
Private -String owner
-String foo()
-
Protect #String owner
#foo()
#
Package/internal String owner
foo()
~
Static +String owner
+foo()
underline(下划线)
Abstract +foo() italic(斜体)

LearnOpenGL笔记

本文仅为学习OpenGL过程中的学习笔记,OpenGL细节参考https://learnopengl-cn.github.io/。

OpenGL流水线

​ 在gl中坐标系是3D的,但是窗口却是2D像素数组,这导致gl大部分工作都是关于把3D坐标转变为适应屏幕的2D像素。3D坐标转为2D坐标的处理过程是由OpenGL的图形渲染管线(Graphics Pipeline,大多译为管线,指的是一堆原始图形数据途经一个输送管道,期间经过各种变化处理最终出现在屏幕的过程)管理的。图形渲染管线可以被划分为两个主要部分:第一部分把你的3D坐标转换为2D坐标(几何阶段);第二部分是把2D坐标转变为实际的有颜色的像素(光栅化阶段)。

img

着色器

着色器(Shader)是运行在GPU上的小程序。这些小程序为图形渲染管线的某个特定部分而运行。从基本意义上来说,着色器只是一种把输入转化为输出的程序。着色器也是一种非常独立的程序,因为它们之间不能相互通信;它们之间唯一的沟通只有通过输入和输出。着色器程序是由GLSL语言编写,openGL会编译成具体的着色程序。常规的着色器有以下几种。顶点和片段着色器是最常用的着色器程序。

  • 顶点着色器

  • 几何着色器

  • 片段着色器

顶点着色器

顶点着色器作为顶点数据的入口,顶点数据一般包含位置坐标(xyz轴),纹理坐标,法向量(normal),tangent(切线),副切线(bitangent)。

顶点着色器应该接收的是一种特殊形式的输入。client会通过openGL来声明好顶点数据的结构,顶点着色器程序就可以访问到相应偏移量的数据,顶点着色器作为第一个着色器,将处理好的数据传递传递给下一个着色器。关键字in out描述着色器的输入输出。

片段着色器

片段着色器需要生成一个最终输出的颜色。如果你在片段着色器没有定义输出颜色,OpenGL会把你的物体渲染为黑色(或白色)。片段着色器的主要作用就是描述片段的颜色(可以理解为像素点)。实际使用过程中会配合纹理使用,同时也会涉及到光学模型,让渲染的结果更逼真。

片段着色器的输入由上一个着色器提供,一般为顶点着色器。

几何着色器

几何着色器处理的是图元。比如说渲染的方式是三角形,则输入的是3个顶点数据。几何着色器可以在顶点发送到下一着色器阶段之前对它们随意变换。然而,几何着色器最有趣的地方在于,它能够将(这一组)顶点变换为完全不同的图元,并且还能生成比原来更多的顶点。

几何着色器可以实现法向量可视化,毛发等细节效果。

纹理

diffuse,specular,normal,depth

纹理是一个2D图片(甚至也有1D和3D的纹理),它可以用来添加物体的细节;你可以想象纹理是一张绘有砖块的纸,无缝折叠贴合到你的3D的房子上,这样你的房子看起来就像有砖墙外表了。因为我们可以在一张图片上插入非常多的细节,这样就可以让物体非常精细而不用指定额外的顶点

使用纹理坐标获取纹理颜色叫做采样,纹理坐标[0, 1]。

纹理的资源叫做贴图,其实就是图片。常规的贴图有以下几种:

  • 漫反射贴图
  • 镜面光贴图
  • 法线贴图
  • 视差贴图
  • 放射光贴图

具体细节在光照模型中会具体描述,物理的颜色主要取决于反射或射入我们眼睛中颜色决定。

切线空间

在一个不同的坐标空间中进行光照,这个坐标空间里,法线贴图向量总是指向这个坐标空间的正z方向;所有的光照向量都相对与这个正z方向进行变换。这样我们就能始终使用同样的法线贴图,不管朝向问题。这个坐标空间叫做切线空间(tangent space)。

法线贴图中的法线向量定义在切线空间中,在切线空间中,法线永远指着正z方向。切线空间是位于三角形表面之上的空间:法线相对于单个三角形的本地参考框架。它就像法线贴图向量的本地空间;它们都被定义为指向正z方向,无论最终变换到什么方向。使用一个特定的矩阵我们就能将本地/切线空间中的法线向量转成世界或视图空间下,使它们转向到最终的贴图表面的方向。

使用法线贴图也是一种提升你的场景的表现的重要方式。在使用法线贴图之前你不得不使用相当多的顶点才能表现出一个更精细的网格,但使用了法线贴图我们可以使用更少的顶点表现出同样丰富的细节。

坐标系统

标准化设备坐标(Normalized Device Coordinate, NDC)。也就是说,每个顶点的xyz坐标都应该在**-1.01.0**之间,超出这个坐标范围的顶点都将不可见。我们通常会自己设定一个坐标的范围,之后再在顶点着色器中将这些坐标变换为标准化设备坐标。然后将这些标准化设备坐标传入光栅器(Rasterizer),将它们变换为屏幕上的二维坐标或像素。

将坐标变换为标准化设备坐标,接着再转化为屏幕坐标的过程通常是分步进行的,也就是类似于流水线那样子。在流水线中,物体的顶点在最终转化为屏幕坐标之前还会被变换到多个坐标系统(Coordinate System)。将物体的坐标变换到几个过渡坐标系(Intermediate Coordinate System)的优点在于,在这些特定的坐标系统中,一些操作或运算更加方便和容易,这一点很快就会变得很明显。对我们来说比较重要的总共有5个不同的坐标系统:

  • 局部空间(Local Space,或者称为物体空间(Object Space))
  • 世界空间(World Space)
  • 观察空间(View Space,或者称为视觉空间(Eye Space))
  • 裁剪空间(Clip Space)
  • 屏幕空间(Screen Space)

为了将坐标从一个坐标系变换到另一个坐标系,我们需要用到几个变换矩阵,最重要的几个分别是模型(Model)、观察(View)、投影(Projection)三个矩阵。有了上述3个变化矩阵,就可以实现旋转,平移等效果。

光照模型

冯氏光照模型(Phong Lighting Model)。冯氏光照模型的主要结构由3个分量组成:环境(Ambient)、漫反射(Diffuse)和镜面(Specular)光照。物体的颜色则为3个分量的向量和。

  • 环境光照(Ambient Lighting):即使在黑暗的情况下,世界上通常也仍然有一些光亮(月亮、远处的光),所以物体几乎永远不会是完全黑暗的。为了模拟这个,我们会使用一个环境光照常量,它永远会给物体一些颜色。
  • 漫反射光照(Diffuse Lighting):模拟光源对物体的方向性影响(Directional Impact)。它是冯氏光照模型中视觉上最显著的分量。物体的某一部分越是正对着光源,它就会越亮。
  • 镜面光照(Specular Lighting):模拟有光泽物体上面出现的亮点。镜面光照的颜色相比于物体的颜色会更倾向于光的颜色。

漫反射和镜面光照都和片段表面的法向量和光线向量的夹角相关,需要通过计算获得具体的计算因子。

除冯氏光照模型外,在冯氏模型的拓展还有Blinn-Phong着色模型,引入了半程向量概念,在镜面反射分量上有一个更好的表现。

3D模型数据

3D文件通常包括网格模型、UV纹理、材质贴图。常见的3D模型格式有如下几种

  • OBJ,适用于3D软件模型之间的互导,主要支持多边形模型。
  • DAE,纯文本的模型格式,其本质是一个xml文件,可以实现动态模型。
  • FBX,最好的互导方案
  • STL,文件格式简单, 应用非常广泛

一个非常流行的模型导入库是Assimp。当使用Assimp导入一个模型的时候,它通常会将整个模型加载进一个场景(Scene)对象,它会包含导入的模型/场景中的所有数据。Assimp会将场景载入为一系列的节点(Node),每个节点包含了场景对象中所储存数据的索引,每个节点都可以有任意数量的子节点。Assimp数据结构的(简化)模型如下:

img

  • 和材质和网格(Mesh)一样,所有的场景/模型数据都包含在Scene对象中。Scene对象也包含了场景根节点的引用。
  • 场景的Root node(根节点)可能包含子节点(和其它的节点一样),它会有一系列指向场景对象中mMeshes数组中储存的网格数据的索引。Scene下的mMeshes数组储存了真正的Mesh对象,节点中的mMeshes数组保存的只是场景中网格数组的索引。
  • 一个Mesh对象本身包含了渲染所需要的所有相关数据,像是顶点位置、法向量、纹理坐标、面(Face)和物体的材质。
  • 一个网格包含了多个面。Face代表的是物体的渲染图元(Primitive)(三角形、方形、点)。一个面包含了组成图元的顶点的索引。由于顶点和索引是分开的,使用一个索引缓冲来渲染是非常简单的(见[你好,三角形](https://learnopengl-cn.github.io/01 Getting started/04 Hello Triangle/))。
  • 最后,一个网格也包含了一个Material对象,它包含了一些函数能让我们获取物体的材质属性,比如说颜色和纹理贴图(比如漫反射和镜面光贴图)。

学习网站

https://learnopengl-cn.github.io/

ARM学习笔记

在xcode中,选中文件后Product->Perform Action->Assemble xxx.m就能看到汇编代码,可以更好的了解代码的执行过程。这里简单介绍下常用的一些命令(instruction)。

寄存器

x0-x30 64bit 通用寄存器,如果有需要可以当做32bit使用:WO-W30
FP(x29) 64bit 保存栈帧地址(栈底指针)。栈基址。函数调用的时候的起始栈地址
LR(x30) 64bit 通常称X30为程序链接寄存器,保存子程序结束后需要执行的下一条指令(上一次函数调用的下一条指令地址),即返回地址。
SP 64bit 保存栈指针,使用 SP/WSP来进行对SP寄存器的访问。
PC 64bit 程序计数器,俗称PC指针,总是指向即将要执行的下一条指令,在arm64中,软件是不能改写PC寄存器的。
CPSR 64bit 状态寄存器

状态寄存器条件码

操作码 条件码助记符 标志 含义
0000 EQ Z=1 相等
0001 NE(Not Equal) Z=0 不相等
0010 CS/HS(Carry Set/High or Same) C=1 无符号数大于或等于
0011 CC/LO(Carry Clear/LOwer) C=0 无符号数小于
0100 MI(MInus) N=1 负数
0101 PL(PLus) N=0 正数或零
0110 VS(oVerflow set) V=1 溢出
0111 VC(oVerflow clear) V=0 没有溢出
1000 HI(High) C=1,Z=0 无符号数大于
1001 LS(Lower or Same) C=0,Z=1 无符号数小于或等于
1010 GE(Greater or Equal) N=V 有符号数大于或等于
1011 LT(Less Than) N!=V 有符号数小于
1100 GT(Greater Than) Z=0,N=V 有符号数大于
1101 LE(Less or Equal) Z=1,N!=V 有符号数小于或等于
1110 AL 任何 无条件执行(默认)
1111 NV 任何 从不执行

指令与伪指令

指令有对应的机器码,CPU可以直接识别并执行。而伪指令,没有对应的机器码,它需要经过编译器翻译成指令才能被CPU识别和执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
ldr r1, =0xfff //伪指令
.text
.global _start
_start:
@这是一条伪指令,没有对应的机器码,被编译器翻译为LDR R0,[PC,#0x0008]
LDR R0, =var ; int * R0 = var
@这是一条指令
LDR R1, [R0] ; int R1 = *R0
@指令有对应的机器码,编译器原样执行
MOV R1, R0
NOP
NOP
.data
var: int * var; *var = 0x8;
.word 0x8
.end

常见指令

mov

将某一寄存器的值复制到另一寄存器(只能用于寄存器与寄存器或者寄存器与常量之间传值,不能用于内存地址),如:

1
mov x1, x0    ; 将寄存器x0的值赋值给x1

str&stur

存储指令,(store register) 将寄存器中的值写入到内存中。str和stur都是存储指令,区别在寻址的偏移量正负之分。如:

1
2
str x0, [sp, #0x8]    ; 将寄存器 x0 中的值保存到栈内存 [sp + 0x8] 处 
stur x0, [x29, #-0x8] ; 将寄存器x0中的值保存到栈内存 [x29 - 0x8] 处

ldr&ldur

读取指令(load register);将内存地址的值读取到寄存器中。ldr和ldur的区别与上述存储指令相同,ldr用于正偏移的地址运算,ldur用于负地址。如:

1
2
ldr x0, [sp, #0x8]    ; 将栈地址sp+0x8的值读取到x0寄存器中
ldur x0, [x29, #-0x8] ; 将栈地址x29-0x8的值读取到x0寄存器中

stp

入栈指令(str 的变种指令,可以同时操作两个寄存器),如:

1
stp x29, x30, [sp, #0x10] 	; 将 x29, x30 的值存入 sp 偏移 16 个字节的位置 

ldp

出栈指令(ldr 的变种指令,可以同时操作两个寄存器),如:

1
ldp x29, x30, [sp, #0x10] 	; 将 sp 偏移 16 个字节的值取出来,存入寄存器 x29 和寄存器 x30 

adrp

用来定位数据段中的数据用, 因为 aslr 会导致代码及数据的地址随机化, 用 adrp 来根据 pc 做辅助定位

1
2
adrp	x8, _OBJC_SELECTOR_REFERENCES_@PAGE    ;将'_OBJC_SELECTOR_REFERENCES_'所在section的起始地址保存到x8寄存器中
ldr x1, [x8, _OBJC_SELECTOR_REFERENCES_@PAGEOFF] ; 加上偏移量做一个寻址操作,并将地址保存到x1寄存器中。

sub

减法指令;将两寄存器的值相减 并将结果保存到指定寄存器中,如:

1
sub x0, x0, #0x8    ; x0 = x0 - 0x8

add

加法指定;如:

1
add x0, x0, #0x1    ; x0 = x0 + 0x1

sdiv

除法运算指令;如:

1
sdiv x0, x1, x2       ; x0 = x1 / x2

mul

乘法运算指令;如:

1
mul x0, x1, x2        ; x0 = x1 * x2

and

按位与。如:

1
and x0, x0, #0xf      ; x0 = x0 & 0xf

orr

按位或。如:

1
orr x0, x0, #0xf      ; x0 = x0 | 0xf

eor

按位异或。如:

1
eor x0, x0, #0xf      ; x0 = x0 ^ 0xf

LSL

逻辑左移

LSR

逻辑右移

ASR

算术右移

ROR

循环右移

1
cbz	w8, LBB11_2    ; 如果w8寄存器的值为0,则跳转到‘LBB11_2’处

cbnz

和非 0 比较(Compare),如果结果非零(Non Zero)就转移(只能跳到后面的指令);

1
cbnz	w8, LBB11_2    ; 如果w8寄存器的值不等于0则跳转到‘LBB11_2’label处。

subs

比较指令;

1
subs x0, x1, x2    ; x0 = x1 - x2, 并设置 CPSR 寄存器的 C 标志位

cmp

比较指令,相当于 subs,影响程序状态寄存器CPSR ;

cset

比较指令,满足条件,则并置 1,否则置 0 ,如:

1
2
cmp w8, #2        ; 将寄存器 w8 的值和常量 2 进行比较
cset w8, gt ; 如果是大于(grater than),则将寄存器 w8 的值设置为 1,否则设置为 0

b

(branch)跳转到某地址(无返回), 不会改变 lr (x30) 寄存器的值,只改变pc寄存的值;一般是本方法内的跳转,如 while 循环,if else 等 ,如:

1
b LBB0_1      ; 直接跳转到标签 ‘LLB0_1’ 处开始执行

b.le(条件码)

1
b.le	LBB11_2     ; CPSR寄存器 C=0,则跳转‘LBB11_2’

br

功能同上b指令。不同的是跳转的地址由寄存器传递。

1
br x8    ; x8寄存器的值作为跳转地址

bl

call指令。跳转到某地址(有返回),先将下一指令地址(即函数返回地址)保存到寄存器 lr (x30)中,再进行跳转 ;一般用于不同方法直接的调用 ,如:

1
bl 0x100cfa754	; 先将下一指令地址(‘0x100cfa754’ 函数调用后的返回地址)保存到寄存器 ‘lr’ 中,然后再调用 ‘0x100cfa754’ 函数

blr

跳转到 某寄存器 (的值)指向的地址(有返回),先将下一指令地址(即函数返回地址)保存到寄存器 lr (x30)中,再进行跳转;与bl不同的是,bl指令的内容具体的地址,blr则是读取寄存器内的值。如:

1
blr x20       ; 先将下一指令地址(‘x20’指向的函数调用后的返回地址)保存到寄存器 ‘lr’ 中,然后再调用 ‘x20’ 指向的函数

fcvtzs

(Float Convert To Zero Signed)浮点数 转化为 定点数 (舍入为0),如:

1
2
fcvtzs w0, s0	    ; 将向量寄存器 s0 的值(浮点数,转换成 定点数)保存到寄存器 w0 中
复制代码

cbz

和 0 比较(Compare),如果结果为零(Zero)就转移(只能跳到后面的指令);

ret

子程序(函数调用)返回指令,返回地址已默认保存在寄存器 lr (x30) 中

Label

在汇编代码中常常会看到LBB0_2: ,这样的字样。LBB0_2称之为local label,在跳转中常常会用到

LBB0_2are local labels, which are normally used as branch destinations within a function.

1
b.ge	LBB11_2    ; CPSR的标志是ge时,pc寄存器置为LBB11_2处的地址

注释

ARM中的注释,如:

  1. “@”符号作为注释可以放在语句的开始处
  2. “;”作为流程只能放在语句的末尾
1
sub	sp, sp, #32                     ; =32

伪指令

数据定义伪操作

数据定义伪操作一般用于为特定的数据分配内存单元,同时对该内存单元中的数据进行初始化,觉的数据定义伪操作有:

.byte

在存储器中分配 一个字节的内存单元,用指定的数据对该存储单元进行初始化。

1
.byte 0x1

在当前地址分配一个字节的存储单元,将将其初始化为1,类似于C语言中的char _label = 1.

.short

在存储器中分配2个字节的内存单元,并用指定的数据对该存储单元进行初始化,用于与.byte类似

.word

在存储器中分配4个字节的内存单元,并用指定的数据对该存储单元进行初始化,用于与.byte类似

.long

与.word的功能相同

.quad

.quad的功能是在内存中分配8个字节的存储单元,并用指定的数据对该存储单元进行初始化。

.float

在存储器中分配4个字节的存储空间,并用指定的浮点数据对该空间进行初始化。

.space

.space伪操作用于分配一片连续的内存区域,并将其初始化为指定的值,如果后面的填充值省略不写,则默认在后面填充0

.skip

等同与.space

.string, ascii, .asciz

这3条伪操作的功能都是定义一个字符串:

1
2
_label:
.string "Hello, World!"

.rept

.rept伪操作功能是 重复执行后面的指令,以.rept开始,并以.endr结束,用法:

1
2
3
.rept 3
add r1, r1, #1
.endr

杂项操作伪指令

GNU汇编中还有一些其他的伪操作,在汇编程序中经常会使用到它们,包括而在这些:

.align

.align伪操作可通过添加填充字节的试,使当前位置满足指定的对齐方式。举例:

1
2
.align 2
.string "abcde"

声明后面的字符串的对齐方式是4(2的2次方)字节对齐,这个字符串会占用8个字节的存储空间。

.section

.section伪操作用于定义一个段,一个GNU的源程序至少需要一个段,大的程序可以包含多个代码段和数据段。

可能用来定义自定义段

1
.section	__DATA,__const    ; 表示数据所在的segment和section

.data

.data伪操作用来定义一个数据段

.text

.text伪操作用来定义一个数据段

.include

.include 伪操作用来包含一个头文件。

.extern

.extern用于声明一个外部符号,即告诉编译器当前符号不是在本源文件中定义的,而是在其它源文件中定义的,当前文件需要引用这个符号。

.weak

.weak用来声明一个符号是弱符号,即如果这个符号没有定义,编译器就会忽略,不会报错。

.end

.end代表程序的结束位置

.globl

.globl "___block_descriptor_40_e8_32w_e5_v8?0l"表示一个全局的符号

其他

栈对齐

虽然该函数没有临时变量,但是调用 printf 函数后,编译器自动会加上 该函数返回值 的处理,由于 arm64 规定了整数型返回值放在 x0 寄存器里,因此会隐藏有一个局部变量 int return_value; 的声明在,该临时变量占用 4字节空间;又因为 arm64 下对于使用 sp 作为地址基址寻址的时候,必须要 16byte-alignment(对齐),所以申请了 16字节空间作为临时变量使用。具体参见 这里

参考

arm64 架构之入栈/出栈操作

汇编quad_ARM汇编

block是什么

源码:

1
2
3
4
5
- (void)foo2 {
self.myBlock = ^{
[self foo];
};
}

Assemble后:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
"-[Foo foo2]":                          ; @"\01-[Foo foo2]"
Lfunc_begin4:
; %bb.0:
sub sp, sp, #96 ; =96
stp x29, x30, [sp, #80] ; 16-byte Folded Spill
add x29, sp, #80 ; =80
stur x0, [x29, #-8]
stur x1, [x29, #-16]
add x8, sp, #24 ; =24
str x8, [sp, #8] ; 8-byte Folded Spill
Ltmp12:
adrp x9, __NSConcreteStackBlock@GOTPAGE
ldr x9, [x9, __NSConcreteStackBlock@GOTPAGEOFF]
str x9, [sp, #24]
mov w9, #-1040187392
str w9, [sp, #32]
str wzr, [sp, #36]
adrp x9, "___11-[Foo foo2]_block_invoke"@PAGE
add x9, x9, "___11-[Foo foo2]_block_invoke"@PAGEOFF
str x9, [sp, #40]
adrp x9, "___block_descriptor_40_e8_32s_e5_v8?0l"@PAGE
add x9, x9, "___block_descriptor_40_e8_32s_e5_v8?0l"@PAGEOFF
str x9, [sp, #48]
add x8, x8, #32 ; =32
str x8, [sp, #16] ; 8-byte Folded Spill
ldur x0, [x29, #-8]
bl _objc_retain
ldr x2, [sp, #8] ; 8-byte Folded Reload
str x0, [sp, #56]
ldur x0, [x29, #-8]
adrp x8, _OBJC_SELECTOR_REFERENCES_.2@PAGE
ldr x1, [x8, _OBJC_SELECTOR_REFERENCES_.2@PAGEOFF]
bl _objc_msgSend
ldr x0, [sp, #16] ; 8-byte Folded Reload
mov x1, #0
bl _objc_storeStrong
ldp x29, x30, [sp, #80] ; 16-byte Folded Reload
add sp, sp, #96 ; =96
ret
Ltmp13:
Lfunc_end4:
; -- End function
.p2align 2 ; -- Begin function __11-[Foo foo2]_block_invoke
"___11-[Foo foo2]_block_invoke": ; @"__11-[Foo foo2]_block_invoke"
Lfunc_begin5:
; %bb.0:
sub sp, sp, #32 ; =32
stp x29, x30, [sp, #16] ; 16-byte Folded Spill
add x29, sp, #16 ; =16
str x0, [sp, #8]
str x0, [sp]
Ltmp14:
ldr x0, [x0, #32]
adrp x8, _OBJC_SELECTOR_REFERENCES_@PAGE
ldr x1, [x8, _OBJC_SELECTOR_REFERENCES_@PAGEOFF]
bl _objc_msgSend
Ltmp15:
ldp x29, x30, [sp, #16] ; 16-byte Folded Reload
add sp, sp, #32 ; =32
ret
Ltmp16:
Lfunc_end5:
; -- End function

这里假设起始sp=0x60,方便后续计算。依次解释指令的执行过程和寄存器,栈的值。

1
2
3
4
5
sub	sp, sp, #96                     ; =96
stp x29, x30, [sp, #80] ; 16-byte Folded Spill
add x29, sp, #80 ; =80
stur x0, [x29, #-8]
stur x1, [x29, #-16]
  1. sub指令;sp = sp - 0x60 => sp = 0x00
  2. store pair指令; x29, x30寄存器的值存到sp+0x50=0x50的地址,x29,x30表示fp,lr寄存器。
  3. add指令;fp = sp + 0x50 = 0x50
  4. store指令;x0寄存器的值存到[x29, #-8]=0x50-0x8=0x48的地址。x0=self
  5. 同上,x1存到0x40的地址。x1=@selector(foo2)

此时寄存器和栈的状态如下

寄存器
x0 self
x1 @selector(foo2)
fp 0x50
sp 0x00
栈地址
0x60 上个函数的lr
0x58 上个函数的fp
0x50 self
0x48 @selector(foo2)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
	add	x8, sp, #24                     ; =24
str x8, [sp, #8] ; 8-byte Folded Spill
Ltmp12:
adrp x9, __NSConcreteStackBlock@GOTPAGE
ldr x9, [x9, __NSConcreteStackBlock@GOTPAGEOFF]
str x9, [sp, #24]
mov w9, #-1040187392
str w9, [sp, #32]
str wzr, [sp, #36]
adrp x9, "___11-[Foo foo2]_block_invoke"@PAGE
add x9, x9, "___11-[Foo foo2]_block_invoke"@PAGEOFF
str x9, [sp, #40]
adrp x9, "___block_descriptor_40_e8_32s_e5_v8?0l"@PAGE
add x9, x9, "___block_descriptor_40_e8_32s_e5_v8?0l"@PAGEOFF
str x9, [sp, #48]
  1. x8 = sp + 24 = 0x18
  2. x8寄存器的值存到0x08的地址([sp, #8] = 0x00 + 0x8)
  3. adrp是针对aslr技术,获取偏移后的地址;将__NSConcreteStackBlock的isa读取到x9寄存器
  4. 将x9寄存器的值保存到地址0x18
  5. w9(4字节)赋值#-1040187392
  6. w9寄存器的值存到地址0x20
  7. wzr(word zero register),地址0x24写入4字节的0。
  8. 同上,x9寄存器赋值”___11-[Foo foo2]_block_invoke”的地址
  9. x9寄存器的值存到地址0x28
  10. 同上将”___block_descriptor_40_e8_32s_e5_v8?0l”的地址存到地址0x30
寄存器
x0 self
x1 @selector(foo2)
fp 0x50
sp 0x00
x8 0x18
栈地址
0x60 上个函数的lr
0x58 上个函数的fp
0x50 self
0x48 @selector(foo2)
0x30 “___block_descriptor_40_e8_32s_e5_v8?0l”
0x28 “___11-[Foo foo2]_block_invoke”
0x20 -1040187392; 0
0x18 __NSConcreteStackBlock
0x10
0x08 0x18
0x00
1
2
3
4
5
6
7
8
9
10
11
12
13
add	x8, x8, #32                     ; =32
str x8, [sp, #16] ; 8-byte Folded Spill
ldur x0, [x29, #-8]
bl _objc_retain
ldr x2, [sp, #8] ; 8-byte Folded Reload
str x0, [sp, #56]
ldur x0, [x29, #-8]
adrp x8, _OBJC_SELECTOR_REFERENCES_.2@PAGE
ldr x1, [x8, _OBJC_SELECTOR_REFERENCES_.2@PAGEOFF]
bl _objc_msgSend
ldr x0, [sp, #16] ; 8-byte Folded Reload
mov x1, #0
bl _objc_storeStrong
  1. x8 = x8 + 0x20 = 0x38
  2. x8寄存器值存到地址0x10
  3. x0 = self
  4. 调用_objc_retain函数
  5. x2 = 0x18
  6. x0寄存器值存到地址0x38
  7. OBJC_SELECTOR_REFERENCES.2指的是@selector(setMyBlock:)。x1 = @selector(setMyBlock:)
  8. 调用_objc_msgSend函数
  9. 地址0x10的值读取到x0寄存器
  10. x1 = 0
  11. 调用_objc_storeStrong
寄存器
x0 self
x1 @selector(foo2)
fp 0x50
sp 0x00
x8 0x18
栈地址
0x60 上个函数的lr
0x58 上个函数的fp
0x50 self
0x48 @selector(foo2)
0x38 self
0x30 “___block_descriptor_40_e8_32s_e5_v8?0l”
0x28 “___11-[Foo foo2]_block_invoke”
0x20 -1040187392; 0
0x18 __NSConcreteStackBlock
0x10 0x38
0x08 0x18
0x00
1
2
3
ldp	x29, x30, [sp, #80]             ; 16-byte Folded Reload
add sp, sp, #96 ; =96
ret
  1. 恢复上个函数的fp,lr寄存器值
  2. sp -= 0x60
  3. return

上述就是foo2函数的指令执行过程, 可以发现block初始化的时候是分配在栈空间,block也是个对象,有isa指正,这里是__NSConcreteStackBlock类。

通过clang -rewrite-objc Foo.m可以看到block结构体的声明,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#ifndef BLOCK_IMPL
#define BLOCK_IMPL
struct __block_impl {
void *isa;
int Flags;
int Reserved;
void *FuncPtr;
};

struct __Foo__foo2_block_impl_0 {
struct __block_impl impl;
struct __Foo__foo2_block_desc_0* Desc;
Foo *const __strong self;
__Foo__foo2_block_impl_0(void *fp, struct __Foo__foo2_block_desc_0 *desc, Foo *const __strong _self, int flags=0) : self(_self) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
栈地址
0x38 self
0x30 “___block_descriptor_40_e8_32s_e5_v8?0l”
0x28 “___11-[Foo foo2]_block_invoke”
0x20 -1040187392; 0
0x18 __NSConcreteStackBlock

跟栈去的内存分配完全吻合。从低地址到高地址依次为isa,Flags,Reserved,FuncPtr,Desc,self。其中self就是block捕获的变量。

接着看下block_descriptor到底是什么。

1
2
3
4
5
6
7
8
9
10
11
12
	.private_extern	"___block_descriptor_40_e8_32s_e5_v8?0l" ; @"__block_descriptor_40_e8_32s_e5_v8\01?0l"
.section __DATA,__const
.globl "___block_descriptor_40_e8_32s_e5_v8?0l"
.weak_def_can_be_hidden "___block_descriptor_40_e8_32s_e5_v8?0l"
.p2align 3
"___block_descriptor_40_e8_32s_e5_v8?0l":
.quad 0 ; 0x0
.quad 40 ; 0x28
.quad ___copy_helper_block_e8_32s
.quad ___destroy_helper_block_e8_32s
.quad l_.str
.quad 256 ; 0x100

可以看到___block_descriptor_40_e8_32s_e5_v8在data段。

cpp的结构如下

1
2
3
4
5
6
static struct __Foo__foo_block_desc_0 {
size_t reserved;
size_t Block_size;
void (*copy)(struct __Foo__foo_block_impl_0*, struct __Foo__foo_block_impl_0*);
void (*dispose)(struct __Foo__foo_block_impl_0*);
} __Foo__foo_block_desc_0_DATA = { 0, sizeof(struct __Foo__foo_block_impl_0), __Foo__foo_block_copy_0, __Foo__foo_block_dispose_0};

结合__Foo__foo_block_desc_0的结构,可以得到reserved=0,Block_size=40。跟上述栈的内存分配吻合。而后则是copy和destory函数。

为什么会产生循环引用

1
bl	_objc_retain 			; x0 = self

上述汇编代码中可以看到在构建block时,self被调用retain。从而导致了循环引用的问题出现。

__weak是如何解除循环引用的?

接着上述代码加上weak修饰符,如下👇🏻。

1
2
3
4
5
6
- (void)foo {
__weak Foo *wself = self;
self.myBlock = ^{
[wself foo];
};
}

assemble:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
"-[Foo foo]":                           ; @"\01-[Foo foo]"
Lfunc_begin0:
; %bb.0:
sub sp, sp, #128 ; =128
stp x29, x30, [sp, #112] ; 16-byte Folded Spill
add x29, sp, #112 ; =112
stur x0, [x29, #-8]
stur x1, [x29, #-16]
Ltmp3:
ldur x1, [x29, #-8]
sub x0, x29, #24 ; =24
str x0, [sp, #8] ; 8-byte Folded Spill
bl _objc_initWeak
ldr x1, [sp, #8] ; 8-byte Folded Reload
add x8, sp, #48 ; =48
str x8, [sp, #24] ; 8-byte Folded Spill
adrp x9, __NSConcreteStackBlock@GOTPAGE
ldr x9, [x9, __NSConcreteStackBlock@GOTPAGEOFF]
str x9, [sp, #48]
mov w9, #-1040187392
str w9, [sp, #56]
str wzr, [sp, #60]
adrp x9, "___10-[Foo foo]_block_invoke"@PAGE
add x9, x9, "___10-[Foo foo]_block_invoke"@PAGEOFF
str x9, [sp, #64]
adrp x9, "___block_descriptor_40_e8_32w_e5_v8?0l"@PAGE
add x9, x9, "___block_descriptor_40_e8_32w_e5_v8?0l"@PAGEOFF
str x9, [sp, #72]
add x0, x8, #32 ; =32
str x0, [sp, #16] ; 8-byte Folded Spill
bl _objc_copyWeak
ldr x2, [sp, #24] ; 8-byte Folded Reload
ldur x0, [x29, #-8]
adrp x8, _OBJC_SELECTOR_REFERENCES_.2@PAGE
ldr x1, [x8, _OBJC_SELECTOR_REFERENCES_.2@PAGEOFF]
Ltmp0:
bl _objc_msgSend
Ltmp1:
; %bb.1:
ldr x0, [sp, #16] ; 8-byte Folded Reload
bl _objc_destroyWeak
sub x0, x29, #24 ; =24
bl _objc_destroyWeak
ldp x29, x30, [sp, #112] ; 16-byte Folded Reload
add sp, sp, #128 ; =128
ret

从汇编中可以看到,在初始化wself的时候调用了objc_initWeak函数,在构建block结构的时候使用了objc_copyWeak。没有使用objc_retain,因此self的引用计数没有加1。从而没有循环引用的出现。

block内为什么需要__strong呢?

看👇🏻代码,block中两次引用了wself。

1
2
3
4
5
6
7
- (void)foo {
__weak Foo *wself = self;
self.myBlock = ^{
[wself foo1];
[wself foo1];
};
}

assemble:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
"___10-[Foo foo]_block_invoke":         ; @"__10-[Foo foo]_block_invoke"
Lfunc_begin1:
; %bb.0:
sub sp, sp, #64 ; =64
stp x29, x30, [sp, #48] ; 16-byte Folded Spill
add x29, sp, #48 ; =48
str x0, [sp, #8] ; 8-byte Folded Spill
stur x0, [x29, #-8]
stur x0, [x29, #-16]
Ltmp8:
add x0, x0, #32 ; =32
bl _objc_loadWeakRetained
str x0, [sp] ; 8-byte Folded Spill
adrp x8, _OBJC_SELECTOR_REFERENCES_.2@PAGE
str x8, [sp, #16] ; 8-byte Folded Spill
ldr x1, [x8, _OBJC_SELECTOR_REFERENCES_.2@PAGEOFF]
bl _objc_msgSend
ldr x0, [sp] ; 8-byte Folded Reload
bl _objc_release
ldr x0, [sp, #8] ; 8-byte Folded Reload
add x0, x0, #32 ; =32
bl _objc_loadWeakRetained
ldr x8, [sp, #16] ; 8-byte Folded Reload
str x0, [sp, #24] ; 8-byte Folded Spill
ldr x1, [x8, _OBJC_SELECTOR_REFERENCES_.2@PAGEOFF]
bl _objc_msgSend
ldr x0, [sp, #24] ; 8-byte Folded Reload
bl _objc_release
Ltmp9:
ldp x29, x30, [sp, #48] ; 16-byte Folded Reload
add sp, sp, #64 ; =64
ret
1
2
3
4
5
6
7
8
bl	_objc_loadWeakRetained
str x0, [sp] ; 8-byte Folded Spill
adrp x8, _OBJC_SELECTOR_REFERENCES_.2@PAGE
str x8, [sp, #16] ; 8-byte Folded Spill
ldr x1, [x8, _OBJC_SELECTOR_REFERENCES_.2@PAGEOFF]
bl _objc_msgSend
ldr x0, [sp] ; 8-byte Folded Reload
bl _objc_release

从这块片段可以看到在引用wself时,先调用了objc_loadWeakRetained,refcnt + 1。然后通过objc_msgSend调用了具体的方法,接着调用了objc_release,refcnt - 1。可以看到苹果底层在引用weak时还是比较严谨的。

可以看到在引用两次wself时,每次引用都会先调用objc_loadWeakRetained,紧接着调用objc_msgSend。在多线程的场景下,两次引用中间self存在被释放的情况,当self被释放后,wself就会被weak系统机制置为nil,从而导致在block的执行过程中self被释放,从而导致一些错误。

接着看一下使用strong修饰后的结果。

1
2
3
4
5
6
7
8
- (void)foo {
__weak Foo *wself = self;
self.myBlock = ^{
__strong Foo *sself = wself;
[sself foo];
[sself foo];
};
}

assemble:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
"___10-[Foo foo]_block_invoke":         ; @"__10-[Foo foo]_block_invoke"
Lfunc_begin1:
; %bb.0:
sub sp, sp, #64 ; =64
stp x29, x30, [sp, #48] ; 16-byte Folded Spill
add x29, sp, #48 ; =48
stur x0, [x29, #-8]
stur x0, [x29, #-16]
Ltmp5:
add x0, x0, #32 ; =32
bl _objc_loadWeakRetained
add x8, sp, #24 ; =24
str x8, [sp, #16] ; 8-byte Folded Spill
str x0, [sp, #24]
ldr x0, [sp, #24]
adrp x8, _OBJC_SELECTOR_REFERENCES_@PAGE
str x8, [sp, #8] ; 8-byte Folded Spill
ldr x1, [x8, _OBJC_SELECTOR_REFERENCES_@PAGEOFF]
bl _objc_msgSend
ldr x8, [sp, #8] ; 8-byte Folded Reload
ldr x0, [sp, #24]
ldr x1, [x8, _OBJC_SELECTOR_REFERENCES_@PAGEOFF]
bl _objc_msgSend
ldr x0, [sp, #16] ; 8-byte Folded Reload
mov x1, #0
Ltmp6:
bl _objc_storeStrong
ldp x29, x30, [sp, #48] ; 16-byte Folded Reload
add sp, sp, #64 ; =64
ret

在加上strong修饰后,因为后续代码中都在引用sself,所以sself在最后才通过objc_storeStrong(&sself, nil)的方式进行一次release。从而也保证了在block调用过程中,self的refcnt一直是+1的状态,在block执行过程中不存在被提前释放的情况。

Mach-O混淆原理

混淆流程

1. 获取Mach-O

解压ipa包,获取到可执行文件即Mach-O文件。

2. 修改类名

修改Mach-O文件中的__objc_classname section中的类名字符串为等长度的替换字符串; 为了保证使用NSClassFromString函数可以正确的获取到原始类,这里记录修改以前的字符映射关系后续使用。

3. 重签名

使用codesign,将ipa重签名

原理解析

这里拿最简单的一个例子做介绍,源码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#import <Foundation/Foundation.h>
#import "Animal.h"
#import "Man.h"

void foo(void);

int main(int argc, const char * argv[]) {
@autoreleasepool {
[[Man alloc] init];

[[Animal alloc] init];

foo();
}
return 0;
}

void foo() {
Class cls = NSClassFromString(@"Man");
}

代码中声明了两个类Animal和Man。然后main文件中调用了两个类个构造方法。很简单是吧~

接下来,将项目打包,就可以获取到可执行文件了。

类名的作用

想想程序运行中什么时候会用到类名?

程序在装载的时候构建了一个NXMapTable——gdb_objc_realized_classes,用于存放类,key是类名。runtime中objc_getClass函数就是从这个表中获取到对应名称的类。

那么代码 [[Man alloc] init];到底执行了什么?

Disassembly

使用hopper查看反编译后的汇编代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
0000000100003eaf         push       rbp
0000000100003eb0 mov rbp, rsp
0000000100003eb3 push r14
0000000100003eb5 push rbx
0000000100003eb6 call imp___stubs__objc_autoreleasePoolPush
0000000100003ebb mov r14, rax
0000000100003ebe mov rdi, qword [objc_cls_ref_Man]
0000000100003ec5 call imp___stubs__objc_alloc_init
0000000100003eca mov rbx, qword [_objc_release_100004000]
0000000100003ed1 mov rdi, rax ; argument "instance" for method _objc_release
0000000100003ed4 call rbx ; _objc_release
0000000100003ed6 mov rdi, qword [objc_cls_ref_Animal]
0000000100003edd call imp___stubs__objc_alloc_init
0000000100003ee2 mov rdi, rax ; argument "instance" for method _objc_release
0000000100003ee5 call rbx ; _objc_release
0000000100003ee7 call sub_100003efb
0000000100003eec mov rdi, r14 ; argument "pool" for method imp___stubs__objc_autoreleasePoolPop
0000000100003eef call imp___stubs__objc_autoreleasePoolPop
0000000100003ef4 xor eax, eax
0000000100003ef6 pop rbx
0000000100003ef7 pop r14
0000000100003ef9 pop rbp
0000000100003efa ret
; endp

可以看到在使用alloc] init]创建一个Man对象的时候,__objc_alloc_init函数的入参是objc_cls_ref_Man——类的引用。同命令行使用MachOView看到的内容如下:

1
2
100003ebe    movq 0x42f3(%rip), %rdi
100003ec5 callq "[0x100003f26->__objc_alloc_init]"

根据相对寻址得出rdi寄存器的内容为0x10003ec5 + 0x42f3 = 0x100082b8。该地址的内容位于__objc_classrefs section, 如下:

1
1000081b8 0x1000081f8

指向的是__objc_data section,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
00000001000081d0         struct __objc_class {                                  ; DATA XREF=__objc_class_Man_class
_OBJC_METACLASS_$_NSObject, // metaclass
_OBJC_METACLASS_$_NSObject, // superclass
__objc_empty_cache, // cache
0x0, // vtable
__objc_metaclass_Man_data // data
}
;
; @class Man : NSObject {
; }
__objc_class_Man_class:
00000001000081f8 struct __objc_class { ; DATA XREF=0x100004030, objc_cls_ref_Man
__objc_metaclass_Man_metaclass, // metaclass
_OBJC_CLASS_$_NSObject, // superclass
__objc_empty_cache, // cache
0x0, // vtable
__objc_class_Man_data // data
}
;
; @metaclass Animal {
; }

到这里已经是Man类本身了。由此可见,类的创建跟classname无关(旧版本的sdk编译的时候alloc,init也是通过objc_msgsend调用的)。

那么类名是什么时候被使用呢,然后在看下__objc_class_Man_data

1
2
3
4
5
6
7
8
9
10
11
12
13
14
 __objc_class_Man_data:
0000000100008068 struct __objc_data { ; "Man", DATA XREF=__objc_class_Man_class
0x90, // flags
8, // instance start
8, // instance size
0x0,
0x0, // ivar layout
0x100003f70, // name
0x0, // base methods
0x0, // base protocols
0x0, // ivars
0x0, // weak ivar layout
0x0 // base properties
}

再看看name地址0x100003f70,该地址位于__objc_classname section。

1
100003f70 4d 61 6e 00 ... # Man.

yes, name的地址的内容即为”Man”,寻找的类名。

NSClassFromString

让我们再来看看NSClassFromString方式拿到的类的汇编:

Hopper:

1
2
3
4
5
6
7
8
9
10
        ; ================ B E G I N N I N G   O F   P R O C E D U R E ================


sub_100003efb:
0000000100003efb push rbp ; CODE XREF=EntryPoint+56
0000000100003efc mov rbp, rsp
0000000100003eff lea rdi, qword [cfstring_Man] ; @"Man", argument "aClassName" for method imp___stubs__NSClassFromString
0000000100003f06 pop rbp
0000000100003f07 jmp imp___stubs__NSClassFromString
; endp

再看看cfstring_Man

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
        ; Section __cfstring
; Range: [0x100004010; 0x100004030[ (32 bytes)
; File offset : [16400; 16432[ (32 bytes)
; S_REGULAR

cfstring_Man:
0000000100004010 dq ___CFConstantStringClassReference, 0x7c8, 0x100003f7b, 0x3 ; "Man", DATA XREF=sub_100003efb+4

; Section __cstring
; Range: [0x100003f7b; 0x100003f7f[ (4 bytes)
; File offset : [16251; 16255[ (4 bytes)
; Flags: 0x2
; S_CSTRING_LITERALS

0000000100003f7b db "Man", 0 ; DATA XREF=cfstring_Man

以上就不详细做寻址的步骤了,直接通过hopper可以看到,最终指向的__cstring section中的Man字符串。

由此可以看出”Man“指向不是__objc_classname,而是__cstring。因此修改了__objc_classname后会影响原代码中NSClassFromString获取类。

那么就会想到,把__cstring sectin中的类名也替换掉不就可以了吗?

答案是不可以,__cstring section包含的是文件中所有的字符串,为了避免非NSClassFromString函数调用同样字符串异常,不能直接修改这个section。平替方案是使用宏替换原有代码中NSClassFromString的调用,用原有类名映射出混淆后的类名,然后调用NSClassFromString函数。这样场景的还有一些runtime的函数。

结论

Mach-O混淆是通过修改__objc_classname段来修改类名。在程序运行中类名只是个符号,用来映射类。

&emsp;electron 是一个可以使用 web 技术来创建跨平台原生桌面应用的框架。借助 electron ,我们可以使用纯 JavaScript 来调用丰富的原生 APIs。优点:可以开发跨平台应用;成熟的社区;图形化的开发。缺点:应用体积过大;重型项目性能问题。

&emsp;electron 核心的部分就是两个进程之间的协作——主进程和渲染进程。进程之间通过 ipcMain 和 ipcRenderer 来进行通信。

Read more »

​ ivar是类中的成员变量,@propery声明一个属性的时候就会产生一个成员变量。

获取类的成员变量

​ 在runtime中,通过class_copyIvarList(Class cls, unsigned int * outCount)函数可以获取类的成员变量列表。在这里需要注意的是,class_copyIvarList函数获取的是当前类声明的成员变量,它的父类声明的 成员变量并不会返回。

Read more »

​ 由于对外分外的adhoc包只能安装在provisioning profile中已添加的设备。因此当有新设备添加时,需要单独更新provisioning profile,并对包进行一次重签名。针对新设备包更新的问题,对超级签名的方案进行了调研。

方案概述

​ 超级签名主要思路是让需要安装包的设备自行触发设备添加和重签名的操作,下图为超级签名的签名流程。

Read more »

​ 在iOS开发中,我们会发现给nil发送消息时安全的,但是给null消息就会崩溃。这里探讨一下nil和null两者的区别。

​ 打印nil和[NSNull null]的地址

​ nil: 0x0

​ null: 0x10c054ff0

​ 当向两者发送消息时候,会调用msg_send函数进行方法的调用,msg_send函数的第一个参数调用者本身,这里为nil和null,第二个参数是SEL——方法的名称,而后跟着的是方法的参数。

msg_send(nil, selector)

msg_sen(null, selector)

​ 接下来看msg_send源码的源码,因为参数及性能的问题,msg_send是用汇编写的,以下代码为msg_send起始和末尾片段。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
	ENTRY _objc_msgSend
UNWIND _objc_msgSend, NoFrame

cmp p0, #0 // nil check and tagged pointer check
#if SUPPORT_TAGGED_POINTERS
b.le LNilOrTagged // (MSB tagged pointer looks negative)
#else
b.eq LReturnZero
#endif
...
...
LReturnZero:
// x0 is already zero
mov x1, #0
movi d0, #0
movi d1, #0
movi d2, #0
movi d3, #0
ret

​ 汇编代码中第一个指令就是cmp p0, #0,比较第一个参数和数字0。从前面的打印结果可以看到nil的值为0x0,与0相等,然后调转到LReturnZero。LReturnZero中将寄存器的值还原为0,然后return回去。因此当给nil发消息时,不会崩溃,msg_send函数会返回0。再来看看null,因为null指针的值不是0,因此会继续走寻找方法的函数,然后null本身没有方法,因此最后抛出错误崩溃。

&emsp;&emsp;一个简单程序是如何启动的?简单来说,程序就是一个可执行文件,启动的过程分为以下三个部分:

  1. 执行fork函数,创建出一个新的进程并清理用户空间。
  2. 执行execve函数,加载器加载可执行文件。
  3. 执行地址跳转到ELF文件的入口地址,程序启动。
Read more »