专注、坚持

Swift 拾遗 - inout

2020.07.25 by kingcos
Date Notes
2020-07-25 首次提交

Preface

《Swift 拾遗》是一个关于 Swift 的新文章专辑,这个系列的文章将不会涉及基本语法的层面,而是尝试从底层「拾」起之前所忽视的内容。今天我们将一起简单探究修饰 Swift 中函数参数的 inout 关键字。

本质

我们声明一个使用 inout 参数的函数,并在外界将变量 & 按地址传入:

var foo = 5

func inoutDemoFunc(_ innerFoo: inout Int) {
    innerFoo = 9
}

inoutDemoFunc(&foo) // BREAKPOINT 🔴

// foo == 9

尝试将以上代码转换为汇编(Xcode Menu - Debug - Debug Workflow - Always Show Disassembly):

; inout
demo`main:
    0x100001d60 <+0>:  pushq  %rbp
    0x100001d61 <+1>:  movq   %rsp, %rbp
    0x100001d64 <+4>:  subq   $0x30, %rsp
    0x100001d68 <+8>:  leaq   0x1459(%rip), %rax        ; demo.foo : Swift.Int
    0x100001d6f <+15>: xorl   %ecx, %ecx
    ; 将立即数 5 移动至 rip(0x100001d7c)+ 0x144c,即 0x1000031C8(全局变量 foo)
    0x100001d71 <+17>: movq   $0x5, 0x144c(%rip)        ; _dyld_private + 4
    0x100001d7c <+28>: movl   %edi, -0x1c(%rbp)
->  0x100001d7f <+31>: movq   %rax, %rdi
    0x100001d82 <+34>: leaq   -0x18(%rbp), %rax
    0x100001d86 <+38>: movq   %rsi, -0x28(%rbp)
    0x100001d8a <+42>: movq   %rax, %rsi
    0x100001d8d <+45>: movl   $0x21, %edx
    0x100001d92 <+50>: callq  0x100001e22               ; symbol stub for: swift_beginAccess
    ; 参数传递,移动 rip(0x100001d9e)+ 0x142a,即 0x1000031C8(foo 的内存地址)至 rdi 寄存器 ⬇
    0x100001d97 <+55>: leaq   0x142a(%rip), %rdi        ; demo.foo : Swift.Int
    ; 函数调用 ⬇
    0x100001d9e <+62>: callq  0x100001dc0               ; demo.inoutDemoFunc(inout Swift.Int) -> () at main.swift:316
    0x100001da3 <+67>: leaq   -0x18(%rbp), %rdi
    0x100001da7 <+71>: callq  0x100001e28               ; symbol stub for: swift_endAccess
    0x100001dac <+76>: xorl   %eax, %eax
    0x100001dae <+78>: addq   $0x30, %rsp
    0x100001db2 <+82>: popq   %rbp
    0x100001db3 <+83>: retq

; Step Into
demo`inoutDemoFunc(_:):
    0x100001dc0 <+0>:  pushq  %rbp
    0x100001dc1 <+1>:  movq   %rsp, %rbp
    0x100001dc4 <+4>:  movq   $0x0, -0x8(%rbp)
    0x100001dcc <+12>: movq   %rdi, -0x8(%rbp)
    ; $0x9 即立即数 9 ⬇
->  0x100001dd0 <+16>: movq   $0x9, (%rdi)
    0x100001dd7 <+23>: popq   %rbp
    0x100001dd8 <+24>: retq

观察整个函数调用流程我们会发现,当使用 inout 时,将作为参数传入的变量的内存地址通过 lea 指令被放在 %rdi 中,传递到函数内部;在函数内部修改参数值时,实际修改的是 %rdi 中内存地址所对应的存储空间的实际值。这就验证了 inout 的本质是按引用传递。

再来尝试一个无 inout 修饰参数的函数:

var foo = 5

func demoFunc(_ innerFoo: Int) {
    // innerFoo 默认声明为 let
    var innerFoo = innerFoo
    innerFoo = 9
}

demoFunc(foo)  // BREAKPOINT 🔴

// foo == 5

同样将以上代码转换为汇编:

