Talk about the implementation of function Pointers in Swift
- 2020-05-09 19:22:55
- OfStack
What doesn't Swift have?
The only wall apple engineers built for me was that there was no way to get a pointer to a function in Swift:
Note that the C function pointer is not imported into Swift (from "Using Swift with Cocoa and Objective-C")
But how do we know the location of the hook and where to jump in this case? Let's take a closer look at 1 and see what Swift's func is at the bytecode level.
When you pass a generic parameter to a function, Swift does not pass its address directly, but a pointer to the trampoline function (see below) with some function metadata information. And trampoline itself is part 1 of the structure that wraps the original function.
What does this mean ?
Let's use it as an example:
func call_function(f : () -> Int) {
let b = f()
}
func someFunction() -> Int {
return 0
}
In Swift we just say call_function(someFunction).
But when the Swift compiler processes the code, the performance is much better than calling call_function(&someFunction)
struct swift_func_wrapper *wrapper = ... /* configure wrapper for someFunction() */
struct swift_func_type_metadata *type_metadata = ... /* information about function's arguments and return type */
call_function(wrapper->trampoline, type_metadata);
The structure of one wrapper is as follows:
struct swift_func_wrapper {
uint64_t **trampoline_ptr_ptr; // = &trampoline_ptr
uint64_t *trampoline_ptr;
struct swift_func_object *object;
}
What is swift_func_object To create the object, Swift real time USES a global constant called metadata[N] (each function call is unique to 1, so your func is a generic argument, so for the following code:
func callf(f: () -> ()) {
f();
}
callf(someFunction);
callf(someFunction);
Constants metadata and metadata2 will be created).
The structure of an metadata[N] is a bit like this:
struct metadata {
uint64_t *destructor_func;
uint64_t *unknown0;
const char type:1; // I'm not sure about this and padding,
char padding[7]; // maybe it's just a uint64_t too...
uint64_t *self;
}
Initially metadataN had only two field collections: destructor_func and type. The former is a function pointer that allocates memory as an object created using swift_allocObject(). The latter is the object type recognizer (function or method 0x40 or '@') and is (in some form) used by swift_allocObject() to create a correct object for us func:
swift_allocObject(&metadata2-
>
type, 0x20, 0x7);
Once the func object is created, it has the following structure:
struct swift_func_object {
uint64_t *original_type_ptr;
uint64_t *unknown0;
uint64_t function_address;
uint64_t *self;
}
The first field is a pointer that corresponds to metadata[N]- > The value of type, the second field appears to be 0x4 | 1 < < 24(0x100000004) and implies some possibility (I don't know what). function_address is where we are actually interested in hooking, and self is (immediately) its own pointer (if our object represents a normal function, this field is NULL).
Ok, so what if I start with the frame? In fact, I don't understand why Swift runtime needs them, but anyway, this is what they look like in their raw form:
void* someFunction_Trampoline(void *unknown, void *arg, struct swift_func_object *desc)
{
void* target_function = (void *)desc->function_address;
uint64_t *self = desc->self;
swift_retain_noresult(desc->self); // yeah, retaining self is cool!
swift_release(desc);
_swift_Trampoline(unknown, arg, target_function, self);
return unknown;
}
void *_swift_Trampoline(void *unknown, void *arg, void *target_function, void *self)
{
target_function(arg, self);
return unknown;
}
Let's create it
Imagine 1, you have these functions in your Swift code:
func takesFunc<T>(f : T) {
...
}
func someFunction() {
...
}
And you want to generate them like this:
takesFunc(someFunction)
This 1 line of code translates into a fairly large C program:
struct swift_func_wrapper *wrapper = malloc(sizeof(*wrapper));
wrapper->trampoline_ptr = &someFunction_Trampoline;
wrapper->trampoline_ptr_ptr = &(wrapper.trampoline);
wrapper->object = ({
// let's say the metadata for this function is `metadata2`
struct swift_func_object *object = swift_allocObject(&metadata2->type, 0x20, 0x7);
object->function_address = &someFunction;
object->self = NULL;
object;
});
// global constant for the type of someFunction's arguments
const void *arg_type = &kSomeFunctionArgumentsTypeDescription;
// global constant for the return type of someFunction
const void *return_type = &kSomeFunctionReturnTypeDescription;
struct swift_func_type_metadata *type_metadata = swift_getFunctionTypeMetadata(arg_type, return_type);
takesFunc(wrapper->trampoline_ptr, type_metadata);
The structure "swift_func_type_metadata" is very opaque, so I don't have much to say.
Back to function pointer
Now that we know how a function is represented as a generic type parameter, let's use this to get to your goal: get a pointer to a real function!
All we need to do is note that we already have an trampoline_ptr pointer field address passed as the first parameter, so the offset of the object field is only 0x8. Everything else is easy to combine:
uint64_t _rd_get_func_impl(void *trampoline_ptr)
{
struct swift_func_object *obj = (struct swift_func_object *)*(uint64_t *)(trampoline_ptr + 0x8);
return obj->function_address;
}
Looks like it's time to write
rd_route(
_rd_get_func_impl(firstFunction),
_rd_get_func_impl(secondFunction),
nil
)
But how do we call these C functions from Swift?
To do this, we will use Swift's non-public property: the @asmname property that allows us to provide the C function with an Swift interface. It can be used as follows:
@asmname("_rd_get_func_impl")
func rd_get_func_impl<Q>(Q) -> UInt64;
@asmname("rd_route")
func rd_route(UInt64, UInt64, CMutablePointer<UInt64>) -> CInt;
This is the 1 cut we need to use rd_route() in Swift.
But it can't handle any functions!
That is, you can't hook any function with a generic argument with rd_route(this could be bug for Swift or not, I haven't figured it out yet). But you can easily override them with extensions, specifying the type of parameter directly:
class DemoClass {
class func template <T : CVarArg>(arg : T, _ num: Int) -> String {
return "\(arg) and \(num)";
}
}
DemoClass.template("Test", 5) // "Test and 5"
extension DemoClass {
class func template(arg : String, _ num: Int) -> String {
return "{String}";
}
class func template(arg : Int, _ num: Int) -> String {
return "{Int}";
}
}
-- Your extension's methods for String and Int will be preferred over the original ones */
DemoClass.template("Test", 5) -- "{String}"
DemoClass.template(42, 5) -- "{Int}"
-- But for other types `template(T, Int)` will be used
DemoClass.template(["Array", "Item"], 5) --- "[Array, Item] and 5"
SWRoute
To make it easy to hook functions in Swift, I created a wrapper called SWRoute -- it's just a small class and an C function we wrote about earlier:
_rd_get_func_impl() :
class SwiftRoute {
class func replace<MethodT>(function targetMethod : MethodT, with replacement : MethodT) -> Int
{
return Int(rd_route(rd_get_func_impl(targetMethod), rd_get_func_impl(replacement), nil));
}
}
Note that we do type checking for free because Swift requires target methods and substitutions with the same MethoT type.
And since we can't use a replicated original implementation, I can only pass nil as another argument to the function rd_route(). If you have any ideas on how to integrate this pointer into the Swift code, please let me know!
You can find many instances of SWRoute in the repository.
That's all.