某日在群里看到这张很怪的表情包,想着这不是扯淡么,一看到运行结果顿时傻眼了。。。
先放一个在线编译C代码的网站:https://godbolt.org/
初步结果:只有在比较新的C++上才会触发,稳定复现,因此选定 clang++ test.cpp -O1
作为目标进行研究。
通过 objdump -d a.out
可以看到,_start
函数直接调用了 unreachable
函数。(一口盐汽水就喷了在屏幕上。。。)
gdb 调试,很诡异,也直接就断在了 unreachable 里,说明二进制确实执行到了这里。
什么?还有程序没有main、从别的函数开始执行?这连接器是抽什么风?
说实话,我还是第一次见到 “没有main函数、但是能运行” 的程序,于是查看符号表,发现main函数还在,和unreachable指向同一个偏移,但函数大小为0
也就是说,main函数的函数体被清空了,恰好指向了unreachable的位置,“越界”执行了下一个函数的代码。
clang++ -c -S test.cpp -O1
得到 test.s
汇编文件。 我去掉了汇编大部分不必要的注释,但故意保留了一部分,才能让读者知道,他读的是汇编 。可以看到,main函数里一条指令都没有,全部都是点开头的或者井号开头的注释。
去除全部注释后就是,main 和 _Z11unreachablev 指向同一段汇编代码:
clang++ -emit-llvm -S -c test.cpp -O1
得到 test.ll
IR文件。可以看出,确实在IR阶段main的内容就发生了变换,由原先的死循环被替换成了 unreachable。注意,这个unreachable是一条名叫UnreachableInstruction的指令,并非函数名
对比 -O0
的输出结果。
最终,可以得到结论,这个反常的现象是在IR层面的编译器优化引起的。
2022年我也遇到过编译器优化引发的逻辑错误,被 dmxcsnsbh 一眼看穿,并推荐我文章:https://www.blackhat.com/eu-20/briefings/schedule/#finding-bugs-compiler-knows-but-doesnt-tell-you-dissecting-undefined-behavior-optimizations-in-llvm-21128 。于是我第一反应就是 LLVM 进行了优化,但这个是不是 UB 引起的,一眼没看出来。
通过命令:clang++ -mllvm --print-before-all -mllvm --print-after-all test.cpp -O1
拿到每个pass的所有输入和输出。
经过搜索,找到如下日志,表明经过 LoopDeletionPass
的处理,原先的循环被替换成了 unreachable instruction
:
代码位于:https://llvm.org/doxygen/LoopDeletion_8cpp_source.html
大意就是,删除对执行无影响的死循环。个人观点,已经是死循环了,所以标记为 unreachable 也是说得过去的。
LoopDeletionPass
继承了 LoopPass
,实现 runOnLoop
,调用 deleteLoopIfDead
,内部使用 isLoopDead
判断是否需要被移除,移除时调用来自 LoopUtils.h
的 deleteDeadLoop
来执行。
而 deleteDeadLoop 中,出现了关键的 CreateUnreachable
:
C++里是UB。https://en.cppreference.com/w/cpp/language/ub ,Infinite loop without side-effects 。
C语言里不是UB,见 ISO/IEC 9899 J.2 Undefined behavior
后,并没有找到和loop相关的关键词。
包括官方还给了一个demo,优化后fermat直接返回true(不解释了,不会,自行领悟)
获得两个未经处理的 ll 文件:clang test.c -O0 -emit-llvm -c -S -o test.c.ll
和 clang++ test.cpp -O0 -emit-llvm -c -S -o test.cpp.ll
。
将 optnone
移除掉:sed -i 's/optnone//' test.c.ll
和 sed -i 's/optnone//' test.cpp.ll
。
使用 opt 将 LoopDeletionPass 作用于 C 的 IR 和 CPP 的 IR。(因为需要编译llvm,因为要看LLVM_DEBUG的日志,需要开启 DLLVM_ENABLE_ASSERTIONS=TRUE)
二者执行起来日志分别为:
opt作用于C语言
opt作用于C++语言
显然,二者出现了偏差,略微diff一下。
不清楚哪里引起的,把这个6和7补上去,C语言就可以正确地被处理为unreachable了,研究了很久,超出知识范围了,看不懂,反正就 LoopPass 认为 MaxBackedgeTakenCount
的数据不对,不给删。
结合上文说的C里不算UB、C++里算UB,编译器这么做也是完全正确的。
有时我自己也写点死循环,UB竟在我身边,UB竟是我自己。
int
main(){
while
(
1
) ;
}
void unreachable(){
printf(
"HelloWorld\n"
);
}
int
main(){
while
(
1
) ;
}
void unreachable(){
printf(
"HelloWorld\n"
);
}
编译选项 |
结果 |
gcc全版本 -O0/O1/O2 |
死循环 |
g++全版本 -O0/O1/O2 |
死循环 |
clang(C语言) 全版本 -O0/O1/O2 |
死循环 |
clang++(C++) >= 13.0.0 -O0 |
死循环 |
clang++(C++) >= 13.0.0 -O1/O2 |
打印并正常退出 |
Disassembly of section .text:
0000000000001050
<_start>:
1050
: f3
0f
1e
fa endbr64
1054
:
31
ed xor
%
ebp,
%
ebp
1056
:
49
89
d1 mov
%
rdx,
%
r9
1059
:
5e
pop
%
rsi
105a
:
48
89
e2 mov
%
rsp,
%
rdx
105d
:
48
83
e4 f0
and
$
0xfffffffffffffff0
,
%
rsp
1061
:
50
push
%
rax
1062
:
54
push
%
rsp
1063
:
45
31
c0 xor
%
r8d,
%
r8d
1066
:
31
c9 xor
%
ecx,
%
ecx
1068
:
48
8d
3d
d1
00
00
00
lea
0xd1
(
%
rip),
%
rdi
106f
: ff
15
6b
2f
00
00
call
*
0x2f6b
(
%
rip)
1075
: f4 hlt
1076
:
66
2e
0f
1f
84
00
00
cs nopw
0x0
(
%
rax,
%
rax,
1
)
Disassembly of section .text:
0000000000001050
<_start>:
1050
: f3
0f
1e
fa endbr64
1054
:
31
ed xor
%
ebp,
%
ebp
1056
:
49
89
d1 mov
%
rdx,
%
r9
1059
:
5e
pop
%
rsi
105a
:
48
89
e2 mov
%
rsp,
%
rdx
105d
:
48
83
e4 f0
and
$
0xfffffffffffffff0
,
%
rsp
1061
:
50
push
%
rax
1062
:
54
push
%
rsp
1063
:
45
31
c0 xor
%
r8d,
%
r8d
1066
:
31
c9 xor
%
ecx,
%
ecx
1068
:
48
8d
3d
d1
00
00
00
lea
0xd1
(
%
rip),
%
rdi
106f
: ff
15
6b
2f
00
00
call
*
0x2f6b
(
%
rip)
1075
: f4 hlt
1076
:
66
2e
0f
1f
84
00
00
cs nopw
0x0
(
%
rax,
%
rax,
1
)
gdb a.out
(gdb) b main
Breakpoint
1
at
0x1140
(gdb) r
Starting program:
/
tmp
/
a.out
[Thread debugging using libthread_db enabled]
Using host libthread_db library
"/lib/x86_64-linux-gnu/libthread_db.so.1"
.
Breakpoint
1
,
0x0000555555555140
in
unreachable() ()
(gdb) disassemble $pc
Dump of assembler code
for
function _Z11unreachablev:
=
>
0x0000555555555140
<
+
0
>: push
%
rax
0x0000555555555141
<
+
1
>: lea
0xebc
(
%
rip),
%
rdi
0x0000555555555148
<
+
8
>: call
0x555555555030
<puts@plt>
0x000055555555514d
<
+
13
>: pop
%
rax
0x000055555555514e
<
+
14
>: ret
End of assembler dump.
(gdb)
gdb a.out
(gdb) b main
Breakpoint
1
at
0x1140
(gdb) r
Starting program:
/
tmp
/
a.out
[Thread debugging using libthread_db enabled]
Using host libthread_db library
"/lib/x86_64-linux-gnu/libthread_db.so.1"
.
Breakpoint
1
,
0x0000555555555140
in
unreachable() ()
(gdb) disassemble $pc
Dump of assembler code
for
function _Z11unreachablev:
=
>
0x0000555555555140
<
+
0
>: push
%
rax
0x0000555555555141
<
+
1
>: lea
0xebc
(
%
rip),
%
rdi
0x0000555555555148
<
+
8
>: call
0x555555555030
<puts@plt>
0x000055555555514d
<
+
13
>: pop
%
rax
0x000055555555514e
<
+
14
>: ret
End of assembler dump.
(gdb)
readelf
-
s a.out
Symbol table
'.symtab'
contains
37
entries:
Num: Value Size
Type
Bind Vis Ndx Name
22
:
0000000000001140
0
FUNC GLOBAL DEFAULT
15
main
23
:
0000000000001140
15
FUNC GLOBAL DEFAULT
15
_Z11unreachablev
objdump
-
t a.out
0000000000001140
g F .text
0000000000000000
main
0000000000001140
g F .text
000000000000000f
_Z11unreachablev
readelf
-
s a.out
Symbol table
'.symtab'
contains
37
entries:
Num: Value Size
Type
Bind Vis Ndx Name
22
:
0000000000001140
0
FUNC GLOBAL DEFAULT
15
main
23
:
0000000000001140
15
FUNC GLOBAL DEFAULT
15
_Z11unreachablev
objdump
-
t a.out
0000000000001140
g F .text
0000000000000000
main
0000000000001140
g F .text
000000000000000f
_Z11unreachablev
.globl main
.p2align
4
,
0x90
.
type
main,@function
main:
.cfi_startproc
.Lfunc_end0:
.size main, .Lfunc_end0
-
main
.cfi_endproc
.globl _Z11unreachablev
.p2align
4
,
0x90
.
type
_Z11unreachablev,@function
_Z11unreachablev:
.cfi_startproc
pushq
%
rax
leaq .Lstr(
%
rip),
%
rdi
callq puts@PLT
popq
%
rax
retq
.globl main
.p2align
4
,
0x90
.
type
main,@function
main:
.cfi_startproc
.Lfunc_end0:
.size main, .Lfunc_end0
-
main
.cfi_endproc
.globl _Z11unreachablev
.p2align
4
,
0x90
.
type
_Z11unreachablev,@function
_Z11unreachablev:
.cfi_startproc
pushq
%
rax
leaq .Lstr(
%
rip),
%
rdi
callq puts@PLT
popq
%
rax
retq
main:
_Z11unreachablev:
pushq
%
rax
leaq .Lstr(
%
rip),
%
rdi
callq puts@PLT
popq
%
rax
retq
main:
_Z11unreachablev:
pushq
%
rax
leaq .Lstr(
%
rip),
%
rdi
callq puts@PLT
popq
%
rax
retq
; Function Attrs: mustprogress nofree norecurse noreturn nosync nounwind readnone uwtable willreturn
define dso_local noundef i32 @main() local_unnamed_addr
unreachable
}
; Function Attrs: mustprogress nofree norecurse noreturn nosync nounwind readnone uwtable willreturn
define dso_local noundef i32 @main() local_unnamed_addr
unreachable
}
; Function Attrs: mustprogress noinline norecurse nounwind optnone uwtable
define dso_local noundef i32 @main()
%
1
=
alloca i32, align
4
store i32
0
, i32
*
%
1
, align
4
br label
%
2
2
: ; preds
=
%
0
,
%
2
br label
%
2
, !llvm.loop !
6
}
; Function Attrs: mustprogress noinline norecurse nounwind optnone uwtable
define dso_local noundef i32 @main()
%
1
=
alloca i32, align
4
store i32
0
, i32
*
%
1
, align
4
br label
%
2
[招生]科锐逆向工程师培训(2024年11月15日实地,远程教学同时开班, 第51期)