demo`main:
    0x100001d50 <+0>:  pushq  %rbp
    0x100001d51 <+1>:  movq   %rsp, %rbp
    0x100001d54 <+4>:  subq   $0x30, %rsp
    0x100001d58 <+8>:  leaq   0x1469(%rip), %rax        ; demo.foo : Swift.Int
    0x100001d5f <+15>: xorl   %ecx, %ecx
    0x100001d61 <+17>: movq   $0x5, 0x145c(%rip)        ; _dyld_private + 4
    0x100001d6c <+28>: movl   %edi, -0x1c(%rbp)
->  0x100001d6f <+31>: movq   %rax, %rdi
    0x100001d72 <+34>: leaq   -0x18(%rbp), %rax
    0x100001d76 <+38>: movq   %rsi, -0x28(%rbp)
    0x100001d7a <+42>: movq   %rax, %rsi
    0x100001d7d <+45>: movl   $0x20, %edx
    0x100001d82 <+50>: callq  0x100001e1e               ; symbol stub for: swift_beginAccess
    0x100001d87 <+55>: movq   0x143a(%rip), %rdi        ; demo.foo : Swift.Int
    0x100001d8e <+62>: leaq   -0x18(%rbp), %rax
    0x100001d92 <+66>: movq   %rdi, -0x30(%rbp)
    0x100001d96 <+70>: movq   %rax, %rdi
    0x100001d99 <+73>: callq  0x100001e24               ; symbol stub for: swift_endAccess
    ; 参数传递,这里不同于 inout 使用 lea 指令所做的按地址传递 ⬇
    0x100001d9e <+78>: movq   -0x30(%rbp), %rdi
    ; 函数调用 ⬇
    0x100001da2 <+82>: callq  0x100001db0               ; demo.demoFunc(Swift.Int) -> () at main.swift:316
    0x100001da7 <+87>: xorl   %eax, %eax
    0x100001da9 <+89>: addq   $0x30, %rsp
    0x100001dad <+93>: popq   %rbp
    0x100001dae <+94>: retq

; Step Into
demo`demoFunc(_:):
    0x100001db0 <+0>:  pushq  %rbp
    0x100001db1 <+1>:  movq   %rsp, %rbp
    0x100001db4 <+4>:  movq   $0x0, -0x8(%rbp)
    0x100001dbc <+12>: movq   $0x0, -0x10(%rbp)
    0x100001dc4 <+20>: movq   %rdi, -0x8(%rbp)
->  0x100001dc8 <+24>: movq   %rdi, -0x10(%rbp)
    0x100001dcc <+28>: movq   $0x9, -0x10(%rbp)
    0x100001dd4 <+36>: popq   %rbp
    0x100001dd5 <+37>: retq

当没有使用 inout 时,函数参数值本身将通过 mov 指令被放在 %rdi 中;在函数内部再通过 mov 指令将 %rdi 中的值取出放在 -0x8(%rbp) 中,声明同名变量则是同样将 %rdi 中的值取出放在 -0x10(%rbp) 中,修改则也是针对 -0x10(%rbp) 中的值。这些过程均属于值传递,因此外界的变量也不会被改变。

扩展

汇编指令

- 用例 描述
% %rip 寄存器前缀为 %
(ASCII 0x25)
$ $0x9 立即数前缀为 $
(ASCII 0x24)
(%) (%rdi) %rdi 中存储的内存地址所对应的值(%rdi 为指针地址,(%rdi) 为地址对应的值)
call
Call
调用
callq 0x100000b70 调用函数地址为 0x100000b70 的函数
lea
Load Effective Address
加载有效地址
leaq 0x142a(%rip), %rdi %rip 中的地址值加上 0x142a 并存储到 %rdi 中(地址传递)
mov
Move
移动
movq $0x9, (%rdi) 9 存储在 %rdi 中地址对应的存储空间中(*p = 9

指令后缀

在上文中的汇编指令上,我们经常能见到 lq 等这样的指令后缀,具体如下表:

标识符 全拼
b Byte 8
w Word 16
l Long 32(默认)
q Quadword 64