Clang对两种goto表达式生成不同汇编代码的原因探究
Clang中两种goto间接跳转写法的代码生成差异原因
我发现clang(版本clang-1400.0.29.202)对goto *&&label和goto *(void*)&&label生成的汇编代码存在明显差异:前者生成预期的直接跳转至标签的指令,后者则先将标签地址加载到寄存器,再通过寄存器执行间接跳转。将void *替换为unsigned long或unsigned long long时该问题仍会复现,而gcc在两种情况下均生成直接跳转,且最新版clang在测试中结果一致。
最小复现代码
void run(long *x) { if (x[2]) { #if X goto *&&end; #else goto *(void*)&&end; #endif } beg: x[4] = x[2] ? (long)&&beg : (long)&&end; end: if (x[3]) goto *(void *)x[4]; }
汇编代码差异
diff -u <(cc -DX=1 -O3 -S -o- run.c) <(cc -DX=0 -O3 -S -o- run.c) --- /dev/fd/63 2022-11-04 15:05:34.000000000 -0700 +++ /dev/fd/62 2022-11-04 15:05:34.000000000 -0700 @@ -11,25 +11,24 @@ movq %rsp, %rbp .cfi_def_cfa_register %rbp movq 16(%rdi), %rax + leaq Ltmp0(%rip), %rcx <---- 将标签地址加载到rcx testq %rax, %rax - jne LBB0_2 -Ltmp0: ## Block address taken + je LBB0_1 +## %bb.5: + jmpq *%rcx <---- 通过寄存器执行间接跳转 +Ltmp1: ## Block address taken LBB0_1: testq %rax, %rax - leaq Ltmp1(%rip), %rcx - leaq Ltmp0(%rip), %rdx + leaq Ltmp1(%rip), %rdx cmoveq %rcx, %rdx movq %rdx, 32(%rdi) -LBB0_2: - movq 24(%rdi), %rcx - .p2align 4, 0x90 -Ltmp1: ## Block address taken -LBB0_3: ## =>This Inner Loop Header: Depth=1 - testq %rcx, %rcx - je LBB0_5 -## %bb.4: ## in Loop: Header=BB0_3 Depth=1 +Ltmp0: ## Block address taken +LBB0_2: ## =>This Inner Loop Header: Depth=1 + cmpq $0, 24(%rdi) + je LBB0_4 +## %bb.3: ## in Loop: Header=BB0_2 Depth=1 jmpq *32(%rdi) -LBB0_5: +LBB0_4: popq %rbp retq .cfi_endproc
差异产生的原因
这本质是Clang对两种写法的语义处理和优化路径不同:
- 对于
goto *&&end:这种写法直接使用标签地址常量作为间接跳转的操作数,Clang的优化器能直接识别出这是一个编译期确定的固定标签地址,因此会将其优化为等价的直接跳转指令(如jne LBB0_2),完全跳过间接跳转的流程。 - 对于
goto *(void*)&&end:这里的强制类型转换会让Clang前端将该表达式视为一个通用指针值,而非原生的标签地址常量。即便这个指针的值在编译期是确定的,Clang的优化逻辑中,经过类型转换的表达式会被归类为“运行时可计算的指针”,而非编译期可见的标签常量,因此会生成间接跳转的代码:先把标签地址加载到寄存器,再通过寄存器完成跳转。
简言之,Clang对原生标签地址和经过类型转换的标签地址采取了不同的优化策略,前者能被优化为直接跳转,后者则会被当作普通指针处理,生成间接跳转指令。
内容的提问来源于stack exchange,提问作者nikita
相关产品推荐
相关产品推荐

