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 ) |
指令后缀
在上文中的汇编指令上,我们经常能见到 l
或 q
等这样的指令后缀,具体如下表:
标识符 | 全拼 | 位 |
---|---|---|
b |
Byte | 8 |
w |
Word | 16 |
l |
Long | 32(默认) |
q |
Quadword | 64 |