我想知道在这样的情况下会发生什么事情:
int foo() { return 1; } void bar() { void(*fPtr)(); fPtr = (void(*)())foo; fPtr(); }
返回int的函数的地址分配给void(*)()types的指针,并调用指向的函数。
非常感谢您的时间
1)从C11标准 – 6.5.2.2 – 9
如果函数是用与表示被调用函数的表达式指向的类型(表达式)不兼容的类型定义的,则行为是未定义的
清楚地表明,如果使用与其定义的类型不匹配的类型的指针调用某个函数,则会导致未定义的行为。 但演员没关系
2)关于你的第二个问题 – 如果明确的呼叫约定XXX和实施YYYY –
你可能已经拆开了一个示例程序(甚至是这个程序),并发现它“有效”。 但是有一些轻微的复杂性。 你看,编译器这些天很聪明。 有一些编译器能够进行精确的程序间分析。 一些编译器可能会发现,你有没有定义的行为,它可能会做出一些假设,可能会打破行为。
一个简单的例子 –
由于编译器认为这个函数是用void(*)()
类型来调用的,所以它会假定它不应该返回任何东西,并且可能会删除返回正确值所需的指令。
在这种情况下,调用这个函数的其他函数(以正确的方式)将会得到一个不好的值,因此会有明显的不好的影响。
PS:正如@PeterCordes所指出的那样,任何现代,理智和有用的编译器都不会有这样的优化,并且使用这样的调用可能总是安全的。 但答案的意图和例子(可能太简单了)是提醒在处理UB时必须非常小心。
在实践中发生的事情很大程度上取决于编译器如何实现这一点。 你假设C只是一个超薄的(“明显的”)层,但事实并非如此。
在这种情况下,编译器可以通过一个类型错误的指针(它具有未定义的行为1 )来看到你正在调用函数,因此理论上可以将bar()
编译为:
bar: ret
编译器可以假定程序执行期间不会发生未定义的行为。 调用bar()
总是会导致未定义的行为。 因此,编译器可以假定bar
永远不会被调用,并基于此来优化程序的其余部分。
1 C99,6.3.2.3/8:
如果使用转换的指针调用类型与指向类型不兼容的函数,则行为是不确定的。
关于子问题2:
我所知道的几乎所有x86调用约定( cdecl
, stdcall
, syscall
, fastcall
, pascal
,64位Windows和64位Linux)都将允许void
函数修改ax
/ eax
/ rax
寄存器以及int
函数和void
函数只是返回值在eax
寄存器中传递。
对于我已经使用过的大多数其他CPU(MIPS,Sparc,ARM,V850 / RH850,PowerPC,TriCore)的“默认”调用约定也是如此。 当然,注册名不是eax
而是不同的。
所以当使用这些调用约定时,可以使用void
指针安全地调用int
函数。
不过,有一些调用约定,情况并非如此:我读过一个调用约定,它隐式地为非void
函数使用了一个额外的参数。
仅在asm级别 ,这对于整数类型的所有常规x86调用约定是安全的: eax
/ rax
是调用clobbered,并且调用者不必做任何不同的事情来调用void
函数与int
函数并忽略返回值。
对于非整型返回类型 ,即使在asm中也是一个问题。 结构返回是通过一个隐藏指针arg取代其他参数来完成的,而调用者将通过它来存储,所以最好不要存放垃圾。 (假设情况比这里显示的情况更为复杂,所以当启用优化时,函数不会内联。)请参阅下面的Godbolt链接,以获取通过转储函数指针调用的示例,该指针通过垃圾回收rdi
“指针”
对于传统的32位代码,FP返回值位于x87堆栈的st(0)
,调用者有责任不让x87堆栈不平衡。 float
/ double
/ __m128
返回值在64位ABI或32位代码中使用调用约定(在xmm0
(SSE / SSE2)中返回FP值)是安全的。
在C中,这是UB (请参阅标准中引用的其他答案)。 如有可能/方便,请选择解决方法(请参阅下文)。
未来基于无UB假设的积极优化有可能会破坏这样的代码。 例如,编译器可能会假定任何导致UB的路径都不会被采用,因此导致运行此代码的if()
条件必须始终为false。
请注意,仅仅编译bar()
不能破坏foo()
或其他不调用bar()
函数。 如果bar()
运行的时候只有UB ,所以发出一个破坏的外部可见定义foo()
( 就像@Ajay所建议的 )不是可能的后果。 (除非你使用完整的程序优化,而且编译器证明bar()
总是被调用至少一次)。编译器可以中断调用bar()
函数,但是至少它们的部分会导致UB 。
但是,许多x86编译器允许(偶然或故意使用)。 一些用户期望这个工作,这种事情是存在于一些真实的代码库,所以编译器开发人员可以支持这种使用,即使他们实施积极的优化否则承担这个功能(因此所有路径,导致它在任何调用)永远不会运行。 或者可能不是!
一个实现可以自由定义在ISO C标准没有定义行为的情况下的行为。 但是,我不认为gcc / clang或任何其他编译器明确保证这是安全的。 如果此代码停止工作,编译器开发人员可能会认为它不是一个编译器错误。
我绝对不能推荐这样做 ,因为它可能不会继续安全。 希望如果编译器开发者决定用激进的无UB假设优化来打破它,将会有选项来控制哪种类型的UB被假定为不会发生。 和/或将会有警告。 正如在评论中所讨论的那样 ,是否要承担将来可能出现的短期绩效/便利收益损失的风险取决于外部因素(如生命将处于风险中,以及您计划在将来如何谨慎维护,例如检查编译器警告未来的编译器版本。)
无论如何,如果它的工作,这是因为你的编译器的慷慨, 而不是任何标准的保证。 不过,这种编译器的慷慨可能是故意的和半维护的。
另请参阅关于另一个答案的讨论 :编译器人员实际使用的目标是有用的 ,而不仅仅是符合标准。 C标准允许有足够的自由来制定合规的但不是非常有用的实现。 (很多人会认为即使在定义明确的语义的机器上也没有签名溢出的编译器已经超越了这一点,参见C程序员应该知道的关于未定义的行为 (LLVM博客文章)。
如果编译器不能证明它是UB(例如, 如果它不能静态地确定函数指针指向哪个函数 ),那么几乎没有办法可以中断(如果函数是ABI兼容的话)。 Clang的运行时UB-sanitizer仍然可以找到它,但编译器在代码中没有太多的选择来通过未知的函数指针进行调用。 它只需要按照ABI /呼叫公约所说的那样来进行。 它不能区分将一个函数指针转换为“错误”类型并将其转换回正确的类型(除非将两个不同类型的同一个函数指针取消引用,这意味着一个或另一个必须是UB。编译器会很难证明它,因为第一次调用可能不会返回noreturn
函数不必标记为noreturn
。)
但是请记住,链接时间优化/内联/常量传播可以让编译器看到哪个函数被指向,即使在函数指针作为参数或全局变量。
如果这个函数不是Link-Time-Optimization的一部分,那么你可以对编译器说谎,并给它一个与你想调用的原型相匹配的原型(只要你确定你有asm级的调用约定是兼容的)。
你可以写一个包装函数。 这可能是效率较低(如果它只是尾调用原来的一个额外的jmp
),但如果它内联,那么你克隆功能,使一个版本,不做任何创建返回值的工作。 如果使用返回值的版本,那么与第二个定义的额外的I-cache / uop缓存压力相比,这可能仍然是一个损失。
你也可以使用链接器的东西来定义一个函数的替代名称,所以这两个符号具有相同的地址。 这样,您可以为同一个编译器生成的机器代码块创建两个原型。
使用GNU工具链, 你可以在原型上使用一个属性,使其成为一个弱别名(在asm / linker级别)。 这不适用于所有的目标。 它适用于ELF对象文件,但是关于Windows的IDK。
// 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; }
例如gb7.2和clang5.0的Godbolt 。
gcc7.2通过弱的别名调用foo_void
来使foo
内联! 铛没有,虽然。 我认为这意味着这是安全的,在gcc中也是函数指针强制转换。 或者这意味着这也是潜在的危险。 > <
clang的未定义的行为消毒器通过函数指针进行运行时函数类型信息检查(仅在C ++模式下)。 int ()
不同于void ()
,所以它会在x86上检测并报告这个UB。 (请参阅Godbolt上的主题)。 这可能并不意味着它现在实际上是不安全的,因为在编译时它还没有检测到/警告它。
在获取函数地址的代码中使用上述变通办法,而不是在接收函数指针的代码中。
你希望让编译器看到一个真正的函数,它将最终被调用的签名,而不管你传递的函数指针类型如何。 使用与函数指针最终将被转换的匹配的签名来创建别名/包装器。 如果这意味着你必须把函数指针放在第一位,那就这样吧。
(我认为只要没有解引用就可以创建一个指向错误类型的指针,即使不解引用,甚至创建一个未对齐的指针也是UB,但这是不同的)。
如果你的代码需要在一个地方解析和int foo(args)
相同的函数指针,而在另一个地方需要void foo(args)
,那么就可以避免使用UB。
C11§6.3.2.3第8段:
指向一种类型的函数的指针可以被转换为指向另一种类型的函数的指针并返回; 结果应该等于原始指针。 如果使用转换的指针调用类型与引用类型不兼容的函数,则行为是不确定的 。