asan提供了定位全局对象的构造顺序相关的方法,本文详细了解一下关于c/c++全局变量的构造顺序带来的问题
为了实施这个bug,我们需要两个cpp文件,其代码如下。
# cat initialization_order_fiasco_1.cpp #include <stdio.h> extern int extern_global; int __attribute__((noinline)) read_extern_global() { return extern_global; } int x = read_extern_global() + 1; int main() { printf("%d\n", x); return 0; } # cat initialization_order_fiasco_2.cpp int foo() { return 2; } int extern_global = foo();
根据上面的代码,我们知道有两个全局变量extern_global和x
此时我们通过修改编译顺序来复现问题
g++ -g initialization_order_fiasco_1.cpp initialization_order_fiasco_2.cpp -o initialization_order_fiasco_1_2 g++ -g initialization_order_fiasco_2.cpp initialization_order_fiasco_1.cpp -o initialization_order_fiasco_2_1
此时运行initialization_order_fiasco_1_2,如下
# ./initialization_order_fiasco_1_2 1
如果运行initialization_order_fiasco_2_1,如下
# ./initialization_order_fiasco_2_1 3
可以发现,因为我们g++传入文件的顺序不一致,而两个cpp中关于全局变量extern_global存在依赖,所以就导致了问题产生,按照我们理想的想法,这里的值应该是3,但是有可能是1
我们将得出正常结论的编译方式加入asan来检测,如下
g++ -fsanitize=address -g initialization_order_fiasco_2.cpp initialization_order_fiasco_1.cpp -o asan_2_1
注意,这里先加入initialization_order_fiasco_2.cpp后加入initialization_order_fiasco_1.cpp,也就是说先声明extern_global,再使用extern_global和声明x。
此时运行asan检测,没有上报问题。
现在我们将编译顺序调换,先声明x和使用extern_global,再声明extern_global
g++ -fsanitize=address -g initialization_order_fiasco_1.cpp initialization_order_fiasco_2.cpp -o asan_1_2
此时运行程序
# LD_PRELOAD=/usr/lib/aarch64-linux-gnu/libasan.so.5.0.0 ASAN_OPTIONS=check_initialization_order=true ./asan_1_2 ================================================================= ==133524==ERROR: AddressSanitizer: initialization-order-fiasco on address 0x0000004121e0 at pc 0x000000400948 bp 0x007fc3aff370 sp 0x007fc3aff390 READ of size 4 at 0x0000004121e0 thread T0 #0 0x400944 in read_extern_global() /root/asan/initialization_order/initialization_order_fiasco_1.cpp:4 #1 0x400a10 in __static_initialization_and_destruction_0 /root/asan/initialization_order/initialization_order_fiasco_1.cpp:6 #2 0x400a90 in _GLOBAL__sub_I__Z18read_extern_globalv /root/asan/initialization_order/initialization_order_fiasco_1.cpp:10 #3 0x400c54 in __libc_csu_init (/root/asan/initialization_order/asan_1_2+0x400c54) #4 0x7fa15efd34 in __libc_start_main (/lib/aarch64-linux-gnu/libc.so.6+0x20d34) #5 0x400828 (/root/asan/initialization_order/asan_1_2+0x400828) 0x0000004121e0 is located 0 bytes inside of global variable 'extern_global' defined in 'initialization_order_fiasco_2.cpp:2:5' (0x4121e0) of size 4 registered at: #0 0x7fa17b7a10 (/usr/lib/aarch64-linux-gnu/libasan.so.5.0.0+0x3aa10) #1 0x400bec in _sub_I_00099_1 (/root/asan/initialization_order/asan_1_2+0x400bec) #2 0x400c54 in __libc_csu_init (/root/asan/initialization_order/asan_1_2+0x400c54) #3 0x7fa15efd34 in __libc_start_main (/lib/aarch64-linux-gnu/libc.so.6+0x20d34) #4 0x400828 (/root/asan/initialization_order/asan_1_2+0x400828) SUMMARY: AddressSanitizer: initialization-order-fiasco /root/asan/initialization_order/initialization_order_fiasco_1.cpp:4 in read_extern_global() Shadow bytes around the buggy address: 0x0010000823e0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 0x0010000823f0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 0x001000082400: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 0x001000082410: f9 f9 f9 f9 f9 f9 f9 f9 f9 f9 f9 f9 f9 f9 f9 f9 0x001000082420: 00 00 f9 f9 f9 f9 f9 f9 f9 f9 00 00 00 00 00 00 =>0x001000082430: 04 f9 f9 f9 f9 f9 f9 f9 00 00 00 00[f6]f6 f6 f6 0x001000082440: f6 f6 f6 f6 00 00 00 00 00 00 00 00 00 00 00 00 0x001000082450: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 0x001000082460: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 0x001000082470: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 0x001000082480: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 Shadow byte legend (one shadow byte represents 8 application bytes):
根据上面的信息,得出如下结论
根据上面的信息,我们返回代码补充一个信息
int extern_global = foo();
默认情况下其值是2所以我们根据上面已有信息,我们就清晰的定位出来extern_global的初始化顺序存在问题。
补充一下,关于0x0000004121e0,因为是全局变量,所以我们在运行前就可以获取验证一下, 确定asan报错是正常的。
# objdump -d -j .bss ./asan_1_2 00000000004121e0 <extern_global>:
通过上面的内容,我们定位了extern_global的初始化顺序存在问题。其问题出现的原因在于g++的编译顺序。
我们先拆解g++的编译步骤,我们先编译.o文件
g++ -g -c initialization_order_fiasco_1.cpp -o 1.o g++ -g -c initialization_order_fiasco_2.cpp -o 2.o
此时针对1.o和2.o,我们读取其bss端的值如下
# objdump -d -j .bss 1.o 0000000000000000 <x>: 0: 00 00 00 00 # objdump -d -j .bss 2.o 0000000000000000 <extern_global>: 0: 00 00 00 00
可以看到,在编译阶段,全局变量加载地址还是0,它需要在链接阶段由ld填充实际地址。问题不出在编译阶段,那么接下来我们链接两个o文件
g++ 1.o 2.o -g -o 1_2 g++ 2.o 1.o -g -o 2_1
此时我们得到两个文件1_2/2_1。 对于libc函数调用的流程可以查看文章《程序的启动过程浅析》,静态变量会运行__static_initialization_and_destruction_0
函数来初始化静态全局变量的值,对于glibc的流程如下
_start __libc_start_main __libc_csu_init _GLOBAL__sub_I_XXX (gcc) __static_initialization_and_destruction_0
这里需要注意的是_GLOBAL__sub_I_XXX
是gcc为每个cpp文件生成的用于构造静态全局变量的构造函数,它的运行顺序就是每个cpp的添加顺序。
对于1_2程序,根据上面的推论,我们可以猜测其运行顺序如下
__libc_csu_init _GLOBAL__sub_I__Z18read_extern_globalv __static_initialization_and_destruction_0 _GLOBAL__sub_I__Z3foov __static_initialization_and_destruction_0
那么就是
int x = read_extern_global() + 1;
完成了x的赋值int extern_global = foo();
完成extern_global的赋值。那么这个BUG就出现了。
而对于2_1程序,其正常的原因是如下,我们照常推理其运行顺序
__libc_csu_init _GLOBAL__sub_I__Z3foov __static_initialization_and_destruction_0 _GLOBAL__sub_I__Z18read_extern_globalv __static_initialization_and_destruction_0
int extern_global = foo();
完成extern_global的赋值int x = read_extern_global() + 1;
完成了x的赋值。此时代码正常运行。
根据上面的信息,我们重点在于__libc_csu_init
调用CRT的顺序问题,我们可以对1_2程序打印断点。如下
# gdb ./1_2 (gdb) b _GLOBAL__sub_I__Z3foov (gdb) b _GLOBAL__sub_I__Z18read_extern_globalv (gdb) b __static_initialization_and_destruction_0
为了更好的判断extern_global的变量,这里挂上awatch,如下
(gdb) awatch extern_global Hardware access (read/write) watchpoint 4: extern_global
此时运行结果如下
(gdb) r Starting program: /root/asan/initialization_order/1_2 [Thread debugging using libthread_db enabled] Using host libthread_db library "/lib/aarch64-linux-gnu/libthread_db.so.1". Breakpoint 2, _GLOBAL__sub_I__Z18read_extern_globalv () at initialization_order_fiasco_1.cpp:10 10 } (gdb) c Continuing. Breakpoint 3, __static_initialization_and_destruction_0 (__initialize_p=1, __priority=65535) at initialization_order_fiasco_1.cpp:10 10 } (gdb) c Continuing. Hardware access (read/write) watchpoint 4: extern_global Value = 0 read_extern_global () at initialization_order_fiasco_1.cpp:5 5 } (gdb) c Continuing. Breakpoint 1, 0x00000000004006a8 in _GLOBAL__sub_I__Z3foov () at initialization_order_fiasco_2.cpp:2 2 int extern_global = foo(); (gdb) c Continuing. Breakpoint 3, __static_initialization_and_destruction_0 (__initialize_p=1, __priority=65535) at initialization_order_fiasco_2.cpp:2 2 int extern_global = foo(); (gdb) c Continuing. Hardware access (read/write) watchpoint 4: extern_global Old value = 0 New value = 2 0x000000000040068c in __static_initialization_and_destruction_0 (__initialize_p=1, __priority=65535) at initialization_order_fiasco_2.cpp:2 2 int extern_global = foo(); (gdb) c Continuing. 1 [Inferior 1 (process 137076) exited normally]
可以看到,出问题的程序1_2,在_GLOBAL__sub_I__Z18read_extern_globalv
中extern_global的值默认是0,由运行时默认赋值,然后在_GLOBAL__sub_I__Z3foov
中修改值为2,由代码初始化赋值,但是此时x的值已经在GLOBAL__sub_I__Z18read_extern_globalv
中取到为1了,所以问题出现了。
本文讨论了全局变量的顺序问题导致的BUG,此问题的原因主要在于gcc的链接过程中cpp的顺序会导致__libc_csu_init
运行_GLOBAL__sub_I_XXX
的运行顺序问题,从而导致问题的出现。
关于此问题的官方解释,有兴趣的可以翻阅,本文做简单的介绍:
"static initialization order problem":这里描述了多个静态类初始化如果存在依赖问题,则程序50%概率崩溃,解决办法是:"Construct On First Use"
"Why doesn’t the Construct On First Use Idiom use a static object instead of a static pointer":这里描述首次构造时应该使用对象而不是指针,因为指针会内存泄漏,但是如果使用对象,需要注意静态对象的销毁顺序问题。
https://isocpp.org/wiki/faq/ctors#static-init-order-on-first-use
"What is a technique to guarantee both static initialization and static deinitialization? ":静态对象构造和析构的技术要点
"How do I prevent the “static initialization order problem” for my static data members?":通过static Fred& x = X::x();
防止初始化问题
简单来说就说,如果非要使用静态全局对象,那么需要注意上述的每个要点,且都有利弊,否则不推荐频繁使用静态全局对象。
本文基于asan的官方文章阅读后,对asan调试内存问题的方法做一次上手,一方面能够清楚asan的工作原理,另一方面后面遇到内存问题的时候,可以留一份记忆使用asan来定位内存问题。此文章力求浅显易懂。
为了检测内存问题,asan做了两方面事情
对于1,preload的目的是从glibc的底层api上对函数堆栈进行符号化,例如打印如下格式
#0xabcdf function_name file_name.cc:1234
对于2,影子内存的作用是监听内存是否越界访问等,其思路是:
对系统的内存,按照1/8的方式创建一个影子区域 然后在影子区域的值按照如下方式进行标记 1. 0x0 代表此8个字节均正常访问 2. 0x1-0x7 代表部分字节可正常访问,其值是 8-k 3. 负数(0xf1-0xfe,等) 代表此内存不可访问原因如下
这样,我们通过影子内存可以知道内存的错误访问,然后通过ld preload可以将错误的内存的堆栈及线程相关信息打印出来。从而定位内存问题
asan能够定位的问题如下:
关于影子内存的值的解释,如下
Shadow byte legend (one shadow byte represents 8 application bytes): Addressable(可正常访问): 00 Partially addressable(部分可访问): 01 02 03 04 05 06 07 Heap left redzone(堆左边红区): fa Freed heap region(已释放区域): fd Stack left redzone(栈左边红区): f1 Stack mid redzone(栈中间红区): f2 Stack right redzone(栈右边红区): f3 Stack after return(返回后访问): f5 Stack use after scope(作用域之外访问栈): f8 Global redzone(全局变量红区): f9 Global init order(全局变量初始化顺序): f6 Poisoned by user(被用户下毒/污染): f7 Container overflow(容器溢出): fc Array cookie(数组越界访问): ac Intra object redzone(内部对象红区): bb ASan internal(asan内部访问): fe Left alloca redzone(alloca左红区): ca Right alloca redzone(alloca右红区): cb Shadow gap(间隔区域): cc
关于出错地址到影子区域的内存换算,其公式如下
Shadow = (Mem >> 3) + offset; [0x10007fff8000, 0x7fffffffffff] HighMem [0x02008fff7000, 0x10007fff7fff] HighShadow [0x00008fff7000, 0x02008fff6fff] ShadowGap [0x00007fff8000, 0x00008fff6fff] LowShadow [0x000000000000, 0x00007fff7fff] LowMem
故python转换程序如下
import sys def asan_shadow_addr(addr): result = ((addr >> 3) + 0x0000100000000000) & 0xFFFFFFFFFFFF return hex(result) if __name__ == "__main__": addr_str = sys.argv[1] addr = int(addr_str, 0) output = asan_shadow_addr(addr) print(f"shadow_addr:{output}")
我们创建一个run.sh,这样就不用每次都使用LD_PRELOAD环境变量了
# cat ./run.sh export LD_PRELOAD=/usr/lib/aarch64-linux-gnu/libasan.so.5.0.0 exec $@
下面通过测试代码来上手实验。
对于使用释放后区域,代码示例如下
int main(int argc, char **argv) { int *array = new int[100]; delete [] array; return array[argc]; // BOOM }
运行
# ./run.sh ./use_after_free ================================================================= ==64466==ERROR: AddressSanitizer: heap-use-after-free on address 0x007fa8803e44 at pc 0x0000004007fc bp 0x007fd9d45800 sp 0x007fd9d45820 READ of size 4 at 0x007fa8803e44 thread T0 #0 0x4007f8 in main /root/asan/use_after_free.cpp:4 #1 0x7fac394d8c in __libc_start_main (/lib/aarch64-linux-gnu/libc.so.6+0x20d8c) #2 0x4006a8 (/root/asan/use_after_free+0x4006a8) 0x007fa8803e44 is located 4 bytes inside of 400-byte region [0x007fa8803e40,0x007fa8803fd0) freed by thread T0 here: #0 0x7fac611d54 in operator delete[](void*) (/usr/lib/aarch64-linux-gnu/libasan.so.5.0.0+0xefd54) #1 0x400798 in main /root/asan/use_after_free.cpp:3 #2 0x7fac394d8c in __libc_start_main (/lib/aarch64-linux-gnu/libc.so.6+0x20d8c) #3 0x4006a8 (/root/asan/use_after_free+0x4006a8) previously allocated by thread T0 here: #0 0x7fac610f3c in operator new[](unsigned long) (/usr/lib/aarch64-linux-gnu/libasan.so.5.0.0+0xeef3c) #1 0x400780 in main /root/asan/use_after_free.cpp:2 #2 0x7fac394d8c in __libc_start_main (/lib/aarch64-linux-gnu/libc.so.6+0x20d8c) #3 0x4006a8 (/root/asan/use_after_free+0x4006a8) SUMMARY: AddressSanitizer: heap-use-after-free /root/asan/use_after_free.cpp:4 in main Shadow bytes around the buggy address: 0x001ff5100770: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa 0x001ff5100780: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa 0x001ff5100790: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa 0x001ff51007a0: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa 0x001ff51007b0: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa =>0x001ff51007c0: fa fa fa fa fa fa fa fa[fd]fd fd fd fd fd fd fd 0x001ff51007d0: fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd 0x001ff51007e0: fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd 0x001ff51007f0: fd fd fd fd fd fd fd fd fd fd fa fa fa fa fa fa 0x001ff5100800: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa 0x001ff5100810: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa ==64466==ABORTING
报错解析可以看到在栈区地址0x007fa8803e44读取了4字节,这里0x007fa8803e44是一个400字节的区域
READ of size 4 at 0x007fa8803e44 thread T0 0x007fa8803e44 is located 4 bytes inside of 400-byte region [0x007fa8803e40,0x007fa8803fd0)
pc的位置如下,其值是0x0000004007fc
#0 0x4007f8 in main /root/asan/use_after_free.cpp:4
内存释放的地方在测试代码第3行,如下:
freed by thread T0 here: #0 0x7fae794d54 in operator delete[](void*) (/usr/lib/aarch64-linux-gnu/libasan.so.5.0.0+0xefd54) #1 0x400798 in main /root/asan/use_after_free.cpp:3
内存申请的地方在测试代码第2行
previously allocated by thread T0 here: #0 0x7fae793f3c in operator new[](unsigned long) (/usr/lib/aarch64-linux-gnu/libasan.so.5.0.0+0xeef3c) #1 0x400780 in main /root/asan/use_after_free.cpp:2
对于影子区域内存转换如下
# python3 asan_shadow_addr.py 0x007fa8803e44 shadow_addr:0x100ff51007c8
可以看到0x100ff51007c8的值是0xfd,我们查表如下
Freed heap region: fd
返回代码中看,我们访问了数组的第1项,但访问之前,我们已经delete掉这个数组了。
delete [] array; return array[argc];
代码如下
int main(int argc, char **argv) { int *array = new int[100]; array[0] = 0; int res = array[argc + 100]; // BOOM delete [] array; return res; }
运行
# ./run.sh ./heap_out_of_bounds ================================================================= ==65270==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x007fb3303fd4 at pc 0x0000004008a4 bp 0x007fedcddd30 sp 0x007fedcddd50 READ of size 4 at 0x007fb3303fd4 thread T0 #0 0x4008a0 in main /root/asan/heap_out_of_bounds.cpp:4 #1 0x7fb6e60d8c in __libc_start_main (/lib/aarch64-linux-gnu/libc.so.6+0x20d8c) #2 0x400708 (/root/asan/heap_out_of_bounds+0x400708) 0x007fb3303fd4 is located 4 bytes to the right of 400-byte region [0x007fb3303e40,0x007fb3303fd0) allocated by thread T0 here: #0 0x7fb70dcf3c in operator new[](unsigned long) (/usr/lib/aarch64-linux-gnu/libasan.so.5.0.0+0xeef3c) #1 0x4007e0 in main /root/asan/heap_out_of_bounds.cpp:2 #2 0x7fb6e60d8c in __libc_start_main (/lib/aarch64-linux-gnu/libc.so.6+0x20d8c) #3 0x400708 (/root/asan/heap_out_of_bounds+0x400708) SUMMARY: AddressSanitizer: heap-buffer-overflow /root/asan/heap_out_of_bounds.cpp:4 in main Shadow bytes around the buggy address: 0x001ff66607a0: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa 0x001ff66607b0: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa 0x001ff66607c0: fa fa fa fa fa fa fa fa 00 00 00 00 00 00 00 00 0x001ff66607d0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 0x001ff66607e0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 =>0x001ff66607f0: 00 00 00 00 00 00 00 00 00 00[fa]fa fa fa fa fa 0x001ff6660800: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa 0x001ff6660810: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa 0x001ff6660820: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa 0x001ff6660830: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa 0x001ff6660840: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
报错信息可以看到从0x007fb3303fd4读取了4字节,0x007fb3303fd4属于400字节的区域范围外+8的字节处 0x007fb3303fd0处是小括号,不包含0x007fb3303fd0
READ of size 4 at 0x007fb3303fd4 thread T0 0x007fb3303fd4 is located 4 bytes to the right of 400-byte region [0x007fb3303e40,0x007fb3303fd0)
pc的地址是0x0000004008a4,指向代码第四行的下一条指令
#0 0x4008a0 in main /root/asan/heap_out_of_bounds.cpp:4
影子区域内存转换
# python3 asan_shadow_addr.py 0x007fb3303fd4 shadow_addr:0x100ff66607fa
对于出错原因是堆左红区
Heap left redzone: fa
返回代码看,这里访问了第101项(访问0x007fa7303fd4),因为0x100ff66607fa包含8字节的影子描述,所以此问题和访问第100项问题报错(访问0x007fa7303fd0)是一致的
出错代码如下
int *array = new int[100]; int res = array[argc + 100]; // BOOM
栈越界的测试代码如下
int main(int argc, char **argv) { int stack_array[100]; stack_array[1] = 0; return stack_array[argc + 100]; // BOOM }
运行
==113972==ERROR: AddressSanitizer: stack-buffer-overflow on address 0x007fd34029f4 at pc 0x000000400a94 bp 0x007fd34027c0 sp 0x007fd34027e0 READ of size 4 at 0x007fd34029f4 thread T0 #0 0x400a90 in main /root/asan/stack_of_bounds.cpp:4 #1 0x7fa46bcd8c in __libc_start_main (/lib/aarch64-linux-gnu/libc.so.6+0x20d8c) #2 0x400858 (/root/asan/stack_of_bounds+0x400858) Address 0x007fd34029f4 is located in stack of thread T0 at offset 452 in frame #0 0x400928 in main /root/asan/stack_of_bounds.cpp:1 This frame has 1 object(s): [48, 448) 'stack_array' (line 2) <== Memory access at offset 452 overflows this variable HINT: this may be a false positive if your program uses some custom stack unwind mechanism, swapcontext or vfork (longjmp and C++ exceptions *are* supported) SUMMARY: AddressSanitizer: stack-buffer-overflow /root/asan/stack_of_bounds.cpp:4 in main Shadow bytes around the buggy address: 0x001ffa6804e0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 0x001ffa6804f0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 0x001ffa680500: 00 00 00 00 00 00 f1 f1 f1 f1 f1 f1 00 00 00 00 0x001ffa680510: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 0x001ffa680520: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 =>0x001ffa680530: 00 00 00 00 00 00 00 00 00 00 00 00 00 00[f3]f3 0x001ffa680540: f3 f3 f3 f3 f3 f3 00 00 00 00 00 00 00 00 00 00 0x001ffa680550: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 0x001ffa680560: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 0x001ffa680570: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 0x001ffa680580: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
从报错可以了解0x007fd34029f4处读取了4字节,并且其位于frame的第452字节,其中stack_array范围是[48, 448)
,那么越界访问了第4-8个字节,错在stack_of_bounds.cpp的第4行。摘出错误信息如下
READ of size 4 at 0x007fd34029f4 thread T0 Address 0x007fd34029f4 is located in stack of thread T0 at offset 452 in frame [48, 448) 'stack_array' (line 2) <== Memory access at offset 452 overflows this variable SUMMARY: AddressSanitizer: stack-buffer-overflow /root/asan/stack_of_bounds.cpp:4 in main
影子区域内存转换如下
# python3 asan_shadow_addr.py 0x007fd34029f4 shadow_addr:0x100ffa68053e
可以看到其值是f3,也就是栈右红区
=>0x001ffa680530: 00 00 00 00 00 00 00 00 00 00 00 00 00 00[f3]f3 Stack right redzone: f3
返回代码中看,这里访问了stack_array[101]
正好是栈区的第4-8字节。
当然,如果想复现栈左红区的报错,我们访问数组的-1
项即可stack_array[-1]
,那么错误如下
[48, 448) 'stack_array' (line 2) <== Memory access at offset 44 underflows this variable =>0x001ffc07f0f0: 00 00 00 00 00 00 00 00 f1 f1 f1 f1 f1[f1]00 00 0x001000082240: f9 f9 f9 f9 00 00 f9 f9 f9 f9 f9 f9 f9 f9 00 00 Stack left redzone: f1
测试代码如下
int global_array[100] = {-1}; int main(int argc, char **argv) { return global_array[argc + 100]; // BOOM }
运行
==115097==ERROR: AddressSanitizer: global-buffer-overflow on address 0x0000004111f4 at pc 0x000000400814 bp 0x007ff0360e80 sp 0x007ff0360ea0 READ of size 4 at 0x0000004111f4 thread T0 #0 0x400810 in main /root/asan/global_out_of_bounds.cpp:3 #1 0x7f8fecad8c in __libc_start_main (/lib/aarch64-linux-gnu/libc.so.6+0x20d8c) #2 0x4006d8 (/root/asan/global_out_of_bounds+0x4006d8) 0x0000004111f4 is located 4 bytes to the right of global variable 'global_array' defined in 'global_out_of_bounds.cpp:1:5' (0x411060) of size 400 SUMMARY: AddressSanitizer: global-buffer-overflow /root/asan/global_out_of_bounds.cpp:3 in main Shadow bytes around the buggy address: 0x0010000821e0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 0x0010000821f0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 0x001000082200: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 0x001000082210: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 0x001000082220: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 =>0x001000082230: 00 00 00 00 00 00 00 00 00 00 00 00 00 00[f9]f9 0x001000082240: f9 f9 f9 f9 00 00 f9 f9 f9 f9 f9 f9 f9 f9 00 00 0x001000082250: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 0x001000082260: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 0x001000082270: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 0x001000082280: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
通过上面报错信息可以看出
[0x411060, 0x4111f0)
影子区域内存转换如下
# python3 asan_shadow_addr.py 0x0000004111f4 shadow_addr:0x10000008223e =>0x001000082230: 00 00 00 00 00 00 00 00 00 00 00 00 00 00[f9]f9 Global redzone: f9
对应代码验证确实是访问了全局变量之外的第4-8字节。
return global_array[1 + 100];
这里再多提一个细节,并不是所有的内存问题asan都能检测,我们留意影子区域布局如下
=>0x001000082230: 00 00 00 00 00 00 00 00 00 00 00 00 00 00[f9]f9 0x001000082240: f9 f9 f9 f9 00 00 f9 f9 f9 f9 f9 f9 f9 f9 00 00
可以看到在0x001000082244-0x001000082245两个影子字节处是0,那么意味着如果全局变量溢出的问题在这对应的16字节,asan将不起作用,我们修改代码验证。
我们计算当前错误离为0的影子字节个数为5,那么需要额外越界 5*8/4+1
.此时原来的越界数组值是101,我们加上刚刚计算的11,得出112。那么代码如下
return global_array[112];
编译验证,发现asan检测不出这个溢出问题了。
测试代码如下
int *ptr; __attribute__((noinline)) void FunctionThatEscapesLocalObject() { int local[100]; ptr = &local[0]; } int main(int argc, char **argv) { FunctionThatEscapesLocalObject(); return ptr[argc]; }
值得注意的是:
ASAN_OPTIONS=detect_stack_use_after_return=1
此时运行
# ASAN_OPTIONS=detect_stack_use_after_return=1 ./run.sh ./use_after_return ==115526==ERROR: AddressSanitizer: stack-use-after-return on address 0x007f9e1e6034 at pc 0x000000400b24 bp 0x007fee1358f0 sp 0x007fee135910 READ of size 4 at 0x007f9e1e6034 thread T0 #0 0x400b20 in main /root/asan/use_after_return.cpp:18 #1 0x7fa192cd8c in __libc_start_main (/lib/aarch64-linux-gnu/libc.so.6+0x20d8c) #2 0x4008b8 (/root/asan/use_after_return+0x4008b8) Address 0x007f9e1e6034 is located in stack of thread T0 at offset 52 in frame #0 0x400988 in FunctionThatEscapesLocalObject() /root/asan/use_after_return.cpp:11 This frame has 1 object(s): [48, 448) 'local' (line 12) <== Memory access at offset 52 is inside this variable HINT: this may be a false positive if your program uses some custom stack unwind mechanism, swapcontext or vfork (longjmp and C++ exceptions *are* supported) SUMMARY: AddressSanitizer: stack-use-after-return /root/asan/use_after_return.cpp:18 in main Shadow bytes around the buggy address: 0x001ff3c3cbb0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 0x001ff3c3cbc0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 0x001ff3c3cbd0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 0x001ff3c3cbe0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 0x001ff3c3cbf0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 =>0x001ff3c3cc00: f5 f5 f5 f5 f5 f5[f5]f5 f5 f5 f5 f5 f5 f5 f5 f5 0x001ff3c3cc10: f5 f5 f5 f5 f5 f5 f5 f5 f5 f5 f5 f5 f5 f5 f5 f5 0x001ff3c3cc20: f5 f5 f5 f5 f5 f5 f5 f5 f5 f5 f5 f5 f5 f5 f5 f5 0x001ff3c3cc30: f5 f5 f5 f5 f5 f5 f5 f5 f5 f5 f5 f5 f5 f5 f5 f5 0x001ff3c3cc40: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 0x001ff3c3cc50: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
通过上面报错信息可以看出
local[1]
影子区域内存转换如下
# python3 asan_shadow_addr.py 0x007f9e1e6034 shadow_addr:0x100ff3c3cc06 0x001ff3c3cbf0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 =>0x001ff3c3cc00: f5 f5 f5 f5 f5 f5[f5]f5 f5 f5 f5 f5 f5 f5 f5 f5 Stack after return: f5
根据上面的信息,有个细节值得注意的是
下面演示asan无法检测的问题,我们修改代码,根据推算我们需要访问推前 -6*8/4+1
的地址,那么下毒就没有意义了。
那么修改代码如下
return ptr[-13];
此时运行可以发现asan检测不到问题
测试代码如下
volatile int *p = 0; int main() { { int x = 0; p = &x; } *p = 5; return 0; }
运行
# ./run.sh ./use_after_scope ================================================================= ==117169==ERROR: AddressSanitizer: stack-use-after-scope on address 0x007fd2d42760 at pc 0x000000400a88 bp 0x007fd2d426f0 sp 0x007fd2d42710 WRITE of size 4 at 0x007fd2d42760 thread T0 #0 0x400a84 in main /root/asan/use_after_scope.cpp:15 #1 0x7fb7b40d8c in __libc_start_main (/lib/aarch64-linux-gnu/libc.so.6+0x20d8c) #2 0x400858 (/root/asan/use_after_scope+0x400858) Address 0x007fd2d42760 is located in stack of thread T0 at offset 32 in frame #0 0x400928 in main /root/asan/use_after_scope.cpp:10 This frame has 1 object(s): [32, 36) 'x' (line 12) <== Memory access at offset 32 is inside this variable HINT: this may be a false positive if your program uses some custom stack unwind mechanism, swapcontext or vfork (longjmp and C++ exceptions *are* supported) SUMMARY: AddressSanitizer: stack-use-after-scope /root/asan/use_after_scope.cpp:15 in main Shadow bytes around the buggy address: 0x001ffa5a8490: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 0x001ffa5a84a0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 0x001ffa5a84b0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 0x001ffa5a84c0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 0x001ffa5a84d0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 =>0x001ffa5a84e0: 00 00 00 00 00 00 00 00 f1 f1 f1 f1[f8]f3 f3 f3 0x001ffa5a84f0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 0x001ffa5a8500: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 0x001ffa5a8510: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 0x001ffa5a8520: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 0x001ffa5a8530: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
报错信息如下
那么就是在x的作用域之外访问了x
影子区域内存转换如下
# python3 asan_shadow_addr.py 0x007fd2d42760 shadow_addr:0x100ffa5a84ec =>0x001ffa5a84e0: 00 00 00 00 00 00 00 00 f1 f1 f1 f1[f8]f3 f3 f3 Stack use after scope: f8
这里f8的位置的0-4个字节就是对应实际内存的x的位置
#include <stdlib.h> void *p; int main() { p = malloc(7); p = 0; // The memory is leaked here. return 0; }
上面代码分配了7个字节给p,然后对指针p赋予空指针,这样代码就存在7字节的内存泄漏,运行如下
# ./run.sh ./memory-leak ================================================================= ==118406==ERROR: LeakSanitizer: detected memory leaks Direct leak of 7 byte(s) in 1 object(s) allocated from: #0 0x7f8ee4025c in __interceptor_malloc (/usr/lib/aarch64-linux-gnu/libasan.so.5.0.0+0xed25c) #1 0x400798 in main /root/asan/memory-leak.cpp:6 #2 0x7f8ebc5d8c in __libc_start_main (/lib/aarch64-linux-gnu/libc.so.6+0x20d8c) #3 0x4006c8 (/root/asan/memory-leak+0x4006c8) SUMMARY: AddressSanitizer: 7 byte(s) leaked in 1 allocation(s).
这个堆栈浅显易懂,就不解释了。
本文根据官方的文档,将几种asan定位内存问题进行了上手实验,能够加深对asan工具的使用细节的理解。但故意遗漏了关于全局对象构造顺序相关的问题讨论,这块问题后面会专门提,就不在此篇幅解释了。
单例的目的是保证一个类只有一个实例,这样代码在调用的时候不需要频繁的初始化,但是在调试的过程中,单例可能出现因为多线程访问导致的概率性bug,本文主要讨论单例出现的这种问题
为了支持单例,最明显的代码就是实现一个getInstance函数,这个函数中在第一次访问的时候new自己的对象,之后所有的调用通过getInstance来调用类的函数,代码示例如下:
GlobalConfig *GlobalConfig::getInstance() { if(instance == nullptr) // a instance = new GlobalConfig; // b return instance; }
对于单例,如果在单线程环境下,上述代码不会有任何问题,如果instance是nullptr,则新建一个,如果不是,则返回这个instance。
但是如果是多线程的情况,满足instance都没有创建的情况下,两个线程同时调用了getInstance函数,那么可能出现:
第二个线程判断了instance此时还是nullptr,所以再一次构造了GlobalConfig,这样子我们对这个单例构造了两次。这是不必要的开销。
为了解决多线程环境下出现的单例构造两次的问题,我们可以对这段代码加锁,如下
GlobalConfig *GlobalConfig::getInstance() { LOCK(); if(instance == nullptr) // a instance = new GlobalConfig; // b UNLOCK(); return instance; }
此时我们通过锁解决了上面提到的单例构造两次的问题,因为当第一个线程正在执行时会上锁,这样第二个线程就不会重入代码内部。因为锁也是原子的,所以我们似乎解决了单例构造两次的问题。
但是,上面代码会引入新的问题,我们知道getInstance函数在99%的情况下instance是有值的,而在1%的情况下是需要构造的,这也就导致了这个锁在99%的情况下是不需要的。那么怎么优化这个问题呢?
对于上面的代码,我们需要解决锁带来的性能开销问题,因为我们不能因为1%的场景去让代码执行99%的任务。所以我们需要进行double-check,那么具体是什么样的呢,如下代码
GlobalConfig *GlobalConfig::getInstance() { if(instance == nullptr) { LOCK(); if(instance == nullptr) // a instance = new GlobalConfig; // b UNLOCK(); } return instance; }
这段代码通过重复检测instance的值,从而避免了99%的不必要加锁操作。它看似会正常工作了。
但是我们对构造GlobalConfig这个类需要再分析一下,它主要做如下事情
如果上面步骤1,2,3都是顺序执行的,那么这一切都是正常的,那么假设步骤2和步骤3的顺序颠倒呢?
那么程序是否崩溃就取决于是否访问到没有权限不可访问的区域了,如果访问到了,就崩溃,如果不是,那么可能修改错误的值,或者存在了一个隐藏的bug。
根据上面提到的,步骤2和步骤3在体系架构中真会出现,这主要原因是cpu的乱序执行。
我们知道一条指令在cpu的流水线执行包含如下几个部分
取指---译码---执行---访存---写回
这些步骤是顺序的,但是一般来说CPU会有多级流水线,这样就会出现如下情况。以三级流水线为例
取指---译码---执行---访存---写回 取指---译码---执行---访存---NOP 取指---译码---执行---NOP---写回
而且,我们知道CPU访问内存的速度是慢于CPU自身时钟频率的,所以在取指和访存以及写回三个步骤上会比较慢。所以为了加快CPU的指令执行,CPU乱序执行的就出现了。其含义是:
对于上面的例子,我们知道步骤2是调用构造函数,步骤3是将内存地址返回,对于它们而言,其执行是理论可以乱序的,所以就可以出现运行步骤是 1-->3-->2
这样的现象。
根据上面提到的CPU乱序问题,我们也可以很好解决,那就是让两行代码出现数据依赖就行了,那么代码如下
GlobalConfig *GlobalConfig::getInstance() { if(instance == nullptr) { LOCK(); if(instance == nullptr) // a GlobalConfig* temp = new GlobalConfig; // b instance = temp //c UNLOCK(); } return instance; }
从代码的第b行和第c行看,我们将构造的对象赋值给了temp,然后再将temp传递给instance。它能够完美规避CPU乱序的问题。
那还有什么问题没有呢? 实则不然,我们知道编译器会将指令乱序,这段代码仍可能出现乱序执行的问题。
虽然我们在代码b和代码c行特地构造了数据依赖,让我们人眼直观来看CPU不会针对这行指令进行乱序了,但是我们知道通常情况下代码编译成汇编的时候,编译器会优化代码,如果关闭优化如O(0),则代码执行效率低,所以通常编译默认的优化等级是O(2)。在默认情况下,上述代码编译器会认为temp是没有意义的变量,故合并两行代码。
这样,我们看似自行做的优化,实际上在编译阶段就已经被编译器进行了负优化了。那么我们只能寻找其他办法。
barrier的含义就是栅栏的意思,它同样适用于编译器,在aarch64中,有三种barrier指令,如下
对于上述的问题,我们知道编译器也会执行乱序,所以为了让编译器不做乱序优化,我们在其数据依赖的中间添加任意的barrier指令,避免编译器的错误优化,代码如下
GlobalConfig *GlobalConfig::getInstance() { if(instance == nullptr) { LOCK(); if(instance == nullptr) // a GlobalConfig* temp = new GlobalConfig; // b barrier(); //d instance = temp //c UNLOCK(); } return instance; }
如上,我们添加了第d行,这个barrier可以是任意的barrier指令,例如DMB,它可以使得编译器不进行乱序优化。这样就真正的解决了单例在多线程中的所有问题。
根据上面的介绍,我们为了解决单例在多线程访问中并发的问题做了很多的思考,使得代码看起来非常的乱,甚至是过度优化,对于这种情况,实际上还有最优解。那就是资源最早初始化。
这里表达的意思就是:
如果把getInstance函数放在初始化的单线程中,这样所有的多线程访问都不可能进入instance == nullptr
的逻辑内。
如果就将约束程序员在代码的第一次进入getInstance时是单线程的,那或许比较困难(谁知道其他程序员怎么想~),那么另一种方法或许风险更小,就是在每个线程开始的时候,主动调用如下
GlobalConfig* const instance = GlobalConfig::getInstance();
这样这个instance的值引用的instance并且保存在cache中。从而使得后面调用所有的getInstance都不需要考虑多线程重入的问题。
本文基于线程安全的角度介绍了单例在多线程中的bug,如果我们为了解决一个一个多线程重入导致的问题,那么代码可读性就会很差,并且容易出现负优化,如果我们在这种情况下,对每个线程的最开始主动增加一次getInstance调用,后面的调用实际上利用了cache,那么反而其整体性能最高。
https://www.aristeia.com/Papers/DDJ_Jul_Aug_2004_revised.pdf
在处理内存泄漏问题上,我们有个bug就是循环播放14天视频,会出现内存上升大概20M的样子,也就是这个问题,发现了QThread的隐藏问题:如果QThread完成之后,不显式的连接finished信号并执行deleteLater,那么QThread就会造成内存泄漏。这个内存泄漏用任意的内存泄漏检测工具都可以轻易的检测出来。关于本问题,连续播放视频一周,那么意味着创建了无数个QThread结构实例,但是没有被回收。
本文基于asan获取的堆栈如下
Indirect leak of 272000 byte(s) in 1700 object(s) allocated from: #0 0x7f83cc0d5c in operator new(unsigned long) (/usr/lib/aarch64-linux-gnu/libasan.so.5.0.0+0xeed5c) #1 0x7f7fb9e89c in QThread::QThread(QObject*) (/lib/aarch64-linux-gnu/libQt5Core.so.5+0xa189c) #2 0x7f7fb9ea60 in QThread::createThreadImpl(std::future<void>&&) (/lib/aarch64-linux-gnu/libQt5Core.so.5+0xa1a60) #3 0x5564d9e098 in create<MpvCore::LoadFileInfo()::<lambda()> > /usr/include/aarch64-linux-gnu/qt5/QtCore/qthread.h:235 #4 0x5564d9e098 in MpvCore::LoadFileInfo() core/mpvcore.cpp:1163 #5 0x5564da679c in MpvCore::event(QEvent*) core/mpvcore.cpp:1670 #6 0x7f809088e8 in QApplicationPrivate::notify_helper(QObject*, QEvent*) (/lib/aarch64-linux-gnu/libQt5Widgets.so.5+0x15e8e8) #7 0x7f80911eec in QApplication::notify(QObject*, QEvent*) (/lib/aarch64-linux-gnu/libQt5Widgets.so.5+0x167eec) #8 0x7f7fd6a9c0 in QCoreApplication::notifyInternal2(QObject*, QEvent*) (/lib/aarch64-linux-gnu/libQt5Core.so.5+0x26d9c0) #9 0x7f7fd6d8d4 in QCoreApplicationPrivate::sendPostedEvents(QObject*, int, QThreadData*) (/lib/aarch64-linux-gnu/libQt5Core.so.5+0x2708d4) #10 0x7f7fdc89c4 (/lib/aarch64-linux-gnu/libQt5Core.so.5+0x2cb9c4) #11 0x7f80f44708 in g_main_context_dispatch (/lib/aarch64-linux-gnu/libglib-2.0.so.0+0x51708) #12 0x7f80f44974 (/lib/aarch64-linux-gnu/libglib-2.0.so.0+0x51974) #13 0x7f80f44a18 in g_main_context_iteration (/lib/aarch64-linux-gnu/libglib-2.0.so.0+0x51a18) #14 0x7f7fdc7e70 in QEventDispatcherGlib::processEvents(QFlags<QEventLoop::ProcessEventsFlag>) (/lib/aarch64-linux-gnu/libQt5Core.so.5+0x2cae70) #15 0x7f7fd6916c in QEventLoop::exec(QFlags<QEventLoop::ProcessEventsFlag>) (/lib/aarch64-linux-gnu/libQt5Core.so.5+0x26c16c) #16 0x7f7fd71770 in QCoreApplication::exec() (/lib/aarch64-linux-gnu/libQt5Core.so.5+0x274770) #17 0x5564d7ed60 in main src/main.cpp:68 #18 0x7f7f690d8c in __libc_start_main (/lib/aarch64-linux-gnu/libc.so.6+0x20d8c) #19 0x5564d886a4 (/home/kylin/kylin-video+0x686a4)
根据上面的堆栈,我们看到泄漏存在于QThread的构造函数中。我们翻一下qtbase的源码
class QThreadCreateThread : public QThread { public: explicit QThreadCreateThread(std::future<void> &&future) : m_future(std::move(future)) { } private: void run() override { m_future.get(); } std::future<void> m_future; }; QThread *QThread::createThreadImpl(std::future<void> &&future) { return new QThreadCreateThread(std::move(future)); } QThread::QThread(QObject *parent) : QObject(*(new QThreadPrivate), parent) { Q_D(QThread); // fprintf(stderr, "QThreadData %p created for thread %p\n", d->data, this); d->data->thread = this; }
可以很明显的看到QThread执行了new操作,和asan给出的报告相符合。
我们参照qt的文档如下
When any QObject in the tree is deleted, if the object has a parent, the destructor automatically removes the object from its parent
如果QObject对象有父对象,那么会在父对象中析构所有的成员
但是我们要知道的是QThread继承于QObject,但是没有父对象
qt的文档隐晦的提示你要调用QObject::deleteLater()。文章链接如下
void QThread::finished() This signal is emitted from the associated thread right before it finishes executing. When this signal is emitted, the event loop has already stopped running. No more events will be processed in the thread, except for deferred deletion events. This signal can be connected to QObject::deleteLater(), to free objects in that thread.
根据上面的信息,我们可以总结出来,QThread创建的线程,如果没有父对象,那么需要显式的连接finished信号来执行deleteLater,否则就需要将其绑定到一个对象上。所以解决问题的方法有两种:
QThread* thread = new QThread; Worker* worker = new Worker; worker->setParent(thread); worker->moveToThread(thread);
这种情况下,其对象是否析构取决于worker的管理。
QThread::connect(thread, &QThread::finished, thread, &QThread::deleteLater);
一般QThread不会频繁的绑定对象父子关系,这样不容易管理,所以对于父子关系不清晰的代码,建议直接连接finished即可
对于bug而言,我解决此问题的方法是连接finished,为了测试,下面提供了复现这个问题的测试代码
#include <QThread> #include <QDebug> int main(int argc, char *argv[]) { Q_UNUSED(argc); Q_UNUSED(argv); for (int i = 0; i < 100; ++i) { QThread* thread = QThread::create([i]{ qDebug() << "Thread Started:" << i; }); thread->start(); // QThread::connect(thread, &QThread::finished, thread, &QThread::deleteLater); } QThread::sleep(3); return 0; }
我创建100个线程,如果不连接finished信号调用deleteLater,那么可以很明显的报告内存泄漏,如果添加信号则代码正常。
================================================================= ==52273==ERROR: LeakSanitizer: detected memory leaks Indirect leak of 16000 byte(s) in 100 object(s) allocated from: #0 0x7fbcb04d5c in operator new(unsigned long) (/usr/lib/aarch64-linux-gnu/libasan.so.5.0.0+0xeed5c) #1 0x7fbbfce89c in QThread::QThread(QObject*) (/lib/aarch64-linux-gnu/libQt5Core.so.5+0xa189c) #2 0x7fbbfcea60 in QThread::createThreadImpl(std::future<void>&&) (/lib/aarch64-linux-gnu/libQt5Core.so.5+0xa1a60) #3 0x55855c38b4 in create<main(int, char**)::<lambda()> > /usr/include/aarch64-linux-gnu/qt5/QtCore/qthread.h:199 #4 0x55855c38b4 in main /tmp/qthread/test.cpp:9 #5 0x7fbbac2d8c in __libc_start_main (/lib/aarch64-linux-gnu/libc.so.6+0x20d8c) #6 0x55855c41e0 (/root/test+0x41e0) Indirect leak of 12000 byte(s) in 100 object(s) allocated from: #0 0x7fbcb04d5c in operator new(unsigned long) (/usr/lib/aarch64-linux-gnu/libasan.so.5.0.0+0xeed5c) #1 0x7fbbfce8fc in QThread::QThread(QObject*) (/lib/aarch64-linux-gnu/libQt5Core.so.5+0xa18fc) #2 0x7fbbfcea60 in QThread::createThreadImpl(std::future<void>&&) (/lib/aarch64-linux-gnu/libQt5Core.so.5+0xa1a60) #3 0x55855c38b4 in create<main(int, char**)::<lambda()> > /usr/include/aarch64-linux-gnu/qt5/QtCore/qthread.h:199 #4 0x55855c38b4 in main /tmp/qthread/test.cpp:9 #5 0x7fbbac2d8c in __libc_start_main (/lib/aarch64-linux-gnu/libc.so.6+0x20d8c) #6 0x55855c41e0 (/root/test+0x41e0) Indirect leak of 10400 byte(s) in 100 object(s) allocated from: #0 0x7fbcb04d5c in operator new(unsigned long) (/usr/lib/aarch64-linux-gnu/libasan.so.5.0.0+0xeed5c) #1 0x7fbbfd6e40 in QWaitCondition::QWaitCondition() (/lib/aarch64-linux-gnu/libQt5Core.so.5+0xa9e40) #2 0x7fbbfce8f0 in QThread::QThread(QObject*) (/lib/aarch64-linux-gnu/libQt5Core.so.5+0xa18f0) #3 0x7fbbfcea60 in QThread::createThreadImpl(std::future<void>&&) (/lib/aarch64-linux-gnu/libQt5Core.so.5+0xa1a60) #4 0x55855c38b4 in create<main(int, char**)::<lambda()> > /usr/include/aarch64-linux-gnu/qt5/QtCore/qthread.h:199 #5 0x55855c38b4 in main /tmp/qthread/test.cpp:9 #6 0x7fbbac2d8c in __libc_start_main (/lib/aarch64-linux-gnu/libc.so.6+0x20d8c) #7 0x55855c41e0 (/root/test+0x41e0) Indirect leak of 3200 byte(s) in 100 object(s) allocated from: #0 0x7fbcb04d5c in operator new(unsigned long) (/usr/lib/aarch64-linux-gnu/libasan.so.5.0.0+0xeed5c) #1 0x7fbbfcea54 in QThread::createThreadImpl(std::future<void>&&) (/lib/aarch64-linux-gnu/libQt5Core.so.5+0xa1a54) #2 0x55855c38b4 in create<main(int, char**)::<lambda()> > /usr/include/aarch64-linux-gnu/qt5/QtCore/qthread.h:199 #3 0x55855c38b4 in main /tmp/qthread/test.cpp:9 #4 0x7fbbac2d8c in __libc_start_main (/lib/aarch64-linux-gnu/libc.so.6+0x20d8c) #5 0x55855c41e0 (/root/test+0x41e0) SUMMARY: AddressSanitizer: 41600 byte(s) leaked in 400 allocation(s).