最近看一些资料,接触到一些资料有汇编,发现看不懂,还是有必要学习下。
在iOS中,消息发送是汇编写的,在学习戴铭高手课时hook oc中的objc_send方法,也用到汇编。尝试去阅读,网上也有很多注释,发现看完之后依然一知半解。还是从基础学起。
1. 先看一段C的代码,我们可以放到main.m中:
int addFunction(int a, int b) {
int c = a + b;
return c;
}
2. 选择真机,Xcode选中Assemble转成汇编代码
这样我们可以看到汇编,注意一定要选择Generic iOS Device,模拟器或真机转成的汇编和本文有差异。
3. _addFunction:
忽略掉.file、.loc、.cfi_startproc,可得到以下相关汇编代码:
sub sp, sp, #16 ; =16
str w0, [sp, #12]
str w1, [sp, #8]
ldr w0, [sp, #12]
ldr w1, [sp, #8]
add w0, w0, w1
str w0, [sp, #4]
ldr w0, [sp, #4]
add sp, sp, #16 ; =16
ret
为什么是w0,w1呢,不是r0,r1或x0,x1呢?
w0 - w30访问时,访问的是这些寄存器的低32位。当使用 r0 - r30 访问时,它就是一个64位的数。
3.1 添加注释后的汇编
首先,分配栈所需的所有临时存储空间。栈是一大块函数随时想使用的内存。
ARM中的栈内存是高地址向低地址分布的,意味着你必须从栈指针开始减。
在这里,分配了16个字节长度的内存。
sub sp, sp, #16 ; =16
这里,两个参数被存入栈中。这是通过存储寄存指令(str)实现的。
第一个参数是要存储的寄存器
第二个是存储的位置。方括号代表里面值是内存地址。 这个方括号指令允许你为一个值指定偏移量,
因此[sp, #12]的意思『在栈指针的地址上加上12字节偏移量』
同样地,str w0, [sp, #12]意味着『存储w0寄存器的值到栈指针地址加上12字节内存的位置』。
str w0, [sp, #12]
str w1, [sp, #8]
刚被保存到栈的值又被读取到相同的寄存器内。和str指令相反的,ldr指令是从一个内存中加载内容到寄存器。
ldr w0, [sp, #12]意思是『读取出在栈指针地址加上12字节内存的位置的内容,并将内容赋值给寄存器w0』。
如果你好奇为何w0和w1刚被存储又被加载出来,对,它是很奇怪,这两行明明就是多余的嘛!如果编译器允许基本的编译优化,那么这多余的就会被消除。
ldr w0, [sp, #12]
ldr w1, [sp, #8]
意思是将w0和w1中的内容相加,并将相加的值赋值给r0。
add指令入参可以是两个或者三个,如果是三个,那第一个参数就是存储后两个参数相加的值的寄存器。
所以,这行指令也可以写成:add w0, w0, w1。
add w0, w0, w1
再一次,编译器生成了一些多余的代码:将相加的结果存储起来,又读取到相同的位置。
str w0, [sp, #4]
ldr w0, [sp, #4]
函数即将终止,因此栈指针放回原来的地方。
函数开始时从sp(栈指针)上减去了12个字节而得到12个字节内存使用。现在它把12个字节还回去。
函数必须保证栈指针操作平衡,否则栈指针可能漂移,最终可能超出了已分配的内存。你应该不希望那样...
add sp, sp, #16 ; =16
ret
3.2 简单点理解
sub sp, sp, #16 ; =16 //栈地址减去16,即分配了16字节内存
str w0, [sp, #12] //把w0存储到sp栈中,sp指针上加上12字节的偏移量
str w1, [sp, #8] //把w1存储到sp栈中,sp指针上加上8字节的偏移量
ldr w0, [sp, #12] //读取栈中12字节偏移量的地址到存储器w0中
ldr w1, [sp, #8] //读取栈中8字节偏移量的地址到存储器w1中
add w0, w0, w1 //w0的值加上w1的值存储到w0中
str w0, [sp, #4]//把w0的值存储到sp栈中
ldr w0, [sp, #4]//读取sp栈中的值到w0中
add sp, sp, #16 ; =16 //栈地址加16字节,即回收分配的内存
ret //结束
以上是未优化的汇编代码,有很多重复且没有用的代码。
3.3 Xcode选择release,编译器优化后:
add w0, w1, w0
ret
小结:
- iOS中对象内存是分配在堆上的,局部变量或指针都是在栈上的。OC代码都会被机器编译成汇编,不考虑复杂的场景,汇编都是和栈的内存进行打交道。汇编也和CPU的寄存器打交道。
- 局部变量的值或地址在栈中,而真正的计算是在寄存器中的,使用时需要分配空间,即sub sp, sp, #16 ;寄存器可以把值存储到栈中str w0, [sp, #12];寄存器也可以从栈中取出值,即 ldr w1, [sp, #8];寄存器和寄存器之间也可以相互操作;用完需要释放内存,不然会有内存泄露,即add sp, sp, #1。
参考文章:iOS汇编教程
4. arm64寄存器简单介绍
64位处理器有34个寄存器,包括31个通用寄存器、SP、PC、CPSR。
x0-x7: 用于子程序调用时的参数传递,x0 还用于返回值传递
x0 - x30 是31个通用整形寄存器。每个寄存器可以存取一个64位大小的数。 当使用 r0 - r30 访问时,它就是一个64位的数。当使用 w0 - w30 访问时,访问的是这些寄存器的低32位
查看x0返回值,0x2e=46
pc:表示当前执行的指令的地址
(lldb) register read pc
pc = 0x00000001022c6c50 DataStructureDemo`addFunction + 28 at main.m:14:12
lr:链接寄存器,存放着函数的返回地址:这里存放的是fooFunction地址
(lldb) register read lr
lr = 0x00000001022c6c74 DataStructureDemo`fooFunction + 24 at main.m:18:9
5. _fooFunction:
在上面的代码中添加下面函数,并调用addFunction
void fooFunction() {
int add = addFunction(12, 34);
printf("add = %i", add);
}
5.1 添加注释后的汇编
push {r7, lr} //r7,lr入栈
mov r7, sp//r7=sp即r7保存了栈顶元素
sub sp, #8//sp减8字节
movs r0, #12 //r0 = 12
movs r1, #34//r1 = 34
bl _addFunction //调用函数addFunction; r0,r1是addFunction两个参数
str r0, [sp, #4]//r0是返回结果,把r0存储到sp中
ldr r1, [sp, #4]//取出sp给r1;这两句等价于r1=r0;
movw r0, :lower16:(L_.str-(LPC2_0+4))
movt r0, :upper16:(L_.str-(LPC2_0+4))
//printf函数的第一个参数是一个字符串,可以搜索L_.str,看到.asciz "add = %i"
//前两个指令加载常量的地址,并减去标签的地址(LPC1_0加上4字节)。
add r0, pc
//r0加上pc(程序计数器),这样无论L.str在二进制文件的什么位置都能够准确的存放字符串的位置。
//上面三条指令加载指向所需的字符串的开始地址的指针到r0寄存器。
bl _printf//执行printf函数,r0是参数,且字符串已拼接好
str r0, [sp] @ 4-byte Spill//存储r0到栈中
add sp, #8//恢复栈内存
pop {r7, pc}//恢复r7,pc
6.OC函数
前面的写法都是C语言的写法,OC与C还是有一定区别。看下面源码:
- (int)addValue:(int)a toValue:(int)b {
int c = a + b;
return c;
}
6.1优化版本汇编
adds r0, r3, r2
bx lr
r3,r2是参数a和b,为什么不是r0,r1呢?
因为在OC中有两个隐士的参数:id self, SEL _cmd。
6.2 foo函数
- (void)foo {
int add = [self addValue:12 toValue:34];
NSLog(@"add = %i", add);
}
转换后的汇编:
push {r7, lr}//r7,lr入栈
mov r7, sp//r7=sp
Ltmp10:
movw r1, :lower16:(L_OBJC_SELECTOR_REFERENCES_.2-(LPC4_0+4))
Ltmp11:
movs r2, #12//r2=12
movt r1, :upper16:(L_OBJC_SELECTOR_REFERENCES_.2-(LPC4_0+4))//查找_cmd
movs r3, #34//r3=34
LPC4_0:
add r1, pc//r1=r1+pc
ldr r1, [r1]//表示加载存储在r1指针内的内容并赋值给r1。用伪代码表示r1=*r1
bl _objc_msgSend//调用objc_msgsend
Ltmp12:
mov r1, r0//r1=ro
Ltmp13:
movw r0, :lower16:(L__unnamed_cfstring_-(LPC4_1+4))
movt r0, :upper16:(L__unnamed_cfstring_-(LPC4_1+4))
//给r0赋值,r0=self;
LPC4_1:
add r0, pc//r0=r0+pc
bl _NSLog//调用NSLog
Ltmp14:
pop {r7, pc}//出栈
movw r1, :lower16:(L_OBJC_SELECTOR_REFERENCES_.2-(LPC4_0+4))
movt r1, :upper16:(L_OBJC_SELECTOR_REFERENCES_.2-(LPC4_0+4))//查找_cmd
add r1, pc//r1=r1+pc
这三句放一块解读,没什么问题,r1存入当前selector的字符。selector的引用:其实selector就是存储在数据段的字符串。movw r0, :lower16:(L__unnamed_cfstring_-(LPC4_1+4))
movt r0, :upper16:(L__unnamed_cfstring_-(LPC4_1+4))
add r0, pc//r0=r0+pc
这三句与r1的三句类似,r0=self。
3.总的上面步骤:r0=self,r1=_cmd,r2=12,r3=34;调用objc_msgSend,调用NSLog。整体流程清晰明了。
总结:
1.至此大概了解了OC的整个汇编的过程.举一反三,看下viewDidLoad方法:
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
}
转成汇编:
push {r7, lr}
mov r7, sp
//只要是方法里调用了别的方法,上面两句少不了。
sub sp, #8//分配1个字节
movw r1, :lower16:(L_OBJC_SELECTOR_REFERENCES_-(LPC0_0+4))
Ltmp1:
movt r1, :upper16:(L_OBJC_SELECTOR_REFERENCES_-(LPC0_0+4))
//r1=cmd
movw r2, :lower16:(L_OBJC_CLASSLIST_SUP_REFS_$_-(LPC0_1+4))
movt r2, :upper16:(L_OBJC_CLASSLIST_SUP_REFS_$_-(LPC0_1+4))
//r2=super
add r1, pc//r1+=pc
add r2, pc//r2+=pc
ldr r1, [r1]//r1=*r1;表示加载存储在r1指针内的内容并赋值给r1
ldr r2, [r2]//r2=*r2;表示加载存储在r2指针内的内容并赋值给r2
strd r0, r2, [sp]//str r0, [sp];str r2, [sp + 4]即r0,r2存储到sp中
mov r0, sp//r0=sp
bl _objc_msgSendSuper2//调用super方法
add sp, #8//恢复栈指针
pop {r7, pc}//恢复r7,pc
如果是汇编,能不能反推到正常的代码逻辑呢,找到bl即找到了调用的方法,再找到r0,r1,r2等参数,知道方法的参数。也许复杂的逻辑很难,但孰能生巧。