汇编-指针、OC & Block 反汇编
编译器优化
局部变量&全局变量
int global = 10;
int main(int argc, char * argv[]) {
int a = 20;
int b = global + 1;
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
}
在不进行优化的情况下:
image.png
改成Fastest、Smallest
模式,a
和b
都被优化掉了。
局部变量和全局变量会被优化掉。
函数
int func(int a,int b) {
return a + b;
}
int main(int argc, char * argv[]) {
int value = func(10, 20);
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
}
image.png
func
函数也会被优化掉,因为对程序的执行结果没有影响。修改下:
int func(int a,int b) {
return a + b;
}
int main(int argc, char * argv[]) {
int value = func(10, 20);
NSLog(@"%d",value);
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
}
image.png
可以看到直接将
0x1e
结果入栈。一样也会被优化。
编译配置(Optimization Level)
编译器(从c
到汇编
的编译过程)的优化配置(指定被编译代码的执行速度和二进制文件大小的优化程度)。优化后的代码效率比较高,但是可读性比较差,且编译时间更长。
优化等级配置在Build Settings -> Apple Clang - Code Generation -> Optimization Level
中:
-
None [-O0]
:不优化。
编译器的目标是降低编译消耗,保证调试时输出期望的结果。程序的语句之间是独立的:如果在程序的停在某一行的断点出,我们可以给任何变量赋新值抑或是将程序计数器指向方法中的任何一个语句,并且能得到一个和源码完全一致的运行结果。 -
Fast [-O, O1]
: 大函数所需的编译时间和内存消耗都会稍微增加。
在这种设置下,编译器会尝试减小代码文件的大小,减少执行时间,但并不执行需要大量编译时间的优化。在苹果的编译器中,在优化过程中,严格别名,块重排和块间的调度都会被默认禁止掉。此优化级别提供了良好的调试体验,堆栈使用率也提高,并且代码质量优于None[-O0]。 -
Faster [-O2]
:编译器执行所有不涉及时间空间交换的所有的支持的优化选项。
更高的性能优化Fast[-O1]
。在这种设置下,编译器不会进行循环展开、函数内联或寄存器重命名。和‘Fast[-O1]’项相比,此设置会增加编译时间和生成代码的性能。 -
Fastest [-O3]
:在开启Fast[-O1]
项支持的所有优化项的同时,开启函数内联和寄存器重命名选项
是更高的性能优化Faster[-O2],指示编译器优化所生成代码的性能,而忽略所生成代码的大小,有可能会导致二进制文件变大。还会降低调试体验。 -
Fastest, Smallest [-Os]
:在不显着增加代码大小的情况下尽量提供高性能
这个设置开启了Fast[-O1]项中的所有不增加代码大小的优化选项,并会进一步的执行可以减小代码大小的优化。增加的代码大小小于Fastest[-O3]。与Fast[-O1]相比,它还会降低调试体验。 -
Fastest, Aggressive Optimizations [-Ofast]
:与Fastest, Smallest[-Os]
相比该级别还执行其他更激进的优化
这个设置开启了Fastest[-O3]中的所有优化选项,同时也开启了可能会打破严格编译标准的积极优化,但并不会影响运行良好的代码。该级别会降低调试体验,并可能导致代码大小增加。 -
Smallest, Aggressive Size Optimizations [-Oz]
:不使用LTO
的情况下减小代码大小
与-Os相似,指示编译器仅针对代码大小进行优化,而忽略性能优化,这可能会导致代码变慢。
Xcode
中Debug
模式默认为None[-O0]
,Release
默认为Fastest, Smallest[-Os]
。
指针
指针在汇编中只是地址, 在底层来说就是数据。
指针基本常识
指针的宽度为8
字节。
void func() {
//指针的宽度8字节
int *a;
printf("%lu",sizeof(a));
}
int main(int argc, char * argv[]) {
func();
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
}
image.png
-
sizeof
:是个符号,操作符。这里验证了是常量8
。
指针的运算
指针++
int型指针++:
int *a;
a = (int *)100;
a++;
运算结果:104
,int
占4
字节。
char型指针++:
char *a;
a = (char *)100;
a++;
运算结果:101
,char
占1
字节。
指向指针的指针++:
int **a;
a = (int **)100;
a++;
运算结果:108
,指针占8
字节
指针+
int **a;
a = (int **)100;
a = a + 1;
运算结果:108
。与a++
(++、--
与编译器有关)等价。
指针-
int *a;
a = (int *)100;
int *b;
b = (int *)200;
int x = a - b; // a/4 - b/4 = -25
运算结果:-25
。(a/4 - b/4 = -25
)
- 指针的运算与指向的数据类型宽度(步长)有关。
- 指针的运算单位是执行的数据类型的宽度。
- 结构体和基本类型不能强制转换,普通类型可以通过
&
。
指针的反汇编
void func() {
int* a;
int b = 10;
a = &b;
}
对应的汇编:
TestDemo`func:
0x10098a1c4 <+0>: sub sp, sp, #0x10 ; =0x10
//sp+0x4 给 x8
0x10098a1c8 <+4>: add x8, sp, #0x4 ; =0x4
//1O给w9
0x10098a1cc <+8>: mov w9, #0xa
//w9 入栈
-> 0x10098a1d0 <+12>: str w9, [sp, #0x4]
//x8 指向 sp+0x8。相当于x8指向sp,也就是指向10的地址
0x10098a1d4 <+16>: str x8, [sp, #0x8]
0x10098a1d8 <+20>: add sp, sp, #0x10 ; =0x10
0x10098a1dc <+24>: ret
[sp, #0x8]
是个指针变量。从0x8~0x10
保存的就是指针。
数组和指针
void func() {
int arr[5] = {1,2,3,4,5};
//int *a == &arr[0] == arr
int *a = arr;
for (int i = 0; i < 5; i++) {
printf("%d\n",arr[i]);
printf("%d\n",*(arr + i));
// printf("%d\n",*(arr++));
printf("%d\n",*(a++));
}
}
*(arr++)
会报错。int *a = arr;
之后 a++
就没问题了、
- 数组名和指针变量是一样的,唯一的区别是一个是常量,一个是变量。
int *a == &arr[0] == arr
指针的基本用法
void func() {
char *p1;
char c = *p1;
printf("%c",c);
}
image.png
p1
由于是个指针,没有初始化编译不会报错,运行会报错。在iOS中默认是0
,运行会直接野指针。
指向char
的指针+0
void func() {
char *p1;
char c = *p1;
char d = *(p1 + 0);
}
对应汇编
TestDemo`func:
0x104b661bc <+0>: sub sp, sp, #0x10 ; =0x10
//p1 -> 0x0 x8指向p1
0x104b661c0 <+4>: ldr x8, [sp, #0x8]
//c = [x8] 给到 w9
-> 0x104b661c4 <+8>: ldrb w9, [x8]
0x104b661c8 <+12>: strb w9, [sp, #0x7]
0x104b661cc <+16>: ldr x8, [sp, #0x8]
//d = [x8] 给到 w9
0x104b661d0 <+20>: ldrb w9, [x8]
0x104b661d4 <+24>: strb w9, [sp, #0x6]
0x104b661d8 <+28>: add sp, sp, #0x10 ; =0x10
0x104b661dc <+32>: ret
指向char
的指针+1
void func() {
char *p1;//指针 -> x8 0x0
char c = *p1;// [x8]
char d = *(p1 + 1);//[x8, #0x1]
}
对应汇编:
TestDemo`func:
0x1041f21bc <+0>: sub sp, sp, #0x10 ; =0x10
//p1
0x1041f21c0 <+4>: ldr x8, [sp, #0x8]
//c
-> 0x1041f21c4 <+8>: ldrb w9, [x8]
0x1041f21c8 <+12>: strb w9, [sp, #0x7]
0x1041f21cc <+16>: ldr x8, [sp, #0x8]
//d
0x1041f21d0 <+20>: ldrb w9, [x8, #0x1]
0x1041f21d4 <+24>: strb w9, [sp, #0x6]
0x1041f21d8 <+28>: add sp, sp, #0x10 ; =0x10
0x1041f21dc <+32>: ret
指向int
的指针+1
void func() {
int *p1;//指针 -> x8 0x0
int c = *p1;// [x8]
int d = *(p1 + 1);//[x8, #0x4]
}
TestDemo`func:
0x1040e61bc <+0>: sub sp, sp, #0x10 ; =0x10
//p1 [x8]
0x1040e61c0 <+4>: ldr x8, [sp, #0x8]
//c
-> 0x1040e61c4 <+8>: ldr w9, [x8]
0x1040e61c8 <+12>: str w9, [sp, #0x4]
0x1040e61cc <+16>: ldr x8, [sp, #0x8]
//d
0x1040e61d0 <+20>: ldr w9, [x8, #0x4]
0x1040e61d4 <+24>: str w9, [sp]
0x1040e61d8 <+28>: add sp, sp, #0x10 ; =0x10
0x1040e61dc <+32>: ret
指向int
的指针的指针+1
void func() {
int **p1;//指针 -> x8 0x0
int *c = *p1;// [x8]
int *d = *(p1 + 1);//[x8, #0x8]
}
TestDemo`func:
0x1041821b8 <+0>: sub sp, sp, #0x20 ; =0x20
//p1 [x8]
0x1041821bc <+4>: ldr x8, [sp, #0x18]
//c
-> 0x1041821c0 <+8>: ldr x8, [x8]
0x1041821c4 <+12>: str x8, [sp, #0x10]
0x1041821c8 <+16>: ldr x8, [sp, #0x18]
//d
0x1041821cc <+20>: ldr x8, [x8, #0x8]
0x1041821d0 <+24>: str x8, [sp, #0x8]
0x1041821d4 <+28>: add sp, sp, #0x20 ; =0x20
0x1041821d8 <+32>: ret
这里拉伸了#0x20
,16
字节对齐。
指向指针的指针
void func() {
char **p1;
char c = **p1;
}
取地址的地址在汇编中:
TestDemo`func:
0x102cf61c4 <+0>: sub sp, sp, #0x10 ; =0x10
//初始值
0x102cf61c8 <+4>: ldr x8, [sp, #0x8]
//两次ldr,二级指针在寻址
-> 0x102cf61cc <+8>: ldr x8, [x8]
0x102cf61d0 <+12>: ldrb w9, [x8]
0x102cf61d4 <+16>: strb w9, [sp, #0x7]
0x102cf61d8 <+20>: add sp, sp, #0x10 ; =0x10
0x102cf61dc <+24>: ret
两次ldr,二级指针在寻址。
指针的指针&指针混合偏移
void func() {
char **p1;
char c = *(*(p1 + 2) + 2); // [0x10 + 0x2]
}
image.png
p1
偏移 (2
* 指针) +(2
* char)
void func() {
char **p1;
char c = *(*(p1 + 2) + 2); // [0x10 + 0x2]
char c2 = p1[1][2]; // [0x8 + 0x2]
}
image.png
p1[1][2]
等价于*(*(p1 + 1) + 2)
OC反汇编
创建一个简单的Hotpot
类:
//Hotpot.h
@interface Hotpot : NSObject
@property (nonatomic, copy) NSString *name;
@property (nonatomic, assign) int age;
+ (instancetype)hotpot;
@end
//Hotpot.m
#import "Hotpot.h"
@implementation Hotpot
+ (instancetype)hotpot {
return [[self alloc] init];
}
@end
在main.m
中调用:
#import "Hotpot.h"
int main(int argc, char * argv[]) {
Hotpot *hp = [Hotpot hotpot];
return 0;
}
对应的汇编代码:
image.png
我们都知道OC
方法objc_msgSend
默认有两个参数self cmd
,分别是id
和SEL
类型。
验证下:
(lldb) x 0x1027c95b0
0x1027c95b0: f8 95 7c 02 01 00 00 00 20 96 7c 02 01 00 00 00 ..|..... .|.....
0x1027c95c0: 08 00 00 00 10 00 00 00 08 00 00 00 00 00 00 00 ................
(lldb) po 0x01027c95f8
Hotpot
(lldb) x 0x1027c95a0
0x1027c95a0: bd 65 7c 02 01 00 00 00 a8 af b3 df 01 00 00 00 .e|.............
0x1027c95b0: f8 95 7c 02 01 00 00 00 20 96 7c 02 01 00 00 00 ..|..... .|.....
(lldb) po (SEL)0x01027c65bd
"hotpot"
(lldb) register read x0
x0 = 0x00000001027c95f8 (void *)0x00000001027c95d0: Hotpot
(lldb) register read x1
x1 = 0x00000001027c65bd "hotpot"
(lldb)
接着进入hotpot
方法中,对应汇编如下:
发现没有走
objc_msgSend
方法,直接走了objc_alloc_init
方法。
⚠️:这块和支持的最低版本有关。
在iOS9
中为objc_msgSend
和objc_msgSend
对应alloc
和init
。
在iOS11
中为objc_alloc
和objc_msgSend
,这里优化了alloc
直接调用了objc_alloc
,没有调用objc_msgSend
。
在iOS13
中为objc_alloc_init
,这里同时优化了alloc
和init
。
在hotpot
方法执行完毕后会返回实例对象:
在下面调用了一个
objc_storeStrong
函数(OC
中用strong
修饰的函数都会调用这个函数,例子中hp
局部变量默认就是__strong
)。objc_storeStrong
调用后如果被外部引用引用计数+1
,否则就销毁。在
objc4-818.2
源码中objc_storeStrong
源码(在NSObject.mm
中):
void
objc_storeStrong(id *location, id obj)
{
id prev = *location;
if (obj == prev) {
return;
}
objc_retain(obj);
*location = obj;
objc_release(prev);
}
这个函数有两个参数 id*
和 id
,函数的目的为对strong
修饰的对象retain + 1
,对旧对象release
。
//x8指向 sp + 0x8 地址
0x1022421a0 <+60>: add x8, sp, #0x8 ; =0x8
//x8 就是指向x0的地址
0x1022421a4 <+64>: str x0, [sp, #0x8]
0x1022421a8 <+68>: stur wzr, [x29, #-0x4]
0x1022421ac <+72>: mov x0, x8
0x1022421b0 <+76>: mov x8, #0x0
0x1022421b4 <+80>: mov x1, x8
//objc_storeStrong 第一个参数 &hp,第二个参数 0x0
-> 0x1022421b8 <+84>: bl 0x102242520 ; symbol stub for: objc_storeStrong
0x1022421bc <+88>: ldur w0, [x29, #-0x4]
0x1022421c0 <+92>: ldp x29, x30, [sp, #0x20]
0x1022421c4 <+96>: add sp, sp, #0x30 ; =0x30
0x1022421c8 <+100>: ret
调用objc_storeStrong
的过程就相当于:
//分别传入 &hp 和 0x0
void
objc_storeStrong(id *location, id obj)
{
id prev = *location;//id prev = *hp
if (obj == prev) {
return;
}
objc_retain(obj);// nil
*location = obj;// hp 指向第二个对象 hp = nil
objc_release(prev);//释放老对象 release hp 释放堆空间
}
所以这里objc_storeStrong
调用为了释放对象。
在objc_storeStrong
断点前后验证:
(lldb) p hp
(Hotpot *) $3 = 0x000000028014bf60
(lldb) ni
(lldb) p hp
(Hotpot *) $4 = nil
(lldb)
单步执行后hp
变成了nil
。
工具反汇编
由于大部分情况下OC
代码都比较复杂,自己分析起来比较麻烦。我们一般都借助工具来协助反汇编,一般会用到MachoView
,Hopper
,IDA
。
将刚才的代码稍作修改:
#import "Hotpot.h"
int main(int argc, char * argv[]) {
Hotpot *hp = [Hotpot hotpot];
hp.name = @"cat";
hp.age = 1;
return 0;
}
通过hopper
打开macho
文件
可以看到已经自动解析出了方法名和参数,那么编译器是怎么做到呢?
双击objc_cls_ref_Hotpot
会跳转到对应的地址:
那么000000010000d550
在MachoView
中对应如下:
setName
点进去:
继续点击0x10000663d
:
0x10000663d
在machoview
如下:
可以看到所有方法都在这块。
所以在分析汇编代码的时候编译器就能根据工具找到这些字符串。这也就是能还原的原因。
Block反汇编
在平时开发中经常会用到block
,那么block
汇编是什么样子呢?
int main(int argc, char * argv[]) {
void(^block)(void) = ^() {
NSLog(@"block test");
};
block();
return 0;
}
一般在反汇编的时候我们希望定位到block
的实现(invoke
)
对应汇编如下:
invoke
在0x102c4e160
。
block
源码定义如下(Block_private.h
):
struct Block_layout {
void *isa; //8字节
volatile int32_t flags; // contains ref count //4字节
int32_t reserved;//4字节
BlockInvokeFunction invoke;
struct Block_descriptor_1 *descriptor;
// imported variables
};
也就是isa
往下16
字节就是invoke
。
在hopper
中:
已经告诉我们是一个global block
,双击___block_literal_global
:
双击0x0000000100006160
跳转到invoke
实现:
StackBlock
int main(int argc, char * argv[]) {
int a = 10;
void(^block)(void) = ^() {
NSLog(@"block test:%d",a);
};
block();
return 0;
}
image.png
验证isa
和invoke
:
(lldb) po 0x100a8c000
<__NSStackBlock__: 0x100a8c000>
signature: "<unknown signature>"
(lldb) x 0x100a8c000
0x100a8c000: 30 88 ae df 01 00 00 00 94 3f c5 89 01 00 00 00 0........?......
0x100a8c010: 00 00 00 00 00 00 00 00 24 00 00 00 00 00 00 00 ........$.......
(lldb) po 0x01dfae8830
__NSStackBlock__
(lldb) dis -s 0x100a8a140
TestOC&BlockASM`__main_block_invoke:
0x100a8a140 <+0>: sub sp, sp, #0x30 ; =0x30
0x100a8a144 <+4>: stp x29, x30, [sp, #0x20]
0x100a8a148 <+8>: add x29, sp, #0x20 ; =0x20
0x100a8a14c <+12>: stur x0, [x29, #-0x8]
0x100a8a150 <+16>: str x0, [sp, #0x10]
0x100a8a154 <+20>: ldr w8, [x0, #0x20]
0x100a8a158 <+24>: mov x0, x8
0x100a8a15c <+28>: adrp x9, 2
invoke
的imp
实现通过dis -s
查看汇编实现。
在hopper
中:
block
的实现:
global block
的block
和descriptor
是在一起的,stack block
并不在一起。