0%

1. 对象、类对象、元类

1.1 isa指向、superClass指向

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
/** //先把这里的协议注释掉,用到的时候再打开
@protocol PersonProtocol <NSObject>

@property (nonatomic, copy) NSString *p_address;

- (void)p_func1;
+ (void)p_func1;

@end
*/

@interface Person : NSObject
{
NSString *_hobby;
CGFloat _height;
}

@property (nonatomic, copy) NSString *name;
@property (nonatomic) int age;

- (void)func1;
- (void)func2;
+ (void)func3;
+ (void)func4;

@end

@interface Teacher : Person

@end

// 然后执行,直接打断点。
Person *p = [[Person alloc] init];
Teacher *t = [[Teacher alloc] init];

我们根据述代码进行分析,isa指针的指向。

注意,这里是用的是模拟器

1.2 实例对象的isa

首先我们先看Person的实例p的isa指向情况

1
2
3
4
5
6
7
8
9
10
11
// 首先打印一下p的内存情况
(lldb) po p
<Person: 0x1006460b0>
// 输出p指针的情况
(lldb) x/4gx p
0x1006460b0: 0x011d8001000083f9 0x0000000000000000
0x1006460c0: 0x0000000000000000 0x0000000000000000

(lldb) p p 0x011d8001000083f9
(long) $6 = 1791002988741

这里拿到p指针指向的内存情况,我们知道第一块内存区域存放的是isa指针,直接打印的话,发现就是一串数字,啥也看不出来。还记得上一章中object_getClass反向验证isa指向最后的”&”运算吗?0x011d8001000083f9这个值就是isa->bits,我们用它与ISA_MASK进行&运算。因为这里是用的真机,所以ISA_MASK = 0x00007ffffffffff8,如果是用Mac或者模拟器,根据芯片类型判断是否是ARM64架构还是x86,然后使用对应的值进行换算。

1
2
3
4
5
6
7
// p/x输出内存的16进制
(lldb) p/x 0x011d8001000083f9 & 0x00007ffffffffff8
(long) $2 = 0x00000001000083f8

// 这里po就是Person类
(lldb) po 0x00000001000083f8
Person

打印出来是Person。所以isa指向的就是Person类。那我们做一下验证,直接通过object_getClass方法来找一下Person这个类。

1
2
(lldb) p/x object_getClass(p)
(Class) $12 = 0x00000001000083f8 Person

是不是发现,Person类的内存地址是一样的。如果再实例化一个p1,看p1->isa指向的和p->isa指向的是否是同一个Person类的内存地址。

1
2
3
// 直接使用object_getClass获取类对象。
lldb) p/x object_getClass([Person alloc])
(Class) $26 = 0x00000001000083f8 Person

答案是肯定的,Person类在内存中只有一份,也就是说所有的类对象在内存中都只有一份。

1.3 类对象的isa

接下来,我们继续寻找Person类对象的isa指向情况。

1
2
3
4
5
6
7
8
9
10
11
(lldb) p 0x000001a10018d0c5
(long) $6 = 1791002988741

(lldb) x/4gx 0x00000001000083f8
0x1000083f8: 0x00000001000083d0 0x000000010036a140
0x100008408: 0x0000000100645d60 0x0001803000000003

(lldb) p/x 0x00000001000083d0 & 0x00007ffffffffff8
(long) $4 = 0x00000001000083d0
(lldb) po 0x00000001000083d0
Person

发现Person类对象的isa指向的还是Person,但是这个Person所在的内存地址与Person类对象不一样。

这里就出现了元类的概念(Meta Class)。

1.4 元类的isa

我们继续寻找元类的isa

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 0x00000001000083d0是Person元类所在的内存
(lldb) x/4gx 0x00000001000083d0
0x1000083d0: 0x000000010036a0f0 0x000000010036a0f0
0x1000083e0: 0x0000000100714d10 0x0002e03100000003

(lldb) p/x 0x000000010036a0f0 & 0x00007ffffffffff8
(long) $9 = 0x000000010036a0f0
(lldb) po 0x000000010036a0f0
NSObject

// 拿到NSObject的地址继续x/4gx
(lldb) x/4gx 0x000000010036a0f0
0x10036a0f0: 0x000000010036a0f0 0x000000010036a140
0x10036a100: 0x00000001007877b0 0x0003e03100000007

使用相同的方法找到元类的isa指向的是NSObject,这个NSObject是类对象吗?

NSObject继续x/4gx发现isa锁指向的内存地址是一样的。

我们通过object_getClass([[NSObject alloc] init])来看看NSObject类对象的内存

1
2
3
4
5
6
7
8
9
10
11
12
13
// 获取NSObject类的地址,与p/x NSObject.class效果一致
(lldb) p/x object_getClass([NSObject alloc])
(Class) $13 = 0x000000010036a140 NSObject

(lldb) x/4gx 0x000000010036a140
0x10036a140: 0x000000010036a0f0 0x0000000000000000
0x10036a150: 0x0000000100786740 0x0002801000000003

// 从这里开始,就已经跟上面的内存地址重复了
(lldb) p/x 0x000000010036a0f0 & 0x00007ffffffffff8
(long) $14 = 0x000000010036a0f0
(lldb) po 0x000000010036a0f0
NSObject

到这里,是不是看明白了点啥?NSObject类对象也有指向NSObject的元类,Person的元类的isa指向的是NSObject的元类。

1.5 使用相同的办法查看Teacher的isa

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 这里使用简单的方式,直接使用Teacher类
(lldb) x/4gx Teacher.class
0x100008380: 0x00000001000083a8 0x00000001000083f8
0x100008390: 0x0000000100362370 0x0000803000000000

(lldb) p/x 0x00000001000083a8 & 0x00007ffffffffff8
(long) $18 = 0x00000001000083a8

(lldb) po 0x00000001000083a8
Teacher

(lldb) x/4gx 0x00000001000083a8
0x1000083a8: 0x000000010036a0f0 0x00000001000083d0
0x1000083b8: 0x0000000101138140 0x0001e03100000003

(lldb) p/x 0x000000010036a0f0 & 0x00007ffffffffff8
(long) $20 = 0x000000010036a0f0
// 这里又指向了NSObject
(lldb) po 0x000000010036a0f0
NSObject

看到这里应该发现了点东西吧。实例对象的isa->类对象的isa->NSObject的isa,中间类对象与继承没有一丢丢关系。

1.6 类的继承链

上面我们查看了isa的走向,接下来看一下继承链。首先看一下类的继承关系。

1
2
3
4
5
6
7
8
void testSuperClass(void){
Teacher *t = [Teacher alloc];
Person *p = [Person alloc];

NSLog(@"%@",class_getSuperclass(Teacher.class));
NSLog(@"%@",class_getSuperclass(Person.class));
NSLog(@"%@",class_getSuperclass(NSObject.class));
}

这里输出一下对应类的superclass

1
2
3
Person
NSObject
(null)

从打印出来的信息可以看到:

  • Teacher -> superclass = Person
  • Person -> superClass = NSObject
  • NSObject -> superClass = null

1.7 元类的继承链

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
void testNSObject(void) {
// NSObject实例对象
NSObject *object1 = [NSObject alloc];
// NSObject类
Class cls = object_getClass(object1);
// NSObject元类
Class metaClass = object_getClass(cls);
// NSObject根元类
Class rootMetaClass = object_getClass(metaClass);
// NSObject根根元类
Class rootRootMetaClass = object_getClass(rootMetaClass);
NSLog(@"\n%p 实例对象\n%p 类\n%p 元类\n%p 根元类\n%p 根根元类",object1,cls,metaClass,rootMetaClass,rootRootMetaClass);

// Person元类
Class pMetaClass = object_getClass(Person.class);
Class psuperClass = class_getSuperclass(pMetaClass);
NSLog(@"%@ - %p",pMetaClass,pMetaClass);
NSLog(@"%@ - %p",psuperClass,psuperClass);

// Teacher -> Person -> NSObject
// 元类也有一条继承链
Class tMetaClass = object_getClass(Teacher.class);
Class tsuperClass = class_getSuperclass(tMetaClass);
NSLog(@"%@ - %p",tMetaClass,tMetaClass);
NSLog(@"%@ - %p",tsuperClass,tsuperClass);

// NSObject 根类特殊情况
Class nsuperClass = class_getSuperclass(NSObject.class);
NSLog(@"%@ - %p",nsuperClass,nsuperClass);
// 根元类 -> NSObject
Class rnsuperClass = class_getSuperclass(metaClass);
NSLog(@"%@ - %p",rnsuperClass,rnsuperClass);
}

接下来看一下输出结果:

  1. 首先输出的NSObject的isa走位。NSObject实例对象 -> 类 -> 元类

    1
    2
    3
    4
    5
    0x10124ba40 实例对象
    0x10036a140 类
    0x10036a0f0 元类
    0x10036a0f0 根元类
    0x10036a0f0 根根元类

    从这里的输出结果可以进一步判断出NSObject类对象的元类指向它自己。

  2. Person类对象的元类 -> super

    1
    2
    3
    4
    // Person类对象的元类
    Person - 0x100008498
    // Person类对象的元类 -> super
    NSObject - 0x10036a0f0

    从地址的打印信息可以看出来,person类的元类的super指向的是NSObject类的元类。

  3. Teacher元类

    1
    2
    3
    4
    // Teacher类对象的元类
    Teacher - 0x100008470
    // Teacher类对象的元类 -> super
    Person - 0x100008498

    从这里可以看出来,Teacher元类的super指向的Person的元类,地址信息都是相同的。

从上面的流程可以看到,类的继承关系和对应元类的继承关系是相对应的。可以用一张图完美的诠释isa的走向和super的指向。

总结

  • 每个实例对象的isa指针指向与之对应的类对象(Class)。
  • 每个类对象(Class)都有一个isa指针指向一个唯一的元类(Meta Class)。
  • 每一个元类(Meta Class)的isa指针都指向最上层的元类(Meta Class)(图中的NSObject的Meta Class)。最上层的元类(Meta Class)的isa指针指向自己,形成一个回路。
  • 每一个元类(Meta Class)的Super Class指向它原本Class的Super Class的Meta Class。最上层的Meta Class的Super Class指向NSObject Class本身。
  • 最上层的NSObject Class的Super Class指向nil。
  • 只有Class才有继承关系,实例对象与实例对象不存在继承关系。
  • 每一个类对象(Class)在内存中都只有一份。

2. 通过源码分析

接下来我们从objc的源码上分析这些都是什么东西。

2.1 实例对象 id(Instance)

1
2
/// A pointer to an instance of a class.
typedef struct objc_object *id;

id 这个struct的定义本身就带了 个 *, 所以我们在使用其他NSObject类型的实例时需要在前加上 *, 使 id 时却不用 。

什么是objc_object?

1
2
3
4
/// Represents an instance of a class. 
struct objc_object {
Class isa OBJC_ISA_AVAILABILITY;
};

这个时候我们知道Objective-C中的object在最后会被转换成C的结构体, 在这个struct中有 个 isa 指针,指向它的类别 Class。

2.2 类对象 Class

1
2
/// An opaque type that represents an Objective-C class.
typedef struct objc_class *Class;

Class的本质就是一个objc_class的结构体。

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
// 注意,这个源码是被简化之后的。
struct objc_class : objc_object {

// Class ISA;
Class superclass;
cache_t cache; // formerly cache pointer and vtable
class_data_bits_t bits; // class_rw_t * plus custom rr/alloc flags

Class getSuperclass() const {
return superclass;
}

void setSuperclass(Class newSuperclass) {
superclass = newSuperclass;
}
// 用这个是无法获取rw_t,只能通过内存偏移获取bits,然后再获取rw_t
class_rw_t *data() const {
return bits.data();
}
void setData(class_rw_t *newData) {
bits.setData(newData);
}

bool isRootClass() {
return getSuperclass() == nil;
}
bool isRootMetaclass() {
return ISA() == (Class)this;
}
}

看到这个结构体,大家可能会觉得不对,这个源码是错的,不是我们经常看到的,里头没有那些我们常说的变量,methodLists、ivars等等。
大家看仔细了哦,下面这个实现基本都是大家常看到的:

1
2
3
4
5
6
7
8
struct objc_class {  
...
struct objc_ivar_list * _Nullable ivars OBJC2_UNAVAILABLE;
struct objc_method_list * _Nullable * _Nullable methodLists OBJC2_UNAVAILABLE;
struct objc_cache * _Nonnull cache OBJC2_UNAVAILABLE;
struct objc_protocol_list * _Nullable protocols OBJC2_UNAVAILABLE;
} OBJC2_UNAVAILABLE

里头确实有ivars、methodLists等,但是这个是OBJC2_UNAVAILABLE(我们目前使用的Objective-C的版本是2.0版本)。其内部确实有这些东西的,我们一步步去探究。

继续回到上面的结构体,发现ISA变量被注释掉了,其实也没有影响的,因为objc_class 继承自 objc_object(内部有isa变量)。那我们的属性、方法是存放在哪了呢?

通过查看源码,我们看到有这么一个属性class_data_bits_t bits;,这个东西里可能存放着我们需要的东西。稍后我们做验证。

2.3 元类 Meta Class

OC中一切皆为对象
Class在设计中本身也是一个对象,也有superclass。而这个Class对应的类我们叫“元类”(Meta Class)。也就是说Class中有一个isa指向的是Meta Class。

3 验证属性、方法、协议存在的位置

3.1 验证之前的准备 - 源码

在2.2小结我们说了属性、方法、等可能存在于class_data_bits_t这个结构体内部,我们查看它的源码:

3.1.1 class_data_bits_t

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
struct class_data_bits_t {
friend objc_class;

// Values are the FAST_ flags above.
uintptr_t bits;

public:

class_rw_t* data() const {
return (class_rw_t *)(bits & FAST_DATA_MASK);
}

...

const class_ro_t *safe_ro() const {
class_rw_t *maybe_rw = data();
if (maybe_rw->flags & RW_REALIZED) {
// maybe_rw is rw
return maybe_rw->ro();
} else {
// maybe_rw is actually ro
return (class_ro_t *)maybe_rw;
}
}

...
};

在public的方法中有class_rw_t* data()这个方法,我们进一步探索:

3.1.2 class_rw_t

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
struct class_rw_t {
// Be warned that Symbolication knows the layout of this structure.
uint32_t flags;
uint16_t witness;
#if SUPPORT_INDEXED_ISA
uint16_t index;
#endif

explicit_atomic<uintptr_t> ro_or_rw_ext;

Class firstSubclass;
Class nextSiblingClass;

const method_array_t methods() const {
auto v = get_ro_or_rwe();
if (v.is<class_rw_ext_t *>()) {
return v.get<class_rw_ext_t *>(&ro_or_rw_ext)->methods;
} else {
return method_array_t{v.get<const class_ro_t *>(&ro_or_rw_ext)->baseMethods()};
}
}

const property_array_t properties() const {
auto v = get_ro_or_rwe();
if (v.is<class_rw_ext_t *>()) {
return v.get<class_rw_ext_t *>(&ro_or_rw_ext)->properties;
} else {
return property_array_t{v.get<const class_ro_t *>(&ro_or_rw_ext)->baseProperties};
}
}

const protocol_array_t protocols() const {
auto v = get_ro_or_rwe();
if (v.is<class_rw_ext_t *>()) {
return v.get<class_rw_ext_t *>(&ro_or_rw_ext)->protocols;
} else {
return protocol_array_t{v.get<const class_ro_t *>(&ro_or_rw_ext)->baseProtocols};
}
}
}

确实如我们所说的,这里确实存在着我们想要的东西:methods()、properties()、protocols()等。

那我们该怎么获取到这些数据,来证明这些就是我们想要的东西呢?

3.1.3 内存偏移

我们知道在c语言中,一个数组,获取数组中的某个元素的值有多种方法:

1
2
int a[] = {1,2,3};
printf("index 1 = %d - %d", a[1], *(a+1));

比如上面的代码,我们可以直接输出某个元素的下标,也可以通过内存地址来偏移进行读取,同样,我们也可以采取地址偏移来获取objc_class->bits的值。

需要偏移多少呢?

第一个变量是Class,这是一个结构体,内部有一个isa指针,所以这是8个字节。
第二个变量是cache_t,我们进源码看一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
struct cache_t {
private:
explicit_atomic<uintptr_t> _bucketsAndMaybeMask; // 8
union {
struct {
explicit_atomic<mask_t> _maybeMask; // 4
#if __LP64__
uint16_t _flags; // 2
#endif
uint16_t _occupied; // 2
};
explicit_atomic<preopt_cache_t *> _originalPreoptCache; // 8
};
...
...

};

其实这些就能算出来我们需要多少字节,我已经标好了。静态变量和方法是没有算在结构体内部的哈,而且cache_t内部有一个共用体,所以其所占用的空间一共是8,再加上_bucketsAndMaybeMask变量一共是16个字节。

1
2
3
4
Class ISA;              // 8
Class superclass; // 8
cache_t cache; // 16
class_data_bits_t bits;

所以8+8+16 = 32个字节。

也就是我们获取到的objc_class的isa指针,然后偏移32个字节,也就是0x20。当然也可以直接通过lldb输出sizeOf(cache_t)来获取。

1
2
(lldb) p sizeof(cache_t)
(unsigned long) $4 = 16

我们做一下验证,看看属性在哪。其实需要注意的一点是,我们要获取的是类对象,从类对象中查看我们的变量、方法和协议等,而不是从实例对象中获取,因为实例对象是已经在内存中了,比如属性已经有了具体的值了。

3.1.4 method_array_t

我们继续跟踪源码,查看method_array_t是个啥。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class method_array_t : 
public list_array_tt<method_t, method_list_t, method_list_t_authed_ptr>
{
typedef list_array_tt<method_t, method_list_t, method_list_t_authed_ptr> Super;

public:
method_array_t() : Super() { }
method_array_t(method_list_t *l) : Super(l) { }

const method_list_t_authed_ptr<method_list_t> *beginCategoryMethodLists() const {
return beginLists();
}

const method_list_t_authed_ptr<method_list_t> *endCategoryMethodLists(Class cls) const;
};

我们猜测我们想要的数据是在method_list_t中,而method就是我们的每一个的方法等结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
struct method_t {
struct big {
SEL name;
const char *types;
MethodListIMP imp;
};
// 中间代码有删减
public:
big &big() const {
ASSERT(!isSmall());
return *(struct big *)this;
}
// 中间代码有删减
}

3.1.5 property_array_t

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class property_array_t : 
public list_array_tt<property_t, property_list_t, RawPtr>
{
typedef list_array_tt<property_t, property_list_t, RawPtr> Super;

public:
property_array_t() : Super() { }
property_array_t(property_list_t *l) : Super(l) { }
};

// 同methods方法,我们看一下property_t
struct property_t {
const char *name;
const char *attributes;
};

3.1.6 protocol_array_t

1
2
3
4
5
6
7
8
9
10
11
class protocol_array_t : 
public list_array_tt<protocol_ref_t, protocol_list_t, RawPtr>
{
typedef list_array_tt<protocol_ref_t, protocol_list_t, RawPtr> Super;

public:
protocol_array_t() : Super() { }
protocol_array_t(protocol_list_t *l) : Super(l) { }
};

typedef uintptr_t protocol_ref_t; // protocol_t *, but unremapped

这三个分别对应methods()、properties()、protocols()方法,里头也一个共同点就是protocol_array_t。那我们重点看一下list_array_tt的结构。

3.1.7 list_array_tt

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
class list_array_tt {
struct array_t {
uint32_t count;
Ptr<List> lists[0];

static size_t byteSize(uint32_t count) {
return sizeof(array_t) + count*sizeof(lists[0]);
}
size_t byteSize() {
return byteSize(count);
}
};

protected:
// 这是一个迭代器
class iterator {
const Ptr<List> *lists;
const Ptr<List> *listsEnd;
typename List::iterator m, mEnd;

...
// 迭代器相关的方法
};

public:
union {
Ptr<List> list;
uintptr_t arrayAndFlag;
};

...

}

list_array_tt结构体大概的可以看出来,list_array_tt只是一个list的封装。以property_array_t为例:

list_array_tt<property_t, property_list_t, RawPtr>就是一个存放了property_t类型的数组。

1
2
3
4
5
union {
Ptr<List> list;
uintptr_t arrayAndFlag;
};

这个union共用体才是一个list_array_tt对外暴露的真是结构,一会我们通过lldb进行验证。

4 lldb 验证属性存放的位置

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
(lldb) x/6gx Person.class
0x1000083f8: 0x00000001000083d0 0x000000010036a140
0x100008408: 0x00000001006176b0 0x0001803000000003
0x100008418: 0x00000001012042e4 0x00000001000b9970

// 通过指针偏移0x20,也就是0x1000083f8+0x20,加上强制转换
(lldb) p (class_data_bits_t *)0x100008418
(class_data_bits_t *) $1 = 0x0000000100008418

// 拿到变量bits之后,通过class_data_bits_t -> data()函数获取rw_t
(lldb) p $1->data()
(class_rw_t *) $2 = 0x00000001012042e0

