一道关于swift中defer的面试题目的窥探

1,366 阅读8分钟

近期在某开发群里面看到一道swift的题目,大家讨论了一波,一时好奇其原因,就自己研究了一波,再次记录一下

var a = 1
func add() -> Int {
    defer {
        a = a + 1
    }
    return a
}
print(a) // 这里打印出来a是1?

这里我已经把答案写出来了,print(a)打印的是1,可为啥会是1呢?这里我先卖个关子,我们继续往下看。

defer的一些理解

关于defer语句,在swift中它被应用于什么场景呢?

比如,读取某目录下的文件内容并处理数据,你需要首先定位到文件目录,打开文件夹,读取文件内容以及处理数据,关闭文件以及文件夹。倘若一切顺利,只需按照设定好的程序流程走一轮即可;

然而,事情并不会总是如你所愿,如果中间某个环节失败,比如读取文件内容失败、处理数据失败等等,还需要进行一些后续收尾工作,即关闭文件或关闭文件夹(当然就算顺利执行,也是要关闭的)。 由于在关闭文件之前可能出现异常,导致代码无法继续往下执行,这将会导致内存泄漏,那么这时候defer就可以来处理这种问题,将收尾工作写在defer代码块中,保证收尾工作顺利进行。

以上便是我对于defer的整体印象,我也查了下资料关于defer的说明:

关于swift官方文档中对于defer的说明

Use defer to write a block of code that is executed after all other code in the function, just before the function returns. The code is executed regardless of whether the function throws an error. You can use defer to write setup and cleanup code next to each other, even though they need to be executed at different times.

SwiftGG对其也有说明

在 defer 语句中的语句无论程序控制如何转移都会被执行。在某些情况下,例如,手动管理资源时,比如关闭文件描述符,或者即使抛出了错误也需要执行一些操作时,就可以使用 defer 语句。 如果多个 defer 语句出现在同一作用域内,那么它们执行的顺序与出现的顺序相反。给定作用域中的第一个 defer 语句,会在最后执行,这意味着代码中最靠后的 defer 语句中引用的资源可以被其他 defer 语句清理掉。

题目中print(a)的原因窥探

以上的这些说明不足以解释这个我们的主题,为啥print(a)t打印出来的是1,而不是2?

最近刚好在学习汇编,随即写了个demo,来跟一下汇编看看到底是咋回事?


