使用NSInvocation时,在arm64上出现EXC_BAD_ACCESS崩溃

3

我已经开始准备一个旧项目以支持arm64架构。但是当我尝试在64位设备上执行此代码时,我会在[invocation retainArguments];行上遇到EXC_BAD_ACCESS崩溃。

- (void)makeObjectsPerformSelector: (SEL)selector withArguments: (void*)arg1, ...
{

    va_list argList;

    NSArray* currObjects = [NSArray arrayWithArray: self];
    for (id object in currObjects)
    {
        if ([object respondsToSelector: selector])
        {
            NSMethodSignature* signature = [[object class] instanceMethodSignatureForSelector: selector];

            NSInvocation* invocation = [NSInvocation invocationWithMethodSignature: signature];
            invocation.selector = selector;
            invocation.target = object;

            if (arg1 != nil)
            {
                va_start(argList, arg1);

                char* arg = arg1;

                for (int i = 2; i < signature.numberOfArguments; i++)
                {
                    const char* type = [signature getArgumentTypeAtIndex: i];
                    NSUInteger size, align;
                    NSGetSizeAndAlignment(type, &size, &align);
                    NSUInteger mod = (NSUInteger) arg % align;

                    if (mod != 0)
                        arg += (align - mod);

                    [invocation setArgument: arg
                                    atIndex: i];

                    arg = (i == 2) ? (char*) argList : (arg + size);
                }

                va_end(argList);
            }

            [invocation retainArguments];
            [invocation invoke];
        }
    }
}

看起来这是参数方面的问题。


你能提供调用那段代码并导致崩溃的代码示例吗? - Nikita Ivaniushchenko
给定的代码是NSArray类别,为数组中的每个对象提供执行具有多个参数的选择器的能力。 数组中的每个对象都是侦听器(委托),因为“多个侦听器”设计模式需要。 例如-在从服务器收到响应后,我们应该使每个侦听器执行选择器。 调用位于服务器成功回调中,看起来像- [self.listeners makeObjectsPerformSelector: @selector(serverManager:didLikeVideo:withError:) withArguments: self, operation.video, operation.error, nil]; - abagmut
所以,请不要进行不安全的类型转换,请检查我的更新答案。我不明白为什么你要用复杂的技巧来定位内存中的参数。 - Nikita Ivaniushchenko
5个回答

5
这是针对相同目的的内容。
+ (void)callSelectorWithVarArgs:(SEL)selector onTarget:(id)target onThread:(id)thread wait:(BOOL)wait, ...
{
    NSMethodSignature *aSignature = [[target class] instanceMethodSignatureForSelector:selector];

    if (aSignature)
    {
        NSInvocation *anInvocation = [NSInvocation invocationWithMethodSignature:aSignature];
        void *        arg;
        int           index = 2;

        [anInvocation setSelector:selector];
        [anInvocation setTarget:target];

        va_list       args;
        va_start(args, wait);

        do
        {
            arg = va_arg(args, void *);
            if (arg)
            {
                [anInvocation setArgument:arg atIndex:index++];
            }
        }
        while (arg);

        va_end(args);

        [anInvocation retainArguments];

        if (thread == nil)
        {
            [anInvocation performSelectorOnMainThread:@selector(invoke) withObject:nil waitUntilDone:wait];
        }
        else
        {
            [anInvocation performSelector:@selector(invoke) onThread:thread withObject:nil waitUntilDone:wait];
        }
    }
}

请注意,当需要执行类型转换时,此代码可能不安全。当调用的方法有比我传递给callSelectorWithVarArgs:onTarget:onThread:wait:更长的参数时(例如,调用的方法接收NSUInteger(在arm64上为64位),但我传递int(在arm和arm64上都为32位)),就会从32位变量的起始地址读取64位 - 并且数据中含有垃圾值。 无论如何,您的实现都具有潜在危险性 - 您将传递给包装方法的所有参数都视为与调用方法中的参数具有相同的类型。
这是修改后的代码,可以正常工作:
- (void)makeObjectsPerformSelector:(SEL)selector withArguments: (void*)arg1, ...
{
    NSArray* currObjects = [NSArray arrayWithArray: self];
    for (id object in currObjects)
    {
        if ([object respondsToSelector: selector])
        {
            NSMethodSignature* signature = [[object class] instanceMethodSignatureForSelector: selector];

            NSInvocation* invocation = [NSInvocation invocationWithMethodSignature: signature];
            invocation.selector = selector;
            invocation.target = object;

            [invocation setArgument:&arg1 atIndex:2];

            NSInteger   index = 3;
            void *        arg;

            va_list       args;
            va_start(args, arg1);

            do
            {
                arg = va_arg(args, void *);
                if (arg)
                {
                    [invocation setArgument:&arg atIndex:index++];
                }
            }
            while (arg);

            va_end(args);

            [invocation retainArguments];
            [invocation invoke];
        }
    }
}

你修改过的代码在64位系统上运行良好,但在32位系统上崩溃了,因为我们超出了边界 [NSInvocation setArgument:atIndex:]。所以,我用下面的代码替换了你的循环: for (NSUInteger i = index; i < signature.numberOfArguments; i++) { arg = va_arg(args, void *); if (arg) { [invocation setArgument: &arg atIndex: i]; } } - abagmut
1
我们怎样才能到达那个界限呢?你会调用带有 2,147,483,647 个参数的方法吗? - Nikita Ivaniushchenko
arg = va_arg(args, void *); 对于超出 signature.numberOfArguments 的索引,返回一些指针(未被识别),但不是 nil - abagmut
我该如何复现那个问题? - Nikita Ivaniushchenko
我发现了问题 - 我在arg_list的末尾漏掉了nil - 所以你的解决方案完美地运行了,非常感谢你。 - abagmut

4
这段代码对于 va_list 中不同参数的布局做出了不可移植的假设,这些假设在 arm64 上不起作用。
例如,您可以看到有一些其他技巧(用于解决不同的问题)依赖于 va_list 中参数的布局,在 32 位系统中有效,但在 64 位系统中也无法正常工作。
访问 va_list 中的参数的唯一可移植方法是通过 va_arg,但这需要在编译时固定类型。

1

您正在使用int,您说它在32位上运行良好,但在64位上崩溃。将迭代器更改为NSInteger或NSUInteger。猜测这将解决您的问题。


我非常确定,在方法中不会有少于 2,147,483,647 个参数,所以这不是问题。 - Nikita Ivaniushchenko
你能保证在64位架构上,int类型的高32位没有垃圾数据吗?我觉得不行。 - Helge Becker
抱歉,什么?当将32位变量赋值给64位变量时会执行类型转换。 - Nikita Ivaniushchenko

1
您正在多次使用参数列表。这样做是未定义的行为。您可以通过使用va_copy来解决此问题。
va_start(argList,arg1)移动到外部for循环之外,并使用以下内容创建参数列表的副本:va_list copyArgList; va_copy(copyArgList,argList);。然后像往常一样使用复制的参数列表。
有关va_copy的更多信息。

0

我认为你需要考虑改变这种方法,重新编写代码,采用基于va_arg的更安全的机制,这是遍历可变参数的唯一安全机制。可以参考@Nikita发布的内容。

如果你想继续使用当前的方法,你需要深入了解每个架构的iOS调用约定。你可以在这里找到ARM64的约定:https://developer.apple.com/library/ios/documentation/Xcode/Conceptual/iPhoneOSABIReference/Articles/ARM64FunctionCallingConventions.html

仅从初步的观察来看,这显然不是一件简单的事情,可变参数函数与普通的调用约定不同。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接