// 看一下class_rw_t都有哪些值
(lldb) p *$2
(class_rw_t) $4 = {
flags = 2156396544
witness = 1
ro_or_rw_ext = {
std::__1::atomic<unsigned long> = {
Value = 4295000480
}
}
firstSubclass = Teacher
nextSiblingClass = NSBinder
}
// 如果有Subclass,则会有firstSubclass=Teacher,如果没有子类则是nil

// 我们在class_rw_t中已经查看过源码,可以通过properties()获取属性列表
(lldb) p $2->properties()
(const property_array_t) $5 = {
list_array_tt<property_t, property_list_t, RawPtr> = {
= {
list = {
ptr = 0x0000000100008320
}
arrayAndFlag = 4295000864
}
}
}

结合我们上面分析的结果,property_array_t输出的数据与上方list_array_tt内部的union共用体的结构是一直的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 获取属性列表
(lldb) p $5.list
(const RawPtr<property_list_t>) $6 = {
ptr = 0x0000000100008320
}

//
(lldb) p $6.ptr
(property_list_t *const) $7 = 0x0000000100008320
(lldb) p *$7
(property_list_t) $8 = {
entsize_list_tt<property_t, property_list_t, 0, PointerModifierNop> = (entsizeAndFlags = 16, count = 2)
}
// $8也就是我们的ptr内存储的列表

但是entsize_list_tt又是什么类型?我们又该通过那种方式来获取我们最后想要的property呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
struct entsize_list_tt {
uint32_t entsizeAndFlags;
uint32_t count;
//
Element& getOrEnd(uint32_t i) const {
ASSERT(i <= count);
return *PointerModifier::modify(*this, (Element *)((uint8_t *)this + sizeof(*this) + i*entsize()));
}
// 注意。这里有一个 【&】符号,调用getOrEnd,返回的是一个指针,进行转换
Element& get(uint32_t i) const {
ASSERT(i < count);
return getOrEnd(i);
}
}

entsize_list_tt内部有get方法,来获取其中的元素。

1
2
3
4
5
6
7
8
9
(lldb) p $8.get(0)
(property_t) $9 = (name = "name", attributes = "T@\"NSString\",C,N,V_name")
(lldb) p $8.get(1)
(property_t) $10 = (name = "age", attributes = "Ti,N,V_age")
(lldb)
(lldb) p $8.get(2)
Assertion failed: (i < count), function get, file objc4-818.2/runtime/objc-runtime-new.h, line 624.
error: Execution was interrupted, reason: signal SIGABRT.
The process has been returned to the state before expression evaluation.

到这里,我们就输出了我们定义的2个属性,但是变量却没有在这里提现出来。我们继续看ivar存放在哪。

5. lldb 成员变量

从上面我们知道属性都存放在class_rw_t中,在查看class_data_bits_t源码的时候,也有看到class_ro_t。那ivar会不会就在这里呢。

1
2
3
4
5
6
7
8
9
10
11
12
// class_data_bits_t 内部

const class_ro_t *safe_ro() const {
class_rw_t *maybe_rw = data();
if (maybe_rw->flags & RW_REALIZED) {
// maybe_rw is rw
return maybe_rw->ro();
} else {
// maybe_rw is actually ro
return (class_ro_t *)maybe_rw;
}
}
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
struct class_ro_t {
uint32_t flags;
uint32_t instanceStart;
uint32_t instanceSize;
#ifdef __LP64__
uint32_t reserved;
#endif

union {
const uint8_t * ivarLayout;
Class nonMetaclass;
};

explicit_atomic<const char *> name;
// With ptrauth, this is signed if it points to a small list, but
// may be unsigned if it points to a big list.
void *baseMethodList;
protocol_list_t * baseProtocols;
// 这里存放的是ivars
const ivar_list_t * ivars;

const uint8_t * weakIvarLayout;
property_list_t *baseProperties;

...
...

}

有了获取property的经验,这里就方便多了,我们按照相同的方式来获取。

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
// $1 (class_data_bits_t *) 
(lldb) p $1->safe_ro()
(const class_ro_t *) $11 = 0x00000001000081a0
(lldb) p *$11
(const class_ro_t) $12 = {
flags = 0
instanceStart = 8
instanceSize = 40
reserved = 0
= {
ivarLayout = 0x0000000000000000
nonMetaclass = nil
}
name = {
std::__1::atomic<const char *> = "Person" {
Value = 0x0000000100003edc "Person"
}
}
baseMethodList = 0x00000001000081e8
baseProtocols = 0x0000000000000000
ivars = 0x0000000100008298
weakIvarLayout = 0x0000000000000000
baseProperties = 0x0000000100008320
_swiftMetadataInitializer_NEVER_USE = {}
}

从打印中的内容可以大致的猜测ivar应该存放在ivars。

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
(lldb) p $11.ivars
(const ivar_list_t *const) $13 = 0x0000000100008298
Fix-it applied, fixed expression was:
$11->ivars
(lldb) p $11->ivars
(const ivar_list_t *const) $14 = 0x0000000100008298
(lldb) p *$14
(const ivar_list_t) $15 = {
entsize_list_tt<ivar_t, ivar_list_t, 0, PointerModifierNop> = (entsizeAndFlags = 32, count = 4)
}

// 与property list是相同的结构
(lldb) p $15.get(0)
(ivar_t) $16 = {
offset = 0x0000000100008360
name = 0x0000000100003f06 "_hobby"
type = 0x0000000100003f63 "@\"NSString\""
alignment_raw = 3
size = 8
}
(lldb) p $15.get(1)
(ivar_t) $17 = {
offset = 0x0000000100008368
name = 0x0000000100003f0d "_height"
type = 0x0000000100003f6f "d"
alignment_raw = 3
size = 8
}
(lldb) p $15.get(2)
(ivar_t) $18 = {
offset = 0x0000000100008370
name = 0x0000000100003f15 "_age"
type = 0x0000000100003f71 "i"
alignment_raw = 2
size = 4
}
(lldb) p $15.get(3)
(ivar_t) $19 = {
offset = 0x0000000100008378
name = 0x0000000100003f1a "_name"
type = 0x0000000100003f63 "@\"NSString\""
alignment_raw = 3
size = 8
}
(lldb)

到这里,我们也获取到了变量的位置,也说明了定义的属性会默认生成带下划线的同名变量。

接下来就是方法了。

6. 实例方法

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
(lldb) p $2->methods()
(const method_array_t) $20 = {
list_array_tt<method_t, method_list_t, method_list_t_authed_ptr> = {
= {
list = {
ptr = 0x00000001000081e8
}
arrayAndFlag = 4295000552
}
}
}

// 与属性一致
(lldb) p $20.list
(const method_list_t_authed_ptr<method_list_t>) $21 = {
ptr = 0x00000001000081e8
}
(lldb) p $21.ptr
(method_list_t *const) $22 = 0x00000001000081e8
(lldb) p *$22
(method_list_t) $23 = {
entsize_list_tt<method_t, method_list_t, 4294901763, method_t::pointer_modifier> = (entsizeAndFlags = 27, count = 7)
}

(lldb) p $23.get(0)
(method_t) $24 = {}

我们按照属性的方式继续输出,结果$23.get(0)输出的确实空内容。

在说method_t时,结构体内部有big()的方法:

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
(lldb) p $23.get(0).big()
(method_t::big) $26 = {
name = "func1"
types = 0x0000000100003f5b "v16@0:8"
imp = 0x0000000100003c90 (AL-Objc`-[Person func1])
}
(lldb) p $23.get(1).big()
(method_t::big) $27 = {
name = "func2"
types = 0x0000000100003f5b "v16@0:8"
imp = 0x0000000100003cc0 (AL-Objc`-[Person func2])
}
(lldb) p $23.get(2).big()
(method_t::big) $28 = {
name = "name"
types = 0x0000000100003f53 "@16@0:8"
imp = 0x0000000100003cf0 (AL-Objc`-[Person name])
}
(lldb) p $23.get(3).big()
(method_t::big) $29 = {
name = "setName:"
types = 0x0000000100003f73 "v24@0:8@16"
imp = 0x0000000100003d20 (AL-Objc`-[Person setName:])
}
(lldb) p $23.get(4).big()
(method_t::big) $30 = {
name = "age"
types = 0x0000000100003f7e "i16@0:8"
imp = 0x0000000100003d50 (AL-Objc`-[Person age])
}
(lldb) p $23.get(5).big()
(method_t::big) $31 = {
name = "setAge:"
types = 0x0000000100003f86 "v20@0:8i16"
imp = 0x0000000100003d70 (AL-Objc`-[Person setAge:])
}
(lldb) p $23.get(6).big()
Assertion failed: (i < count), function get, file objc4-818.2/runtime/objc-runtime-new.h, line 624.
error: Execution was interrupted, reason: signal SIGABRT.
The process has been returned to the state before expression evaluation.
(lldb)

我们在类中声明的方法都在method_list_t中,这里有我们自己声明的方法,还有属性自动生成的set和get方法。

发现这里并没有我们的类方法。因为类方法在元类里。

7. 类方法

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
64
65
66
67
68
69
70
71
(lldb) x/4gx Person.class
0x1000083f8: 0x00000001000083d0 0x000000010036a140
0x100008408: 0x00000001006176b0 0x0001803000000003

// 获取元类
(lldb) p 0x00000001000083d0 & 0x00007ffffffffff8
(long) $33 = 4295001040
(lldb) po $33
Person

(lldb) x/6gx $33
0x1000083d0: 0x000000010036a0f0 0x000000010036a0f0
0x1000083e0: 0x00000001006959d0 0x0002e03100000003
0x1000083f0: 0x0000000101204304 0x00000001000083d0
// 获取元类的bits
(lldb) p (class_data_bits_t *)0x1000083f0
(class_data_bits_t *) $34 = 0x00000001000083f0
(lldb) p $34->data()
(class_rw_t *) $35 = 0x0000000101204300
(lldb) p *$35
(class_rw_t) $36 = {
flags = 2684878849
witness = 1
ro_or_rw_ext = {
std::__1::atomic<unsigned long> = {
Value = 4302330705
}
}
firstSubclass = 0x00000001000083a8
nextSiblingClass = 0x00007fff883ac410
}
(lldb) p $35->methods()
(const method_array_t) $37 = {
list_array_tt<method_t, method_list_t, method_list_t_authed_ptr> = {
= {
list = {
ptr = 0x0000000100008168
}
arrayAndFlag = 4295000424
}
}
}
(lldb) p $37.list
(const method_list_t_authed_ptr<method_list_t>) $38 = {
ptr = 0x0000000100008168
}
(lldb) p $38.ptr
(method_list_t *const) $39 = 0x0000000100008168
(lldb) p *$39
(method_list_t) $40 = {
entsize_list_tt<method_t, method_list_t, 4294901763, method_t::pointer_modifier> = (entsizeAndFlags = 27, count = 2)
}
// count = 2告诉我们有2个

(lldb) p $40.get(0).big()
(method_t::big) $41 = {
name = "func3"
types = 0x0000000100003f5b "v16@0:8"
imp = 0x0000000100003c00 (AL-Objc`+[Person func3])
}
(lldb) p $40.get(1).big()
(method_t::big) $42 = {
name = "func4"
types = 0x0000000100003f5b "v16@0:8"
imp = 0x0000000100003c30 (AL-Objc`+[Person func4])
}
(lldb) p $40.get(2).big()
Assertion failed: (i < count), function get, file objc4-818.2/runtime/objc-runtime-new.h, line 624.
error: Execution was interrupted, reason: signal SIGABRT.
The process has been returned to the state before expression evaluation.
(lldb)

这里是获取类方法所在的位置。

8 协议

属性、变量、方法都已经有所了解,接下来看一下协议。把我们一开始注释的协议打开,重新运行。

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
(lldb) x/6gx Person.class
0x1000088d0: 0x00000001000088a8 0x000000010036a140
0x1000088e0: 0x0000000100362370 0x0000803400000000
0x1000088f0: 0x0000000100604204 0x00000001000b9970
(lldb) p (class_data_bits_t *)0x1000088f0
(class_data_bits_t *) $4 = 0x00000001000088f0
(lldb) p $4->data()
(class_rw_t *) $5 = 0x0000000100604200
(lldb) p $5->protocols()
(const protocol_array_t) $6 = {
list_array_tt<unsigned long, protocol_list_t, RawPtr> = {
= {
list = {
ptr = 0x0000000100008560
}
arrayAndFlag = 4295001440
}
}
}
(lldb) p $6.list
(const RawPtr<protocol_list_t>) $7 = {
ptr = 0x0000000100008560
}
(lldb) p $7.ptr
(protocol_list_t *const) $8 = 0x0000000100008560
(lldb) p *$8
(protocol_list_t) $9 = (count = 1, list = protocol_ref_t [] @ 0x00007ff5f7e1e9f8)
(lldb)

到此时,就不知道怎么处理,我们看一下protocol_list_t

1
2
3
4
5
6
7
8
struct protocol_list_t {
// count is pointer-sized by accident.
uintptr_t count;
protocol_ref_t list[0]; // variable-size

...

}

这就是其内部的主要结构。我们用list[0]打印一下:

1
2
(lldb) p $9.list[0]
(protocol_ref_t) $11 = 4295002464

上面我们已经说过protocol_ref_t只是一个定义:

1
typedef uintptr_t protocol_ref_t;  // protocol_t *, but unremapped

接下来强转一下,看是否可以转成protocol_t *类型。

1
2
3
4
5
6
(lldb) p (protocol_t *)$11
(protocol_t *) $12 = 0x0000000100008960

// 我们查看protocol_t内部的结构有demangledName()方法.
(lldb) p $12->demangledName()
(const char *) $13 = 0x0000000100003b51 "PersonProtocol"

到这里呢,协议存放的位置也找到了。

9. 补充添加协议之后

9.1 多了4个属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
(lldb) p $5->properties()
(const property_array_t) $15 = {
list_array_tt<property_t, property_list_t, RawPtr> = {
= {
list = {
ptr = 0x0000000100008710
}
arrayAndFlag = 4295001872
}
}
}
(lldb) p $15.list
(const RawPtr<property_list_t>) $16 = {
ptr = 0x0000000100008710
}
(lldb) p $16.ptr
(property_list_t *const) $17 = 0x0000000100008710
(lldb) p *$17
(property_list_t) $18 = {
entsize_list_tt<property_t, property_list_t, 0, PointerModifierNop> = (entsizeAndFlags = 16, count = 7)
}

添加协议之后,又返回去重新打印了一下属性列表,发现这里变成了7个。明明之前只有2个属性

1
2
3
4
// 之前打印的数据
(property_list_t) $8 = {
entsize_list_tt<property_t, property_list_t, 0, PointerModifierNop> = (entsizeAndFlags = 16, count = 2)
}

再加上协议中定义的一个,加起来也才3个,为什么会变成7个?这7个又是哪个属性?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
(lldb) p $18.get(0)
(property_t) $19 = (name = "name", attributes = "T@\"NSString\",C,N,V_name")
(lldb) p $18.get(1)
(property_t) $20 = (name = "age", attributes = "Ti,N,V_age")
(lldb) p $18.get(2)
(property_t) $21 = (name = "p_address", attributes = "T@\"NSString\",C,N")
(lldb) p $18.get(3)
(property_t) $22 = (name = "hash", attributes = "TQ,R")
(lldb) p $18.get(4)
(property_t) $23 = (name = "superclass", attributes = "T#,R")
(lldb) p $18.get(5)
(property_t) $24 = (name = "description", attributes = "T@\"NSString\",R,C")
(lldb) p $18.get(6)
(property_t) $25 = (name = "debugDescription", attributes = "T@\"NSString\",R,C")

发现,添加了协议之后,会增加hash、superclass、description、debugDescription4个属性。是因为我们定义的协议都遵循<NSObject>协议,在<NSObject>协议内部有这4个属性的声明。

9.2 方法找不到了

我们按照上面获取方法等顺序,结果在最后

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
p $2->methods()
(const method_array_t) $3 = {
list_array_tt<method_t, method_list_t, method_list_t_authed_ptr> = {
= {
list = {
ptr = 0x0000000100722d01
}
arrayAndFlag = 4302449921
}
}
}
(lldb) p $3.list
(const method_list_t_authed_ptr<method_list_t>) $4 = {
ptr = 0x0000000100722d01
}
(lldb) p $4.ptr
(method_list_t *const) $5 = 0x0000000100722d01
(lldb) p *$6
(method_list_t) $7 = {
entsize_list_tt<method_t, method_list_t, 4294901763, method_t::pointer_modifier> = (entsizeAndFlags = 0, count = 2281701376)
}

我们在第6节的时候,输出过

1
2
3
4
(lldb) p *$22
(method_list_t) $23 = {
entsize_list_tt<method_t, method_list_t, 4294901763, method_t::pointer_modifier> = (entsizeAndFlags = 27, count = 7)
}

一共有7个,但是这里怎么变成了这么大的一个值???
有知道的大佬,欢迎指导,谢谢。

总结

  • 实例对象、类对象、元类。一切皆为对象,主要因为objc_object这个结构体。
  • isa的走位图,superClass的指向
  • 属性、变量,实例方法、类方法的存放
    • 属性、变量的区别,存放的位置
    • 实例方法放在类对象的列表;类方法的存放在元类的方法列表
  • 添加协议之后
    • 多了4个属性
    • 方法找不到了,待补充

这里我们以name的get方法为例子,说明一下这都是什么意思:

1
2
- (NSString *)getName;
{(struct objc_selector *)"name", "@16@0:8", (void *)_I_Person_name}

@16@0:8

  • ‘@’:第一个@表示返回值,对象
  • ‘16’:16个字节
  • ‘@’:第二个@表示对象类型(id)
  • ‘0’:我们知道@表示对象,0表示从0开始,占8个字节
  • ‘:’:SEL,方法明
  • ‘8’:表示从8开始,占8个字节,满足一共16个字节

sel 和 imp
sel:方法名
imp:方法实现。函数指针地址

总结

  • 实例对象、类对象、元类。一切皆为对象,主要因为objc_object这个结构体。
  • isa的走位图,superClass的指向
  • 属性、变量,实例方法、类方法的存放
    • 属性、变量的区别,存放的位置
    • 实例方法放在类对象的列表;类方法的存放在元类的方法列表
  • sel、imp的区别

calloc

在上一节中,我们知道开辟空间会用到calloc这个函数,那我们就追一下这个函数的内部逻辑。

但是发现calloc这个方法没有办法继续下一步追踪了,如图:

我们发现calloc定义在malloc/_malloc.h文件下,我们也找到了对应的源码。我这里用的是一份可运行的源码。

接下来,直接上代码,在main函数中:

1
2
3
// 开辟40个字节的空间
void *p = calloc(1, 40);
NSLog(@"%lu",malloc_size(p));

直接运行,查看malloc.c -> calloc的内部实现。

1
2
3
4
5
void *
calloc(size_t num_items, size_t size)
{
return _malloc_zone_calloc(default_zone, num_items, size, MZ_POSIX);
}

可以看到calloc只是调用了_malloc_zone_calloc方法,其它啥也没做,这里需要注意的是,default_zone参数,它是一个虚拟的默认zone,通过它可以做一些事情,如下:

_malloc_zone_calloc

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
static void *
_malloc_zone_calloc(malloc_zone_t *zone, size_t num_items, size_t size,
malloc_zone_options_t mzo)
{
MALLOC_TRACE(TRACE_calloc | DBG_FUNC_START, (uintptr_t)zone, num_items, size, 0);

void *ptr;
if (malloc_check_start) {
internal_check();
}
ptr = zone->calloc(zone, num_items, size);

if (os_unlikely(malloc_logger)) {
malloc_logger(MALLOC_LOG_TYPE_ALLOCATE | MALLOC_LOG_TYPE_HAS_ZONE | MALLOC_LOG_TYPE_CLEARED, (uintptr_t)zone,
(uintptr_t)(num_items * size), 0, (uintptr_t)ptr, 0);
}

MALLOC_TRACE(TRACE_calloc | DBG_FUNC_END, (uintptr_t)zone, num_items, size, (uintptr_t)ptr);
if (os_unlikely(ptr == NULL)) {
malloc_set_errno_fast(mzo, ENOMEM);
}
return ptr;
}

calloc直接调用的是_malloc_zone_calloc。我们看一下这个函数的实现。通过返回值,我们确定重点代码应该与ptr有关。所以_malloc_zone_calloc精简如下:

1
2
3
4
5
6
7
8
9
10
11
12
static void *
_malloc_zone_calloc(malloc_zone_t *zone, size_t num_items, size_t size,
malloc_zone_options_t mzo)
{
MALLOC_TRACE(TRACE_calloc | DBG_FUNC_START, (uintptr_t)zone, num_items, size, 0);

void *ptr;
// ptr初始化,这里打一个断点
ptr = zone->calloc(zone, num_items, size);

return ptr;
}

运行到这里,发现zone->calloc进去是一个函数的声明,有没有下文了。这是,我们用lldb调试,打印一下数据:

