Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Swizzling a method with variable arguments and forward the message - Bad Access

I'm implementing a "Code Injector Class", that through method swizzling can give you the possibility to do something like this:

FLCodeInjector *injector = [FLCodeInjector injectorForClass:[self class]];
[injector injectCodeBeforeSelector:@selector(aSelector:) code:^{
    NSLog(@"This code should be injected");
}];

aSelector can be a method with variable number of arguments, and variable return type. Arguments / and return type can be objects or primitive type.

First, I attach the code of injectCodeBeforeSelector: to let you understand what I'm doing (I removed not interesting parts of the code):

- (void)injectCodeBeforeSelector:(SEL)method code:(void (^)())completionBlock
{

    NSString *selector = NSStringFromSelector(method);

    [self.dictionaryOfBlocks setObject:completionBlock forKey:selector];

    NSString *swizzleSelector = [NSString stringWithFormat:@"SWZ%@", selector];

    // add a new method to the swizzled class
    Method origMethod = class_getInstanceMethod(self.mainClass, NSSelectorFromString(selector));
    const char *encoding = method_getTypeEncoding(origMethod);

    [self addSelector:NSSelectorFromString(swizzleSelector) toClass:self.mainClass methodTypeEncoding:encoding];
    SwizzleMe(self.mainClass, NSSelectorFromString(selector), NSSelectorFromString(swizzleSelector));

}

-(void)addSelector:(SEL)selector toClass:(Class)aClass methodTypeEncoding:(const char *)encoding
{
    class_addMethod(aClass,
                    selector,
                    (IMP)genericFunction, encoding);
}

Basically, I use class_addMethod to add the fake/swizzle method to the destination class, then do the swizzle. The implementation of the method is set to a function like this:

id genericFunction(id self, SEL cmd, ...) {
    // call the block to inject
    ...
    // now forward the message to the right method, since method are swizzled
    // I need to forward to the "fake" selector SWZxxx

    NSString *actualSelector = NSStringFromSelector(cmd);
    NSString *newSelector = [NSString stringWithFormat:@"SWZ%@", actualSelector];
    SEL finalSelector = NSSelectorFromString(newSelector);

    // forward the argument list
    va_list arguments;
    va_start ( arguments, cmd );

    return objc_msgSend(self, finalSelector, arguments);
}

now the problem: I have an EXC_BAD_INSTRUCTION ( objc_msgSend_corrupt_cache_error ()) on the last line. The problem happens if i forward the va_list arguments to the fake selector. If I change the last line to

return objc_msgSend(self, cmd, arguments);

no error, but obviously an infinite recursion starts.

I've tried to:

  • use va_copy
  • remove the swizzle before sending the message

but no results. I think that the problem is related to this fact: va_list is not a simple pointer, it can be something similar to an offset relative to the stack address of the method. So, I can't call objc_msgsend of a function (the swizzled function) with the arg list of another function (the non-swizzled one).

I tried to change approach and copy all arguments in an NSInvocation, but I had other problems managing the return value of the invocation, and even copy arguments one by one (managing all different types) requires a lot of code, so I preferred to return to this approach, that seems cleaner to me (imho)

Do you have any suggestion? Thanks

like image 726
LombaX Avatar asked Oct 03 '22 07:10

LombaX


1 Answers

The main problem here is how the variable arguments are passed to the function.

Usually, they are passed on the stack, but as far as I know, it's not the case with the ARM ABI, where registers are used, at least when possible.

So you've got two issues here.
First, the compiler may mess up those registers, while executing the code of your own method.
I'm not sure about this, as I don't know much about the ARM ABI, so you should check in the reference.

Second issue, more important, you are actually passing a single variable argument to obj_msgSend (the va_list). So the target method won't receive what it expects, obviously.

Imagine the following:

void bar( int x, ... )
{}

void foo( void )
{
    bar( 1, 2, 3, 4 );
}

On ARM, this will mean, for the foo function:

movs    r0, #1
movt    r0, #0
movs    r1, #2
movt    r1, #0
movs    r2, #3
movt    r2, #0
movs    r3, #4
movt    r3, #0
bl      _bar

Variables arguments are passed in R1, R2 and R3, and the int argument in R0.

So in your case, as a call to objc_msdSend was used to call your method, R0 should be the target object's pointer, R1 the selector's pointer, and variable arguments should begin on R2.

And when issuing your own call to objc_msdSend, you're at least overwriting the content of R2, with your va_list.

You should try not to care about the variable arguments. With a little luck, if the code previous to the objc_msgSend call (where you get the final selector) doesn't mess up those registers, the correct values should still be there, making them available for the target method.

This would of course only work on the real device, and not in the simulator (simulator is x86, so variadic parameters are here passed on the stack).

like image 187
Macmade Avatar answered Oct 07 '22 19:10

Macmade