GNU C标签指针转函数指针遇代码优化消失问题问询
奇怪的GCC/Clang优化行为:标签地址与函数指针的诡异问题
重要警告:
本代码仅用于趣味探索,请勿在生产环境中使用。编译运行本代码造成的任何损害,我们不承担责任。本代码不安全、不可移植且存在明显风险,特此警告。
先来看这段充满“陷阱”的代码:
#include <stdio.h> int fun() { return 5; } typedef int(*F)(void) ; int main(int argc, char const *argv[]) { void *ptr = &&hi; F f = (F)ptr; int c = f(); printf("TT: %d\n", c); if(c == 5) goto bye; //else goto bye; /* <---- 这是最关键的一行,请注意 */ hi: c = 5; asm volatile ("movl $5, %eax"); asm volatile ("retq"); bye: return 66; }
先搞懂代码的基本逻辑
fun()函数只是用来生成参考汇编的,实际根本没被调用F是个函数指针类型,指向无参、返回int的函数- 这里用了GCC的专属扩展Labels-as-Values(就是
&&hi这个写法)来拿标签hi的地址,然后强行转成函数指针去调用,把返回值存到c里打印出来 - 最后用
if判断c是不是5,是的话跳去bye标签退出程序
不同编译条件下的诡异表现
当注释掉else goto bye;这行时:
- 用GCC
-O1编译,汇编会老老实实调用hi标签的代码,程序正常输出TT: 5,最后返回66
但一旦取消注释这行,各种奇怪的情况就来了:
- GCC
-O0编译:ptr直接指向了bye标签,hi那段代码直接被删了,连printf都没执行 - Clang
-O0编译:hi的代码根本没生成,程序直接崩溃 - GCC
-O1编译:hi代码被移除,程序崩溃 - Clang
-O1编译:指针值被设成了1,程序直接异常
逐个解答你的问题
1. 为啥加了else分支,编译器就觉得两个标签等价?
咱们先想清楚:加了else goto bye;之后,不管if条件成立不成立,程序最终都会跳去bye标签。换句话说,hi标签所在的代码块完全是不可达的——从标准C的控制流来看,没有任何合法路径能走到hi那里(你是靠非标准的标签地址跳转硬闯进去的)。
编译器眼里,不可达代码就是“垃圾”,可以安全删掉。而且因为没有合法的goto引用hi标签,编译器会觉得&&hi这个地址根本没意义,甚至直接把它等同于&&bye(反正所有路径最终都到bye),或者干脆把这个无效地址优化掉。
2. Clang为啥生成值为1的指针?
这就是未定义行为的典型表现了。首先你用的Labels-as-Values是GCC的非标准扩展,Clang对它的支持本来就没那么“贴心”。当hi被判定为不可达后,&&hi的处理就完全没有标准约束了——编译器想怎么处理都行。
Clang在这里把无效的标签地址换成1,不是故意搞事情,只是未定义行为下的一种随机(或者说任意)输出。毕竟触发未定义行为后,编译器做任何事情都是符合规则的,生成1只是其中一种可能而已。
3. 这段代码违反了哪些C标准规范?
咱们一条条数:
- 用了非标准扩展:Labels-as-Values(
&&label)是GCC独有的,C标准里根本没有获取标签地址的语法,更不允许把标签地址转成函数指针调用。 - 非法的类型转换:把
void*(标签地址)强制转成函数指针类型F,这在C标准里是未定义行为。虽然有些平台允许函数指针和void*互转,但这是实现定义的,不是标准规定的可移植行为。 - 伪造函数调用:你通过函数指针调用的方式,本质是跳去
main函数内部的标签代码块执行,这相当于在函数调用的上下文里跑非函数的代码片段。标准C要求函数调用必须指向合法的函数入口,这种“伪造”的调用完全违反了函数调用的语义。 - 不可达代码的未定义行为:当编译器判定
hi不可达后,对&&hi的任何使用都会触发未定义行为——C标准没规定编译器怎么处理不可达标签的地址,所以编译器删代码、生成奇怪的指针值,都是合法的操作。
内容的提问来源于stack exchange,提问作者Ferenc Deak
相关产品推荐
相关产品推荐

