从 汇编 验证Swift的 inout 本质

从 汇编 验证Swift的 inout 本质_第1张图片

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

我的核心思想是

学 的 越 多,老 的 越 快

不 想 认 输,只 好 变 秃

从 汇编 验证Swift的 inout 本质_第2张图片

& 地址传递

接下来,看看 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 的内存分布

从 汇编 验证Swift的 inout 本质_第3张图片

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

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

 rax = 0x00000001006318c0

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

如下图

从 汇编 验证Swift的 inout 本质_第4张图片

得出 -> 第 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

也同样是 引用传递

具体流程如下

从 汇编 验证Swift的 inout 本质_第5张图片

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 被改变了

从 汇编 验证Swift的 inout 本质_第6张图片

看汇编

   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 
  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 的 带有属性观察器的属性也类似计算属性

如下图:

从 汇编 验证Swift的 inout 本质_第7张图片

Copy in Copy out

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

函数具有单一职责的特性

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

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

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

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

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

总结

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

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

读者可以自行分析

谢谢你的阅读

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

推荐:

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

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

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

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

申请即送:

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

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

你可能感兴趣的:(从 汇编 验证Swift的 inout 本质)