swiftLearning`main:
    0x1000008d0 <+0>:   pushq  %rbp
    0x1000008d1 <+1>:   movq   %rsp, %rbp
    0x1000008d4 <+4>:   subq   $0x80, %rsp
    0x1000008db <+11>:  movq   $0x1, 0x8ba(%rip)         ; _swift_FORCE_LOAD_$_swiftCompatibility50
    0x1000008e6 <+22>:  movl   %edi, -0x34(%rbp)
    0x1000008e9 <+25>:  movq   %rsi, -0x40(%rbp)
->  0x1000008ed <+29>:  callq  0x100000a00               ; swiftLearning.add() -> Swift.Int at main.swift:13
    0x1000008f2 <+34>:  leaq   0x8a7(%rip), %rsi         ; swiftLearning.a : Swift.Int
    0x1000008f9 <+41>:  xorl   %edi, %edi
    0x1000008fb <+43>:  movl   %edi, %ecx
    0x1000008fd <+45>:  movq   %rsi, %rdi
    0x100000900 <+48>:  leaq   -0x18(%rbp), %rsi
    0x100000904 <+52>:  movl   $0x21, %edx
    0x100000909 <+57>:  movq   %rax, -0x48(%rbp)
    0x10000090d <+61>:  callq  0x100000e86               ; symbol stub for: swift_beginAccess
    0x100000912 <+66>:  movq   -0x48(%rbp), %rax
    0x100000916 <+70>:  movq   %rax, 0x883(%rip)         ; swiftLearning.a : Swift.Int
    0x10000091d <+77>:  leaq   -0x18(%rbp), %rdi
    0x100000921 <+81>:  callq  0x100000e92               ; symbol stub for: swift_endAccess
    0x100000926 <+86>:  movq   0x6e3(%rip), %rax         ; (void *)0x00007fff9cf6eb18: type metadata for Any
    0x10000092d <+93>:  addq   $0x8, %rax
    0x100000931 <+97>:  movl   $0x1, %edi
    0x100000936 <+102>: movq   %rax, %rsi
    0x100000939 <+105>: callq  0x100000e50               ; symbol stub for: Swift._allocateUninitializedArray<A>(Builtin.Word) -> (Swift.Array<A>, Builtin.RawPointer)
    0x10000093e <+110>: leaq   0x85b(%rip), %rcx         ; swiftLearning.a : Swift.Int
    0x100000945 <+117>: xorl   %r8d, %r8d
    0x100000948 <+120>: movl   %r8d, %esi
    0x10000094b <+123>: movq   %rcx, %rdi
    0x10000094e <+126>: leaq   -0x30(%rbp), %rcx
    0x100000952 <+130>: movq   %rsi, -0x50(%rbp)
    0x100000956 <+134>: movq   %rcx, %rsi
    0x100000959 <+137>: movl   $0x20, %ecx
    0x10000095e <+142>: movq   %rdx, -0x58(%rbp)
    0x100000962 <+146>: movq   %rcx, %rdx
    0x100000965 <+149>: movq   -0x50(%rbp), %rcx
    0x100000969 <+153>: movq   %rax, -0x60(%rbp)
    0x10000096d <+157>: callq  0x100000e86               ; symbol stub for: swift_beginAccess
    0x100000972 <+162>: movq   0x827(%rip), %rax         ; swiftLearning.a : Swift.Int
    0x100000979 <+169>: leaq   -0x30(%rbp), %rdi
    0x10000097d <+173>: movq   %rax, -0x68(%rbp)
    0x100000981 <+177>: callq  0x100000e92               ; symbol stub for: swift_endAccess
    0x100000986 <+182>: movq   0x67b(%rip), %rax         ; (void *)0x00007fff9cf64ff8: type metadata for Swift.Int
    0x10000098d <+189>: movq   -0x58(%rbp), %rcx
    0x100000991 <+193>: movq   %rax, 0x18(%rcx)
    0x100000995 <+197>: movq   -0x68(%rbp), %rax
    0x100000999 <+201>: movq   %rax, (%rcx)
    0x10000099c <+204>: callq  0x100000b00               ; default argument 1 of Swift.print(_: Any..., separator: Swift.String, terminator: Swift.String) -> () at <compiler-generated>
    0x1000009a1 <+209>: movq   %rax, -0x70(%rbp)
    0x1000009a5 <+213>: movq   %rdx, -0x78(%rbp)
    0x1000009a9 <+217>: callq  0x100000b20               ; default argument 2 of Swift.print(_: Any..., separator: Swift.String, terminator: Swift.String) -> () at <compiler-generated>
    0x1000009ae <+222>: movq   -0x60(%rbp), %rdi
    0x1000009b2 <+226>: movq   -0x70(%rbp), %rsi
    0x1000009b6 <+230>: movq   -0x78(%rbp), %rcx
    0x1000009ba <+234>: movq   %rdx, -0x80(%rbp)
    0x1000009be <+238>: movq   %rcx, %rdx
    0x1000009c1 <+241>: movq   %rax, %rcx
    0x1000009c4 <+244>: movq   -0x80(%rbp), %r8
    0x1000009c8 <+248>: callq  0x100000e56               ; symbol stub for: Swift.print(_: Any..., separator: Swift.String, terminator: Swift.String) -> ()
    0x1000009cd <+253>: movq   -0x80(%rbp), %rdi
    0x1000009d1 <+257>: callq  0x100000e8c               ; symbol stub for: swift_bridgeObjectRelease
    0x1000009d6 <+262>: movq   -0x78(%rbp), %rdi
    0x1000009da <+266>: callq  0x100000e8c               ; symbol stub for: swift_bridgeObjectRelease
    0x1000009df <+271>: movq   -0x60(%rbp), %rdi
    0x1000009e3 <+275>: callq  0x100000e8c               ; symbol stub for: swift_bridgeObjectRelease
    0x1000009e8 <+280>: xorl   %eax, %eax
    0x1000009ea <+282>: addq   $0x80, %rsp
    0x1000009f1 <+289>: popq   %rbp
    0x1000009f2 <+290>: retq   
 

swiftLearning`add():
    0x100000a00 <+0>:  pushq  %rbp
    0x100000a01 <+1>:  movq   %rsp, %rbp
    0x100000a04 <+4>:  subq   $0x30, %rsp
    0x100000a08 <+8>:  leaq   0x791(%rip), %rdi         ; swiftLearning.a : Swift.Int
    0x100000a0f <+15>: xorl   %eax, %eax
    0x100000a11 <+17>: movl   %eax, %ecx
    0x100000a13 <+19>: leaq   -0x18(%rbp), %rdx
    0x100000a17 <+23>: movl   $0x20, %esi
    0x100000a1c <+28>: movq   %rsi, -0x20(%rbp)
    0x100000a20 <+32>: movq   %rdx, %rsi
    0x100000a23 <+35>: movq   -0x20(%rbp), %r8
    0x100000a27 <+39>: movq   %rdx, -0x28(%rbp)
    0x100000a2b <+43>: movq   %r8, %rdx
    0x100000a2e <+46>: callq  0x100000e86               ; symbol stub for: swift_beginAccess
    0x100000a33 <+51>: movq   0x766(%rip), %rax         ; swiftLearning.a : Swift.Int
    0x100000a3a <+58>: movq   -0x28(%rbp), %rdi
    0x100000a3e <+62>: movq   %rax, -0x30(%rbp)
    0x100000a42 <+66>: callq  0x100000e92               ; symbol stub for: swift_endAccess
