Execution jumps into the trampoline and then immediately jumps out, or bounces, hence the term trampoline.
Trampolines are similar to a “jump table”.
SwiftTrace replaces these function pointers with a pointer to a unique assembly language “trampoline” entry point which has destination function and data pointers associated with it.
The principle behind SwiftTrace is to use Trampoline technology to archive the effect of tracing some Swift functions.
The technology of using Trampoline is also available in the iOS system library.
The above picture can briefly summarize the technology of using Trampoline.
Forward the corresponding function to Trampolines, and then execute the corresponding logic through the intermediate code.
We need to solve the problems before using Trampolines.
- Get the representation of Swift Class in memory.
- Replace the Swift function.
- Construct Trampolines.
Get the representatiachion of Swift Class in memory
We can refer to Swift Metadata.h. Here directly copied the definition in SwiftTrace project. In this way, we can get the representation of Swift Class in memory, and modify the function table in it to achieve the purpose of replacing the functions.
private struct TargetClassMetadata {
let MetaClass: uintptr_t = 0, SuperClass: uintptr_t = 0
let CacheData1: uintptr_t = 0, CacheData2: uintptr_t = 0
let Data: uintptr_t = 0
let Flags: UInt32 = 0
let InstanceAddressPoint: UInt32 = 0
let InstanceSize: UInt32 = 0
let InstanceAlignMask: UInt16 = 0
let Reserved: UInt16 = 0
let ClassSize: UInt32 = 0
var ClassAddressPoint: UInt32 = 0
let Description: uintptr_t = 0
var IVarDestroyer: SIMP? = nil
}
Replace Swift functions
Note that not all Swift functions can be replaced.
We can get a pointer to each function in Swift’s function table through the following function.
typealias SIMP = @convention(c) () -> Void
class Pandora {
class func openBox(_ ClassType: AnyClass, _ handler: (UnsafeMutablePointer<SIMP>) -> Void) {
let meta: UnsafeMutablePointer<TargetClassMetadata> = autoBitCast(ClassType)
let vs = address(of: &meta.pointee.IVarDestroyer)
let ve = address(of: &meta.pointee).advanced(by: Int(meta.pointee.ClassSize - meta.pointee.ClassAddressPoint))
let c = (ve - vs) / MemoryLayout<SIMP?>.size
let fs = UnsafeMutablePointer(mutating: vs.assumingMemoryBound(to: SIMP?.self))
for i in 0..<c {
if let _: IMP = autoBitCast(fs[i]) {
handler(&fs[i]!)
}
}
}
}
Then replace the function address in the upper layer. For example, the following code replaces the bar
function in ExampleClass
.
class ExampleClass {
func bar() {
print("ORIGIN BAR")
}
}
Pandora.openBox(ExampleClass.self) {
var info = Dl_info()
if dladdr(UnsafePointer(autoBitCast($0.pointee)), &info) != 0 {
if let fname = demangle(symbol: info.dli_sname) {
if fname == "TrampolineSwift.ExampleClass.bar() -> ()" {
swizzFunc(ofp: $0) {
print("Trace Bar")
}
}
}
}
}
Finally, forward the Swift function to Trampolines.
private var MEMO: [(SIMP, SIMP?)] = [] // Only 4 place
func swizzFunc(ofp: UnsafeMutablePointer<SIMP>, nf: SIMP?) {
MEMO.append((ofp.pointee, nf))
ofp.pointee = autoBitCast(trampolineStartAddress() + cursor * trampolineSize)
cursor += 1
}
Construct Trampolines.
The following code is written in assembly, where _trampolineStart
is the entry address of Trampolines
. _trampolineImpl
is an intermediate processing code segment. The logic is very simple, that is, the value of the rsp
register is used as the first parameter of _swiftCustomTrampolineHandler
, which is passed through rdi
. (The value of rsp
is required to trace back who called _trampolineImpl
)
.globl _trampolineStart
.p2align 4
_trampolineStart:
call _trampolineImpl
call _trampolineImpl
call _trampolineImpl
call _trampolineImpl
.globl _trampolineImpl
.p2align 4
_trampolineImpl:
subq $0x8, %rsp
movq 0x08(%rsp), %rdi
pushq %rbp
movq %rsp, %rbp
call _swiftCustomTrampolineHandler
popq %rbp
addq $0x8, %rsp
movq %rax, (%rsp)
ret
After constructing Trampolines, you can implement the swiftCustomTrampolineHandler
function. swiftCustomTrampolineHandler
gets the address of the replaced function by calculating the value of rsp
distance _trampolineStart
as an offset. (That is, the backtracking mentioned above)
private var MEMO: [(SIMP, SIMP?)] = [] // Only 4 place
@_silgen_name("swiftCustomTrampolineHandler")
func swiftCustomTrampolineHandler(address: Int) -> SIMP {
let offset: Int = -autoBitCast(trampolineStartAddress() - address) / trampolineSize - 1
if let nf = MEMO[offset].1 {
nf()
}
return MEMO[offset].0
}
In this way, We can forward Swift functions to trampolines
, and then dispatch them uniformly through swiftCustomTrampolineHandler
. Finally, swiftCustomTrampolineHandler
returns the original function address to execute the logic of the original function.
Reference
- https://en.wikipedia.org/wiki/Trampoline_(computing)
- https://software.intel.com/sites/default/files/article/402129/mpx-linux64-abi.pdf
- https://github.com/johnno1962/SwiftTrace/blob/master/SwiftTrace/xt_forwarding_trampoline_x64.s
- https://en.wikipedia.org/wiki/X86_calling_conventions
- https://github.com/apple/swift/blob/master/docs/StandardLibraryProgrammersManual.md
- https://www.rightpoint.com/rplabs/switch-method-dispatch-table