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

时间:2017-11-02 10:02:47

标签: c windows assembly calling-convention

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

int foo()
{
    return 1;
}
void bar()
{
    void(*fPtr)();
    fPtr = (void(*)())foo;
    fPtr();
}

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

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

5 个答案:

答案 0 :(得分:7)

1) 从C11标准 - 6.5.2.2 - 9

  

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

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

2) 关于你的第二个问题 - 如果定义明确的呼叫惯例XXX和实施YYYY -

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

一个简单的例子 -

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

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

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

答案 1 :(得分:4)

实践中发生的事情很大程度上取决于编译器如何实现它。你假设C只是asm上的一个薄(“明显”)层,但它不是。

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

bar:
    ret

编译器可以假设在程序执行期间从未发生过未定义的行为。调用bar()始终会导致未定义的行为。因此,编译器可以假定永远不会调用bar,并根据它优化程序的其余部分。

1 C99,6.3.2.3/8:

  

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

答案 2 :(得分:4)

关于子问题2:

我所知道的几乎所有x86调用约定(cdeclstdcallsyscallfastcallpascal,64位Windows和64位Linux )将允许void函数修改ax / eax / rax寄存器,int函数与void函数之间的差异为只是返回的值在eax寄存器中传递。

"默认"同样如此。在我已经使用过的大多数其他CPU上调用约定(MIPS,Sparc,ARM,V850 / RH850,PowerPC,TriCore)。注册名称不是eax,但当然不同。

因此,在使用这些调用约定时,您可以使用int指针安全地调用void函数。

但是有些调用约定并非如此:我已经读过一个调用约定,该约定隐式地为非void函数使用了一个额外的参数...

答案 3 :(得分:1)

仅在asm级别,这对于整数类型的所有正常x86调用约定都是安全的:eax / rax是call-clobbered,而调用者没有&# 39; t必须做任何不同的事情来调用void函数与int函数并忽略返回值。

对于非整数返回类型,即使在asm中也是一个问题。结构返回是通过一个隐藏的指针arg完成的,它取代了其他的args,调用者将通过它存储,所以最好不要保存垃圾。 (假设情况比这里显示的情况更复杂,那么当启用优化时,函数不会内联。)请参阅下面的Godbolt链接,了解调用通过铸造函数指针的示例,该指针导致存储通过垃圾"指针"在rdi

对于传统的32位代码,FP返回值在x87堆栈的st(0)中,并且调用者有责任不让x87堆栈不平衡。 float / double / __m128返回值在64位ABI中可以安全忽略,或者使用在xmm0中返回FP值的调用约定在32位代码中忽略( SSE / SSE2)。

在C中,这是UB (请参阅标准中引号的其他答案)。如果可能/方便,请选择一种解决方法(见下文)。

基于无UB假设的未来积极优化可能会破坏这样的代码。例如,编译器可能会假设从未采用任何导致UB的路径,因此导致此代码运行的if()条件必须始终为false。

请注意,仅仅编译bar()无法中断foo()或其他不会调用bar()的功能。 There's only UB if bar() ever runs,因此为foo()like @Ajay suggests)发出破坏的外部可见定义不是可能的后果。 (除非您使用整个程序优化并且编译器证明bar()总是至少被调用一次。)编译器可以破坏调用bar()的函数,但至少它们的部分是通往UB。

然而,许多当前的x86编译器允许(偶然或故意)。有些用户希望这可以工作,并且这种事情存在于一些真正的代码库中,因此编译器开发人员可能会支持这种用法,即使他们实现积极的优化,否则将承担此功能(因此在任何调用者中都会导致它的所有路径) )永远不要跑。或许不是!

在ISO C标准使行为未定义的情况下,实现可以自由定义行为。但是,我不认为gcc / clang或任何其他编译器明确保证这是安全的。如果此代码停止工作,编译器开发人员可能会或可能不会将其视为编译器错误。

我绝对不能推荐这样做,因为它可能不会继续保持安全。希望如果编译器开发人员决定用积极的no-UB假设优化来破解它,那么将有一些选项来控制假设哪种UB不会发生。和/或会有警告。 As discussed in comments,是否承担未来可能出现的短期性能/便利优势的风险取决于外部因素(例如生命将面临风险,以及您计划在未来如何谨慎维护,例如检查编译器警告与未来的编译器版本。)

