调用具有可变参数的方法并转发消息 - Bad Access

Lom*_*baX 5 objective-c variadic-functions objective-c-runtime swizzling ios

我正在实现一个"Code Injector Class",通过方法调配可以让你做到这样的事情:

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

aSelector可以是具有可变数量的参数和变量返回类型的方法.参数/和返回类型可以是对象或基本类型.

首先,我附上代码,injectCodeBeforeSelector:让你了解我在做什么(我删除了代码的有趣部分):

- (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);
}
Run Code Online (Sandbox Code Playgroud)

基本上,我使用class_addMethod将伪/ swizzle方法添加到目标类,然后进行调整.该方法的实现设置为这样的函数:

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);
}
Run Code Online (Sandbox Code Playgroud)

现在问题是:我在最后一行有一个EXC_BAD_INSTRUCTION(objc_msgSend_corrupt_cache_error()).如果我将va_list参数转发给假选择器,则会出现问题.如果我将最后一行更改为

return objc_msgSend(self, cmd, arguments);
Run Code Online (Sandbox Code Playgroud)

没有错误,但显然无限递归开始.

我试过:

  • 使用va_copy
  • 在发送消息之前删除混合

但没有结果.我认为问题与这个事实有关:va_list不是一个简单的指针,它可以类似于相对于方法的堆栈地址的偏移量.所以,我不能用另一个函数的arg列表调用一个函数的objc_msgsend(swizzled函数)(非swizzled函数).

我试图改变方法并复制NSInvocation中的所有参数,但是我在管理调用的返回值时遇到了其他问题,甚至逐个复制参数(管理所有不同类型)需要大量代码,所以我更喜欢返回这种方法,对我来说似乎更清洁(imho)

你有什么建议吗?谢谢

Mac*_*ade 3

这里的主要问题是如何将变量参数传递给函数。

通常,它们在堆栈上传递,但据我所知,ARM ABI 的情况并非如此,至少在可能的情况下使用寄存器。

所以你这里有两个问题。
首先,编译器在执行您自己的方法的代码时可能会弄乱这些寄存器。
我对此不太确定,因为我对 ARM ABI 不太了解,所以你应该检查参考资料。

第二个问题,更重要的是,您实际上将单个变量参数传递给obj_msgSend(the va_list)。因此,目标方法显然不会收到它所期望的结果。

想象一下以下情况:

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

void foo( void )
{
    bar( 1, 2, 3, 4 );
}
Run Code Online (Sandbox Code Playgroud)

在 ARM 上,这对于函数来说意味着foo

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
Run Code Online (Sandbox Code Playgroud)

变量参数传入R1R2R3,以及int中的参数R0

因此,在您的情况下,作为objc_msdSend用于调用您的方法的调用,R0应该是目标对象的指针、R1选择器的指针,并且变量参数应该从 开始R2

当您向 发出自己的调用时objc_msdSend,您至少会R2用您的 . 覆盖 ,的内容va_list

您应该尽量不关心变量参数。幸运的是,如果调用之前的代码objc_msgSend(获得最终选择器的地方)没有弄乱这些寄存器,那么正确的值应该仍然存在,使它们可用于目标方法。

当然,这只适用于真实设备,而不适用于模拟器(模拟器是 x86,因此这里的可变参数在堆栈上传递)。