使用void(*)()指针调用函数返回int时会发生什么?

Mar*_* K. 3 c windows assembly calling-convention

我想知道在这种情况下会发生什么:

int foo()
{
    return 1;
}
void bar()
{
    void(*fPtr)();
    fPtr = (void(*)())foo;
    fPtr();
}
Run Code Online (Sandbox Code Playgroud)

函数返回int的地址被赋给void(*)()类型的指针,并调用指向的函数.

  1. 标准对此有何评价?
  2. 无论对第一个问题的回答如何:我们可以安全地调用这样的函数吗?在实践中,结果不应该只是被调用者(foo)会在EAX/RAX中放置一些内容而调用者(bar)会忽略rax内容并继续执行该程序?我对Windows调用约定 x86和x64 感兴趣.

非常感谢你的时间

Aja*_*iya 7

1)从C11标准 - 6.5.2.2 - 9

如果函数定义的类型与表示被调用函数的表达式指向的类型(表达式)不兼容,则行为未定义

清楚地说明,如果使用与其定义的类型不匹配的类型的指针调用函数,则会导致未定义的行为.但演员阵容还可以.

2)关于你的第二个问题 - 如果定义明确的电话会议XXX和实施YYYY -

你可能已经拆解了一个示例程序(甚至是这个程序),并发现它"有效".但是有轻微的并发症.你看,这些天的编译器非常聪明.有些编译器能够执行精确的过程间分析.某些编译器可能会发现您的行为未定义,并且可能会做出一些可能会破坏行为的假设.

一个简单的例子 -

由于编译器发现此函数是使用类型调用的void(*)(),因此它将假定它不应返回任何内容,并且它可能会删除返回正确值所需的指令.

在这种情况下,调用此函数的其他函数(以正确的方式)将得到一个错误的值,因此它会产生明显的不良影响.

PS:正如@PeterCordes所指出的,任何现代的,理智的和有用的编译器都不会有这样的优化,并且使用这样的调用可能总是安全的.但答案的意图和例子(可能过于简单化)是提醒人们在处理UB时必须非常小心.

  • @PeterCordes我理解你关于有用的编译器的观点,我完全同意它,但我想如果我写了一个答案说我可以这样做,那就错了.OP必须充分意识到UB可能带来的后果(优化相关的行为变化也难以跟踪).是的,这个特殊的例子可能适用于有用的编译器.但是我们不希望OP扩展它并将它应用于一个可能出错的例子.我希望你理解我的关注和这个答案背后的理由. (2认同)

mel*_*ene 5

实践中会发生什么很大程度上取决于编译器如何实现这一点。您假设 C 只是 asm 上的一个薄(“明显”)层,但事实并非如此。

在这种情况下,编译器可以看到您正在通过类型错误的指针(具有未定义的行为1)调用函数,因此理论上它可以编译bar()为:

bar:
    ret
Run Code Online (Sandbox Code Playgroud)

编译器可以假设在程序执行期间永远不会发生未定义的行为。调用bar()总是导致未定义的行为。因此编译器可以假设bar永远不会被调用并基于此优化程序的其余部分。


1 C99,6.3.2.3/8:

如果使用转换后的指针调用类型与指向类型不兼容的函数,则行为未定义。

  • @RbMm 你可以自由地做你想做的,你可以测试结果 - 在一个特定编译器的一个特定版本上运行一组优化选项。下一个版本的编译器(或不同的编译器)的工作方式可能有所不同。不能保证看似可以使用 UB 的代码将来会继续运行。 (2认同)
  • 这是一个很好的观点。如果编译器可以在编译时看到 UB,即使在没有 asm 级调用约定问题的目标上(例如在具有任何 Windows 调用约定的 x86 上),也可能会损坏。一个高质量的实现可能会认为它是一个错误,如果它坏了,但这个问题并不排除有敌意但符合标准的编译器的可能性。我同意 RbMn 的观点,这在实践中可能是安全的,但*不*,因为任何标准都可以保证这一点。这只是编译器 QOI 问题。出于 API 原因的函数指针转换确实会发生。 (2认同)