美文网首页
从 汇编 验证Swift的 inout 本质

从 汇编 验证Swift的 inout 本质

作者: 一意孤行的程序猿 | 来源:发表于2020-06-15 15:49 被阅读0次

    inout 的哲学

    我 时 常 想 着 改 变 自 己

    在清晨上班的路上,在傍晚下班的公交

    就像这个 change 函数

    func change(num: Int) {
         num = 20
    }
    
    var age = 18
    print(change(num: age)) // 运行错误
    

    永 远 18

    直到有一天,

    func change(num: inout Int) {
         num = 20
    }
    var age = 18
    print(change(num: &age)) 
    // 20
    

    我突然长大了

    路人:

    我懂了,我懂了,作者你是想告诉我们,想改变就要付出,没有in就没有out

    口好渴

    这鸡汤我先干为敬

    ...

    fun pee

    我的核心思想是

    学 的 越 多,老 的 越 快

    不 想 认 输,只 好 变 秃

    & 地址传递

    接下来,看看 inout 到底干了什么

    change(num: &age)

    &符号,这里表示取址符,取 全局变量age 的 内存地址

    不难猜测出是将 age 的内存地址 传到函数内,修改 age 内存地址指向的值

    怎么证明这一点呢?

    好的,断点落在 change(num: &age)

       1\.  0x100000ed1 <+17>: movq   $0x12, 0x1144(%rip)       ; _dyld_private + 4
       2\.  0x100000edc <+28>: movl   %edi, -0x1c(%rbp)
    -> 3\.  0x100000edf <+31>: movq   %rax, %rdi
       4\.  0x100000ee2 <+34>: leaq   -0x18(%rbp), %rax
       5\.  0x100000ee6 <+38>: movq   %rsi, -0x28(%rbp)
       6\.  0x100000eea <+42>: movq   %rax, %rsi
       7\.  0x100000eed <+45>: movl   $0x21, %edx
       8\.  0x100000ef2 <+50>: callq  0x100000f78               ; symbol stub for: swift_beginAccess
       9\.  0x100000ef7 <+55>: leaq   0x1122(%rip), %rdi        ; inout.age : Swift.Int
       10\. 0x100000efe <+62>: callq  0x100000f20               ; inout.change(num: inout Swift.Int) -> Swift.Int at main.swift:11
    

    第1行:

    movq $0x12, 0x1144(%rip)

    将 8个字节 的Int 型 18 ,放入 0x1144(%rip) 这块内存地址中,0x1144(%rip)

    之前文章说过,这个形式(0xXXXX(rip%))代表全局变量的地址值, 这里应该是 变量age 的地址值

    第2行:

    rip% : 指向下一条指令的地址

    将第二行 的 0x100000edc(rip的地址) + 0x1144 = 0x100002020

    0x100002020 就是 存储 18 的内存地址

    第9行:

    leaq 0x1122(%rip), %rdi

    将 0x1122(%rip) 地址值 传给rdi, rdi 表参数,也就是将 地址 0x1122(%rip) 当做参数 ,传递给 第十行

    这个 0x1122(%rip) ,通过 (下一条指令地址值 + 0x1122)可以算出 值 就是 0x100002020

    就是 18 的地址值

    18 的地址值,当做参数 传给了change

    第10行:

    既然将 地址值传入 了函数 change,那就继续深入change 内部

    inout`change(num:):
    -> 1\.  0x100000f60 <+0>:  pushq  %rbp
       2\. 0x100000f61 <+1>:  movq   %rsp, %rbp
       3\. 0x100000f64 <+4>:  movq   $0x0, -0x8(%rbp)
       4\. 0x100000f6c <+12>: movq   %rdi, -0x8(%rbp)
       5\. 0x100000f70 <+16>: movq   $0x14, (%rdi)
       6\. 0x100000f77 <+23>: popq   %rbp
       7\. 0x100000f78 <+24>: retq   
    

    第4行:

    movq %rdi, -0x8(%rbp)

    既然rdi% 是 age 18 的内存地址,这句话就是说把 18 放入了 -0x8(%rbp)

    -0x8(%rbp) 是函数change 的 栈空间,后续释放

    第5行:

    movq $0x14, (%rdi)

    因为此时 rdi 指向的还是 age 的内存地址,未曾发生改变 ,第5行将立即数 20 存入 rdi

    作为返回值 出栈赋值 给 age

    so

    age 变成了 20

    小结:

    从上面简单的例子,应该可以暂时总结

    inout 的本质 确实是 引用传递,也就是 引用地址传递

    Class的 存储属性 传递

    定义一个class,以及 存储属性 age,看一下 存储属性是在inout 中是如何 传递的?

    func change(num: inout Int) {
         num = 20
    }
    
    class Person {
        var age: Int
    }
    
    var p = Person()
    
    -> change(num: &p.age)
    

    p 的字节占用是 8个字节,指的是 栈空间的 8个字节作为地址,指向堆空间的 内存分布

    分析关键点的汇编代码

    初始化

      1  0x100001a04 <+36>:  callq  0x100001d50               ; inout.Person.__allocating_init() -> inout.Person at main.swift:16
      2  0x100001a09 <+41>:  leaq   0x1798(%rip), %rcx        ; inout.p : inout.Person
      3  0x100001a10 <+48>:  xorl   %r8d, %r8d
      4  0x100001a13 <+51>:  movl   %r8d, %edx
    ->5  0x100001a16 <+54>:  movq   %rax, 0x178b(%rip)        ; inout.p : inout.Person
    

    第1行:

    __allocating_init

    我们都知道 类class 的内存是存放于堆空间的,__allocating_init 就是向堆空间 申请内存

    这里我们了解一下class 的内存分布

    第5行: 内存申请完毕,作为存放返回值rax%,返回的就是Person申请的 在 堆空间的内存地址

    通过断点 第5行, register read rax 得到 一个地址值

     rax = 0x00000001006318c0
    

    打开Debug -> DebugWorkflow -> ViewMemory ,输入此地址

    如下图

    得出 -> 第 16个字节确实存放的是 0x12,也就是p.age 的值 18

    传参

      ....
    ->1\.  0x100001a77 <+151>: movq   %rdx, %rdi
      2\.  0x100001a7a <+154>: movq   %rax, -0x80(%rbp)
      3\.  0x100001a7e <+158>: callq  0x100001af0               ; inout.change(num: inout Swift.Int) -> () at main.swift:12
    

    由案例1 分析可得,rdi% 作为参数,这里打印出的地址值 是0x1006318D0

    发现了吗?

    0x1006318D00x00000001006318c016个字节

    意味着什么?

    函数入参的地址是 Person 地址 偏移 16个字节,就是 age 的内存地址

    小结

    类对象 Class 的存储属性,inout 函数也是通过 改变 age 的内存地址里的值,来改变 age

    也同样是 引用传递

    具体流程如下

    Class的 计算属性 传递

    添加一个计算属性 count

    func change(num: inout Int) {
         num = 20
    }
    
    class Person {
        var age = 18
    
        var count: Int {
            set {
                age  = newValue * 2
            }
            get {
                return age / 2
            }
        }
    }
    
    var p = Person()
    change(num: &p.count)
    
    print(p.count)
    

    首先我们试着打印 p 的内存占用大小

    • MemoryLayout.size(ofValue: p)

    得出的结果依旧是8个字节,这意味着

    • 计算属性是不占用 类的内存大小的,它相当于一个方法的调用,存放于当前函数 的栈空间

    试着猜想一下?

    如果计算属性不占用p 的内存空间,它就意味着无法从 p 得到 count 的内存地址

    调用 inout 函数 必然是 无法改变 count 属性的,因为没有 地址的输入

    这才符合 上述的验证

    那么结果是

    print(p.count)
    
    // 20
    count 被改变了
    

    看汇编

       1\. 0x1000015d4 <+36>:  callq  0x100001bb0               ; inout.Person.__allocating_init() -> inout.Person at main.swift:16
       ...
       ..
    -> 2\. 0x100001648 <+152>: callq  *%rdx
       3\. 0x10000164a <+154>: movq   %rdx, %rdi
       4\. 0x10000164d <+157>: movq   %rax, -0x80(%rbp)
       5\. 0x100001651 <+161>: callq  0x1000016c0               ; inout.change(num: inout Swift.Int) -> () at main.swift:12
       6\. 0x100001658 <+168>: movq   -0x78(%rbp), %rdi
       7\. 0x100001660 <+176>: callq  *%rax
    

    同样的在初始化 Person 之后,我们看到了 第3行 rdi% 的值 是 从rdx% 得来的

    第2行

    callq *%rdx

    这是一个间接调用指令,rdx% 存放的是一个用于跳转的间接地址

    这为什么是 间接地址呢?

    因为 类的继承关系,属性很有可能被重写,系统不确定 此 计算属性的 的 setter getter 是否被重写

    只能在运行时 去查找对应的方法地址

    所以 这里是 间接寻址

    好,继续敲入 si,进入内部

    inout`Person.count.modify:
      2.1  0x100001b06 <+22>: movq   %rax, -0x10(%rbp)
      2.2  0x100001b0a <+26>: callq  0x100001a10               ; inout.Person.count.getter : Swift.Int at main.swift:23
      2.3  0x100001b0f <+31>: movq   -0x8(%rbp), %rcx
      2.4  0x100001b13 <+35>: movq   %rax, 0x8(%rcx)
      2.5  0x100001b17 <+39>: leaq   0x12(%rip), %rax          ; inout.Person.count.modify : Swift.Int at <compiler-generated>
      2.6  0x100001b1e <+46>: movq   -0x10(%rbp), %rdx
      2.7  0x100001b22 <+50>: addq   $0x10, %rsp
      2.8  0x100001b26 <+54>: popq   %rbp
      2.9  0x100001b27 <+55>: retq   
    

    第 2-> 1行:

    movq %rax, -0x10(%rbp)

    将 寄存器rax% 存放的地址 指向 -0x10(%rbp) 栈空间

    第 2-> 2行:

    映入眼帘的就是 count 的getter 方法,也就是说在 change 函数 之前,会先拿到 count 的值 ,age = 18,那么count 就是9

    (lldb) register read  rax
         rax = 0x0000000000000009
    

    第 2-> 6行:

    movq -0x10(%rbp), %rdx

    此时的 -0x10(%rbp) 指向的 是rax% 的地址值,赋值给 rdx%

    rdx% 存放的就是 9的地址 ,结束调用

    以上

    callq *rdx 结束

    第5行:

    change 函数调用,同之前分析

    此时 rdi% 通过change 返回 的rax% 已经修改为 20,作后续 的参数使用

    第7行:

    callq *%rax,传入 rdi%

    敲下 si 进入 callq *%rax,可以看到一个熟悉的面孔

    inout`Person.count.modify:
    ->  
        0x100001b3e <+14>: callq  0x100001980               ; inout.Person.count.setter : Swift.Int at main.swift:20
    

    count 的 setter 函数,到此我想你已经明白了。

    小结

    • Class 的 计算属性 不同于 存储属性,并非直接将 地址传入

      • 通过 计算属性的 getter 取值,然后将 值 存放于一个 地址中

      • 将地址 传入inout ,修改 地址存放的值

      • 结果传入计算属性的 setter

    • Class 的 带有属性观察器的属性也类似计算属性

    如下图:

    Copy in Copy out

    inout 的本质 就是引用地址的 传递

    函数具有单一职责的特性

    inout 函数就像 是一个黑盒,我们要做的仅仅是传入需要修改的变量的地址

    Copy in Copy out 仅仅是这种行为方式

    • 参数传入,拷贝一份 临时变量的地址

    • 函数内修改 临时变量 的值

    • 函数返回, 临时变量 被赋予给 原始参数

    总结

    本文只针对了 Class 的计算 和 存储 属性做了 简单的验证, 对于 Struct 也大同小异

    不同的地方可能仅仅是 Class 与 Struct 的内存分布不同

    读者可以自行分析

    谢谢你的阅读

    让我们在强者的道路上越走越秃吧!!!

    推荐👇:

    • 020 持续更新,精品小圈子每日都有新内容,干货浓度极高。

    • 结实人脉、讨论技术 你想要的这里都有!

    • 抢先入群,跑赢同龄人!(入群无需任何费用)

    • (直接搜索群号:789143298,快速入群)
    • 点击此处,与iOS开发大牛一起交流学习

    申请即送:

    • BAT大厂面试题、独家面试工具包,

    • 资料免费领取,包括 数据结构、底层进阶、图形视觉、音视频、架构设计、逆向安防、RxSwift、flutter,

    相关文章

      网友评论

          本文标题:从 汇编 验证Swift的 inout 本质

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