iOS 汇编入门学习

最近看一些资料,接触到一些资料有汇编,发现看不懂,还是有必要学习下。
在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等参数,知道方法的参数。也许复杂的逻辑很难,但孰能生巧。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,142评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,298评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,068评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,081评论 1 291
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,099评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,071评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,990评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,832评论 0 273
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,274评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,488评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,649评论 1 347
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,378评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,979评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,625评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,796评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,643评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,545评论 2 352

推荐阅读更多精彩内容