Swift & Trampolines

visitor
4 min readMay 14, 2020

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

--

--