变量参数函数:在iOS6464上使用va_arg进行错误访问

时间:2016-02-21 13:06:30

标签: c++ ios unity3d

我有这样的功能:

typedef long long myint64;

typedef enum {
    INT32_FIELD,
    CHARP_FIELD,
    INT64_FIELD,
} InfoType;

int32_t ReadInfo(void *handle, InfoType info, ...)
{
    va_list arg;
    va_start(arg, info);
    void *argPtr = va_arg(arg, void*);
    va_end(arg);

    int32_t ret = 0;
    int32_t *paramInt = NULL;
    char **paramCharp = NULL;
    myint64 *paramInt64 = NULL;

    switch (info) {
    case INT32_FIELD:
        paramInt = static_cast<int32_t*>(argPtr);
        *paramInt = functionWhichReturnsInt32();
        break;
    case CHARP_FIELD:
        paramCharp = static_cast<char**>(argPtr);
        *paramCharp = functionWhichReturnsCharPtr();
        break;
    case INT64_FIELD:
        paramInt64 = static_cast<myint64*>(argPtr);
        *paramInt64 = functionWhichReturnsInt64();
        break;
     default:
        ret = -1;
        break;
    }
    return ret;
}

从分离的c文件中调用此函数。此文件不包含ReadInfo函数的定义:

extern "C" {int32_t CDECL ReadInfo(intptr_t, int32_t, int32_t*);}

int32_t readInt()
{
    int32_t value = 0;
    int32_t *ptr = &value;
    ReadInfo(handle, INT32_FIELD, ptr);
    return value;
}

此调用仅在iOS arm64下失败。 arm7s和win32可以正常使用此调用。 (是的,我们唯一的64位目标平台是iOS arm64。) 在调试器中,我发现readInt函数中的ptr地址与我所使用的不同:     void argPtr = va_arg(arg,void );

我是否在使用arg_list工作错误?

P.S。它不是一个普通的Objective C应用程序。它是原生Unity插件的一部分。但是在iOS中,Unity代码只是从C#转换为Objective C / C ++。这就是为什么你可以看到第二个声明:

extern "C" {int32_t CDECL ReadInfo(intptr_t, int32_t, int32_t*);}

2 个答案:

答案 0 :(得分:0)

这个问题的原因在于IL2CPP,它生成带有变量参数的函数调用。并且它不使用我的类型,如InfoType,myint64。它使用特定于平台的信息变量类型。我估计大小可能不同。

我只为Unity API添加了3个新功能:

int32_t ReadInfoInt(void *handle, InfoType info, int *ret);
int32_t ReadInfoInt64(void *handle, InfoType info, myint64 *ret);
int32_t ReadInfoStr(void *handle, InfoType info, char **ret);

在这个函数中我只调用ReadInfo。

它是100%的解决方法,但它比用IL2CPP更好。

答案 1 :(得分:0)

这不是IL2CPP的问题,而是iOS或编译器的问题。

即使在最新的Xcode(10.1)和iOS(12.1)上,以下代码也可以重现该问题

typedef int __cdecl (*PInvokeFunc) (const char*, int);

int test()
{
    PInvokeFunc fp = (PInvokeFunc)printf;
    fp("Hello World: %d", 10);
    return 0;
}

预期输出为:Hello World: 10,但在iOS上它将为Hello World: ??? (Random number)

我在macOS和Linux上尝试了相同的代码,并且它们都可以正常工作。

我不确定它是否与Apple文档有关:

  

可变函数   具有可变数量参数的iOS ABI与通用版本完全不同。

     

通用过程调用标准的阶段A和B照常执行-特别是,即使大于16字节的可变参数聚合也通过引用传递给调用方分配的临时内存。之后,固定参数会像通常在iOS中一样分配给寄存器和堆栈插槽。

     

然后将NSRN向上舍入为8个字节的下一个倍数,并将每个可变参数指定给适当数量的8字节堆栈插槽。

     

C语言要求在调用之前提升小于int的参数,但除此之外,此ABI不会指定堆栈上未使用的字节。

     

此更改的结果是,类型va_list是char *的别名,而不是通用PCS中指定的结构类型的别名。编译C ++代码时,它也不在std名称空间中。

https://developer.apple.com/library/archive/documentation/Xcode/Conceptual/iPhoneOSABIReference/Articles/ARM64FunctionCallingConventions.html


更新:

Apple工程师的回信:

  

使用铸造函数指针添加其他调用约定不会更改被调用方的表示方式,只会更改调用方执行其调用的方式。 printf已经有一个调用约定,并且您所做的可能会在某些平台上适用于某些组合,而不适用于其他平台。您想改为声明一个包装函数,该包装函数具有所需的调用约定,并调用所需的函数。您需要手动封送参数。

也就是说,可变参数函数不能是直接p /调用,除非IL2CPP为其生成包装函数。仅函数指针是不够的。