We know that Swift has three distribution methods: static distribution (direct distribution), VTable distribution (function table distribution) and message distribution. Below we respectively from SIL intermediate language, and assembly perspective experience Swift method distribution.
Problems in the introduction
Before we get started, let’s take a look at a question:
There is a Framework (with only one class and one method) and a Swift App project (calling this method). The code is as follows, and the Framework is compiled directly into the App project:
// Framework
public class SwiftMethodDispatchTable {
public func getMethodName(a) -> String {
let name = "SwiftMethodDispatchTable"
print("Method name: \(name)")
return name
}
}
// App,ViewController.swift
class ViewController: UIViewController {
override func viewDidLoad(a) {
super.viewDidLoad()
readMethodName()
}
func readMethodName(a) {
let name = SwiftMethodDispatchTable().getMethodName()
print("read method name:", name)
}
}
Copy the code
If we change the name of the getMethodName method in the Framework (getMethodName_new), recompile it, and overwrite the compiled binary (as shown below) over the Framework binary integrated in the App project, The Headers and Modules directories remain the same, as does the name of the method called in the App (which is still getMethodName).
So the question is: what is the result of App recompilation?
- A compiler error
- Run error
- The normal operation
Let’s get back to the subject and finally answer this question.
SIL
Let’s start by looking at three similar classes that all contain getMethodName methods that use different distributions, and look at the differences in their distributions using printMethodName:
- Static distributed
final public class SwiftMethodDispatchStatic {
public init(a) {}
func printMethodName(a) -> String {
let name = getMethodName()
return name
}
public func getMethodName(a) -> String {
let name = "SwiftMethodDispatchStatic"
print("Method name: \(name)")
return name
}
}
Copy the code
- Vtable dispatch
public class SwiftMethodDispatchTable {
public init(a) {}
func printMethodName(a) -> String {
let name = getMethodName()
return name
}
public func getMethodName(a) -> String {
let name = "SwiftMethodDispatchTable"
print("Method name: \(name)")
return name
}
}
Copy the code
- Messages distributed
public class SwiftMethodDispatchMessage {
public init(a) {}
func printMethodName(a) -> String {
let name = getMethodName()
return name
}
@objc dynamic public func getMethodName(a) -> String {
let name = "SwiftMethodDispatchMessage"
print("Method name: \(name)")
return name
}
}
Copy the code
Swiftc-emit – Silgen -o xxx.swift, excerpt below.
Static distributed
// SwiftMethodDispatchStatic.printMethodName() sil hidden [ossa] @$s25SwiftMethodDispatchStaticAAC05printB4NameSSyF : $@convention(method) (@guaranteed SwiftMethodDispatchStatic) -> @owned String { // %0 "self" // users: %3, %1 bb0(%0 : @guaranteed $SwiftMethodDispatchStatic): debug_value %0 : $SwiftMethodDispatchStatic, let, name "self", argno 1 // id: %1 // function_ref SwiftMethodDispatchStatic.getMethodName() %2 = function_ref @$s25SwiftMethodDispatchStaticAAC03getB4NameSSyF : $@convention(method) (@guaranteed SwiftMethodDispatchStatic) -> @owned String // user: %3 %3 = apply %2(%0) : $@convention(method) (@guaranteed SwiftMethodDispatchStatic) -> @owned String // users: %8, %5, %4 debug_value %3 : $String, let, name "name" // id: %4 %5 = begin_borrow %3 : $String // users: %7, %6 %6 = copy_value %5 : $String // user: %9 end_borrow %5 : $String // id: %7 destroy_value %3 : $String // id: %8 return %6 : $String // id: %9 } // end sil function '$s25SwiftMethodDispatchStaticAAC05printB4NameSSyF' sil_vtable [serialized] SwiftMethodDispatchStatic { #SwiftMethodDispatchStatic.init! allocator: (SwiftMethodDispatchStatic.Type) -> () -> SwiftMethodDispatchStatic : @$s25SwiftMethodDispatchStaticAACABycfC // SwiftMethodDispatchStatic.__allocating_init() #SwiftMethodDispatchStatic.deinit! deallocator: @$s25SwiftMethodDispatchStaticAACfD // SwiftMethodDispatchStatic.__deallocating_deinit }Copy the code
Focus on these two lines in the SIL above:
// function_ref SwiftMethodDispatchStatic.getMethodName()
%2 = function_ref @$s25SwiftMethodDispatchStaticAAC03getB4NameSSyF : $@convention(method) (@guaranteed SwiftMethodDispatchStatic) -> @owned String // user: %3
Copy the code
Function_ref keyword indicates that getMethodName method is invoked by method Pointers, and through the symbol s25SwiftMethodDispatchStaticAAC03getB4NameSSyF method to locate the address, Sil_vtable also does not contain this method.
Vtable dispatch
// SwiftMethodDispatchTable.printMethodName() sil hidden [ossa] @$s24SwiftMethodDispatchTableAAC05printB4NameSSyF : $@convention(method) (@guaranteed SwiftMethodDispatchTable) -> @owned String { // %0 "self" // users: %3, %2, %1 bb0(%0 : @guaranteed $SwiftMethodDispatchTable): debug_value %0 : $SwiftMethodDispatchTable, let, name "self", argno 1 // id: %1 %2 = class_method %0 : $SwiftMethodDispatchTable, #SwiftMethodDispatchTable.getMethodName : (SwiftMethodDispatchTable) -> () -> String, $@convention(method) (@guaranteed SwiftMethodDispatchTable) -> @owned String // user: %3 %3 = apply %2(%0) : $@convention(method) (@guaranteed SwiftMethodDispatchTable) -> @owned String // users: %8, %5, %4 debug_value %3 : $String, let, name "name" // id: %4 %5 = begin_borrow %3 : $String // users: %7, %6 %6 = copy_value %5 : $String // user: %9 end_borrow %5 : $String // id: %7 destroy_value %3 : $String // id: %8 return %6 : $String // id: %9 } // end sil function '$s24SwiftMethodDispatchTableAAC05printB4NameSSyF' sil_vtable [serialized] SwiftMethodDispatchTable { #SwiftMethodDispatchTable.init! allocator: (SwiftMethodDispatchTable.Type) -> () -> SwiftMethodDispatchTable : @$s24SwiftMethodDispatchTableAACABycfC // SwiftMethodDispatchTable.__allocating_init() #SwiftMethodDispatchTable.printMethodName: (SwiftMethodDispatchTable) -> () -> String : @$s24SwiftMethodDispatchTableAAC05printB4NameSSyF // SwiftMethodDispatchTable.printMethodName() #SwiftMethodDispatchTable.getMethodName: (SwiftMethodDispatchTable) -> () -> String : @$s24SwiftMethodDispatchTableAAC03getB4NameSSyF // SwiftMethodDispatchTable.getMethodName() #SwiftMethodDispatchTable.deinit! deallocator: @$s24SwiftMethodDispatchTableAACfD // SwiftMethodDispatchTable.__deallocating_deinit }Copy the code
In vtable mode, the method reference becomes:
%2 = class_method %0 : $SwiftMethodDispatchTable, #SwiftMethodDispatchTable.getMethodName : (SwiftMethodDispatchTable) -> () -> String, $@convention(method) (@guaranteed SwiftMethodDispatchTable) -> @owned String // user: %3
Copy the code
The class_method keyword here indicates how getMethodName uses the methods of the class object, that is, the table of functions, and this can be verified by the information in the sil_vtable, The vTable table of the SwiftMethodDispatchTable class contains this method.
Messages distributed
// SwiftMethodDispatchMessage.printMethodName() sil hidden [ossa] @$s26SwiftMethodDispatchMessageAAC05printB4NameSSyF : $@convention(method) (@guaranteed SwiftMethodDispatchMessage) -> @owned String { // %0 "self" // users: %3, %2, %1 bb0(%0 : @guaranteed $SwiftMethodDispatchMessage): debug_value %0 : $SwiftMethodDispatchMessage, let, name "self", argno 1 // id: %1 %2 = objc_method %0 : $SwiftMethodDispatchMessage, #SwiftMethodDispatchMessage.getMethodName! foreign : (SwiftMethodDispatchMessage) -> () -> String, $@convention(objc_method) (SwiftMethodDispatchMessage) -> @autoreleased NSString // user: %3 %3 = apply %2(%0) : $@convention(objc_method) (SwiftMethodDispatchMessage) -> @autoreleased NSString // user: %5 // function_ref static String._unconditionallyBridgeFromObjectiveC(_:) %4 = function_ref @$sSS10FoundationE36_unconditionallyBridgeFromObjectiveCySSSo8NSStringCSgFZ : $@convention(method) (@guaranteed Optional<NSString>, @thin String.Type) -> @owned String // user: %7 %5 = enum $Optional<NSString>, #Optional.some! enumelt, %3 : $NSString // users: %9, %7 %6 = metatype $@thin String.Type // user: %7 %7 = apply %4(%5, %6) : $@convention(method) (@guaranteed Optional<NSString>, @thin String.Type) -> @owned String // users: %13, %10, %8 debug_value %7 : $String, let, name "name" // id: %8 destroy_value %5 : $Optional<NSString> // id: %9 %10 = begin_borrow %7 : $String // users: %12, %11 %11 = copy_value %10 : $String // user: %14 end_borrow %10 : $String // id: %12 destroy_value %7 : $String // id: %13 return %11 : $String // id: %14 } // end sil function '$s26SwiftMethodDispatchMessageAAC05printB4NameSSyF' sil_vtable [serialized] SwiftMethodDispatchMessage { #SwiftMethodDispatchMessage.init! allocator: (SwiftMethodDispatchMessage.Type) -> () -> SwiftMethodDispatchMessage : @$s26SwiftMethodDispatchMessageAACABycfC // SwiftMethodDispatchMessage.__allocating_init() #SwiftMethodDispatchMessage.printMethodName: (SwiftMethodDispatchMessage) -> () -> String : @$s26SwiftMethodDispatchMessageAAC05printB4NameSSyF // SwiftMethodDispatchMessage.printMethodName() #SwiftMethodDispatchMessage.deinit! deallocator: @$s26SwiftMethodDispatchMessageAACfD // SwiftMethodDispatchMessage.__deallocating_deinit }Copy the code
In messaging, the reference becomes:
%2 = objc_method %0 : $SwiftMethodDispatchMessage, #SwiftMethodDispatchMessage.getMethodName! foreign : (SwiftMethodDispatchMessage) -> () -> String, $@convention(objc_method) (SwiftMethodDispatchMessage) -> @autoreleased NSString // user: %3Copy the code
The objc_method keyword indicates that the method has been switched to using method dispatch in OC, i.e., message dispatch, and that the return type has changed to NSString in the method signature, and that the getMethodName method has been removed from the Vtable.
From SIL’s point of view, the way methods are distributed in Swift makes sense.
assembly
Let’s look at the differences between these distributions at the assembly level.
Before we start, let’s prepare some sample code. In the first App project, we added several methods mentioned above:
@IBAction func StaticDispatch(_ sender: Any) {
_ = SwiftMethodDispatchStatic().getMethodName()
}
@IBAction func VTableDispatch(_ sender: Any) {
_ = SwiftMethodDispatchTable().getMethodName()
}
@IBAction func MessageDispatch(_ sender: Any) {
_ = SwiftMethodDispatchMessage().getMethodName()
}
Copy the code
We make a breakpoint at the call and Debug in assembly mode (select Debug Workflow from the Debug menu, check Always Show Disassembly) :
Let’s take a look at the differences in assembly code in different distributions
Static distributed
Here is the assembly code for the StaticDispatch method:
After the instruction bl 0x1047414cc on line 17, there is a comment: Symbol stub for… getMethodName… So let’s see what 0x1047414c actually points to.
The BL instruction jumps to the specified subroutine name to execute the code
The Symbol stub represents the symbol placeholder for the code; the actual code is repositioned based on the placeholder symbol.
Run the following command:
image lookup --address 0x1047414cc
Copy the code
The results are as follows:
Address: SwiftMethodDispatchAppDemo[0x00000001000054cc] (SwiftMethodDispatchAppDemo.__TEXT.__stubs + 36)
Summary: SwiftMethodDispatchAppDemo`symbol stub for: SwiftMethodDispatch.SwiftMethodDispatchStatic.getMethodName() -> Swift.String
Copy the code
Here we can see that the address 0x1047414cc corresponds to the offset address 0x00000001000054cc, which we look for in the __TEXT,__stubs section of MachO file.
We use the MachOView tool to view the binary information of the app
Download this project to compile and run:
Github.com/emptyglass1…
Select * from MachOView where __TEXT,__stubs section = 000054cc;
The corresponding value is a string of symbols: _ $s19SwiftMethodDispatch0abC6StaticC03getB4NameSSyF, after demangle transformation of value is:
SwiftMethodDispatch.SwiftMethodDispatchStatic.getMethodName() -> Swift.String
This is consistent with the image Lookup command, because SwiftMethodDispatch is a dynamic library, and the symbols above need to be relocated in the dynamic library. We will open SwiftMethodDispatch again in MachOView.
In the Symbol Table search Symbol: _ $s19SwiftMethodDispatch0abC6StaticC03getB4NameSSyF, corresponding address code migration is the Symbol: 32 f4
Continue to look for the 32F4 address in the __TEXT, __TEXT section, starting with the line marked in the image below: getMethodName
Debugging App in Xcode assembly code can be compared to the code is consistent:
As you can see from the above lookup process, the Swift method uses the method’s memory address almost directly when using static distribution (since it is an external symbol, symbol relocation is required by the dynamic library). If the statically distributed method is stored in the binary file of the App, the address of the call is the memory entry address of the method, without any conversion (self-validation).
Vtable dispatch
Let’s take a look at the vtable distribution and similarly look at assembly code through breakpoints:
Step stop at line 16, check the value of register x8, and run the following command in Xcode’s debug area:
register read x8
The results are as follows:
x8 = 0x0000000100c782a0 type metadata for SwiftMethodDispatch.SwiftMethodDispatchTable
Copy the code
LDR x8, [x8, #0x60] this command reads the memory address of x8 + 0x60 (4 bytes) into the X8 register.
X8 + 0x60 = 0x0000000100C78300
image lookup --address 0x0000000100c78300
Address: SwiftMethodDispatch[0x0000000000008300] (SwiftMethodDispatch.__DATA.__data + 160)
Summary: type metadata for SwiftMethodDispatch.SwiftMethodDispatchTable + 96
Copy the code
The metadata stored here is the SwiftMethodDispatchTable class metadata. The internal structure of the metadata remains to be explored, but it is known that the methods defined by the object are included.
Then use MachOView to check the binary file information of SwiftMethodDispatch and locate the contents of the offset address 0x8300:
You can see that the value 0x8300 refers to another address: 2CCC.
Reposition the address 0x2CCC, which is the entry address of a function:
We continue to debug in the Xcode to 19 lines, and then enter the call stack, you can see into the SwiftMethodDispatchTable. GetMethodName () method, and the binary see above assembly code is consistent, The offset address is also matched by the image lookup:
image lookup --address 0x100c72ccc
Address: SwiftMethodDispatch[0x0000000000002ccc] (SwiftMethodDispatch.__TEXT.__text + 152)
Summary: SwiftMethodDispatch`SwiftMethodDispatch.SwiftMethodDispatchTable.getMethodName() -> Swift.String at SwiftMethodDispatchTable.swift:18
Copy the code
You’ve successfully called the getMethodName method at this point.
As you can see from the above procedure, the method is called with the offset address (0x60) of the class metadata data, based on which the actual entry address of the method can be located. The swiftModule interface file provided by the Swift dynamic library is sufficient to locate the offset addresses of methods in metadata at compile time.
Messages distributed
Finally, let’s look at the assembly code for message distribution:
For a bit more code this time, we step at line 16 to see and calculate the address x8 + 0xb80 points to:
(lldb) register read x8
x8 = 0x0000000100bb0000 (void *)0x00000001020acb98: ObjectiveC._convertBoolToObjCBool(Swift.Bool) -> ObjectiveC.ObjCBool
(lldb) image lookup --address 100bb0b80
Address: SwiftMethodDispatchAppDemo[0x000000010000cb80] (SwiftMethodDispatchAppDemo.__DATA.__objc_selrefs + 128)
Summary: "getMethodName"
Copy the code
Look in the binary file of App according to the offset address cb80:
This address holds a pointer to 0x5A9C.
0x5A9C points to a string getMethodName. The __TEXT and __objC_methName sections are the SEL names of the OC methods.
Run to line 17 in Xcode and read the contents of the X8 register. You can see that the result is getMethodName:
In Xcode you can see that the objc_msgSend method is called at line 19 and we start debugging the method:
Stop at line 16 and check the value of register X17:
register read x17
x17 = 0x0000000100c7387c SwiftMethodDispatch`@objc SwiftMethodDispatch.SwiftMethodDispatchMessage.getMethodName() -> Swift.String at <compiler-generated>
Copy the code
This address is the entry address of the OC method for getMethodName:
image lookup --address 0x0000000100c7387c
Address: SwiftMethodDispatch[0x000000000000387c] (SwiftMethodDispatch.__TEXT.__text + 3144) Summary: SwiftMethodDispatch`@objc SwiftMethodDispatch.SwiftMethodDispatchMessage.getMethodName() -> Swift.String at
Copy the code
It can be viewed in MachO of the dynamic library:
GetMethodName should have two methods, one for OC and one for Swift, and two addresses in memory:
@objc SwiftMethodDispatch.SwiftMethodDispatchMessage.getMethodName() -> Swift.String
SwiftMethodDispatch.SwiftMethodDispatchMessage.getMethodName() -> Swift.String
Copy the code
When Xcode enters the br x17 instruction on line 16, you can see that you have entered the getMethodName method (OC version) :
This confirms that the call to the getMethodName method has been converted to a message sent to getMethodName, following OC’s message sending logic (the OC compatible version generated by Swift compilation), Take a look at the assembly parsing of objc_msgSend.
As you can see from the above procedure, in Swift if a method is marked to be executed by sending a message, the SEL of the method is stored in the binary __TEXT,__objc_methname section. The SEL is used to find the corresponding method entry at call time.
Issue review
Now let’s go back to the question of whether the Swift method can be called without modifying the interface information after changing its name.
According to the characteristics distributed by Swift method, the getMethodName method in the problem uses function table distribution. Since the interface is not changed, its offset address is unchanged, and the compilation can pass normally when App runs. At runtime, the offset address of the class metadata is directly located to the entry address of the method, without involving the relocation of the new method name, so the renamed method can be successfully executed.
But if you make a little change and add another method at the top of the getMethodName source code, the offset address is changed, and the runtime executes the newly added method. If the method’s parameter type does not match the return value, an error is reported, and if it does, it can still execute.
The Demo project
The Demo project for this article is at Github.
Illustration: unsplash.com/photos/tKs_…