1
2
(lldb) p zone->calloc
(void *(*)(_malloc_zone_t *, size_t, size_t)) $0 = 0x00000001002f4b93 (.dylib`default_zone_calloc at malloc.c:385)

感觉发现了新大陆:default_zone_callocmalloc.c文件的第385行。

1
2
3
4
5
6
7
static void *
default_zone_calloc(malloc_zone_t *zone, size_t num_items, size_t size)
{
zone = runtime_default_zone();

return zone->calloc(zone, num_items, size);
}

到这里,又有一个zone。这个才是创建的runtime时的default zone。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
MALLOC_NOEXPORT malloc_zone_t* lite_zone = NULL;

MALLOC_ALWAYS_INLINE
static inline malloc_zone_t *
runtime_default_zone() {
// lite_zone = null,所以会执行创建
return (lite_zone) ? lite_zone : inline_malloc_default_zone();
}

⬇️

static inline malloc_zone_t *
inline_malloc_default_zone(void)
{
// 只创建一次,内部是一个os_once
_malloc_initialize_once();
// malloc_report(ASL_LEVEL_INFO, "In inline_malloc_default_zone with %d %d\n", malloc_num_zones, malloc_has_debug_zone);
// 可以看到malloc_zones是一个(malloc_zone_t **)类型的数据
// malloc_zone_t **malloc_zones = (malloc_zone_t **)0xdeaddeaddeaddead;
return malloc_zones[0];
}

这里看一下malloc_zones的内容:

_malloc_zone_t

上面的大部分内容都是围绕着_malloc_zone_t展开的。

这里可以看到malloc_zones[0]的元素就是一个malloc_zone_t *类型的数据,根据lldb打印出来的数据和查看到的内容可以看到malloc_zone_t中存放的就是一些我们在开辟空间时需要调用的方法。

看一下_malloc_zone_t结构体

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#define MALLOC_ZONE_FN_PTR(fn) fn

typedef struct _malloc_zone_t {
/* Only zone implementors should depend on the layout of this structure;
Regular callers should use the access functions below */
void *reserved1; /* RESERVED FOR CFAllocator DO NOT USE */
void *reserved2; /* RESERVED FOR CFAllocator DO NOT USE */
size_t (* MALLOC_ZONE_FN_PTR(size))(struct _malloc_zone_t *zone, const void *ptr);
void *(* MALLOC_ZONE_FN_PTR(malloc))(struct _malloc_zone_t *zone, size_t size);
void *(* MALLOC_ZONE_FN_PTR(calloc))(struct _malloc_zone_t *zone, size_t num_items, size_t size);
void *(* MALLOC_ZONE_FN_PTR(valloc))(struct _malloc_zone_t *zone, size_t size); /* same as malloc, but block returned is set to zero and is guaranteed to be page aligned */
void (* MALLOC_ZONE_FN_PTR(free))(struct _malloc_zone_t *zone, void *ptr);
void *(* MALLOC_ZONE_FN_PTR(realloc))(struct _malloc_zone_t *zone, void *ptr, size_t size);
void (* MALLOC_ZONE_FN_PTR(destroy))(struct _malloc_zone_t *zone);
const char *zone_name;

...

boolean_t (* MALLOC_ZONE_FN_PTR(claimed_address))(struct _malloc_zone_t *zone, void *ptr);
} malloc_zone_t;

结构体中的数据与我们图片上的内容是一样的,定义了很多的方法。以calloc为例。

1
2
3
4
5
6
7
8
void 	*(* MALLOC_ZONE_FN_PTR(calloc))(struct _malloc_zone_t *zone, size_t num_items, size_t size);

转换之后的方法为:

calloc(_malloc_zone_t *zone, size_tnum_items, size_t size) {
// 调用.dylib中的方法,nano_malloc.c : 878
nano_calloc(zone, num_items, size);
}

我们也可以用同样的方式打印一下看看:

1
2
(lldb) po zone->calloc
(.dylib`nano_calloc at nano_malloc.c:878)

打印出来的信息已经很全面了,同样告诉我们了nano_calloc方法在nano_malloc.c文件中。

nano_calloc

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
static void *
nano_calloc(nanozone_t *nanozone, size_t num_items, size_t size)
{
size_t total_bytes;
// alloc之前需要先判断申请的空间大小是否合理
if (calloc_get_size(num_items, size, 0, &total_bytes)) {
return NULL;
}

// 如果获取到的大小在系统约定的最大值范围内则,直接进行malloc。
if (total_bytes <= NANO_MAX_SIZE) {
// 所以一定会走这里
void *p = _nano_malloc_check_clear(nanozone, total_bytes, 1);
if (p) {
return p;
} else {
/* FALLTHROUGH to helper zone */
}
}
malloc_zone_t *zone = (malloc_zone_t *)(nanozone->helper_zone);
// 如果初始化失败,则继续执行。
return zone->calloc(zone, 1, total_bytes);
}

nano_calloc中,我们同样的先看返回值,最底部的是又继续调用了zone->calloc,感觉不太对,上面就找到了还有一个返回p的位置。运行也确实会走到这里。

接下来就是重点了哈~

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
static void *
_nano_malloc_check_clear(nanozone_t *nanozone, size_t size, boolean_t cleared_requested)
{
MALLOC_TRACE(TRACE_nano_malloc, (uintptr_t)nanozone, size, cleared_requested, 0);

void *ptr;
size_t slot_key; // 插槽的key
// 获取真正的内存大小,以及slot_key
size_t slot_bytes = segregated_size_to_fit(nanozone, size, &slot_key); // Note slot_key is set here
// 拿到插槽的index
mag_index_t mag_index = nano_mag_index(nanozone);
//
nano_meta_admin_t pMeta = &(nanozone->meta_data[mag_index][slot_key]);

ptr = OSAtomicDequeue(&(pMeta->slot_LIFO), offsetof(struct chained_block_s, next));
if (ptr) {
// 这里都是error的判断
} else {
// 死循环获取内存指针,拿到返回
ptr = segregated_next_block(nanozone, pMeta, slot_bytes, mag_index);
}

if (cleared_requested && ptr) {
memset(ptr, 0, slot_bytes); // TODO: Needs a memory barrier after memset to ensure zeroes land first?
}
return ptr;
}

接下来看一下开辟空间的算法:segregated_size_to_fit

segregated_size_to_fit

1
2
3
4
5
6
7
8
9
10
11
12
13
14
static MALLOC_INLINE size_t
segregated_size_to_fit(nanozone_t *nanozone, size_t size, size_t *pKey)
{
size_t k, slot_bytes;

if (0 == size) {
size = NANO_REGIME_QUANTA_SIZE; // Historical behavior
}
k = (size + NANO_REGIME_QUANTA_SIZE - 1) >> SHIFT_NANO_QUANTUM; // round up and shift for number of quanta
slot_bytes = k << SHIFT_NANO_QUANTUM; // multiply by power of two quanta size
*pKey = k - 1; // Zero-based!

return slot_bytes;
}

这个算法在上一节中计算alignedInstanceSize类似。这里的主要逻辑是先右移4位,再左移4位。相当于把后4位抹零。拿我们开辟空间传进来的40为例计算一下:

1
2
3
4
(size + NANO_REGIME_QUANTA_SIZE - 1) = 40 + 15 -1
54 -> 0011 0110
>> 4 0000 0011 // 右移4位
<< 4 0011 0000 = 48 // 左移4位

这里计算出来的大小就是48。而slot_key=47。之后就是获取内存指针。

这就是calloc的流程,同时,再一次验证了,iOS在内存中是16字节对齐。

结构体的内存对齐

