首先:此代码被认为是纯粹的乐趣,请在生产中不要执行任何此类操作。在任何环境下编译并执行这段代码后,对于您,您的公司或您的驯鹿造成的任何伤害,我们概不负责。以下代码不安全,不可移植,并且非常危险。被警告。下面的长帖子。您被警告了。
现在,在免责声明之后:让我们考虑以下代码:
#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; /* <---- This is the most important line. Pay attention to it */
hi:
c = 5;
asm volatile ("movl $5, %eax");
asm volatile ("retq");
bye:
return 66;
}
首先,我们有一个函数fun
,我纯粹是为了创建该汇编代码而创建的。
然后我们声明一个函数指针F
,该函数指向不带参数且返回整数的函数。
然后,我们使用不太知名的GCC扩展名https://gcc.gnu.org/onlinedocs/gcc/Labels-as-Values.html来获取标签hi
的地址,这在clang中也适用。然后,我们做一些邪恶的事情,创建一个名为f的函数指针F
,并将其初始化为上面的标签。
然后,最糟糕的是,我们实际上调用了此函数,并将其返回值分配给名为C
的局部变量,然后我们将其打印出来。
以下是if
,用于检查分配给c
的值是否确实是我们需要的值,如果是,请转到bye
,以使他的应用程序正常退出,退出代码为66。被视为正常的退出代码。
下一行已被注释掉,但是我可以说这是整个应用程序中最重要的一行。
标签hi
之后的代码段是将c的值赋值为5,然后进行两行汇编以将eax
的值初始化为5,并实际上从“函数”调用中返回。如前所述,有一个参考函数fun
会生成相同的代码。
现在我们编译该应用程序,并在我们的在线平台上运行它:https://gcc.godbolt.org/z/K6z5Yc
它生成以下程序集(-O1
处于打开状态,O0
给出了相似的结果,尽管时间更长一点:]
# else goto bye is COMMENTED OUT
fun:
mov eax, 5
ret
.LC0:
.string "TT: %d\n"
main:
push rbx
mov eax, OFFSET FLAT:.L3
call rax
mov ebx, eax
mov esi, eax
mov edi, OFFSET FLAT:.LC0
mov eax, 0
call printf
cmp ebx, 5
je .L4
.L3:
movl $5, %eax
retq
.L4:
mov eax, 66
pop rbx
ret
[重要行是mov eax, OFFSET FLAT:.L3
,其中L3
对应于我们的hi
标签,其后一行:call rax
实际调用它。
并且运行类似:
ASM generation compiler returned: 0
Execution build compiler returned: 0
Program returned: 66
TT: 5
现在,让我们重新访问应用程序中最重要的一行,并取消对其的注释。
使用-O0
,我们得到由gcc生成的以下程序集:
# else goto bye is UNCOMMENTED
# even gcc -O0 "knows" hi: is unreachable.
fun:
push rbp
mov rbp, rsp
mov eax, 5
pop rbp
ret
.LC0:
.string "TT: %d\n"
main:
push rbp
mov rbp, rsp
sub rsp, 48
mov DWORD PTR [rbp-36], edi
mov QWORD PTR [rbp-48], rsi
mov QWORD PTR [rbp-8], OFFSET FLAT:.L4
mov rax, QWORD PTR [rbp-8]
mov QWORD PTR [rbp-16], rax
mov rax, QWORD PTR [rbp-16]
call rax
mov DWORD PTR [rbp-20], eax
mov eax, DWORD PTR [rbp-20]
mov esi, eax
mov edi, OFFSET FLAT:.LC0
mov eax, 0
call printf
cmp DWORD PTR [rbp-20], 5
nop
.L4:
mov eax, 66
leave
ret
和以下输出:
ASM generation compiler returned: 0
Execution build compiler returned: 0
Program returned: 66
所以,正如您所见,从未调用过我们的printf
,罪魁祸首是mov QWORD PTR [rbp-8], OFFSET FLAT:.L4
行,其中L4
实际上对应于我们的bye
标签。
而且从生成的程序集中我所看到的,不是将hi
添加到生成的代码之后的零件中的一段代码。
但是至少该应用程序运行并且至少具有一些用于将c
与5进行比较的代码。
[在另一端,带有O0
的clang产生了以下噩梦,这些噩梦会崩溃:
# else goto bye is UNCOMMENTED
# clang -O0 also doesn't emit any instructions for the hi: block
fun: # @fun
push rbp
mov rbp, rsp
mov eax, 5
pop rbp
ret
main: # @main
push rbp
mov rbp, rsp
sub rsp, 48
mov dword ptr [rbp - 4], 0
mov dword ptr [rbp - 8], edi
mov qword ptr [rbp - 16], rsi
mov qword ptr [rbp - 24], 1
mov rax, qword ptr [rbp - 24]
mov qword ptr [rbp - 32], rax
call qword ptr [rbp - 32]
mov dword ptr [rbp - 36], eax
mov esi, dword ptr [rbp - 36]
movabs rdi, offset .L.str
mov al, 0
call printf
cmp dword ptr [rbp - 36], 5
jne .LBB1_2
jmp .LBB1_3
.LBB1_2:
jmp .LBB1_3
.LBB1_3:
mov eax, 66
add rsp, 48
pop rbp
ret
.L.str:
.asciz "TT: %d\n"
如果启用某些优化,例如O1
,则来自gcc:
# else goto bye is UNCOMMENTED
# gcc -O1
fun:
mov eax, 5
ret
.LC0:
.string "TT: %d\n"
main:
sub rsp, 8
mov eax, OFFSET FLAT:.L3
call rax
mov esi, eax
mov edi, OFFSET FLAT:.LC0
mov eax, 0
call printf
.L3:
mov eax, 66
add rsp, 8
ret
并且应用程序崩溃,这是可以理解的。再次,编译器完全删除了我们的hi
部分(mov eax, OFFSET FLAT:.L3
脚尖移到L3
,它对应于bye
部分),不幸的是,在rsp
之前增加ret
是个好主意,因此确保我们最终到达了一个完全与我们所需要的地方不同的地方。
然后c发出了更可疑的东西:
# else goto bye is UNCOMMENTED
# clang -O1
fun: # @fun
mov eax, 5
ret
main: # @main
push rax
mov eax, 1
call rax
mov edi, offset .L.str
mov esi, eax
xor eax, eax
call printf
mov eax, 66
pop rcx
ret
.L.str:
.asciz "TT: %d\n"
1
吗? c到底是怎么结束的?
某种程度上,我理解编译器认为不需要在if
和if
都移到同一位置的else
之后的死代码,但是我的知识和见识在这里停止了。
所以,亲爱的C和C ++专家,汇编迷和编译器粉碎者,这里出现了问题:
为什么?
您为什么认为如果我们添加了else
分支,编译器决定应该将这两个标签视为等效,或者为什么clang在其中添加了1,最后但并非最不重要:对C语言有深刻理解的人标准可能会指出这段代码与正常情况的差异如此之大,以至于我们最终陷入了这种非常奇怪的情况。