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

从 汇编 验证Swift的 inout 本质

作者: overla5 | 来源:发表于2020-05-27 11:01 被阅读0次
image

距 离 上 次 分 享 已 经 是 5 个 月 前 了

掀 开 布 满 灰 尘 的 电 脑

原 则 上 我 不 会 主 动

但 很 难 对 网 易 云 说 不

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

我的核心思想是

学 的 越 多,老 的 越 快

不 想 认 输,只 好 变 秃

image

& 地址传递

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

image

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

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

 rax = 0x00000001006318c0

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

如下图

image

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

也同样是 引用传递

具体流程如下

image

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

看汇编

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

如下图:

image

Copy in Copy out

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

函数具有单一职责的特性

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

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

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

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

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

总结

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

读者也可以自行分析,不同的地方可能仅仅是 Class 与 Struct 的内存分布不同

汇编代码笔者也只是略懂皮毛

但却比我们凭空想象来的确切

谢谢你的阅读

还请指正~

相关文章

  • 从 汇编 验证Swift的 inout 本质

    inout 的哲学 我 时 常 想 着 改 变 自 己 在清晨上班的路上,在傍晚下班的公交 就像这个 change...

  • 从 汇编 验证Swift的 inout 本质

    序 距 离 上 次 分 享 已 经 是 5 个 月 前 了 掀 开 布 满 灰 尘 的 电 脑 原 则 上 我 不...

  • Swift~属性、汇编分析inout本质

    本文源自本人的学习记录整理与理解,其中参考阅读了部分优秀的博客和书籍,尽量以通俗简单的语句转述。引用到的地方如有遗...

  • swift学习-inout的本质

    如果实参有物理内存地址,而且没有设置属性观察器:直接将实参的内存地址传入函数(实参进行引用传递) 如果实参是计算属...

  • 2017-12-26

    swift泛型的使用 ''' func exchange( a: inout T, b : inout T){//...

  • 从 简单汇编基础 到 Swift 不简单的 a + 1

    从 简单汇编基础 到 Swift 不简单的 a + 1从 简单汇编基础 到 Swift 不简单的 a + 1

  • Swift inout参数本质

    输入输出参数 (In-Out Parameter) • 可以用inout定义一个输入输出参数:可以再函数内部修改外...

  • Swift汇编分析inout关键字

    因为inout关键字比较简单,因此该文章篇幅相对比较短小。我们直到在swift中inout通常用来在函数内修改外部...

  • Swift - mutating & inout

    Swift - mutating & inout 前言 曾几何时,刚用swift的时候,我想修改传入的参数,发现不...

  • 无标题文章

    swift 语法 1.inout() 希望传入参数通过函数改变 func swapTwoInts( a:Int, ...

网友评论

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

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