->  0x100000a47 <+71>: callq  0x100000a60               ; $defer #1 () -> () in swiftLearning.add() -> Swift.Int at <compiler-generated>
    0x100000a4c <+76>: movq   -0x30(%rbp), %rax
    0x100000a50 <+80>: addq   $0x30, %rsp
    0x100000a54 <+84>: popq   %rbp
    0x100000a55 <+85>: retq   
  
swiftLearning`$defer #1 () in add():
    0x100000a60 <+0>:   pushq  %rbp
    0x100000a61 <+1>:   movq   %rsp, %rbp
    0x100000a64 <+4>:   subq   $0x60, %rsp
    0x100000a68 <+8>:   leaq   0x731(%rip), %rax         ; swiftLearning.a : Swift.Int
    0x100000a6f <+15>:  xorl   %ecx, %ecx
    0x100000a71 <+17>:  movq   %rax, %rdi
    0x100000a74 <+20>:  leaq   -0x18(%rbp), %rsi
    0x100000a78 <+24>:  movl   $0x20, %edx
    0x100000a7d <+29>:  callq  0x100000e86               ; symbol stub for: swift_beginAccess
    0x100000a82 <+34>:  movq   0x717(%rip), %rax         ; swiftLearning.a : Swift.Int
    0x100000a89 <+41>:  leaq   -0x18(%rbp), %rdi
    0x100000a8d <+45>:  movq   %rax, -0x38(%rbp)
    0x100000a91 <+49>:  callq  0x100000e92               ; symbol stub for: swift_endAccess
->  0x100000a96 <+54>:  movq   -0x38(%rbp), %rax
    0x100000a9a <+58>:  incq   %rax
    0x100000a9d <+61>:  seto   %r8b
    0x100000aa1 <+65>:  movq   %rax, -0x40(%rbp)
    0x100000aa5 <+69>:  movb   %r8b, -0x41(%rbp)
    0x100000aa9 <+73>:  jo     0x100000af0               ; <+144> at main.swift:15:15
    0x100000aab <+75>:  leaq   0x6ee(%rip), %rdi         ; swiftLearning.a : Swift.Int
    0x100000ab2 <+82>:  xorl   %eax, %eax
    0x100000ab4 <+84>:  movl   %eax, %ecx
    0x100000ab6 <+86>:  leaq   -0x30(%rbp), %rdx
    0x100000aba <+90>:  movl   $0x21, %esi
    0x100000abf <+95>:  movq   %rsi, -0x50(%rbp)
    0x100000ac3 <+99>:  movq   %rdx, %rsi
    0x100000ac6 <+102>: movq   -0x50(%rbp), %r8
    0x100000aca <+106>: movq   %rdx, -0x58(%rbp)
    0x100000ace <+110>: movq   %r8, %rdx
    0x100000ad1 <+113>: callq  0x100000e86               ; symbol stub for: swift_beginAccess
    0x100000ad6 <+118>: movq   -0x40(%rbp), %rcx
    0x100000ada <+122>: movq   %rcx, 0x6bf(%rip)         ; swiftLearning.a : Swift.Int
    0x100000ae1 <+129>: movq   -0x58(%rbp), %rdi
    0x100000ae5 <+133>: callq  0x100000e92               ; symbol stub for: swift_endAccess
    0x100000aea <+138>: addq   $0x60, %rsp
    0x100000aee <+142>: popq   %rbp
    0x100000aef <+143>: retq   
    0x100000af0 <+144>: ud2    
  

上面的代码中主要分为三部分,mainadd(),defer,其中main就不用多说了,add()就是我们题目中定义的func add(){}的汇编,而defer则是func add(){}中的defer所对应的汇编实现了,主要的函数调用步骤我分为以下几步说明

  1. main中,我们看到1被赋值到全局变量区0x1000011A0
;main函数中
    0x1000008db <+11>:  movq   $0x1, 0x8ba(%rip)         ; _swift_FORCE_LOAD_$_swiftCompatibility50 // 将1存放到全局变量区,地址为0x1000011A0
    0x1000008ed <+29>:  callq  0x100000a00               ; swiftLearning.add() -> Swift.Int at main.swift:13 这里就要调用到add()方法中
  1. 接下来会在callq 0x100000a00来到add()函数中,其中的重点部分解读如下
;add函数中
    0x100000a82 <+34>:  movq   0x717(%rip), %rax         ; swiftLearning.a : Swift.Int// 将全局变量取出来赋值给rax,可以看到此处的0x717(%rip)就是地址0x717+0x100000a89=0x1000011A0,也就是刚才存起来的1
    0x100000a89 <+41>:  leaq   -0x18(%rbp), %rdi
    0x100000a8d <+45>:  movq   %rax, -0x38(%rbp)         ;将rax中的地址取出来,传给内存区域-0x38(%rbp) 
    
    0x100000a33 <+51>: movq   0x766(%rip), %rax         ; swiftLearning.a : Swift.Int 将全局变量取出来赋值给rax,可以看到此处的0x766(%rip)就是地址0x766+0x100000a3a=0x1000011A0,也就是刚才存起来的1
    0x100000a3a <+58>: movq   -0x28(%rbp), %rdi
    0x100000a3e <+62>: movq   %rax, -0x30(%rbp)         ;这一步将1放在缓存-0x30(%rbp) 中
    0x100000a42 <+66>: callq  0x100000e92               ; symbol stub for: swift_endAccess
->  0x100000a47 <+71>: callq  0x100000a60               ; $defer #1 () -> () in  这里调用defer函数
    
  1. 到这里defer之前已经调用完毕,接下来是defer函数
   
    0x100000a91 <+49>:  callq  0x100000e92               ; symbol stub for: swift_endAccess
    0x100000a96 <+54>:  movq   -0x38(%rbp), %rax         ; 将内存区域-0x38(%rbp) 中的地址去取来,再传给rax,此时rax中的地址是0x1000011A0
    0x100000a9a <+58>:  incq   %rax                      ; 将rax寄存其中的值自增
  1. defer函数在此后代码中知道ret,再没有对rax进行操作,代码到这里,我们已将看到defer中的a = a + 1,但是这就完了么?我们继续往下解读重点代码
    0x100000a33 <+51>: movq   0x766(%rip), %rax         ; swiftLearning.a : Swift.Int 这里将全局区0x1000011A0的地址赋值给了rax寄存器
    0x100000a3a <+58>: movq   -0x28(%rbp), %rdi
    0x100000a3e <+62>: movq   %rax, -0x30(%rbp)         ; 这里将rax寄存器的值付给了内存区域-0x30(%rbp)保存起来
    0x100000a42 <+66>: callq  0x100000e92               ; symbol stub for: swift_endAccess
->  0x100000a47 <+71>: callq  0x100000a60               ; $defer #1 () -> () in swiftLearning.add() -> Swift.Int at <compiler-generated>这里调用defer 这一步执行完以后,defer中的代码就调用完了
    0x100000a4c <+76>: movq   -0x30(%rbp), %rax         ; 这里又给rax赋值了,而这里的-0x30(%rbp)缓存区放的值就是在defer调用之前存入的0x1000011A0,自此rax又被赋值为地址0x1000011A0
    0x100000a50 <+80>: addq   $0x30, %rsp
    0x100000a54 <+84>: popq   %rbp
    0x100000a55 <+85>: retq                             ; 这里才把add()方法执行完,并return回去
  1. add执行完以后直接将rax的地址当做返回值的地址,返回并print,此时就得到了那个1,相当于饶了一大圈,其实rax的值还是取的0x1000011A0,并没有拿defer中自增操作后的值

到这里我们可以看到,虽然defer中执行了a = a + 1,但是在add函数return之前,rax又被赋值0x1000011A0,而在函数调用前,我们已经看到0x1000008db <+11>: movq $0x1, 0x8ba(%rip),可以得到我们在add执行完之后,返回值还是0x1000011A0,解释了为啥print打印出来还是1的原因

(lldb) register read rax
     rax = 0x0000000000000001
1

这里总结一下,虽然defer是在函数返回之前会执行,但是里面的操作并不会影响返回值,返回值在defer执行完之后,又去取了原来的值,所以print的值还是1