首页
学习
活动
专区
工具
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元无门槛券

私享最新 技术干货

扫码加入开发者社群
领券
http://www.vxiaotou.com