【问题标题】:C function pointer casting to void pointerC函数指针转换为void指针
【发布时间】:2011-07-31 14:12:46
【问题描述】:

我正在尝试运行以下程序,但遇到了一些奇怪的错误:

文件 1.c:

typedef unsigned long (*FN_GET_VAL)(void);

FN_GET_VAL gfnPtr;

void setCallback(const void *fnPointer)
{
    gfnPtr = *((FN_GET_VAL*) (&fnPointer));
}

文件 2.c:

extern FN_GET_VAL gfnPtr;

unsigned long myfunc(void)
{
    return 0;
}

main()
{
   setCallback((void*)myfunc);
   gfnPtr(); /* Crashing as value was not properly 
                assigned in setCallback function */
}

这里 gfnPtr() 在使用 gcc 编译时在 64 位 suse linux 上崩溃。但它成功调用了 gfnPtr() VC6 和 SunOS。

但是,如果我按照下面给出的更改功能,它就会成功运行。

void setCallback(const void *fnPointer)
{
    int i; // put any statement here
    gfnPtr = *((FN_GET_VAL*) (&fnPointer));
}

请帮助解决问题的原因。谢谢。

【问题讨论】:

    标签: c pointers function-pointers


    【解决方案1】:

    C 标准不允许将函数指针转换为void*。您只能转换为另一种函数指针类型。在C11 standard, 6.3.2.3 §8:

    指向一种类型的函数的指针 可以转换为指向 a 的指针 另一种类型的功能并返回 再次

    重要的是,在使用指针调用函数之前,您必须转换回原始类型(从技术上讲,转换为兼容类型。“兼容”的定义在6.2.7)。

    请注意,由于使用它们的上下文,许多(但不是全部)C 编译器也必须遵循 POSIX 标准,它要求函数指针可以转换为 void* 并返回。这对于某些系统功能是必需的(例如dlsym)。

    【讨论】:

    • 感谢您的回复。我会注意不要混合数据和函数指针。但在这种情况下,我无法弄清楚发生这种情况的原因。如果我使用 -m32(32 位)编译和运行,它工作得非常好,但是如果我使用 -m64(64 位)编译,就会出现问题。此外,如果我在分配上方添加一个像 int i; 这样的语句,那么它工作正常。不确定原因可能是堆栈损坏,但如何检查。
    • 我同意标准不允许将函数指针转换为void*,根据 6.3.2.3;但我想知道为什么 6.3.2.3p3 说“指向任何对象或函数的指针”,因为这一定不能发生。顺便说一句,J.5.7 说“指向对象或 void 的指针可以转换为指向函数的指针,从而允许将数据作为函数调用”,反之亦然,但这是 J.5 通用扩展,因此不可移植。
    • @Manoj 原因可能是一些优化(即使在没有-O 的情况下编译)。也许在某个地方,编译器认为函数myfunc 没有被使用,或者类似的东西。
    • 这取决于您使用的标准——POSIX 标准要求 C 编译器支持将函数指针转换为 void * 并安全地返回。
    • @ChrisDodd 这是真的。问题标记为 C,而不是 POSIX,但我会添加注释。
    【解决方案2】:

    不幸的是,该标准不允许在数据指针和函数指针之间进行转换(因为这在一些真正晦涩的平台上可能没有意义),即使 POSIX 和其他人需要这样的转换。一种解决方法是不强制转换指针,而是将指针强制转换为指针(编译器可以这样做,它将在所有正常平台上完成工作)。

    typedef void (*FPtr)(void); // Hide the ugliness
    FPtr f = someFunc;          // Function pointer to convert
    void* ptr = *(void**)(&f);  // Data pointer
    FPtr f2 = *(FPtr*)(&ptr);   // Function pointer restored
    

    【讨论】:

    • “因为这在一些非常晦涩的平台上可能没有意义” 真正晦涩的平台,如 Linux、Mac OS X、iOS、Microsoft Windows 和 Android,它们具有数据执行保护。使用数据执行保护,代码和数据不可互换。注意:POSIX 要求强制转换为 void 指针,但它不要求这样的结果指针可用,除非转换回函数指针。
    • @JonathanBaldwin 您所说的仅在代码指针与数据指针大小不同的平台上才有意义。指针只是一个地址,因此永远不会在可执行区域中。但是,地址指向的数据可能位于可执行区域。所以你可能无法读取指针指向的内存。但是您当然可以使用此方法将其转换为从函数指针到 void * 和从 void * 到函数指针。除非平台的函数指针大小与数据指针大小不同,否则它将起作用。
    • @rxantos 当然,在具有 DXP 的 ia32 系统上实现从数据到函数指针的转换是微不足道的,但这没有意义,因为如果不将它转换回数据指针或使用系统调用在 DXP 中打孔(即使该区域可执行。)就 C 而言,支持这一点不值得为您提到的平台提供不一致的支持,其中包括 16 位 DOS 和 Windows (还记得 near 和 far 指针吗?)毕竟,如果一个平台,比如 POSIX,想要直接允许这个,他们可以让它成为一个扩展。
    【解决方案3】:

    关于数据指针和代码指针,我有三个经验法则:

    • 不要不要混合数据指针和代码指针
    • 不要混合数据指针和代码指针
    • 不要永远混用数据指针和代码指针!

    在以下函数中:

    void setCallback(const void *fnPointer)
    {
        gfnPtr = *((FN_GET_VAL*) (&fnPointer));
    }
    

    您有一个 数据指针,您可以将其转换为函数指针。 (更不用说您首先获取指针本身的地址,然后将其转换为指向指针的指针,然后再取消引用它)。

    尝试改写为:

    void setCallback(FN_GET_VAL fnPointer)
    {
         gfnPtr = fnPointer;
    }
    

    此外,您可以(或应该)在设置指针时放弃演员表:

    main()
    {
       setCallback(myfunc);
       gfnPtr();
    }
    

    作为额外的奖励,您现在可以使用编译器执行的正常类型检查。

    【讨论】:

    • 感谢您的回复。我会注意不要混合数据和函数指针。但在这种情况下,我无法弄清楚发生这种情况的原因。如果我使用 -m32(32 位)编译和运行,它工作得非常好,但是如果我使用 -m64(64 位)编译,就会出现问题。此外,如果我在分配上方添加一个像 int i; 这样的语句,那么它工作正常。不确定原因可能是堆栈损坏,但如何检查。
    • +1 用于分析获取函数指针的地址与将& 应用于函数名称(这是一个无操作)不同。
    【解决方案4】:

    我会建议一个可能的部分解释。

    @Manoj 如果您检查(或可以提供)两个编译器生成的 SetCallback 的程序集列表,我们可以获得明确的答案。

    首先,Pascal Couq 的陈述是正确的,Lindydancer 展示了如何正确设置回调。我的回答只是试图解释实际问题。

    我认为问题源于 Linux 和其他平台使用不同的 64 位模型这一事实(请参阅64-bit models on Wikipedia)。请注意,Linux 使用 LP64(int 是 32 位)。我们需要更多关于其他平台的细节。如果是 SPARC64,则使用 ILP64(int 为 64 位)。

    据我了解,这个问题只在 Linux 下观察到,如果你引入一个 int 局部变量就消失了。您是否尝试过关闭或开启优化?这种 hack 很可能不会对优化产生任何有益影响。

    在这两种 64 位模型下,指针都应该是 64 位的,无论它们指向代码还是数据。但是,情况可能并非如此(例如分段内存模型);因此,Pascal 和 Lindydancer 的告诫。

    如果指针大小相同,则剩下的可能是堆栈对齐问题。引入本地 int(在 Linux 下为 32 位)可能会改变对齐方式。只有当 void* 和函数指针具有不同的对齐要求时,这才会产生影响。一个令人怀疑的场景。

    不过,不同的 64 位内存模型很可能是您观察到的原因。欢迎您提供组装清单,以便我们进行分析。

    【讨论】:

    • 您链接到的维基百科条目是错误的。 Sparc 上 Solaris 的 64 位 ABI 是 LP64 而不是 ILP64。顺便说一句,SPARC64 是富士通 Sparc 实施的品牌名称。
    【解决方案5】:

    与其他人所说的不同,是的,您可以将 void* 指针用作函数指针,但使用它的语义非常棘手。

    如您所见,您无需将其转换为void*,只需像往常一样分配即可。我像这样运行您的代码并对其进行了编辑。

    file1.c:

    typedef unsigned long (*FN_GET_VAL)(void);
    
    extern FN_GET_VAL gfnPtr;
    
    void setCallback(const void *fnPointer)
    {
        gfnPtr = ((FN_GET_VAL) fnPointer);
    }
    

    file2.c:

    int main(void)
    {
        setCallback(myfunc);
        (( unsigned long(*)(void) )gfnPtr)();
    }
    

    【讨论】:

    • 赋值涉及转换,就像转换一样(根据定义,它是显式转换),因此这是未定义的行为。这可能适用于某些 C 实现,无论是作为扩展还是偶然(例如,因为相同的表示和不会破坏代码的优化),但 C 标准不能保证。
    猜你喜欢
    • 2012-11-21
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-09-20
    • 1970-01-01
    • 2019-04-13
    • 2021-07-22
    相关资源
    最近更新 更多