结构体内存对齐3打原则:

  1. 数据成员对⻬规则:结构体(struct)的数据成员,第一个数据成员放在offset为0的地方,以后每个数据成员存储的起始位置要从该成员大小或者成员的子成员大小(只要该成员有子成员,比如说是数组,结构体等)的整数倍开始(比如int为4字节,则要从4的整数倍地址开始存储。
  2. 结构体作为成员:如果一个结构里有某些结构体成员,则结构体成员要从其内部最大元素大小的整数倍地址开始存储。(struct a里存有struct b,b里有char,int, double等元素,那b应该从8的整数倍开始存储)
  3. 收尾工作:结构体的总大小,也就是sizeof的结果,必须是其内部最大成员的整数倍。不足的要补⻬。

开始代码之前,先了解一下各种数据类型所占的内存大小:

接下来上代码,看看结构体的内存:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
struct Struct1 {
double a;
char b;
int c;
short d;
}str1;

struct Struct2 {
double a;
int b;
char c;
short d;
}str2;

NSLog(@"str1 = %lu, str2-%lu",sizeof(str1),sizeof(str2));

输出的结果很明白的哈。 str1 = 24, str2-16

我们分析一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
struct Struct1 {
double a; // 8 (0-7)
char b; // 1 [8 1] (8)
int c; // 4 [9 10 11 12] 9不是4的整数倍(12 13 14 15)
short d; // 2 [16 17]
}str1;

// 内部需要的大小为: 17
// 最大属性 : 8
// 结构体整数倍: 24

struct Struct2 {
double a; //8 (0-7)
int b; //4 (8 9 10 11)
char c; //1 (12)
short d; //2 (13 14) 13不是2的整数倍,从14开始(14 15)
}str2;

// 内部需要的大小为: 15
// 最大属性 : 8
// 结构体整数倍: 16
NSLog(@"str1 = %lu, str2-%lu",sizeof(str1),sizeof(str2));

如果结构体内部套用结构体呢?

1
2
3
4
5
6
7
8
struct Struct3 {
double a; //8 (0-7)
int b; //4 (8 9 10 11)
char c; //1 (12)
struct Struct2 str_2; // 13 14 15 (16 - 31)
}str3;

NSLog(@"str3 = %lu", sizeof(str3));

按照内存对齐第二条原则,结构体成员从其内部最大成员的size的整数倍开始。c的位置是12,接下来的位置是13,不满足8的整数倍。所以按照原则,前面补齐,从16位开始。所以str3结构体的大小为32。

对象的内存对齐

老样子,先看代码哈:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@interface Person : NSObject

@property (nonatomic, copy) NSString *name;
@property (nonatomic, assign) int age;
@property (nonatomic, assign) int score;
@property (nonatomic, assign) long height;
@property (nonatomic) char c1;
@property (nonatomic) char c2;

@end

main() {

Person *person = [Person alloc];
person.name = @"name";
person.age = 18;
person.score = 20;
person.height = 180;
person.c1 = 'a';
person.c2 = 'b';

NSLog(@"%@ - %lu - %lu - %lu",person,sizeof(person),class_getInstanceSize([Person class]),malloc_size((__bridge const void *)(person)));

}

我们在NSLog上打个断点。有时间的话,可以把属性先注释掉,从0开始,一个属性一个属性的加起来看输出的是什么结果。

  1. po 这里直接输出person指向的对象的内存地址。

    1
    2
    (lldb) po person
    <Person: 0x600002adbcf0>
  2. 这里有一个需要介绍的点,上一章中有用到x person命令,输出的内容与View Memory中显示的是一致的。而x/8gx就是进行排序。8代表的是输出8组内存。如果是4那就是输出4组内容。每一块都是8个字节。

    1
    2
    3
    4
    5
    (lldb) x/8gx person
    0x600002adbcf0: 0x0000000103930808 0x0000001200006261
    0x600002adbd00: 0x0000000000000014 0x000000010392b038
    0x600002adbd10: 0x00000000000000b4 0x0000000000000000
    0x600002adbd20: 0x0000c1c5c19bbd20 0x00000000000007fb

    0x600002adbcf0:是person指向的首地址。后面存放的都是属性的值。内存都是连续的。

  3. 我们分别输出内存里的内容。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    (lldb) po 0x0000000103930808
    Person // isa指针

    (lldb) po 0x00000012
    18

    (lldb) 0x61是十进制的97 -> a

    (lldb) po 0x0000000000000014
    20

    (lldb) po 0x000000010392b038
    name

    (lldb) po 0x00000000000000b4
    180

    0x0000001200006261:这一块地址上内容是被拆开的。我们知道int是4个字节,char是1个字节,所以前面的几位是int的值,后面的再进行拆分,分别是两个char类型的数据。

    苹果在内存上也是做了足够多的优化,虽然在内存上是16个字节对齐的,以空间换时间,提高读取效率,但是在内部实现上,还是进行了大量的优化,这样做的目的是最大限度的节约内存同时保证数据的安全性,但是一定要注意的是,属性是按8个字节对齐的。

    但是为啥两个int类型的数据没有放在一起呢?可能是系统内部做的优化,可以试一下,把所有的char类型注释掉,两个int类型的数据就会存放在一起。可能是会将char类型的数据优先进行填充吧。另外可以多试一下,3个char类型的会怎么样,等等…

  4. 最后输出的结果是

    1
    <Person: 0x600002adbcf0> - 8 - 40 - 48

    我们分析一下输出的内容:

    • person:当前的类对象,存放的指针,指向的内存地址。
    • sizeof(person):person存放的就是一个指针,8个字节。
    • class_getInstanceSize([Person class]):这个类真正需要的空间。属性是8个字节对齐的。
    • malloc_size((__bridge const void *)(person)):内存中需要开辟的空间。内存空间是16个字节对齐的。

float、double

加如改变其中的一个属性位double类型的,又会是什么情况呢?我这里把上面的height改为了double类型。看一下输出结果

1
2
3
4
5
6
7
(lldb) x/8gx person
0x600000045530: 0x000000010626d808 0x0000001200006261
0x600000045540: 0x0000000000000014 0x0000000106268038
0x600000045550: 0x4066800000000000 0x0000000000000000
0x600000045560: 0x0000000000000000 0x0000000000000000
(lldb) po 0x4066800000000000
4640537203540230144

诶~~~ 怎么没有输出180呢?是因为对于float、double类型的数据,系统会做一次特殊的转换处理。我们没有办法直接从内存中读出double类型的值。

但是我们可以通过转化double类型的数据来看是否位上面对应的值。

1
2
(lldb) p/x (double)180.0
(double) $4 = 0x4066800000000000

转换后,发现正好是对应的数据。

这里当然有读取浮点数的方法p/f [0x000000123]

1
2
(lldb) p/f 0x4066800000000000
(long) $5 = 180

这就是内存补齐的内容

总结:

  • calloc的流程。
  • 结构体的对齐规则。
  • 类对象的属性是8个字节对齐的,但是在内存空间是16个字节对齐。
  • x/4gx的使用

引用

libmalloc源码
苹果更多源码

1. 先看一个问题

我们先看一段代码,打印一下输出结果。

1
2
3
4
5
6
Person *p1 = [Person alloc];
Person *p2 = [p1 init];
Person *p3 = [p1 init];
NSLog(@"%@ - %p - %p",p1,p1,&p1);
NSLog(@"%@ - %p - %p",p2,p2,&p2);
NSLog(@"%@ - %p - %p",p3,p3,&p3);

看输出结果

1
2
3
<Person: 0x600000c40470> - 0x600000c40470 - 0x7ffee9114068
<Person: 0x600000c40470> - 0x600000c40470 - 0x7ffee9114060
<Person: 0x600000c40470> - 0x600000c40470 - 0x7ffee9114058

前两个打印的都是当前对象的指针地址,而最后一个为啥会不一样?
首先需要明白p和&p的区别:p是当前变量指向的地址。&p是存放当前变量所在的地址。
这里第一个%p打印就是[Person alloc]生成的地址。第二个%p是存放的是指向【生成的对象】的地址。

也就是当前alloc生成一个对象开辟了一块内存空间。p1、p2、p3分别开辟一块地址指向alloc开辟的空间。

我们可以通过汇编模式或者符号断点查看源码所在的位置。这里不细说了,比较简单。最后定位的源码位置在libobjc.A.dylib->objc_init

2. alloc的执行过程

那我们接下来要看alloc是怎么执行的。需要看objc的源码。objc4源码是可以直接下载的。我们这里用的是最新的818.2版本的。

通过一系列风骚的操作,我们让源码可以运行起来。通过断点和源码我们分析一下alloc的执行过程。

2.1 _objc_rootAlloc

在main.m中,创建一个对象,打上断点。

1
2
Person *p = [Person alloc];
Person *p1 = [p init];

这里只是alloc的最基本的方法。没有什么代码量

1
2
3
4
5
6
7
8
+ (id)alloc {
return _objc_rootAlloc(self);
}

id _objc_rootAlloc(Class cls)
{
return callAlloc(cls, false/*checkNil*/, true/*allocWithZone*/);
}

2.2 callAlloc

这里是核心代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
static ALWAYS_INLINE id
callAlloc(Class cls, bool checkNil, bool allocWithZone=false)
{
#if __OBJC2__ // 是个宏定义
// slowpath表示括号内的条件可能性教小
if (slowpath(checkNil && !cls)) return nil;
// fastpath表示括号内的条件可能性教大
// 如果有自定义的allocWithZone(hasCustomAWZ)
if (fastpath(!cls->ISA()->hasCustomAWZ())) {
return _objc_rootAllocWithZone(cls, nil); // 1.
}
#endif

// No shortcuts available. 根据传进来的参数判断
if (allocWithZone) { // 2.
return ((id(*)(id, SEL, struct _NSZone *))objc_msgSend)(cls, @selector(allocWithZone:), nil);
}
return ((id(*)(id, SEL))objc_msgSend)(cls, @selector(alloc)); // 3.
}
  1. 该处内容是现阶段alloc执行的代码
  2. 根据callAlloc调用传进来的参数判断,基本都会执行【1】。
  3. 通过消息发送,执行alloc,这里有个很有意思的点,源码跑起来就能知道。

2.3 _objc_rootAllocWithZone

1
2
3
4
5
6
7
id
_objc_rootAllocWithZone(Class cls, malloc_zone_t *zone __unused)
{
// allocWithZone under __OBJC2__ ignores the zone parameter
return _class_createInstanceFromZone(cls, 0, nil,
OBJECT_CONSTRUCT_CALL_BADALLOC);
}

2.4 _class_createInstanceFromZone

这里是重中之重。alloc的流程都在这里完美的展示出来。

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
static ALWAYS_INLINE id
_class_createInstanceFromZone(Class cls, size_t extraBytes, void *zone,
int construct_flags = OBJECT_CONSTRUCT_NONE,
bool cxxConstruct = true,
size_t *outAllocatedSize = nil)
{
ASSERT(cls->isRealized());

// Read class's info bits all at once for performance
bool hasCxxCtor = cxxConstruct && cls->hasCxxCtor();
bool hasCxxDtor = cls->hasCxxDtor();
bool fast = cls->canAllocNonpointer();
size_t size;

// 1. 既然要生成一个对象,首先要做的就是开辟空间,但是要开辟多少?
// 由对象的ivars决定。在iOS中,字节是8自己对齐,而内存是16字节对齐,所以小于16字节会补齐
size = cls->instanceSize(extraBytes);
if (outAllocatedSize) *outAllocatedSize = size;

id obj;
if (zone) {
obj = (id)malloc_zone_calloc((malloc_zone_t *)zone, 1, size);
} else {
// 2. 算出来需要多少空间,这里进行开辟
obj = (id)calloc(1, size);
}
// 极少数情况下,obj会创建失败
if (slowpath(!obj)) {
if (construct_flags & OBJECT_CONSTRUCT_CALL_BADALLOC) {
return _objc_callBadAllocHandler(cls);
}
return nil;
}

if (!zone && fast) {
// 3. 空间有了,这里进行对象关联
obj->initInstanceIsa(cls, hasCxxDtor);
} else {
// Use raw pointer isa on the assumption that they might be
// doing something weird with the zone or RR.
obj->initIsa(cls);
}

if (fastpath(!hasCxxCtor)) {
return obj;
}

construct_flags |= OBJECT_CONSTRUCT_FREE_ONFAILURE;
return object_cxxConstructFromClass(obj, cls, construct_flags);
}

2.4.1 cls->instanceSize

1
2
3
4
5
6
7
8
9
10
11
inline size_t instanceSize(size_t extraBytes) const {
// 有缓存的情况下
if (fastpath(cache.hasFastInstanceSize(extraBytes))) {
return cache.fastInstanceSize(extraBytes);
}
// 新开辟
size_t size = alignedInstanceSize() + extraBytes;
// CF requires all objects be at least 16 bytes.
if (size < 16) size = 16;
return size;
}

如果有缓存的情况下,则会执行fastInstanceSize。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
size_t fastInstanceSize(size_t extra) const
{
ASSERT(hasFastInstanceSize(extra));

if (__builtin_constant_p(extra) && extra == 0) {
return _flags & FAST_CACHE_ALLOC_MASK16;
} else {
size_t size = _flags & FAST_CACHE_ALLOC_MASK;
// remove the FAST_CACHE_ALLOC_DELTA16 that was added
// by setFastInstanceSize
// 内存对齐是16字节
return align16(size + extra - FAST_CACHE_ALLOC_DELTA16);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 新开辟空间时计算对象所在空间
// Class's ivar size rounded up to a pointer-size boundary.
uint32_t alignedInstanceSize() const {
return word_align(unalignedInstanceSize());
}
// 8字节对齐
static inline size_t word_align(size_t x) {
// 通过算法得到8字节的倍数
return (x + WORD_MASK) & ~WORD_MASK;
}
// 16字节对齐 fastInstanceSize
static inline size_t align16(size_t x) {
// 通过算法得到16字节的倍数
return (x + size_t(15)) & ~size_t(15);
}

这里举例说明一下算法:

1
2
3
4
5
// WORD_MASK = 7
(x + 7) & ~7;
// x = 2, x + 7 = 9 -> 0000 1001 ⬇️ x=10 17 = 0001 0001
// 7 = 0000 0111 -> !7 = 1111 1000 ⬇️ !7 = 1111 1000
// 9 & !7 = 0000 1000 = 8 (17 & !7 = 0001 0000 = 16)

所以:word_align计算出来的都是8的倍数。
align16计算出来的都是16的倍数。

这里需要注意的是在ARM64下,内存开辟都是16个字节进行对齐的。所以计算的大小的都是16的倍数。

2.4.2 calloc

calloc申请开辟内存,返回地址指针。

2.4.3 obj->initInstanceIsa

生成的对象与class进行关联。通过isa指针。(isa之后会有说明,每一个类都有一个isa指针)。

2.4.4 总结

这个图很好的说明了alloc的流程。

3. init

1
2
3
4
5
6
7
8
9
10
11
- (id)init {
return _objc_rootInit(self);
}

id
_objc_rootInit(id obj)
{
// In practice, it will be hard to rely on this function.
// Many classes do not properly chain -init calls.
return obj;
}

init其实是工厂方法,从上面的代码可以看到,只是return self。这里有一个重要的点,就是大部分的实现都会交给子类去重新自定义init方法。

4. new

1
2
3
+ (id)new {
return [callAlloc(self, false/*checkNil*/) init];
}

我们看到new的实现就是执行了,callAlloc(),然后执行了init操作。与实际上的[[Person alloc] init]并没有什么区别。

但是看到有一些博客上说,还是有区别,因为init被重写之后,调用new可能会造成少些东西。这里不能苟同哈,所有的方法调用,在OC中都是objc_msgSend,会去寻找方法列表的。所以不会存在什么不同。

5. 扩展知识

我们已经知道了,本身写一个Person类,需要开辟16个字节的空间,那需要申请多大内存空间是由什么因素决定的?
我们可以试一下分别添加一个属性,两个属性,试一下。自己动手试一下哈,看看2.4.1小结处返回的size是多少。其申请内存的大小其实是成员变量说了算。

我这里添加了两个NSString属性,分别赋值A和B。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
(lldb) po p
<Person: 0x600002f86cc0>
(lldb) x p
0x600002f86cc0: 60 76 e3 06 01 00 00 00 40 20 e3 06 01 00 00 00 `v......@ ......
0x600002f86cd0: 60 20 e3 06 01 00 00 00 00 00 00 00 00 00 00 00 ` ..............

(lldb) x/4gx p
0x600002f86cc0: 0x0000000106e37660 0x0000000106e32040
0x600002f86cd0: 0x0000000106e32060 0x0000000000000000
(lldb) po 0x0000000106e37660 // isa指针,指向class
Person
(lldb) po 0x0000000106e32040
A
(lldb) po 0x0000000106e32060
B

通过x p命令我们可以打印出p的内存地址。在源码的运行过程中,断点到size计算那里,打印出来size的大小是32个字节。也就是会空8个字节。

如果声明了4个bool值,则4个bool值则会依次放在内存中,例如:0x0000000001010101,这个就涉及到字节的对齐以及iOS系统对属性的重排(内存优化)。

6. 真正的alloc流程

当我们执行[Person alloc]的时候,直接吧断点放在callAlloc->objc_msgSend,则会先执行消息转发。原因是系统内部会通过llvm的函数方法把alloc指向到objc_alloc

1
2
3
4
5
6
// Calls [cls alloc].
id
objc_alloc(Class cls)
{
return callAlloc(cls, true/*checkNil*/, false/*allocWithZone*/);
}

这里先执行一次callAlloc(cls, true, false),注意这里的参数。然后执行到calAlloc中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
static ALWAYS_INLINE id
callAlloc(Class cls, bool checkNil, bool allocWithZone=false)
{
#if __OBJC2__
if (slowpath(checkNil && !cls)) return nil;
if (fastpath(!cls->ISA()->hasCustomAWZ())) {
return _objc_rootAllocWithZone(cls, nil); //1.
}
#endif

// No shortcuts available.
if (allocWithZone) {
return ((id(*)(id, SEL, struct _NSZone *))objc_msgSend)(cls, @selector(allocWithZone:), nil); // 2.
}
return ((id(*)(id, SEL))objc_msgSend)(cls, @selector(alloc)); // 3.
}

通过运行,我们发现会先执行第3个return。所执行的还是alloc,这时候执行的才是真正的alloc。然后就可以顺着第2节的内容继续了。

如图,是完整的alloc会执行两次的流程图:

总结:

  • alloc的流程
  • alloc的两次执行过程
  • init
  • new

有什么不对的欢迎指正。

1. 状态寄存器

CPU内部的寄存器中,有一种特殊的寄存器(对于不同的处理器,个数和结构都可能不同)。这种寄存器在ARM中,被称为状态寄存器就是CPSR(current program status register)寄存器。

CPSR和其他寄存器不一样,其他寄存器是用来存放数据的,都是整个寄存器具有一个含义。而CPSR寄存器是按位起作用的,也就是说,它的每一位都有专门的含义,记录特定的信息。

CPSR寄存器是32位的。

  • CPSR的低8位(包括I、F、T和M[4:0])称为控制位,程序无法修改,除非CPU运行于特权模式下,程序才能修改控制位!
  • N、Z、C、V均为条件码标志位。它们的内容可被算术或逻辑运算的结果所改变,并且可以决定某条指令是否被执行!

1.1 N (Negative)

cpsr的第31位是N,它记录相关指令执行后的结果,结果是负数,则N=1,非负数则N=0。

在ARM64的指令集中,逻辑运算或者算数运算(add、sub、or等)指令的执行会影响状态寄存器的值。

n=1:结果是负数
n=0:结果位非负数,包括0

1.2 Z(Zero)

cpsr的第30位是Z,0标志位。它记录相关指令执行后其结果是否为。如果结果为0,那么Z = 1;如果结果不为0,那么Z = 0.

z=1:结果为0
z=0:结果不为0

1.3 C(Carry)

cpsr第29位是c进位标志位,一般情况进行下无符号述的运算。

加法运算:当运算结果产生了进位时(无符号数溢出)c=1,没有溢出c=0
减法运算:包括(CPM)当运算时产生了借位(无符号数溢出),c=0,没有溢出c=1

1.3.1 进位

两个数据相加,比如正常的十进制运算,5+5=10,向十位数进1,个位数为0。但是超过其最大的位数时,发生溢出,导致进位的值无法保存,也就是说进位的值丢失了。但是CPU在运算的时候,并不会丢弃这个进位的值,二手放在寄存器里了,也就是cpsr的c位。

1.3.2 借位

两个数据做减法操作,有可能向更高位借位。比如:10-5=5,各位不够减,需要向十位去借。这时候会用c位来标记借位。

1.4 V(Overflow)

cpsr的第28位是V,溢出标志位。在进行有符号数运算的时候,如果超过了机器所能标识的范围,称为溢出。

  • 正数 + 正数 = 负数。溢出
  • 负数 + 负数 = 正数。溢出
  • 正数 + 负数 不可能发生溢出

1.5 总结

NZ:是否为0,判断正负
CV:无符号,有符号判断是否溢出

2. 全局变量、常量

开始这一节之前,先知道我们的内存分区划分:

代码区:存放代码,可读,可执行
栈区:参数、局部变量、临时数据,可读可写
堆区:动态申请,可读可写

全局变量:可读可写
常量区:只读

接下来,我们分析代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 定义一个全局变量
int g = 12;

// 定义一个方法
int func(int a,int b){
// 'haha'就是一个常量,在常量区
printf("haha");
// 局部变量c
int c = a + g + b;
return c;
}

int main(int argc, char * argv[]) {

func(10, 20);

return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
}

执行之后,走汇编流程,进行查看

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
Demo`func:
-> 0x100bc211c <+0>: sub sp, sp, #0x20 ; =0x20
0x100bc2120 <+4>: stp x29, x30, [sp, #0x10]
0x100bc2124 <+8>: add x29, sp, #0x10 ; =0x10
0x100bc2128 <+12>: stur w0, [x29, #-0x4]
0x100bc212c <+16>: str w1, [sp, #0x8]
0x100bc2130 <+20>: adrp x0, 1
0x100bc2134 <+24>: add x0, x0, #0xf9f ; =0xf9f
// ① 这里执行了printf操作,大致可以判断,x0中存的就是'haha'
0x100bc2138 <+28>: bl 0x100bc25b0 ; symbol stub for: printf
0x100bc213c <+32>: ldur w8, [x29, #-0x4]
// ② 这里获取的是全局变量
0x100bc2140 <+36>: adrp x9, 3
0x100bc2144 <+40>: add x9, x9, #0x648 ; =0x648
0x100bc2148 <+44>: ldr w10, [x9]
0x100bc214c <+48>: add w8, w8, w10
0x100bc2150 <+52>: ldr w10, [sp, #0x8]
0x100bc2154 <+56>: add w8, w8, w10
0x100bc2158 <+60>: str w8, [sp, #0x4]
0x100bc215c <+64>: ldr w8, [sp, #0x4]
0x100bc2160 <+68>: mov x0, x8
0x100bc2164 <+72>: ldp x29, x30, [sp, #0x10]
0x100bc2168 <+76>: add sp, sp, #0x20 ; =0x20
0x100bc216c <+80>: ret

①这里执行了printf操作,这里看一些是否真的打印了’haha’。我们追一下x0寄存器的变化。

1
0x100bc2130 <+20>: adrp   x0, 1

这里有一个关键字需要注意一下:
adrp: 针对address page操作
这一行代码有三个操作:

  1. 将1左移12位(即在1后加3个0变成1000)
  2. 将当前寄存器的地址的低12位清0,即当前行的地址的后3位清0。0x1002b2184 -> 0x100bc2130
  3. 把0x1000+0x100bc2000赋值给x0,x0=0x100bc3000,即当前行地址的倒数第4位与x0后面的数字相加

说白了,这句代码的意思就是找到某一页地址的开始。我们走断点,打印一下x0.

1
2
(lldb) register read x0
x0 = 0x0000000100bc3000

下一句的代码是,add x0, x0, #0xf9f,就x0的地址+0x0xf9f
获取x0的值位0x100bc3f9f

1
2
3
4
5
6
(lldb) register read x0
x0 = 0x0000000100bc3f9f "haha"

(lldb) x 0x0000000100bc3f9f
0x100bc3f9f: 68 61 68 61 00 01 00 00 00 1c 00 00 00 02 00 00 haha............
0x100bc3faf: 00 24 00 00 00 00 00 00 00 24 00 00 00 02 00 00 .$.......$......

我们知道’h’的ASCII码是97,对应的16进制就行0x61,’a’是0x68。就是我们的常量’haha’。

这样也就拿到了常量的值。
需要注意的是,我们的常量是在编译的时候就已经确定了地址。这里通过当前寄存器的地址为参照,偏移一定的值来获取常量所在的页数。

那继续看一下全局变量

1
2
0x100bc2140 <+36>: adrp   x9, 3
0x100bc2144 <+40>: add x9, x9, #0x648 ; =0x648

我们使用相同的方式,打印一下x9的值。

1
2
3
4
5
6
7
8
9
10
11
// adrp的断点
(lldb) register read x9
x9 = 0x0000000100bc5000

// add 的断点
lldb) register read x9
x9 = 0x0000000100bc5648 g // 这里拿到的是变量g,而g在内存中的值是0c
(lldb) x 0x0000000100bc5648
0x100bc5648: 0c 00 00 00 0c 00 00 00 38 5e 44 29 02 00 00 00 ........8^D)....
0x100bc5658: f0 33 bc 00 01 00 00 00 d0 4f bc 00 01 00 00 00 .3.......O......

我们可以通过相同的方式获取全局变量的值,g=12。

所以局部变量和全局变量都是通过adrp以当前寄存器的地址为参照来查找address来获取值的。

2.1 汇编还原高级语言

这里使用一个牛逼的工具Hopper,可以查看对应的方法转化成汇编之后的代码。

  1. build成功之后,在我们的工程里有一个’Products’文件,里头有对应的xxx.app
  2. 点击show in finder,找到对应的app,右键显示包内容。
  3. 找到与项目同名的黑乎乎的东西(可执行文件),直接拖到Hopper里头就可以了。

我们找到对应的方法func

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
_func:
000000010000611c sub sp, sp, #0x20 ; CODE XREF=_main+32
0000000100006120 stp x29, x30, [sp, #0x10]
0000000100006124 add x29, sp, #0x10
// 这里我们可以知道有两个变量,w0、w1分别存起来
0000000100006128 stur w0, [x29, #-0x4]
000000010000612c str w1, [sp, #0x8]
// adrp操作,获取页数
0000000100006130 adrp x0, #0x100007000 ; argument #1 for method imp___stubs__printf
// 这里直接把结果给出来了。haha存放在x0
0000000100006134 add x0, x0, #0xf9f ; "haha"
// 执行printf
0000000100006138 bl imp___stubs__printf
// 取值,这个位置的值就是w0
000000010000613c ldur w8, [x29, #-0x4]
// adrp操作,根据页数获取值
0000000100006140 adrp x9, #0x100009000
// 获取变量_g。我们可以通过地址去找对应的值。可以在Hopper中找到对应的值
0000000100006144 add x9, x9, #0x648 ; _g
// 赋值w10 = _g
0000000100006148 ldr w10, x9
// 执行加法操作 w8 += w10
000000010000614c add w8, w8, w10
// 取值。也就是获取第二个参数的值w1
0000000100006150 ldr w10, [sp, #0x8]
// 执行加法操作 w8 += w10
0000000100006154 add w8, w8, w10
// 把w8的值存起来
0000000100006158 str w8, [sp, #0x4]
// 取值w8
000000010000615c ldr w8, [sp, #0x4]
// 把w8的值给x0(x0存放返回值)
0000000100006160 mov x0, x8
// 释放内存,return
0000000100006164 ldp x29, x30, [sp, #0x10]
0000000100006168 add sp, sp, #0x20
000000010000616c ret

汇编的代码逻辑已经直接表示出来了。最终可以得出一个函数的方法

1
2
3
4
int func(int w1, int w2) {
printf("haha");
return w1 + _g + w2;
}

2. 条件判断

2.1 cmp(Compare)比较指令

cmp把一个寄存器的内容和另一个寄存器的内容(或立即数)进行比较。但不存储结果,只是更改标志。

一般cmp做完判断后会进行跳转,后面通常会跟上b指令。

cmp比较,其实是一个减法操作,但是不会改变两个比较的值。通过减法的结果去比较。

  • BL 标号:跳转到标号处执行
  • B.LT 标号:比价结果是小于(less than),执行标号,否则不跳转
  • B.LE 标号:比较结果是小于等于(less than or qeual to),执行标号,否则不跳转
  • B.GT 标号:比较结果是大于(greater than),执行标号,否则不跳转
  • B.GE 标号:比较结果是大于等于(greater than or equal to),执行标号,否则不跳转
  • B.EQ 标号:比较结果是**等于(equal to)**,执行标号,否则不跳转
  • B.NE 标号:比较结果是不等于(not equal to),执行标号,否则不跳转
  • B.LS 标号:比较结果是无符号小于等于,执行标号,否则不跳转
  • B.LO 标号:比较结果是无符号小于,执行标号,否则不跳转
  • B.HI 标号:比较结果是无符号大于,执行标号,否则不跳转
  • B.HS 标号:比较结果是无符号大于等于,执行标号,否则不跳转

b.gt #0x10000f8d:这个地址是else的跳转地址

3. 循环

使用汇编的时候一定是在真机上运行。或者直接选中真机,直接Command+Bbuid之后,找到对应的可执行文件,直接放在Hopper里就行。

3.1 do-while

1
2
3
4
5
6
7
8
void loopFunc() {
int nsum = 0;
int i = 0;
do {
nsum += 10;
i ++;
} while (i < 100);
}

看一下汇编下是什么代码逻辑:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
FunctionDemo`loopFunc:
-> 0x102c2a764 <+0>: sub sp, sp, #0x10 ; =0x10
// 这里是把0放在sp+0xc里头,w:低32位,zr==zero
0x102c2a768 <+4>: str wzr, [sp, #0xc] // 假设#0xc存的是a
0x102c2a76c <+8>: str wzr, [sp, #0x8] // 假设#0x8存的是b,之后用的都是w8,会比较乱
// ① 读取a的值
0x102c2a770 <+12>: ldr w8, [sp, #0xc]
// 执行a += 10的操作。
0x102c2a774 <+16>: add w8, w8, #0xa ; =0x1
// 然后把a的值存起来。
0x102c2a778 <+20>: str w8, [sp, #0xc]
// 取值b
0x102c2a77c <+24>: ldr w8, [sp, #0x8]
// b += 1
0x102c2a780 <+28>: add w8, w8, #0x1 ; =0x1
// 把b存起来
0x102c2a784 <+32>: str w8, [sp, #0x8]
0x102c2a788 <+36>: ldr w8, [sp, #0x8]
// 比较b的值,b与100比较
0x102c2a78c <+40>: cmp w8, #0x64 ; =0x64
// 如果 lt(小于)if b < 100 跳转到0x102c2a770继续执行。执行①
0x102c2a790 <+44>: b.lt 0x102c2a770 ; <+12> at main.m:22:14
0x102c2a794 <+48>: add sp, sp, #0x10 ; =0x10
0x102c2a798 <+52>: ret

我们直接使用Hopper工具查看汇编,其实比在Xcode中更方便。

3.2 while

1
2
3
4
5
6
7
8
void whileFunc() {
int i = 0;
int nsum = 0;
while (i < 10) {
nsum += 10;
i += 1;
}
}

我们在Hopper里查看源码:

①处是一个比较,判断w8的值和10的大小,如果b.ge则执行loc_100006128的代码。b.ge是大于等于。
②是直接跳转到loc_10000610c的代码。

3.3 for

1
2
3
4
5
6
void forFunc() {
int nsum = 0;
for (int i = 0; i < 10; i ++) {
nsum += i;
}
}

我们在Hopper里查看源码:

for循环与while循环区别不大。

4. switch

4.1 三个case的switch

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
void switchFunc(int a) {
int nsum = 0;
switch (a) {
case 1:
printf("1");
break;
case 2:
printf("2");
break;
case 3:
printf("3");
break;
default:
printf("default");
break;
}
}

还是运行xCode,查看汇编源码:

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
Demo`switchFunc:
-> 0x100bea0e8 <+0>: sub sp, sp, #0x10 ; =0x10
0x100bea0ec <+4>: str w0, [sp, #0xc]
0x100bea0f0 <+8>: str wzr, [sp, #0x8]
0x100bea0f4 <+12>: ldr w8, [sp, #0xc]
0x100bea0f8 <+16>: cmp w8, #0x1 ; =0x1
0x100bea0fc <+20>: str w8, [sp, #0x4]
0x100bea100 <+24>: b.eq 0x100bea128 ; <+64> at main.m:48:18
0x100bea104 <+28>: b 0x100bea108 ; <+32> at main.m
0x100bea108 <+32>: ldr w8, [sp, #0x4]
0x100bea10c <+36>: cmp w8, #0x2 ; =0x2
0x100bea110 <+40>: b.eq 0x100bea138 ; <+80> at main.m:51:18
0x100bea114 <+44>: b 0x100bea118 ; <+48> at main.m
0x100bea118 <+48>: ldr w8, [sp, #0x4]
0x100bea11c <+52>: cmp w8, #0x3 ; =0x3
0x100bea120 <+56>: b.eq 0x100bea148 ; <+96> at main.m:54:18
0x100bea124 <+60>: b 0x100bea158 ; <+112> at main.m:60:18
0x100bea128 <+64>: ldr w8, [sp, #0x8]
0x100bea12c <+68>: add w8, w8, #0x1 ; =0x1
0x100bea130 <+72>: str w8, [sp, #0x8]
0x100bea134 <+76>: b 0x100bea164 ; <+124> at main.m:63:1
0x100bea138 <+80>: ldr w8, [sp, #0x8]
0x100bea13c <+84>: add w8, w8, #0xa ; =0xa
0x100bea140 <+88>: str w8, [sp, #0x8]
0x100bea144 <+92>: b 0x100bea164 ; <+124> at main.m:63:1
0x100bea148 <+96>: ldr w8, [sp, #0x8]
0x100bea14c <+100>: add w8, w8, #0x14 ; =0x14
0x100bea150 <+104>: str w8, [sp, #0x8]
0x100bea154 <+108>: b 0x100bea164 ; <+124> at main.m:63:1
0x100bea158 <+112>: ldr w8, [sp, #0x8]
0x100bea15c <+116>: subs w8, w8, #0x1 ; =0x1
0x100bea160 <+120>: str w8, [sp, #0x8]
0x100bea164 <+124>: add sp, sp, #0x10 ; =0x10
0x100bea168 <+128>: ret

看汇编源码,其实就是简单的if-else比较,只不过这里换成了b.eq(等于),然后跳转到对应的代码块。

4.2 4个case的switch

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
void switchFunc(int a) {
int nsum = 0;
switch (a) {
case 1:
printf("1");
break;
case 2:
printf("2");
break;
case 3:
printf("3");
break;
case 4:
printf("4");
break;
default:
printf("default");
break;
}
}

int main(int argc, char * argv[]) {
switchFunc(4);
}

我们改变一下代码,再加一个case,运行一下:

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
64
65
66
Demo`switchFunc:
0x1005460ac <+0>: sub sp, sp, #0x20 ; =0x20
0x1005460b0 <+4>: stp x29, x30, [sp, #0x10]
0x1005460b4 <+8>: add x29, sp, #0x10 ; =0x10
// 1. 把w0的值存起来,w0 = 4
0x1005460b8 <+12>: stur w0, [x29, #-0x4]
// 2. 把0存起来
0x1005460bc <+16>: str wzr, [sp, #0x8]
// 3. 取值w8 = 4
0x1005460c0 <+20>: ldur w8, [x29, #-0x4]
// 4. 这里是第一个case 1. w8 = w8 - 1 = 3
0x1005460c4 <+24>: subs w8, w8, #0x1 ; =0x1
// 5. 把x8的值给x9,x9 = 0x0000000000000003
0x1005460c8 <+28>: mov x9, x8
// 6. 这个ubfx语法,往下翻有详细解释。把x9的高32位清零,x9 = 0x0000000000000003
0x1005460cc <+32>: ubfx x9, x9, #0, #32
// 7. x9的值和3进行比较
0x1005460d0 <+36>: cmp x9, #0x3 ; =0x3
// 8. 把x9的值放在sp对应的内存地址中
0x1005460d4 <+40>: str x9, [sp]
// 9. 如果7中比较的结果是一个【无符号大于】,则执行0x100546134,其实就是执行了default操作
-> 0x1005460d8 <+44>: b.hi 0x100546134 ; <+136> at main.m
// 10. adrp:地址操作,x8=0x100546000,左边的地址标号后12位清零,然后加上0x0000
0x1005460dc <+48>: adrp x8, 0
// 11. x8 = 0x10054614c,然后通过view memory,看里头的值。
0x1005460e0 <+52>: add x8, x8, #0x14c ; =0x14c
// 12. 取值x11 = 3
0x1005460e4 <+56>: ldr x11, [sp]
// 13. ldrsw:取值,先计算中括号内部x11, lsl #2:意思是x11左移2位,
// 然后加上x8获取一个地址,把地址里的值给x10
// 3<<2 = 二进制数3:11<<2 = 1100,也就是十进制12,
// x8+12 = 0x10054614c+0xc = 0x100546158
// 这是一个寻址操作,把0x100546158地址的值给x10,
// 通过view memory查看x10 = 0xffffffd8 = -40,是一个负值
0x1005460e8 <+60>: ldrsw x10, [x8, x11, lsl #2]
// x9 = 0x10054614c + (-40) = 0x100546124
0x1005460ec <+64>: add x9, x8, x10
// 执行跳转到 0x100546124
0x1005460f0 <+68>: br x9

// 从这里开始就是case的位置了。
0x1005460f4 <+72>: adrp x0, 1
0x1005460f8 <+76>: add x0, x0, #0xf8c ; =0xf8c
0x1005460fc <+80>: bl 0x100546598 ; symbol stub for: printf
0x100546100 <+84>: b 0x100546140 ; <+148> at main.m:63:1
0x100546104 <+88>: adrp x0, 1
0x100546108 <+92>: add x0, x0, #0xf8e ; =0xf8e
0x10054610c <+96>: bl 0x100546598 ; symbol stub for: printf
0x100546110 <+100>: b 0x100546140 ; <+148> at main.m:63:1
0x100546114 <+104>: adrp x0, 1
0x100546118 <+108>: add x0, x0, #0xf90 ; =0xf90
0x10054611c <+112>: bl 0x100546598 ; symbol stub for: printf
0x100546120 <+116>: b 0x100546140 ; <+148> at main.m:63:1
// 直接跳转到这里,执行adrp操作,获取x0
0x100546124 <+120>: adrp x0, 1
0x100546128 <+124>: add x0, x0, #0xf92 ; =0xf92
// 执行printf操作。
0x10054612c <+128>: bl 0x100546598 ; symbol stub for: printf
0x100546130 <+132>: b 0x100546140 ; <+148> at main.m:63:1
0x100546134 <+136>: adrp x0, 1
0x100546138 <+140>: add x0, x0, #0xf94 ; =0xf94
0x10054613c <+144>: bl 0x100546598 ; symbol stub for: printf
0x100546140 <+148>: ldp x29, x30, [sp, #0x10]
0x100546144 <+152>: add sp, sp, #0x20 ; =0x20
0x100546148 <+156>: ret

发现不一样了啊,不是if-else判断了。

0x10092e0e0 <+24>: ubfx x9, x9, #0, #32
x9寄存器是64位,w8是32位,相当于x9的低32位。
这里的目的就是x9从0位开始到32位清零,也就x9的高32位清零然后赋值给x9

b.hi 无符号大于

br x9:b是跳转,br是不影响lr寄存器的跳转。直接跳到x9(x9是一个地址标号)

ldrsw x10, [x8, x11, lsl #2]: 这里先计算中括号内部。而在中括号内部先计算x11。

  1. lsl表示左移,x11, lsl #2表示x11左移2位。
  2. 加上x8的值。生成一个新的地址。
  3. []表示寻址,也就是说把生成的地址中的值给x10。

看一下我们获取参数是否正确。看一下view memory

偏移表中为什么存储的是地址的偏移量?为什么不直接存对应的地址?

是因为地址只有在运行的时候才会开辟的,每次运行的值都不一样,所以直接存偏移量,然后通过偏移表的起始位置进行计算就可以直接定位了。

4.2.1 switch case 偏移表

我们看第11. x8 = 0x10054614c,这个地址正好是当前汇编函数的末尾0x100546148+0x4。所以,这个函数栈空间后有一堆数据,存放一些值,这一堆数据就是case创建的偏移表。而这些值就是我们要偏移的值。

偏移表中的个数是由 (case的最大值 - case的最小值) + 1。看一下上面的图0x10054614c的位置存放的4个值,分别是0xffffffa8=-88, 0xffffffb8=-72, 0xffffffc8=-56, 0xffffffd8=-40

偏移值是一个负数,是因为是以x8 = 0x10054614c寄存器中存的地址为基数,偏移一个负数得到一个地址,这个地址就是在函数开辟的栈空间内。

我们通过一张图重新看一下这个过程:

4.3 4个不连续case的switch

我们知道了,3个连续的case是if-else比较,4个连续的case就不是if-else了,而是会生成一个表。那么4个不连续的case呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
void switchFunc(int a) {
int nsum = 0;
switch (a) {
case 1:
printf("1");
break;
case 200:
printf("2");
break;
case 30:
printf("3");
break;
case 4:
printf("4");
break;
default:
printf("default");
break;
}
}

这里就不在放汇编源码了,4个不连续的case,与3个case一样,也是通过if-else比较来执行代码块的。

总结

  1. 假设switch语句的分支比较少的时候(例如3,少于4的时候没有意义)没有必要使用此结构,相当于if。
  2. 各个case分支常量的差值较大的时候,编译器会在效率还是内存进行取舍,这个时候编译器还是会编译成类似于if,else的结构。
  3. 在分支比较多的时候:在编译的时候会生成一个表(跳转表每个地址四个字节)

1. 关于CPU的补充

1.1 寄存器

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

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

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

1.2 高速缓存

iPhoneX上搭载的ARM处理器A11它的1级缓存的容量是64KB,2级缓存的容量8M.

CPU每执行一条指令前都需要从内存中将指令读取到CPU内并执行。而寄存器的运行速度相比内存读写要快很多,为了性能,CPU还集成了一个高速缓存存储区域.当程序在运行时,先将要执行的指令代码以及数据复制到高速缓存中去(由操作系统完成).CPU直接从高速缓存依次读取指令来执行.

1.3 寄存器

1.3.1 数据地址寄存器

数据地址寄存器通常用来做数据计算的临时存储、做累加、计数、地址保存等功能。定义这些寄存器的作用主要是用于在CPU指令中保存操作数,在CPU中当做一些常规变量来使用。
ARM64中:

  • 64位 x0-x30,XZR(零寄存器)
  • 32位 w0-w30,WZR(零寄存器)

1.3.2. 浮点和向量寄存器

因为浮点数的存储以及其运算的特殊性,CPU中专门提供浮点数寄存器来处理浮点数。

  • 64位: d0-d31
  • 32位: d0-d31

现在的CPU支持向量运算.(向量运算在图形处理相关的领域用得非常的多)为了支持向量计算系统了也提供了众多的向量寄存器.

向量寄存器 128位:V0-V31

1.3.3 SP、FP寄存器

说这两个,需要先说一下栈。

栈是一种具有特殊的访问方式的存储空间,先进后处,后进先出。(Last In Out First)

  • sp寄存器在任意时刻会保存栈顶的地址。
  • fp寄存器也成为x29寄存器。属于通用寄存器,在默写时刻我们利用它保存栈底的地址。

需要注意的是,ARM64里面对栈的操作是16个字节对齐的。

这个图很好的说明了栈是从高地址往低地址开始读写操作的,堆是从低地址向高地址开始的,当栈不断的开辟空间,堆也不断的开辟空间,导致两个区域重叠,就会导致崩溃。也就是常说的堆栈溢出。(堆、栈上的空间是不固定的)

这里我们说个题外话,是不是所有的死循环都会导致崩溃?答案是否定的,只有不断的开辟空间的死循环才会导致崩溃,上一章我们最后的例子就是很好的说明,因为没有开辟空间。

2. 函数调用栈

以下代码是常见的函数调用开辟和恢复栈空间。

1
2
3
4
5
6
7
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

这里需要注意的是: 读、写数据都是往高地址读、写。

2.1 内存读写指令

  • str指令:store register,将数据从寄存器中读出来,存在内存中。每次操作8个字节
  • ldr指令:load register,将数据从内存中读出来,存在寄存器中。每次操作8个字节
  • stp指令:str指令的变种,每次操作16个字节。
  • ldp指令:ldr指令的变种,每次操作16个字节。

2.2 堆栈操作

1
2
3
4
5
6
_ABTest:
sub sp, sp, #0x20 ; 开辟栈空间,在当前sp所在的位置减去32个字节。
stp x0, x1, [sp, #0x10] ; 之所以用[],是因为sp存的是一个地址,这里的操作是寻址,把x0,x1的值放在对应的位置,但是栈的读写都是在高位,所以这里还需要加上一个值,写在高位
ldp x1, x0, [sp, #0x10] ; 这里是交换x0,x1的值。注意,当前的操作不会改变sp的值,寄存器中的值进行交换
add sp, sp, #0x20 ; 这里恢复栈空间。
ret
  1. 我们将上面的代码放在“.s”文件中,在ViewControler中声明int ABTest();方法.
  2. 在viewDidLoad中调用ABTest();,并在这一行打上断点。运行触发断点之后,按住ctrl键的同时点击小箭头,进入汇编,(按住ctrl是为了不让程序执行下一步)
  3. 在右下命令行中输入register read sp查看当前sp所在的位置,是sp = 0x000000016fbf1290
  4. 点击下一步,开辟栈空间,重复第3步的操作,查看sp = 0x000000016fbf1270
  5. 进入View Memory,定位到sp所在的位置,查看在0x000000016fbf1280位置的值是什么。
  6. 这个时候,分别执行register write x0 0x0aregister write x1 0x0b,修改x0,x1的值,执行下一步。
  7. 发现在左边通用寄存器中x0,x1的值已经发生变化。这时候重复第5步操作。查看是否已经发生变化。(需要切换页)
  8. 执行下一步,交换x0,x1的值。我们发现左边,通用寄存器中x0,x1的值已经发生了变化,这时候重复第5步,查看内存中的值是否有变化?是没有发生变化的哈~
  9. 销毁当前栈空间。重复第3步,查看当前sp的地址。是sp = 0x000000016fbf1290

如图:

3. bl和ret指令

3.1 bl

bl其实存在两个操作:

  1. 将下一条指令的地址放入lr(x30)寄存器。也就是保存回家的路。
  2. 转到对应的跳转中执行指令,当指令执行完成后,会根据lr中的地址,返回继续执行。

通俗的讲就是离家出走了,执行ret的时候,根据lr中的地址,找到回家的路。

3.2 ret

默认使用lr(x30)寄存器的值,通过底层指令提示CPU此处作为下条指令地址。这是ARM64平台的特色指令,它面向硬件方面做了优化处理。

3.3 x30寄存器(lr寄存器)

x30寄存器存放的是函数的返回地址,当ret指令执行时,会寻找x30寄存器保存的地址值。

这也就是,为啥上一章,最后的代码会造成循环引用的原因,因为x30寄存器的地址指向的就是当前bl的下一行代码。

3.4 操作

我们简写一下上一章的代码

1
2
3
4
5
6
7
8
9
_A:
mov x0, 0xaa
bl _B
mov x0, 0xaa
ret

_B:
mov x0, #0xbb
ret
  1. 在ViewDidLoad中执行A(),并打断点。执行上面的代码。按住ctrl键点击小剪头,进入A的汇编。查看当前lr寄存器中存放的地址是谁。然后按照下图所示进行操作,进入ViewDidLoad的汇编。

  2. 我们看到了19行执行了 bl A的操作,也就是在ViewDidLoad中执行A()操作。而lr寄存器所存储的地址就是第20行所在的位置,也就是存储了执行A之后返回ViewDidLoad的地址。0x1003ce56c

  3. 点击继续执行,修改x0寄存器的值,继续下一步。执行bl B

  4. 这时候我们发现lr寄存器中存储的值已经被修改了,变成了A汇编代码中bl B下一行的地址。lr = 0x1003ce904,这里修改了x0的值。

  5. 下一步。继续执行B中的ret操作,发现回到了A,回到了0x1003ce904,继续执行发现修改了x0的值。

  6. 下一步,执行ret,发现又回到了A中的0x1003ce904,不断的执行,发现压根回不去ViewDidLoad了。

这就是上一章中说的问题,lr寄存器的值被修改了,导致回不去了。那我们应该怎么处理呢?

最合理的方案是在执行bl操作之前,将bl的下一行地址存放在栈中。如果将值存放在其他寄存器中是绝对不安全的,因为你不知道什么时候就会被系统覆盖。

3.4.1 解决死循环

我们为了解决上面的问题,我们查看系统是怎么处理这个问题的。

1
2
3
4
5
6
7
8
void c() {
d();
return;
}

void d() {

}

同样,在ViewDidLoad中执行c()

1
2
3
4
5
6
Demo`c:
-> 0x1005464e0 <+0>: stp x29, x30, [sp, #-0x10]!
0x1005464e4 <+4>: mov x29, sp
0x1005464e8 <+8>: bl 0x1005464f4 ; d at ViewController.m:38:1
0x1005464ec <+12>: ldp x29, x30, [sp], #0x10
0x1005464f0 <+16>: ret

在c的汇编里头,我们仔细看下系统是什么处理lr寄存器的。
我们看到了x29和x30两个寄存器。x29是fp寄存器,指向栈底;x30寄存器就是lr寄存器。

  1. stp x29, x30, [sp, #-0x10]! 这是汇编代码简写的形式的。这句话的意思是sp -= 0x10开辟空间,把x29和x30寄存器的值存放在开辟的空间里。“!”的操作是针对sp的,“[]”的操作是针对x29,x30寻址的。需要注意的是,先存值,在改变sp。
  2. mov x29, sp 将sp的值赋给x29寄存器。啥意思,fp跟sp指向相同的位置。栈顶栈底指向同一位置,啥情况?之后说哈~
  3. bl操作,执行d()
  4. ldp x29, x30, [sp], #0x10 跟第一句差不多,“[]”就是寻址,将sp对应的两个地址的值赋值给x29,x30。第一步是存,这一步是取。然后执行 sp += 0x10的操作,释放栈空间。
  5. 执行ret操作,我们就能轻松的回到ViewDidLoad了。因为lr寄存器中的地址正是我们一开始存的值。

在执行的过程中,我们一步步查看lr寄存器的值看是怎么变化的。就能清晰明了了。

这个时候,我们就可以修改上面的代码了

1
2
3
4
5
6
7
8
9
10
11
_A:
str x30, [sp, #-0x10]! ;仿造系统方法,因为x29寄存器暂时没有用处,所以只使用x30。
mov x0, 0xaa
bl _B
mov x0, 0xaa
ldr x30, [sp], #0x10
ret

_B:
mov x0, #0xbb
ret

执行该代码,我们按照栈操作3.4的流程,查看整体流程,看x30寄存器存放读取的过程,配合View Memory使用会更爽哈~

这里把代码做一下修改,在A中str x30, [sp, #-0x8]!将16个字节改成8个字节会怎样?跑一遍试试看

会发生crash对不对。因为在ARM64里面,对栈的操作是16个字节对齐的。所以开辟空间操作一定是16字节的倍数来进行的。

4. 函数的参数和返回值

ARM64下,函数的参数是存放在x0-x7(32位w0-w7)这个8个寄存器里面的。如果超过8个参数,就会入栈。
函数的返回值是放在x0(32位是w0)寄存器里的。

这里有一个点,在OC中,一般情况下,定义函数最多可以有几个参数?这里有一个小坑哈~
在runtime里,我们知道,函数调用都是通过objc_msgsend来处理的,而这里个里头已经存在了两个默认参数,一个是self,一个obj

当我们不知道怎么处理带参数的函数时,就看系统是怎么实现的。

1
2
3
4
/// 我们定义一个函数,在viewDidLoad中执行。
int sumA(int a, int b) {
return a + b;
}

执行之后,按住control点击进汇编:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
首先我们来到viewDidLoad中,
`-[ViewController viewDidLoad]:
; 这里我们有看到赋值,sumA(10+20),我们看到w0=10,w1=20
0x104d125d4 <+68>: mov w0, #0xa
0x104d125d8 <+72>: mov w1, #0x14
-> 0x104d125dc <+76>: bl 0x104d12570 ; sumA at ViewController.m:16 ; 这里有bl指令,继续执行跳转到sumA操作。
0x104d125e0 <+80>: ldp x29, x30, [sp, #0x20]
0x104d125e4 <+84>: add sp, sp, #0x30 ; =0x30
0x104d125e8 <+88>: ret

---------------------------------------------------------------------
FunctionDemo`sumA:
-> 0x100d3a4dc <+0>: sub sp, sp, #0x10 ; 开辟16个字节的空间
0x100d3a4e0 <+4>: str w0, [sp, #0xc] ; 寻址把w0存放在sp+0xC的位置
0x100d3a4e4 <+8>: str w1, [sp, #0x8] ; 寻址把w1存放在sp+0x8的位置
0x100d3a4e8 <+12>: ldr w8, [sp, #0xc] ; 把sp+0xC位置的值给w8
0x100d3a4ec <+16>: ldr w9, [sp, #0x8] ; 把sp+0x8位置的值给w9
0x100d3a4f0 <+20>: add w0, w8, w9 ; 执行加法操作,并赋值给w0
0x100d3a4f4 <+24>: add sp, sp, #0x10 ; 释放栈空间
0x100d3a4f8 <+28>: ret ; ret

通过上面汇编之后的代码,我们可以看到整个的流程,相当于生成了两个临时量变去存储传进来的值,然后把返回值存储在w0寄存器里。

1
2
3
4
5
6
/// 我们定义一个函数,在viewDidLoad中执行。
int sumA(int a, int b) {
int a1 = 1; // 生成局部变量a1,b1
int b1 = b;
return a1 + b1;
}

通过上面系统的实现方案,我们就可以自己写一个带有参数,返回值的方法。在“.s”文件中实现

1
2
3
4
5
.global _sumB

_sumB:
add x0, x0, x1
ret

4.2 验证超过8个参数的情况

多余的参数会存放在调用方法所在的栈空间里,然后在调用的方法里去取别人的栈中存放的参数。

1
2
3
4
5
6
7
8
int test(int a, int b, int c, int d, int e, int f, int g, int h, int i) {
return a+b+c+d+e+f+g+h+i;
}

- (void)viewDidLoad {
[super viewDidLoad];
test(1,2,3,4,5,6,7,8,9);
}

执行代码,我们看汇编之后的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
`-[ViewController viewDidLoad]:
...
... 这中间省略了一大部分代码,我们直接从这里看
; 这里打印 sp = 0x000000016f4c53c0
0x10093e594 <+64>: bl 0x10093e9b4 ; symbol stub for: objc_msgSendSuper2
; 这个是调用super viewDidLoad
0x10093e598 <+68>: mov w0, #0x1 ; 将1存到w0寄存器中
0x10093e59c <+72>: mov w1, #0x2
0x10093e5a0 <+76>: mov w2, #0x3
0x10093e5a4 <+80>: mov w3, #0x4
0x10093e5a8 <+84>: mov w4, #0x5
0x10093e5ac <+88>: mov w5, #0x6
0x10093e5b0 <+92>: mov w6, #0x7 ; 这些值我们是可以在通用寄存器里看到的
0x10093e5b4 <+96>: mov w7, #0x8 ; 将8存到w7寄存器中
; x8 = 0x0000000100940ce8 "viewDidLoad"
0x10093e5b8 <+100>: mov x8, sp ; 这里是把sp栈顶的位置放在x8寄存器中。
; x8 = 0x000000016f4c53c0
0x10093e5bc <+104>: mov w10, #0x9 ; 把9放在w10寄存器
0x10093e5c0 <+108>: str w10, [x8] ; 把w10寄存器中的值,放在x8寄存器所在的地址里
; 也就是在sp的位置,存放了9这个变量。
-> 0x10093e5c4 <+112>: bl 0x10093e4dc ; sumA at ViewController.m:16 ; 这里执行 sumA
0x10093e5c8 <+116>: ldp x29, x30, [sp, #0x30] ; x29,x30取值,是为了函数返回
0x10093e5cc <+120>: add sp, sp, #0x40 ; =0x40 ; 释放栈空间
0x10093e5d0 <+124>: ret

接下来,我们看test的汇编代码情况:

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
`test:
; 开辟空间之前 sp = 0x000000016f4c53c0
-> 0x10093e4dc <+0>: sub sp, sp, #0x30 ; =0x30
; 开辟栈空间后,sp=0x000000016f4c5390
0x10093e4e0 <+4>: ldr w8, [sp, #0x30] ; 这是从sp+0x30的位置取值,放在w8寄存器里。
; sp+0x30就是开辟当前栈空间之前的位置,也就是viewDidLoad开辟空间的栈顶位置,这个位置是x8寄存器指向的位置,存放的是变量9
0x10093e4e4 <+8>: str w0, [sp, #0x2c] ; 把w0寄存器的值存放在栈sp+0x2c里头,也就是sp偏移4个字节,正好存放一个int类型的数据。
0x10093e4e8 <+12>: str w1, [sp, #0x28]
0x10093e4ec <+16>: str w2, [sp, #0x24]
0x10093e4f0 <+20>: str w3, [sp, #0x20]
0x10093e4f4 <+24>: str w4, [sp, #0x1c]
0x10093e4f8 <+28>: str w5, [sp, #0x18]
0x10093e4fc <+32>: str w6, [sp, #0x14]
0x10093e500 <+36>: str w7, [sp, #0x10]
0x10093e504 <+40>: str w8, [sp, #0xc] ; w8寄存器的值放在sp+0xc里,w8=9
0x10093e508 <+44>: ldr w8, [sp, #0x2c] ; 赋值操作 w8=1
0x10093e50c <+48>: ldr w9, [sp, #0x28] ; w9 = 2
0x10093e510 <+52>: add w8, w8, w9 ; w8 = w8+w9 = 1+2 = 3
0x10093e514 <+56>: ldr w9, [sp, #0x24] ; w9 = 3
0x10093e518 <+60>: add w8, w8, w9 ; w8 += w9 = 3 + 3
0x10093e51c <+64>: ldr w9, [sp, #0x20]
0x10093e520 <+68>: add w8, w8, w9
0x10093e524 <+72>: ldr w9, [sp, #0x1c]
0x10093e528 <+76>: add w8, w8, w9
0x10093e52c <+80>: ldr w9, [sp, #0x18]
0x10093e530 <+84>: add w8, w8, w9
0x10093e534 <+88>: ldr w9, [sp, #0x14]
0x10093e538 <+92>: add w8, w8, w9
0x10093e53c <+96>: ldr w9, [sp, #0x10]
0x10093e540 <+100>: add w8, w8, w9
0x10093e544 <+104>: ldr w9, [sp, #0xc]
0x10093e548 <+108>: add w0, w8, w9 ; 计算完成
0x10093e54c <+112>: add sp, sp, #0x30 ; =0x30 ,释放栈空间
0x10093e550 <+116>: ret

这里会把9这个参数存放在viewDidLoad所开辟的栈空间里。执行test后,1-8会存放在test函数所开辟的空间中,然后把9这个参数从viewDidLoad所开辟的栈空间里拿回来,是通过x8寄存器来定位地址获取9这个参数的。相当于从别人家借东西,会存在sp计算的问题,会影响效率。

我们一定要知道的一点是,栈的读写都是从高位往低位进行读写,栈空间的读写都是基于上述原则进行操作的。
以上操作,配合View Memory查看内存中的数据会更清晰。

4.2.1 release下操作

我们的这一系列操作都是在debug模式下进行的,加法的计算产生的汇编代码竟然是如此繁杂。如果我们切换到release下运行,会有什么情况发生?

在release下,编译器会进行优化,我们的test方法,只是做了调用,没有任何实际意义,所以在release下根本不会有bl指令。

如果我们执行printf("%d", sumA(1,2,3,4,5,6,7,8,9));呢?

其实差别不大,经过系统优化之后,就只剩下mov w8, #0x2d这一句代码了,0x2d = 45。就是这么简单直接。

4.3 验证返回值

如果返回值超过8个字节,x0寄存器存不下的时候,会通过栈空间来返回。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
struct NumA getStructA(int a, int b, int c, int d, int e, int f) {
struct NumA num;
num.a = a;
num.b = b;
num.c = c;
num.d = d;
num.e = e;
num.f = f;
return num;
}

- (void)viewDidLoad {
[super viewDidLoad];
struct NumA num = getStructA(1,2,3,4,5,6);
}

这里呢,我们返回一个结构体,正常来说,结构体的大小是根据结构体中的变量决定的。这里有6个int类型的变量也就是24个字节。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
`getStructA:
-> 0x1025424a0 <+0>: sub sp, sp, #0x20 ; =0x20 开辟栈空间
0x1025424a4 <+4>: str w0, [sp, #0x1c]
0x1025424a8 <+8>: str w1, [sp, #0x18]
0x1025424ac <+12>: str w2, [sp, #0x14]
0x1025424b0 <+16>: str w3, [sp, #0x10]
0x1025424b4 <+20>: str w4, [sp, #0xc]
0x1025424b8 <+24>: str w5, [sp, #0x8]
0x1025424bc <+28>: ldr w9, [sp, #0x1c]
0x1025424c0 <+32>: str w9, [x8]
0x1025424c4 <+36>: ldr w9, [sp, #0x18]
0x1025424c8 <+40>: str w9, [x8, #0x4]
0x1025424cc <+44>: ldr w9, [sp, #0x14]
0x1025424d0 <+48>: str w9, [x8, #0x8]
0x1025424d4 <+52>: ldr w9, [sp, #0x10]
0x1025424d8 <+56>: str w9, [x8, #0xc]
0x1025424dc <+60>: ldr w9, [sp, #0xc]
0x1025424e0 <+64>: str w9, [x8, #0x10]
0x1025424e4 <+68>: ldr w9, [sp, #0x8]
0x1025424e8 <+72>: str w9, [x8, #0x14]
0x1025424ec <+76>: add sp, sp, #0x20 ; =0x20
0x1025424f0 <+80>: ret

这里我们又看到了一个熟悉的x8寄存器。然后通过w9寄存器,不断的赋值给x8寄存器对应的空间里。那这个x8寄存器是怎么个情况呢,我们返回viewDidLoad对应的汇编代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
`-[ViewController viewDidLoad]:
...
... ;这里也是截取部分代码
0x1025425ac <+64>: bl 0x1025429b4 ; symbol stub for: objc_msgSendSuper2
0x1025425b0 <+68>: add x8, sp, #0x8 ; =0x8
0x1025425b4 <+72>: mov w0, #0x1
0x1025425b8 <+76>: mov w1, #0x2
0x1025425bc <+80>: mov w2, #0x3
0x1025425c0 <+84>: mov w3, #0x4
0x1025425c4 <+88>: mov w4, #0x5
0x1025425c8 <+92>: mov w5, #0x6
0x1025425cc <+96>: bl 0x1025424a0 ; getStructB at ViewController.m:46
-> 0x1025425d0 <+100>: ldp x29, x30, [sp, #0x40]
0x1025425d4 <+104>: add sp, sp, #0x50 ; =0x50
0x1025425d8 <+108>: ret

我们看到x8寄存器的位置是sp偏移8个字节。也就是返回值所在的空间是在viewDidLoad开辟的栈空间里。

这里会当前返回值存放在viewDidLoad所开辟的栈空间里,因为知道返回的是什么类型的数据,在viewDidLoad开辟空间时,就已经把返回值所需要的空间给预留出来了。通过x8寄存器来定位返回值所在的空间。

那么,这里为什么要偏移8个字节?

我们知道,ARM64对栈的操作是16个字节进行对齐的。而结构体占有24个字节,我们只能通过补齐来确保是16个字节的倍数来开辟空间。

执行对应的方法,对返回值的变量进行存储(根据x8寄存器来定位相应的地址存储变量的值)。

5. 函数的局部变量

1
2
3
4
5
6
7
8
9
10
int sumC(int a, int b) {
int c = 10;
return a+b+c;
}

- (void)viewDidLoad {
[super viewDidLoad];

sumC(1,2);
}

运行,进入汇编模式

1
2
3
4
5
6
7
Demo`-[ViewController viewDidLoad]:
0x1026ae45c <+68>: mov w0, #0x1
0x1026ae460 <+72>: mov w1, #0x2
-> 0x1026ae464 <+76>: bl 0x1026ae3e8 ; sumC at ViewController.m:75
0x1026ae468 <+80>: ldp x29, x30, [sp, #0x20]
0x1026ae46c <+84>: add sp, sp, #0x30 ; =0x30
0x1026ae470 <+88>: ret

sumC(1, 2):1和2分别放在了w0、w1寄存器中。然后执行bl,进入函数sumC

1
2
3
4
5
6
7
8
9
10
11
12
13
Demo`sumC:
-> 0x1026ae3e8 <+0>: sub sp, sp, #0x10 ; =0x10
0x1026ae3ec <+4>: str w0, [sp, #0xc]
0x1026ae3f0 <+8>: str w1, [sp, #0x8]
0x1026ae3f4 <+12>: mov w8, #0xa
0x1026ae3f8 <+16>: str w8, [sp, #0x4]
0x1026ae3fc <+20>: ldr w8, [sp, #0xc]
0x1026ae400 <+24>: ldr w9, [sp, #0x8]
0x1026ae404 <+28>: add w8, w8, w9
0x1026ae408 <+32>: ldr w9, [sp, #0x4]
0x1026ae40c <+36>: add w0, w8, w9
0x1026ae410 <+40>: add sp, sp, #0x10 ; =0x10
0x1026ae414 <+44>: ret
  1. 开辟16个字节的内存空间
  2. 把w0放在[sp+0xc],w1放在[sp+0x8]
  3. w8赋值等于0xa,这里就是我们的局部变量c=10
  4. 然后把w8放在[sp+0x4]里头
  5. 一堆操作,ret

看到了吧,函数的参数和局部变量都是放在栈里的。

6. 函数嵌套

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
int funcSum(int a, int b, int c) {
int d = a + b + c;
printf("%d", d);
return d;
}

int totalSum(int a, int b) {
int c = 10;
int d = funcSum(a, b, c);
return d;
}


- (void)viewDidLoad {
[super viewDidLoad];
totalSum(1, 2);
}

我们执行上面的含有局部变量的嵌套函数,看是怎么在汇编下执行的。

1
2
3
4
5
6
7
8
9
10
11
Demo`-[ViewController viewDidLoad]:
...
...
0x1002fa43c <+64>: bl 0x1002fa8d0 ; symbol stub for: objc_msgSendSuper2
// totalSum(1, 2)
0x1002fa440 <+68>: mov w0, #0x1 // 将1存在w0寄存器里
0x1002fa444 <+72>: mov w1, #0x2 // 2存放在w1寄存器里
-> 0x1002fa448 <+76>: bl 0x1002fa3bc ; totalSum at ViewController.m:86
0x1002fa44c <+80>: ldp x29, x30, [sp, #0x20] ; x29、x30寄存器取值(lr寄存器获取回家的路)
0x1002fa450 <+84>: add sp, sp, #0x30 ; =0x30
0x1002fa454 <+88>: ret

这一坨汇编代码,已经看过无数次了,这里不细说了,直接走totalSum看看是怎么处理的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Demo`totalSum:
-> 0x1002fa3bc <+0>: sub sp, sp, #0x20 ; =0x20
0x1002fa3c0 <+4>: stp x29, x30, [sp, #0x10]
0x1002fa3c4 <+8>: add x29, sp, #0x10 ; =0x10
0x1002fa3c8 <+12>: stur w0, [x29, #-0x4] ; 把totalSum的参数w0存放在栈底的位置
0x1002fa3cc <+16>: str w1, [sp, #0x8] ; 把w1的值放在栈顶+8个字节的位置
0x1002fa3d0 <+20>: mov w8, #0xa ; 获取局部变量10,放在w8寄存器
0x1002fa3d4 <+24>: str w8, [sp, #0x4] ; w8的值放在sp+4个字节的位置
0x1002fa3d8 <+28>: ldur w0, [x29, #-0x4] ; 重新对w0赋值,取值的位置就是之前w0存放的位置 w0=1
0x1002fa3dc <+32>: ldr w1, [sp, #0x8] ; w1取值w1=2
0x1002fa3e0 <+36>: ldr w2, [sp, #0x4] ; w2 = 10
0x1002fa3e4 <+40>: bl 0x1002fa35c ; funcSum at ViewController.m:80 ;执行嵌套函数 funcSum。
0x1002fa3e8 <+44>: str w0, [sp] ; 把w0的值存在sp对应的位置。
0x1002fa3ec <+48>: ldr w0, [sp] ; 获取w0
0x1002fa3f0 <+52>: ldp x29, x30, [sp, #0x10] ; 找到回家的路
0x1002fa3f4 <+56>: add sp, sp, #0x20 ; =0x20 释放
0x1002fa3f8 <+60>: ret

这里用到了sturldur。这两个的本质与strldr没有区别,只是带u的偏移的是一个负值。

这里也有用到x29寄存器,还有印象吗?x29寄存器就是fp寄存器,指向的是栈底的位置。从栈的存储空间来看,栈底的地址比栈顶大,所以sp栈顶开辟空间都是减去一个值,而用栈底fp做关键值时,要想获取数据都必须在sp-fp之间拿值,所以基于fp的操作都是【减】。

这里为什么把局部变量的值存在w8里面,就是因为w0-w7是存放函数参数的参数,之前说过,w8用来获取局部变量。

funcSum函数的汇编就不说了,与之前的没什么区别。

这里需要提一句的是,为啥要把参数先存放在内存里,然后再取出来,难道就不嫌麻烦吗?其主要目的就是为了保护参数,防止被改变。

到最后w0/x0寄存器还是用来存放返回值。

7. 补充内容

  1. 一个函数的参数,在函数执行完毕之后,是否能拿到这个参数的值?我们用4.2小结的代码来解释一下。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    int test(int a, int b, int c, int d, int e, int f, int g, int h, int i) {
    return a+b+c+d+e+f+g+h+i;
    }

    - (void)viewDidLoad {
    [super viewDidLoad];
    test(1,2,3,4,5,6,7,8,9);
    }

    这个test函数有9个参数,我们知道,x0-x7(w0-w7)这个8个寄存器是存放函数变量的,如果超过8个参数,则会存放在viewDidLoad函数开辟的栈空间内,也就是说1-8这8个参数是在test函数开辟的栈空间。这8个参数在test函数执行完毕之后,随着空间的释放就拿不到了,而9这个参数存放在viewDidLoad的栈空间,我们还可以拿到。

  2. 在4.3小结,我们返回的是一个结构体,而不是一个指针,假如,我们添加一个函数,来调用这个返回的结构体,这个结构体能不能用。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    struct NumA getStructA(int a, int b, int c, int d, int e, int f) {
    struct NumA num;
    num.a = a;
    num.b = b;
    num.c = c;
    num.d = d;
    num.e = e;
    num.f = f;
    return num;
    }

    struct NumA returnStruct() {
    struct NumA num = getStructA(1,2,3,4,5,6);
    return num;
    }

    - (void)viewDidLoad {
    [super viewDidLoad];
    struct NumB num = returnStruct();
    printf("a = %d\n", num.a); // 这里是否能输出,还是会crash
    }

    肯定是可以输出的,在viewDidLoad函数执行时,就已经创建了struct NumB所需要的空间了,返回的数据都存在于viewDidLoad的栈空间里,所以还是可以正常执行的。

总结

  1. 栈:引出SP、FP寄存器。SP:保存栈顶地址,FP:保存栈底的地址。(栈顶的地址比栈底的地址小,所以获取栈顶的值都是通过sub sp, sp #0x10,是减去一个空间,在存值的时候一般都是[sp+#0x08])
  2. stp/str 存值(16个字节/8个字节)
  3. ldp/ldr 取值(16个字节/8个字节)
  4. stur/ldur 本质上与str/ldr没有区别,带【u】的操作的是一个负值。
  5. bl指令:通过lr(x30)寄存器,保存回家的路,bl跳转到对应的方法
  6. lr寄存器的值会通过保存在栈空间,来确保能够正确的返回。
  7. 函数的参数:存放在x0-x7寄存器,超过8个,则放在栈里。
  8. 返回值:使用x0寄存器保存,如果大于8个字节,会利用栈空间传递。
  9. 函数的局部变量放在栈里,嵌套函数的值也是放在栈里
  10. 会把变量的值放在内存里保护起来,用的时候在去取值

1. 准备工作

  1. 我们需要一台iPhone手机,根据自己的条件,要求系统是iOS11.0 - 14.3,写这篇博客时所能处理的越狱系统是这个,过期之后请自行查找资料。
  2. 越狱的网站:https://unc0ver.dev/,下载安装包(ipa文件),可以按照对应的步奏进行操作,本篇所说的有另外一种,使用重签名的机制,比较简单。

2. 开始越狱

  1. 打开我们的工程文件,请自行下载。

  2. 在”Signing & Capabilities” 下,修改”Bundle Identifier”为自己的,修改”Team” 使用”personal team”。

  3. 在”Build Phases” - “Run Script”下,先将script运行代码注释掉。

    1
    2
    // 重签名机制的脚本文件
    #./appSign.sh
  4. 运行当前的空工程文件到手机上。

  5. 运行完成之后,在回到”Build Phases” - “Run Script”下,将注释打开,重新运行。

  6. “unc0ver”这个APP已经出现在手机上,点击APP,直接开始”Jailbreak”。手机重启几次之后,就可以了。

  7. 在”Setting”中,可以选择自己想使用的选项。其中 “Restore RootFS”可以回到未越狱状态。

3. 为什么使用unc0ver

  1. 它可以在越狱之后,通过简单的操作回到当初未越狱的状态,对系统侵扰是最低的。
  2. 这个是一个安装在手机上的APP,可以随时删除。
  3. 开发者更新比较及时。写本篇时,iOS系统可更新的版本是14.4.2,而unc0ver已经支持到14.3版本了。

1. 初识汇编

汇编语言(Assembly Language)是任何一种用于电子计算机、微处理器、微控制器或其他可编程器件的低级语言,亦称为符号语言。在汇编语言中,用助记符代替机器指令的操作码,用地址符号或标号代替指令或操作数的地址。在不同的设备中,汇编语言对应着不同的机器语言指令集,通过汇编过程转换成机器指令。特定的汇编语言和特定的机器语言指令集是一一对应的,不同平台之间不可直接移植。

一个APP安装在手机上面的可执行文件本质上是二进制文件。因为iPhone手机本质上执行的指令是二进制,是由手机上的CPU执行的。所以静态分析是建立在分析二进制上面。

2. 发展历程

2.1 编程语言

从1946年第一台电子计算机问世,人类和机器的交流方式和语言就成为了软件工程师和计算机从业者的主要研究方向。在过去的几十年,编程语言有了长足的发展。

2.2 机器语言

计算机的硬件作为一种电路元件,它的输出和输入只能是有电或者没电,也就是所说的高电平和低电平,所以计算机传递的数据是由“0” 和“1”组成的二进制数,所以说二进制的语言是计算机语言的本质。

2.3 汇编语言

不难看出机器语言作为一种编程语言,灵活性较差可阅读性也很差,为了减轻机器语言带给软件工程师的不适应,人们对机器语言进行了升级和改进:用一些容易理解和记忆的字母,单词来代替一个特定的指令。这就是助记符。

2.4 高级语言

人们需要设计一个能够不依赖于计算机硬件,能够在不同机器上运行的程序。这样可以免去很多编程的重复过程,提高效率,同时这种语言又要接近于数学语言或人的自然语言。这就诞生了高级编程语言。比如:C、C++、Java、OC、Swift。

我们的代码在终端设备上的执行过程,如下图:
1171618126145_.pic_hd

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

2.5 汇编语言的特点

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

2.6 用途

  • 编写驱动程序、操作系统(比如Linux内核的某些关键部分)
  • 对性能要求极高的程序或者代码片段,可与高级语言混合使用(内联汇编)
  • 软件安全
    • 病毒分析与防治
    • 逆向\加壳\脱壳\破解\外挂\免杀\加密解密\漏洞\黑客
  • 理解整个计算机系统的最佳起点和最有效途径
  • 为编写高效代码打下基础
  • 弄清代码的本质

越底层越单纯,但是使用起来越困难,同时也是程序员都需要了解的非常重要的语言。

2.7 汇编语言的分类

  • 8086汇编 (8086处理器时16bit的CPU)
  • Win32汇编
  • Win64汇编
  • ARM汇编(Mac,iOS)

在iPhone里面,用的的ARM汇编,又根据CPU的架构不同而有差异。

架构 设备
armv6 古老的iPhone3G之前,iPod Touch(第一代,第二代)
armv7 iPhone3GS, iPhone4, iPhone4S, iPad, iPad2, iPad3, iPad Mini, iPod Touch 3G, iPod Touch4
armv7s iPhone5, iPhone5C, iPad4
armv64 iPhone5S及以后的设备,iPad Air,iPad Mini2以后

3. 几个必要的知识点

3.1 bit

bit:表示『位』或者『比特』,是计算机运算的基础单位,是二进制数的最小单元。1 bit就是1位二进制数,只能存放0或者1。

3.2 Byte

Byte:表示『字节』,是计算机文件大小的基本单位。1Byte = 8bit,表示一个字节可以存放8位无符号数。一个汉子是2个字节,16位。一个英文字母是1个字节。

1个字节可以表示为:0000 0000,1111 1111。

3.3 总线

总线(Bus)是计算机各种功能部件之间传送信息的公共通信干线,它是由导线组成的传输线束, 按照计算机所传输的信息种类,计算机的总线可以划分为数据总线、地址总线和控制总线,分别用来传输数据、数据地址和控制信号。

总线是一种内部结构,它是cpu、内存、输入、输出设备传递信息的公用通道,主机的各个部件通过总线相连接,外部设备通过相应的接口电路再与总线相连接,从而形成了计算机硬件系统。

在计算机系统中,各个部件之间传送信息的公共通路叫总线,微型计算机是以总线结构来连接各个功能部件的。

简单来说

  • 每一个CPU芯片都有许多管脚,这些管脚和总线相连,CPU通过总线跟外部器件交互。
  • 总线就是一根根导线的集合

3.3.1 总线分类

一般情况下分为5大类:

  1. 数据总线:在CPU与RAM之间来回传送需要处理或需要存储的数据。它的宽度决定了CPU单次数据传送量,也就是数据传送的速度。例如:8086微处理器子长16位,其数据总线宽度也是16位,所以单次最大传递2个字节的数据。
  2. 地址总线:用来指定在RAM之中存储的数据的地址。它的宽度决定了CPU的寻址能力。例如8086的地址总线宽度是20位,那么它的寻址能力是1M(2^20)。
  3. 控制总线:将微处理器控制单元的信号,传送到周边设备。它的宽度决定了CPU对其他器件的控制能力。
  4. 扩展总线:外部设备和计算机主机进行数据通信的总线,例如ISA,PCI总线。
  5. 局部总线:取代更高速数据传输的扩展总线。

其中数据总线、地址总线、控制总线也统称为系统总线。

3.4 进制

常用的进制有2进制,8进制,10进制,16进制。
2进制:00 11 11 100 101,逢2进1
8进制 0 1 2 3 4 5 6 7 10, 逢8进1
16进制 0 1 2 3 4 5 6 7 8 9 A B C D E F 10, 逢16进1

还有一些可以自己定义的进制,约定几个符合来表示对应的数据:
比如3进制 我们用a 3 1表示,通常约定好一些符合来表示对应的数据,以此来达到加密的效果。

运算等一些列就不说了~~~

3.5 数据的宽度

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

1
2
3
4
int test() {
int cTemp = 0x1FFFFFFFF;
return cTemp;
}

在上面的代码中,我们定义了一个int类型的变量,但是赋值的是9位16进制数。但是这里打印cTemp的值发现是-1

1
2
3
4
5
6
7
8
9
(lldb) p cTemp
(int) $0 = -1
(lldb) p &cTemp
(int *) $1 = 0x000000016f8e926c
(lldb) x 0x000000016f8e926c
0x16f8e926c: ff ff ff ff b0 92 8e 6f 01 00 00 00 88 a5 51 00 .......o......Q.
0x16f8e927c: 01 00 00 00 10 00 00 00 00 00 00 00 02 00 00 00 ................
(lldb) p (uint)cTemp
(uint) $0 = 4294967295

通过上面的打印,发现cTemp所在的地址钟存放的是ff ff ff ff,而前面的1没有了,这就是发生了溢出。但是通过转换无符号数,发现是可以正常打印的。这就说明了,地址钟存放的数据是没有发生变化的,只是由于位数的限制导致的有符号和无符号的区别,导致的数据不一致。

在通常的二进制中,第一位表示正负,0表示正 1表示负。

1
2
3
// int 4个字节,表示32位。第一位为符号为。
F F F F F F F F
1111 1111 1111 1111 1111 1111 1111 1111

这里提一嘴我们经常用的宽带,都是100M的,200M的,但是这个的意思是100Mbps,说的也是宽度,除以8才是对应的数据传输速度。

4. CPU & 寄存器

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

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

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

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

4.1 浮点和向量寄存器

因为浮点数的存储以及其运算的特殊性,CPU中专门提供浮点数寄存器来处理浮点数

  • 浮点寄存器 64位: D0 - D31 32位: S0 - S31

现在的CPU支持向量运算.(向量运算在图形处理相关的领域用得非常的多)为了支持向量计算系统了也提供了众多的向量寄存器.

  • 向量寄存器 128位:V0-V31

4.2 通用寄存器

通用寄存器也称数据地址寄存器通常用来做数据计算的临时存储、做累加、计数、地址保存等功能。定义这些寄存器的作用主要是用于在CPU指令中保存操作数,在CPU中当做一些常规变量来使用。

ARM64拥有有32个64位的通用寄存器 x0 到 x30,以及XZR(零寄存器),这些通用寄存器有时也有特定用途。

  • 那么w0 到 w28 这些是32位的. 因为64位CPU可以兼容32位.所以可以只使用64位寄存器的低32位.
  • 比如 w0 就是 x0的低32位!

图片上标注了1、2、3、4分别有所对应。
1 - 点击进入汇编
2 - 进入汇编之后,选择『All Variables, register』,会显示如图所示
3 - 浮点和向量寄存器,存放对应的v0-v31,d0-d31,s0-s31
4 - 通用寄存器,存放x0-x28,fp,lr,sp,pc,w0-w28

1
2
3
4
mov x0, #0xa0           ; x0 = 0xa0
mov x1,#0x00 ; x1 = 0x00
add x1, x0, #0x14 ; x1 = x0 + 0x14
mov x0,x1 ; x0 = x1

4.3 pc寄存器(program counter)

  • 为指令指针寄存器,它指示了CPU当前要读取指令的地址

  • 在内存或者磁盘上,指令和数据没有任何区别,都是二进制信息

  • CPU在工作的时候把有的信息看做指令,有的信息看做数据,为同样的信息赋予了不同的意义

    • 比如 1110 0000 0000 0011 0000 1000 1010 1010
    • 可以当做数据 0xE003008AA
    • 也可以当做指令 mov x0, x8
  • CPU根据什么将内存中的信息看做指令?

    • CPU将pc指向的内存单元的内容看做指令
    • 如果内存中的某段内容曾被CPU执行过,那么它所在的内存单元必然被pc指向过

4.4 高速缓存

iPhoneX上搭载的ARM处理器A11它的1级缓存的容量是64KB,2级缓存的容量8M.

CPU每执行一条指令前都需要从内存中将指令读取到CPU内并执行。而寄存器的运行速度相比内存读写要快很多,为了性能,CPU还集成了一个高速缓存存储区域.当程序在运行时,先将要执行的指令代码以及数据复制到高速缓存中去(由操作系统完成).CPU直接从高速缓存依次读取指令来执行.

4.5 bl指令

CPU从何处执行指令是由pc中的内容决定的,我们可以通过改变pc的内容来控制CPU执行目标指令。

ARM64提供了一个mov指令(传送指令),可以用来修改大部分寄存器的值,比如
mov x0,#10、mov x1,#20。但是,mov指令不能用于设置pc的值,ARM64没有提供这样的功能

ARM64提供了另外的指令来修改PC的值,这些指令统称为转移指令,最简单的是bl指令

4.5.1 bl指令 – 练习

现在有两段代码!假设程序先执行A,请写出指令执行顺序.最终寄存器x0的值是多少?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
.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
  1. 打开Xcode,新建一个工程,在新建文件是选择 『Assembly File』,生成的是『.s』文件。然后将上面的代码复制粘贴。

  2. 在ViewController中,进行函数声明。这里需要注意的是,方法明需要一致。

    1
    2
    //函数的声明~~
    int A();
  3. viewDidLoad中直接调用A(),打断点,然后跳转到汇编。

1
2
3
4
5
6
7
8
9
10
11
12
_A:
mov x0, #0xa0 ; x0 = a0
mov x1,#0x00 ; x1 = 0
add x1, x0, #0x14 ; x1 = x0 + 0x14
mov x0,x1 ; x0 = x1
bl _B ; 跳转到方法 _B --> 执行B
mov x0, #0x0 ; 从B回来之后执行 x0 = 0
ret ; return, 继续点击会发现啥?

_B:
add x0, x0, #0x10 ; x0 += 10
ret ; return,方法结束回到A继续执行

这里会有一点问的哈~会导致循环了。为啥会导致循环了呢?翻下一章。

5. 总结

  1. 汇编基础知识,发展、用途、特点
  2. 几个知识点:bit,Byte,总线,数据宽度
  3. CPU 寄存器。浮点寄存器(64位: D0 - D31 32位: S0 - S31);向量寄存器(128位:V0-V31);通用寄存器(32位w0-w28,64位x0-x28),PC寄存器。
  4. bl指令

1. 密码学

1.1 密码学概述

密码学是指研究信息加密,破解密码的技术科学。密码学的起源可追溯到2000年前。而当今的密码学是以数学为基础的。

1.2 发展

  1. 在1976年以前,所有的加密方法都是同一种模式:加密、解密使用同一种算法。在交互数据的时候,彼此通信的双方就必须将规则告诉对方,否则没法解密。那么加密和解密的规则(简称密钥),它保护就显得尤其重要。传递密钥就成为了最大的隐患。这种加密方式被成为对称加密算法(symmetric encryption algorithm)
  2. 1976年,两位美国计算机学家 迪菲(W.Diffie)、赫尔曼( M.Hellman ) 提出了一种崭新构思,可以在不直接传递密钥的情况下,完成密钥交换。这被称为“迪菲赫尔曼密钥交换”算法。开创了密码学研究的新方向。
  3. 1977年三位麻省理工学院的数学家 罗纳德·李维斯特(Ron Rivest)、阿迪·萨莫尔(Adi Shamir)和伦纳德·阿德曼(Leonard Adleman)一起设计了一种算法,可以实现非对称加密。这个算法用他们三个人的名字命名,叫做RSA算法

2. RSA

RSA加密方式比较特殊,需要两个密钥:公开密钥简称公钥(publickey)和私有密钥简称私钥(privatekey)。公钥加密,私钥解密;私钥加密,公钥解密。这个加密算法就是伟大的RSA。

2.1 欧拉函数

首先考虑什么是离散对数:在整数中,离散对数(英语:Discrete logarithm)是一种基于同余运算和原根的一种对数运算。

互质关系:如果两个正整数,除了1以外,没有其他公因数,我们就称这两个数是互质关系(coprime)。

有一个正整数n,请问在小于等于n的正整数之中,有多少个与n构成互质关系?
计算这个值的方式就是欧拉函数。用φ(n)来表示。φ -> phi

例如:

计算8的欧拉函数,和8互质的数有1、3、5、7
φ(8) = 4

例如计算7的欧拉函数,和7互质的数有1、2、3、4、5、6
φ(7) = 6

例如计算56的欧拉函数
φ(56) = φ(8) * φ(7) = 4 * 6 = 24

2.2.1 欧拉函数的特性:

  1. 当n是质数的时候,φ(n)=n-1。
  2. 如果n可以分解成两个互质的整数之积,如n=AB则:φ(AB)=φ(A)* φ(B)

根据以上两点得到:
如果N是两个质数P1 和 P2的乘积则φ(N)=φ(P1)* φ(P2)=(P1-1)*(P2-1)

2.2 欧拉定理

如果两个正整数m和n互质,那么m的φ(n)次方减去1,可以被n整除。

m^φ(n) % n = 1

也就是说,m的φ(n)次方减去1,能被n整出。

例如:

1
2
3
4
5
// m = 3, n = 11, φ(n) = 10
// 再Python3的环境下,快速输出结果,如下

>>> 3**10%11
1

2.2.1 费马小定理

欧拉定理的特殊情况:如果两个正整数m和n互质,而且n为质数!那么φ(n)结果就是n-1。

m^(n-1) % n = 1

这就是一开始所说的那种情况,如果n为质数,则φ(n)= n-1

2.3 模反元素

如果两个正整数e和x互质,那么一定可以找到整数d,使得 ed-1 被x整除。那么d就是e对于x的“模反元素”。
e*d % x = 1

2.4 公式推导过程:

  1. 1^k = 1, 1的k次方等于1

  2. 1 * m = m

  3. 根据欧拉定理:m^φ(n) % n = 1,公式两边分别执行k次方。

  4. (m^φ(n) % n)^k = 1^k 公式简化后:

  5. m^k*φ(n) % n = 1,等式两边分别乘以m

  6. (m^k*φ(n) % n) * m = 1 * m 简化后:

  7. m^k*φ(n)+1 % n = m

  8. 根据模反元素的公式:ed % x = 1,则 ed = k*x + 1

到这里,有没有发现什么?看推导的第7步和第8步的公式,kφ(n)+1 和 kx+1是不是很相似。则 e * d = k*φ(n)+1

那最后是不是可以得出:m^ed % n = m

这就是大名鼎鼎的迪菲赫尔曼密钥交换
我们假设 m = 3,n = 17,
服务器生成随机数 e = 15,客户端生成随机数 d = 13

服务器 m^e % n,结果是 3^15 % 17 = 6, 把明文6传给客户端。
客户端 m^d % n,结果是 3^13 % 17 = 12, 把明文12传给服务器。

两端拿到的数据分别进行解密:
客户端 6^13 % 17 = 10
服务器 12^15 % 17 = 10

迪菲赫尔曼密钥交换:
3^1512 % n = 3^1315 % n
m^ed % n = 10 = m^de % n

从这个时候开始,开创了密码学研究的新方向,也为RSA加密打下了基础。RSA对上面的公式进行了拆分:

m^e % n = c
c^d % n = m

这就是RSA诞生的原理。其中
m^e % n = c 进行加密
c^d % n = m 进行解密

n、e是公钥,n、d是私钥,m是明文,c是密文。

验证: m = 4, n = 15, φ(n) = 8, e = 3,
求出d(d是e对于φ(n)的“模反元素”) d = 11,19

1
2
3
4
5
6
7
8
9
10
11
// python3环境
>>> 4**33%15
4

// 加入m = 5
>>> 5**33%15
5

// 加入m = 15
>>> 15**33%15
0

到此,就看到了整个的RAS的基本过程。

3. RSA总结

3.1 说明

  1. n会非常大,长度一般为1024个二进制位。(目前人类已经分解的最大整数,232个十进制位,768个二进制位)
  2. 由于需要求出φ(n),所以根据欧函数特点,最简单的方式n 由两个质数相乘得到: 质数:p1、p2,Φ(n) = (p1 -1) * (p2 - 1)
  3. 最终由φ(n)得到e 和 d。总共生成6个数字:p1、p2、n、φ(n)、e、d

3.2 关于RSA的安全:

除了公钥用到了n和e 其余的4个数字是不公开的。目前破解RSA得到d的方式如下:

  1. 要想求出私钥 d。由于e*d = φ(n)*k + 1。要知道e和φ(n);
  2. e是知道的,但是要得到 φ(n),必须知道p1 和 p2。
  3. 由于 n=p1*p2。只有将n因数分解才能算出。

3.3 RSA特点

  1. 相对来说比较安全(非对称加密)
  2. 效率低
  3. 加密小数据,一般加密核心数据

4. 终端演示

由于Mac系统内置OpenSSL(开源加密库),所以我们可以直接在终端上使用命令来玩RSA。OpenSSL中RSA算法常用指令主要有三个:

命令 含义
genrsa 生成并输入一个RSA密钥
rsatul 使用RSA密钥进行加密、解密、签名和验证等运算
rsa 处理RSA密钥的格式转化等问题
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 使用命令行演示
// 1. 生成RSA密钥,密钥长度为1024bit
> openssl genrsa -out private.pem 1024

// 2. 从私钥中提取公钥
> openssl rsa -in private.pem -pubout -out public.pem

// 3. 将私钥转换为明文信息
> openssl rsa -in private.pem -text -out private.txt

// 4. 通过公钥加密 首先创建一个message.txt文件,并输入内容
> vi message.txt
> openssl rsautl -encrypt -in message.txt -inkey public.pem -pubin -out enc.txt

// 5. 通过私钥解密
> openssl rsautl -decrypt -in enc.txt -inkey private.pem -out dec.txt

// 6. 通过私钥加密数据
> openssl rsautl -sign -in message.txt -inkey private.pem -out penc.txt
// 7. 公钥解密数据
> openssl rsautl -verify -in penc.txt -inkey public.pem -pubin -out pdec.txt

5. 代码演示

由于Xcode是没有办法直接使用pem文件的,所以需要转化,首先需要生成请求的cer文件(可以理解为证书请求文件)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
> openssl req -new -key private.pem -out rsacert.csr
-----
> Country Name (2 letter code) []:CN
> State or Province Name (full name) []:Beijing
> Locality Name (eg, city) []:Chaoyang
> Organization Name (eg, company) []:Wangjing
> Organizational Unit Name (eg, section) []:alan.com
> Common Name (eg, fully qualified host name) []:alan.com
> Email Address []:alan@163.com

> Please enter the following 'extra' attributes
> to be sent with your certificate request
// 这里不使用密码,其他根据要求自己填写。
> A challenge password []:

拿到csr文件之后,需要去请求签名文件,一般用于服务器,下发证书,比如https

1
> openssl x509 -req -days 3650 -in rsacert.csr -signkey private.pem -out rsacert.crt

然后我们通过crt证书生成Xcode可用的p12证书。

1
2
> openssl pkcs12 -export -out p.p12 -inkey private.pem -in rsacert.crt
// 注意这里需要输入密码,123456

在iOS系统钟,ctr文件是无法直接使用的,所以还需要把ctr文件转化为der文件。

1
> openssl x509 -outform der -in rsacert.crt -out rsacert.der

生成的rsacert.der和p.p12文件就相当于我们要使用的公钥和私钥。

正常情况下,客户端不会同时存在公钥和私钥,因为这样做没有意义。客户端一般存放的是公钥,私钥在服务端。代码使用就不放了,这里有一套Github Objective-c-RSA代码。

总结

  • RSA终端命令
  • RSA特点
    • RSA安全系数非常高(整个业务逻辑非常安全)
    • 加密效率非常低(不能做大数据加密)
    • 用来加密关键数据

感谢~

什么是Universal Links?

iOS9之后,Apple推出的一种通用链接,能够方便的通过https链接来启动APP,通过唯一的网址,不需要特别的schema就可以链接一个特定的视图到APP。
这也就设计到universal links的几个特性:

  1. Unique:唯一性,不像自定义Url schemes那样,因为他使用到了你网站的http或者https链接
  2. Secure:安全性,当用户安装应用时,iOS会检测你上传到服务器上的文件,以确保你的网站允许其代表应用打开你的应用。
  3. Flexible:灵活性,当没有安装你的app时universal links也是可以正常使用的,当点击link时会直接在safari中打开url。
  4. Simple:一个url可以为app和web提供服务。
  5. Private:其他app可以与你的app通信,不需要知道你的应用程序是否安装。

配置Universal links需要网站与app协同处理,两端都需要做一些工作。

创建并上传关联文件

首先,你的网站必须支持https。

然后,创建apple-app-site-association文件,注意一定是没有.json后缀的,在未压缩的情况下,文件大小不能超过128KB。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{
"applinks": {
"apps": [],
"details": [
{
"appID": "9JA89QQLNQ.com.apple.wwdc",
"paths": [ "/wwdc/news/", "/videos/wwdc/2015/*"]
},
{
"appID": "ABCD1234.com.apple.wwdc",
"paths": [ "*" ]
}
]
}
}

appID:是由TeamID+BundleId组成,TeamID可以通过开发者账号来查看,Bundle ID可以直接在工程里查看。

paths:是一个数组类型用来指定网站路径,并且是大小写敏感的:
使用* 指定整个网站,在域名下的任何地址都可以打开App。

“/wwdc/news/”指定链接,以指定网站的某些部分.

还可以使用“?”匹配任何单个字符,”/foo/*/bar/201?/mypage”

创建好apple-app-site-association文件后,将其上传到web服务器的跟目录下或者.well-known子目录下。文件是通过https访问不需要有任何重定向。然后你可以直接在浏览器中输入domain/apple-app-site-association或者domain/.well-known/apple-app-site-association访问你所上传的文件。

通过Apple的测试网站可以检测你上传的文件是否正确。传送门

注意:

在用Nginx处理文件的MIME Type配置时,在域名下针对该文件进行处理,也就是说response的Content-Type必须设置为”application/json“。
使用Nginx处理文件的MIME Type配置,在server的某个域名下针对该文件处理

1
2
3
location /apple-app-site-association {
default_type application/json;
}

项目配置

  1. 需要支持Universal links的话,需要将开发者中心的配置APPlication Services列表中的ASSociated Domains变成Enabled。

  2. 在项目targets->Capabilities->Associated Domains中配置App link。在这里需要注意一下,有的域名为www.abc.com和abc.com,这两个对于Universal links来说是不同的域名,所以,如果你的网站是这两个都需要做处理的话,需要将这两个域名都放在associate domains列表下。在添加时都要将applinks:放在域名前面。
    例如:

    1
    applinks:www.abc.com
  3. 不需要手动的添加entitlements,当添加domain之后,系统会自动添加相应的文件到工程,如果没有的情况下,只能通过手动添加。

  4. 在AppDelegate中调用方法来处理Universal links的回调。

1
2
3
4
5
6
7
8
9
10
11
12
13
- (BOOL)application:(UIApplication *)application continueUserActivity:(NSUserActivity *)userActivity restorationHandler:(void (^)(NSArray * _Nullable))restorationHandler
{
if ([userActivity.activityType isEqualToString:NSUserActivityTypeBrowsingWeb]) {
NSURL *webUrl = userActivity.webpageURL;
if ([webUrl.host isEqualToString:@"domain"]) {
//打开对应页面
}else{
//不能打开,使用Safari 打开
[[UIApplication sharedApplication]openURL:webUrl];
}
}
return YES;
}

这里回传过来一个NSUserActivity类型的数据,对该数据处理来跳转相应的界面。也就不再多说了。
到此整体的流程就已经处理完了。相关的内容介绍还是推荐看官方文档,写的比较细致。

注意事项

  1. apple-app-site-association文件上传一定不要有.json后缀,那些隐藏后缀名的需要注意。
  2. web server必须支持https
  3. 上传文件最好在根目录下,官方说也可以在根目录下的.well-known目录下。
  4. Xcode中配置Associated Domians时,一定要添加applinks:前缀。最多添加20-30条。
  5. app在安装的时候会访问domain获取apple-app-site-association文件,这个可以通过抓包来获取。不是在打开app时访问。
  6. 直接dev打包到手机上应该就可以测试,保证网络畅通。
  7. 最简单的方式检测Universal links是否有效,将那个链接拷贝的备忘录中(imessage、邮件),直接点击链接会跳转到App,或者长按,会在弹出的ActionSheet中第二个显示在xxx中打开
  8. 一定是从上级页面(网页)点击触发 的Universal links。
  9. 用 Safari 打开目标域名,或者在其他 App 里用 SFSafariViewController, WKWebView, UIWebView 打开目标域名,都可以达到效果。

那些坑

1. 跨域跳转:饿了么遇到的一个问题

这里的问题,就是我们将链接复制在备忘录里发现可以拉起App,但是放在网页里,点击却是没有任何效果,这个几乎就是跨域的锅。

这个应该是在iOS9.2之后出现的问题。假设当前网页是abc.com/a,在这里有一个链接跳转到abc.com/b,还是在同一个域名abc.com下。这时点击,系统将不会进行拉起App的操作,必须在不同的域名下比如abcd.com/b,这样才会根据关联文件去判断是否要拉起app。这个时候就需要在Xcode中添加一个域名在碰到跨域问题不能拉起App时,链接改为其他的域名。
这里也有解释

2. 选择性跳转

这个也是一个坑,苹果会通过Universal links记录用户的行为习惯,当你通过一个Universal links成功的拉起了App,这时你发现在status bar右上角有一个小按钮(带一个小箭头)。当你点击了之后会成功的在Safari中打开,这时,苹果就认为你不需要这个Universal links拉起App,此后在通过这个Universal links点击时,都是在safari或者网页中打开,不会再拉起App。这个是有办法补救的,通过在Safari中点击链接长按,在app中打开。此后通过Universal links就可以拉起App了。也就说,苹果会记录最后一次Universal links的跳转情况来判断是否需要拉起app。

这里感觉第一个的跨域跳转与第二个的选择性跳转是很类似的,第一个在当前域名下点击Universal links不能拉起app就相当于当前系统认为你这个universal links本身就在浏览器中打开的,认为不需要拉起app。

When a user taps a universal link that you handle, iOS also examines the user’s recent choices to determine whether to open your app or your website. For example, a user who has tapped a universal link to open your app can later choose to open your website in Safari by tapping a breadcrumb button in the status bar. After the user makes this choice, iOS continues to open your website in Safari until the user chooses to open your app by tapping OPEN in the Smart App Banner on the webpage.

相关网站

官方文档

通过Apple的测试网站可以检测你上传的文件是否正确。传送门

前言

看本地cocoaPod库 cd ~/.cocoapods/repos在repos文件夹下面,可以看到你当前存在的一些库,当你创建了自己的私有仓库时,也会在这里显示。

按照正常逻辑来看,私有库是一个地址,版本号管理库是一个地址,最好不要把两者放在一起,会显得很乱。
所以创建私有库,我们需要创建两个库:一个是私有仓库repo,用来做私有库的版本管理;另一个是私有代码库,用来做代码编写。

创建私有仓库 repo

这里的私有空间是指私有库存放的位置,repo是repository(存储库)的缩写,如果私有库的podSpec和代码存放在一起就会显得很乱,所有通常的做法就是再私有空间只存放私有库的podSpec文件(可以理解为版本号)。

创建远程私有repo

这里使用码云,可以免费创建多个私有工程。例如创建了一个私有存储库:https://gitee.com/devAlan/PrivateRepo。(已不存在)

添加到本地仓库中

pod repo add [本地仓库repo名称] [远程repo地址]
pod repo add PrivateRepo https://gitee.com/devAlan/PrivateRepo
使用上面步骤就可以将远程的私有版本存储仓库添加到本地。在Finder中查看repos中就可以发现增加了一个文件夹PrivateRepo。

创建私有代码库

创建代码私有库有两种方式,第一种是我们正常的使用Xcode创建项目的过程,另一种是使用pod模板来创建的过程,推荐使用模板来创建。

创建一个私人代码库

创建一个私人代码库,在创建时添加MIT License和README。
使用模板创建时,创建空仓库。

将仓库clone到本地,添加项目工程文件。

添加.podspec文件

“.podspec”文件是这个代码库的pod描述文件,使用pod命令创建空白模板。

pod spec create 项目名称

这里使用上面的私有代码库创建pod spec create privateAdd

需要注意一下:生成的默认的podspec文件中,所有没有注释掉的都必须填写正确。

使用pod模板创建

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
pod lib creare [项目名称](最好与远端一致)

// 以下为模板创建时,会用到的一些选项问题,根据实际选择就好。
What platform do you want to use?? [ iOS / macOS ]
> iOS

// 选择编程语言
What language do you want to use?? [ Swift / ObjC ]
> ObjC

/// 是否需要添加本地库
Would you like to include a demo application with your library? [ Yes / No ]
> No

// 是否有frameworks
Which testing frameworks will you use? [ Specta / Kiwi / None ]
> None

//
Would you like to do view based testing? [ Yes / No ]
> No

// 创建的文件前缀
What is your class prefix?
> HM

上述命令操作完成之后,会生成相应的模板文件。
代码文件都存放在/[项目名称]/class/下。
图片都存放在/[项目名称]/Assets/下。

将本地 Lib 工程与远程私有 lib Git 仓库关联

1
2
3
4
5
6
7
8
9
git remote add origin 远程仓库地址  
git错误处理
git pull --rebase origin master
git push -u origin master

git push origin master #同步操作
文件冲突 (保留一份 license 和 readme)
git pull origin master --allow-unrelated-histories
git mergetool

编辑“.podspec”文件

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
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
#
# Be sure to run `pod spec lint PrivateAdd_v2.podspec' to ensure this is a
# valid spec and to remove all comments including this before submitting the spec.
#
# To learn more about Podspec attributes see http://docs.cocoapods.org/specification.html
# To see working Podspecs in the CocoaPods repo see https://github.com/CocoaPods/Specs/
#

# 声明
Pod::Spec.new do |s|

# ――― Spec Metadata ―――――――――――――――――――――――――――――――――――――――――――――――――――――――――― #
#
# These will help people to find your library, and whilst it
# can feel like a chore to fill in it's definitely to your advantage. The
# summary should be tweet-length, and the description more in depth.
#
# 私有库名称
s.name = "PrivateAdd_v2"
# 版本号,跟当前版本tag有关
s.version = "0.0.1"
# 项目描述
s.summary = "A short description of PrivateAdd_v2."

# This description is used to generate tags and improve search results.
# * Think: What does it do? Why did you write it? What is the focus?
# * Try to keep it short, snappy and to the point.
# * Write the description between the DESC delimiters below.
# * Finally, don't worry about the indent, CocoaPods strips it!
s.description = <<-DESC
DESC

# 主页,自动生成的podspec文件中所有的EXAMPLE都需要改
s.homepage = "http://EXAMPLE/PrivateAdd_v2"
# 项目截图
# s.screenshots = "www.example.com/screenshots_1.gif", "www.example.com/screenshots_2.gif"


# ――― Spec License ――――――――――――――――――――――――――――――――――――――――――――――――――――――――――― #
#
# Licensing your code is important. See http://choosealicense.com for more info.
# CocoaPods will detect a license file if there is a named LICENSE*
# Popular ones are 'MIT', 'BSD' and 'Apache License, Version 2.0'.
#

# 项目遵守的协议
s.license = "MIT (example)"
# s.license = { :type => "MIT", :file => "FILE_LICENSE" }


# ――― Author Metadata ――――――――――――――――――――――――――――――――――――――――――――――――――――――――― #
#
# Specify the authors of the library, with email addresses. Email addresses
# of the authors are extracted from the SCM log. E.g. $ git log. CocoaPods also
# accepts just a name if you'd rather not provide an email address.
#
# Specify a social_media_url where others can refer to, for example a twitter
# profile URL.
#

# 作者信息
s.author = { "liujiajia" => "liujiajia@gerrit.babytree-inc.com" }
# Or just: s.author = "liujiajia"
# s.authors = { "liujiajia" => "liujiajia@gerrit.babytree-inc.com" }
# s.social_media_url = "http://twitter.com/liujiajia"

# ――― Platform Specifics ――――――――――――――――――――――――――――――――――――――――――――――――――――――― #
#
# If this Pod runs only on iOS or OS X, then specify the platform and
# the deployment target. You can optionally include the target after the platform.
#

# s.platform = :ios # 平台
# 平台以及版本号
# s.platform = :ios, "5.0"

# When using multiple platforms
# s.ios.deployment_target = "5.0"
# s.osx.deployment_target = "10.7"
# s.watchos.deployment_target = "2.0"
# s.tvos.deployment_target = "9.0"


# ――― Source Location ―――――――――――――――――――――――――――――――――――――――――――――――――――――――――― #
#
# Specify the location from where the source should be retrieved.
# Supports git, hg, bzr, svn and HTTP.
#

#当前私有库资源的git地址,相应的tag,这里直接使用代码来标识使用和当前tag一样的值
s.source = { :git => "http://EXAMPLE/PrivateAdd_v2.git", :tag => "#{s.version}" }


# ――― Source Code ―――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――― #
#
# CocoaPods is smart about how it includes source code. For source files
# giving a folder will include any swift, h, m, mm, c & cpp files.
# For header files it will include any header in the folder.
# Not including the public_header_files will make all headers public.
#

# 需要引用的文件代码,classes文件夹下的所有.h.m文件。这里的路径都是相对路径
s.source_files = "Classes", "Classes/**/*.{h,m}"
# 排除掉哪些文件,不被包含的文件,这里是Classes/Exclude文件夹下的所有
s.exclude_files = "Classes/Exclude"

# s.public_header_files = "Classes/**/*.h"


# ――― Resources ―――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――― #
#
# A list of resources included with the Pod. These are copied into the
# target bundle with a build phase script. Anything else will be cleaned.
# You can preserve files from being cleaned, please don't preserve
# non-essential files like tests, examples and documentation.
#

# s.resource = "icon.png" # 资源文件
# s.resources = "Resources/*.png" # 所有资源文件

# s.preserve_paths = "FilesToSave", "MoreFilesToSave"


# ――― Project Linking ―――――――――――――――――――――――――――――――――――――――――――――――――――――――――― #
#
# Link your library with frameworks, or libraries. Libraries do not include
# the lib prefix of their name.
#

# s.framework = "UIKit" # 依赖的系统framework,去掉后缀
# s.frameworks = "SomeFramework", "AnotherFramework"

# s.library = "iconv" # 依赖的系统library
# s.libraries = "iconv", "xml2", "z" # 依赖的library,去除前缀和后缀,例如“libz.tbd”,则直接引用为"z"

# s.vendored_frameworks = "*.framework", "*.framework" #依赖那些静态库
# s.vendored_libraries = "*.a", "*.a" # 依赖哪些静态library


# ――― Project Settings ――――――――――――――――――――――――――――――――――――――――――――――――――――――――― #
#
# If your library depends on compiler flags you can set them in the xcconfig hash
# where they will only apply to your library. If you depend on other Podspecs
# you can include multiple dependencies to ensure it works.

# s.requires_arc = true # 是否开启ARC

# s.xcconfig = { "GCC_PREPROCESSOR_DEFINITIONS" => 'HMTIMEINPREGNACYAPP=1', "HEADER_SEARCH_PATHS" => "$(SDKROOT)/usr/include/libxml2" }
# 这里配置一些内容,"GCC_PREPROCESSOR_DEFINITIONS":定义一个宏,
# "HEADER_SEARCH_PATHS":如果引用该私有库,则会在主工程中寻找文件夹下的内容

# s.dependency "JSONKit", "~> 1.4" # 使用了pod,依赖哪些库

# s.prefix_header_contents = <<-EOS #这里会生成一个默认pch文件,作用于Xcode工程中添加的pch文件一样。
# #import <UIKit/UIKit.h>
# EOS

end

这里有一个需要注意的地方,在创建私有库的时候,找不到MIT LICENSE证书,这里讲license修改一下

1
s.license      = { :type => "MIT", :file => "LICENSE" }

二级库subspec

1
2
3
4
5
6
7
s.subspec 'UIView+Category' do |ss| 
ss.source_files = "PrivateAdd_v3/PrivateAdd_v3/Classes/UIView+Category/*.{h,m}"
ss.framework = "UIKit"
# ss.vendored_libraries = '/*.a'
# ss.vendored_frameworks = '/*.framework'
# ss.resource = 'Moments/ForPregnacy/**/*.bundle', 'Moments/ForPregnacy/**/*.png', 'Moments/ForPregnacy/**/*.mp3'
end

使用subspec关键字,并添加指定文件就行。

验证私有库

修改完相应的代码块之后,开始验证私有库配置是否正确。

本地验证

1
2
3
pod lib lint
// 如果使用了私有库,则使用--sources来标明,给出具体地址
pod lib lint --sources=[私有仓库repo地址],[私有库git地址],https://github.com/CocoaPods/Specs.git

如果出现错误,基本上都是podspec文件中的配置出现了问题,例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
$ pod lib lint

-> PrivateAdd_v2 (0.0.1)
- WARN | attributes: Missing required attribute `license`.
- WARN | license: Missing license type.
- ERROR | description: The description is empty.
- ERROR | [iOS] unknown: Encountered an unknown error (The `PrivateAdd_v2` pod failed to validate due to 1 error:
- WARN | attributes: Missing required attribute `license`.
- WARN | license: Missing license type.
- ERROR | description: The description is empty.

) during validation.

[!] PrivateAdd_v2 did not pass validation, due to 2 errors and 2 warnings.
You can use the `--no-clean` option to inspect any issue.

这就需要将上面的错误修改掉。

一般情况下出现警告在后面添加pod lib lint --allow-warnings,就可以通过验证了。验证通过会提示passed validation。

如果依赖一些静态库(.framework,.a),则在命令后面添加--use-libraries,表示依赖了静态库。

如果报错不是很明显,这命令后添加--verbose,会输出验证的过程,并给出报错信息。

远端验证

pod spec lint,一般情况下私有库都是存在远程git上的,所以基本上只在第一次验证使用本地验证之后,就可以直接使用远端验证了。
但是一般情况下,创建的私有代码库如果是私有的(并不是官方的),这时候,我们在做远端验证时,需要添加代码地址。,不然pod会默认从官方repo查询。

1
2
3
4
pod spec lint --sources=[私有仓库repo地址],[私有库git地址],https://github.com/CocoaPods/Specs.git

// 或者只添加前面两项,最后一项可以忽略
pod spec lint --sources=[私有仓库repo地址],[私有库git地址]

如果出现一些警告,可以直接在命令最后添加--allow-warnings来屏蔽警告。例如:

1
2
3
pod spec lint --sources=https://gitee.com/devAlan/PrivateRepo,https://gitee.com/devAlan/PrivateAdd_v3.git,https://github.com/CocoaPods/Specs.git --allow-warnings

pod spec lint --sources=https://gitee.com/devAlan/PrivateRepo,https://gitee.com/devAlan/PrivateAdd_v3.git --allow-warnings

发布私有库

将podspec文件推送到私有空间repo,在发布时一定要记住,已经做了tag处理。

1
2
pod repo push [私有仓库repo] [私有库podspec文件地址] --allow-warnings
pod repo push [私有仓库repo] [私有库podspec文件地址] --sources=[私有仓库repo地址],[私有库地址],https://gitee.com/devAlan/PrivateAdd_v2.git --allow-warnings

由于基本上的所有命令操作都是在该私有库文件夹下,所以不用管podspec的文件位置。

1
pod repo push PrivateRepo PrivateAdd.podspec --allow-warnings

执行该命令时,相当于直接将podspec文件push到repo的Git地址上。

引用私有库

本地路径引用

做好验证之后,如果需要测试,可以直接在podfile中引用私有库的本地路径

1
2
// ../是回到上一级文件目录,是以当前podfile所在文件位置决定
pod 'Private_v2', :path => '../../Private_v2'

这个时候如果改动Private_v2中的代码就是直接改动。

repo引用

在podfile中先添加source,然后直接pod引入,当然在引用私有库的时候,不仅需要需要私有仓库repo的权限,还需要有私有库的权限。

1
2
3
4
5
6
source 'https://gitee.com/devAlan/PrivateRepo'

pod 'Private_v2'

#或者使用下面的方式
#pod 'Private_v2', :git=> 'https://gitee.com/devAlan/PrivateRepo'

pod 常用命令

  1. 安装podfile中依赖的命令
    pod install
    pod update
    --no-repo-update 不需要更新repo
    --repo-update 更新repo

  2. repo相关
    pod repo list
    pod repo remove xxx 移除一个本地repo
    pod repo add [本地仓库repo名称] [远程repo地址] 远程版本空间添加到本地

  3. 私有库创建相关
    pod lib lint 本地验证
    pod spec lint 远程验证
    pod repo push [本地仓库repo] [*.podspec] 发布版本到repo
    --source=[私有repo地址][私有代码库地址][pod地址]
    --allow-warnings 忽略warning
    --use-libraries 使用了第三方的framework或者.a文件
    --verbose 打印流程,可以快速定位失败原因

  4. podspec中使用到的*
    /** 子目录下所有文件夹
    /* 目录下所有文件

遇到的问题

  1. 私有库依赖主项目中的某些文件,如果是强制性的依赖,导致本地验证和远程验证都无法通过,所以更别说发布了。
    这种是没有完全的组件化。我们需要在podspec中添加HEADER_SEARCH_PATHS,添加指定位置文件路径。这里的路径也是相对路径。

    1
    2
    s.xcconfig = { "GCC_PREPROCESSOR_DEFINITIONS" => 'HMTIMEINPREGNACYAPP=1', 
    "HEADER_SEARCH_PATHS" => "$(SRCROOT)/usr/include/libxml2", $(SRCROOT)/../Commen/** }

    但是这样做仍然无法通过验证,这就需要我们将私有空间repo pull到本地,手动创建版本号文件夹添加podspec文件,然后push。

  2. 在上面的代码中还有一个 GCC_PREPROCESSOR_DEFINITIONS,这是定义一个宏,可以在Project-Build Settings - Preprocessor Macros下看到。

    可以配合一些没有必要引入私有代码库中的文件来使用。