无论如何,如果它有效,那是因为编译器的慷慨,因为任何标准保证。但是,这种编译器的慷慨可能是有意和半维护的。

See also discussion on another answer:人们实际使用的编译器旨在有用,而不仅仅是符合标准。 C标准允许足够的自由度来实现兼容但不是非常有用的实现。 (许多人认为,即使在具有明确定义的语义的机器上也没有签名溢出的编译器已经超过了这一点。另请参阅What Every C Programmer Should Know About Undefined Behavior(LLVM博客文章)。)

如果编译器无法证明它是UB(例如,如果它不能静态地确定函数指针指向哪个函数),那么'它几乎不可能破坏(如果函数是ABI兼容的)。 Clang的运行时UB-sanitizer仍然可以找到它,但是编译器在代码生成器中没有多少选择来通过未知函数指针进行调用。它只需调用ABI /调用约定所说的方式。它无法区分将函数指针转换为"错误"键入并将其强制转换为正确的类型(除非您使用两种不同的类型取消引用相同的函数指针,这意味着一个或另一个必须是UB。但编译器很难证明它,因为第一个调用可能不会返回。noreturn函数 标记为noreturn。)

但请记住,链接时优化/内联/常量传播可以让编译器看到即使在将函数指针作为arg或全局变量获取的函数中也指向哪个函数。

解决方法(在获取地址之前的函数):

如果该功能不能成为Link-Time-Optimization的一部分,那么您可能会欺骗编译器并为其提供一个与您想要调用它的方式匹配的原型(只要您确定自己)得到了asm级别的调用约定。)

你可以写一个包装函数。它可能效率较低(如果只是尾部调用原始版本,则额外jmp),但如果它内联,那么您将克隆该函数以制作一个不做的版本任何创建返回值的工作。如果与第二个定义的额外I-cache / uop缓存压力相比,如果使用返回值的版本也是如此便宜,那么这可能仍然是一个损失。

您还可以使用链接器内容为函数定义备用名称,以便两个符号具有相同的地址。这样,您可以为同一块编译器生成的机器代码创建两个原型。

使用GNU工具链,you can use an attribute on a prototype to make it a weak alias (at the asm / linker level).这对所有目标都不起作用;它适用于ELF目标文件,但IDK适用于Windows。

 // in GNU C:
int foo(void) { return 4; }

// include this line in a header if you want; weakref is per translation unit
// a definition (or prototype) for  foo  doesn't have to be visible.
static void foo_void(void) __attribute((weakref("foo")));  // in C++, use the mangled name

int bar_safe(void) {
    void (*goo)(void) = (void(*)())foo_void;
    goo();
    return 1;
}

example on Godbolt for gcc7.2 and clang5.0

gcc7.2通过对foo的弱别名调用内联foo_void!但是,clang并不是这样。我认为这意味着这是安全的,gcc中的函数指针转换也是如此。或者,这意味着这也有潜在危险。 >。<

clang的undefined-behavior sanitizer通过函数指针进行运行时函数typeinfo检查(仅限C ++模式)。 int ()void ()不同,因此它会在x86上检测并报告此UB。 (参见Godbolt上的asm)。但这并不意味着它目前实际上并不安全,因为它在编译时尚未发现/警告它。

在获取函数地址的代码中使用上述变通办法,而不是在接收函数指针的代码中。

你想让编译器看到一个带有最终将被调用的签名的真实函数,而不管你传递它的函数指针类型。创建一个别名/包装器,其签名与函数指针最终将被转换为匹配。如果这意味着您必须先将函数指针强制转换为传递它,那就这样吧。

(我认为创建一个指向错误类型的指针是安全的,只要它没有被解除引用。甚至可以创建一个未对齐的指针,即使你没有&#39 ; t dereference,但那是不同的。)

如果您的代码需要在一个地方将int foo(args)void foo(args)相同的函数指针展开,而在另一个地方展开{{1}},那么您就可以避免使用UB。

答案 4 :(得分:0)

C11§6.3.2.3第8段:

  

指向一种类型的函数的指针可以转换为指向a的指针   另一种类型的功能又回来了;结果应该比较   等于原始指针。如果转换的指针用于调用   一个类型与引用类型不兼容的函数,   行为未定义