首页
学习
活动
专区
工具
TVP
发布
精选内容/技术社群/优惠产品,尽在小程序
立即前往

从 汇编 验证Swift的 inout 本质

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

发现了吗?

0x1006318D0 比 0x00000001006318c0 多 16个字节

意味着什么?

函数入参的地址是 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 的内存分布不同

读者可以自行分析

谢谢你的阅读

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

  • 发表于:
  • 原文链接https://kuaibao.qq.com/s/20200617A04EWJ00?refer=cp_1026
  • 腾讯「腾讯云开发者社区」是腾讯内容开放平台帐号(企鹅号)传播渠道之一,根据《腾讯内容开放平台服务协议》转载发布内容。
  • 如有侵权,请联系 cloudcommunity@tencent.com 删除。

扫码

添加站长 进交流群

领取专属 10元无门槛券

私享最新 技术干货

扫码加入开发者社群
领券