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.


Related articles: