美文网首页
iOS 汇编入门学习

iOS 汇编入门学习

作者: 某非著名程序员 | 来源:发表于2019-11-24 22:46 被阅读0次

最近看一些资料,接触到一些资料有汇编,发现看不懂,还是有必要学习下。
在iOS中,消息发送是汇编写的,在学习戴铭高手课时hook oc中的objc_send方法,也用到汇编。尝试去阅读,网上也有很多注释,发现看完之后依然一知半解。还是从基础学起。

1. 先看一段C的代码,我们可以放到main.m中:

int addFunction(int a, int b) {
    int c = a + b;
    return c;
}

2. 选择真机,Xcode选中Assemble转成汇编代码

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

小结:

  1. iOS中对象内存是分配在堆上的,局部变量或指针都是在栈上的。OC代码都会被机器编译成汇编,不考虑复杂的场景,汇编都是和栈的内存进行打交道。汇编也和CPU的寄存器打交道。
  2. 局部变量的值或地址在栈中,而真正的计算是在寄存器中的,使用时需要分配空间,即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。


register

x0-x7: 用于子程序调用时的参数传递,x0 还用于返回值传递
x0 - x30 是31个通用整形寄存器。每个寄存器可以存取一个64位大小的数。 当使用 r0 - r30 访问时,它就是一个64位的数。当使用 w0 - w30 访问时,访问的是这些寄存器的低32位

查看x0返回值,0x2e=46


register read x0

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}//出栈
  1. 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就是存储在数据段的字符串。

  2. 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等参数,知道方法的参数。也许复杂的逻辑很难,但孰能生巧。

相关文章

网友评论

      本文标题:iOS 汇编入门学习

      本文链接:https://www.haomeiwen.com/subject/inurwctx.html