我们人类(我们假设是人类在读这本书,而不是某种形式的人工智能,现在谁知道呢)擅长许多错综复杂的任务;但我们也不擅长许多平凡的任务。 这就是我们发明计算机的原因--用软件来驱动它们!
井。 我们并不擅长发现隐藏在 C(或汇编)代码内部的细节-内存错误是我们人类可以使用帮助的最好例子。 所以,你猜怎么着:我们发明了软件工具来帮助我们-它们做着平凡而乏味的工作,检测和检查我们数以百万计和数十亿行的代码和二进制文件,并且在捕捉我们的错误方面变得非常有效。 当然,说到底,最好的工具仍然是你的大脑,但人们可能会问:谁会调试你用来调试的工具,用什么来调试?答案当然是更多的工具,还有你这个人类程序员。
在本章中,读者将学习使用两款同类最佳的内存调试工具:
- 瓦尔格林德(氏)Memcheck
- 消毒剂工具(ASAN)
提供了总结和比较它们的功能的有用的表格。 此外,还可以看到 glibc 通过mallopt(3)
进行的 malloc 调优。
This particular chapter has no source code of it's own; instead, we use the source code from the preceding chapter, Chapter 5, *Linux Memory Issues. *Our membugs
program test cases will be tried and tested under both Valgrind and ASan to see if they can catch the memory bugs that our memugs program's test cases work hard to provide. Thus, we definitely suggest you look over the previous chapter, and the membugs.c
source code, to regain familiarity with the test cases we will be running.
一般而言,在这些领域的范围内,有两种工具:
- 动态分析工具
- 静态分析工具
动态分析工具本质上是通过检测运行时流程来工作的。 因此,为了最大限度地利用它们,必须投入大量精力来确保这些工具实际运行在所有可能的代码路径上;要做到这一点,必须仔细而艰苦地编写测试用例,以确保完整的代码覆盖*。* 这是一个关键点,我们将再次提及(重要的是,第 19 章、故障排除和最佳实践涵盖了这些要点)。 虽然功能非常强大,但动态分析工具通常会导致显著的运行时性能损失和更多的内存使用。
另一方面,静态分析工具处理源代码;从这个意义上说,它们类似于编译器。 它们通常远远超出了典型的编译器,帮助开发人员发现各种潜在的 bug。 也许最初的 Unixlint程序可以被认为是当今强大的静态分析器的前身。 如今,功能非常强大的商业静态分析器(带有花哨的 GUI 前端)已经存在,人们在它们上面花费的金钱和时间都是物有所值的。 缺点是,这些工具可能会引发很多误报;更好的工具可以让程序员执行有用的过滤。 我们不会在本文中讨论静态分析器(请参阅 GitHub 存储库上的进一步阅读部分,以获取 C/C++的静态分析器列表)。
现在,让我们来看看一些现代内存调试工具;它们都属于高级动态分析工具类。 一定要学会如何有效地使用它们--它们是对抗各种未定义行为(UB)的必要武器。
Valgrind(发音为val-grinned)是一套强大工具的工具框架。 它是开源软件(OSS),根据 GNU GPL 版本的条款发布。 2;它最初是由朱利安·苏厄德(Julian Seward)开发的。 Valgrind 是一套屡获殊荣的内存调试和分析工具套件。 它已经发展成为创建动态分析工具的框架。 实际上,它实际上是一个虚拟机;Valgrind 使用一种称为动态二进制插装(DBI)的技术来插装代码。 在其主页上阅读更多内容:http://valgrind.org/。
Valgrind 的巨大优势在于它的工具套件--主要是Memcheck工具(Memcheck)。 下表中(按字母顺序)列出了其他几种检查器和性能分析工具:
| 有效研磨工具名称 | 目的 | | 高速缓存研磨 | CPU 缓存探查器。 | | Callgrind | Cachegrind 的扩展;提供更多调用图信息。 KCachegrind 是 cachegrind/callgrind 的一个很好的 GUI 可视化工具。 | | DRD | PthreadsBug 检测器。 | | 赫尔格林德 | 用于多线程应用(主要是 P 线程)的数据竞争检测器。 | | 群山,山地 | 堆分析器(堆使用图表、最大分配跟踪)。 | | Memcheck | 内存错误检测器;包括越界(OOB)访问(读|写在|溢出下)、未初始化的数据访问、UAF、UAR、内存泄漏、双重释放和重叠内存区域错误。 这是默认工具。 |
请注意,表中没有列出一些较少使用的工具(如 lakey、nulgrind、no)和一些实验工具(exp-bbv、exp-dhat、exp-sgcheck)。
选择一个工具,使 Valgrind 通过--tool=
选项运行(将前述任一项作为参数)。 在本书中,我们只关注 Valgrind 的 Memcheck 工具。
Memcheck 是 Valgrind 的默认工具;您不需要显式传递它,但可以使用valgrind --tool=memcheck <program-to-execute with params>
语法进行传递。
作为一个简单的示例,让我们在df(1)
实用程序(在 Ubuntu 机器上)上运行 Valgrind:
$ lsb_release -a
No LSB modules are available.
Distributor ID: Ubuntu
Description: Ubuntu 17.10
Release: 17.10
Codename: artful
$ df --version |head -n1
df (GNU coreutils) 8.26
$ valgrind df
==1577== Memcheck, a memory error detector
==1577== Copyright (C) 2002-2017, and GNU GPL'd, by Julian Seward et al.
==1577== Using Valgrind-3.13.0 and LibVEX; rerun with -h for copyright info
==1577== Command: df
==1577==
Filesystem 1K-blocks Used Available Use% Mounted on
udev 479724 0 479724 0% /dev
tmpfs 100940 10776 90164 11% /run
/dev/sda1 31863632 8535972 21686036 29% /
tmpfs 504692 0 504692 0% /dev/shm
tmpfs 5120 0 5120 0% /run/lock
tmpfs 504692 0 504692 0% /sys/fs/cgroup
tmpfs 100936 0 100936 0% /run/user/1000
==1577==
==1577== HEAP SUMMARY:
==1577== in use at exit: 3,577 bytes in 213 blocks
==1577== total heap usage: 447 allocs, 234 frees, 25,483 bytes allocated
==1577==
==1577== LEAK SUMMARY:
==1577== definitely lost: 0 bytes in 0 blocks
==1577== indirectly lost: 0 bytes in 0 blocks
==1577== possibly lost: 0 bytes in 0 blocks
==1577== still reachable: 3,577 bytes in 213 blocks
==1577== suppressed: 0 bytes in 0 blocks
==1577== Rerun with --leak-check=full to see details of leaked memory
==1577==
==1577== For counts of detected and suppressed errors, rerun with: -v
==1577== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 0 from 0)
$
Valgrind 从字面上接管并运行其中的df
进程,检测所有动态内存访问。 然后打印报告。 在前面的代码中,行的前缀是==1577==
;这只是df
进程的 PID。
由于没有发现运行时内存错误,因此不会出现任何输出(当我们在 Valgrind 的控制下运行membugs
程序时,您很快就会看到不同之处)。 在内存泄漏方面,报告指出:
definitely lost: 0 bytes in 0 blocks
所有这些都是零值,所以没问题。 如果definitely lost
下的值为正数,则这确实表明存在内存泄漏错误,必须进一步调查和修复。 其他标签-indirectly
/possibly lost
,still reachable
-通常是由于代码库中复杂或间接的内存处理(实际上,它们通常是人们可以忽略的假阳性)。
still reachable
通常表示在进程退出时,一些内存块没有被应用显式释放(但在进程死亡时被隐式释放)。 以下语句显示了这一点:
- 在退出时使用:213 个块中的 3577 个字节
- 总堆使用量:447 个分配,234 个释放,25,483 个字节
在总共 447 个分配中,只完成了 234 个释放,留下了 447-234=213 个未释放的块。
好,现在是有趣的部分:让我们在 Valgrind 下运行我们的membugs
程序测试用例(来自前面的第 5 章和Linux 内存问题),看看它是否捕获了测试用例努力提供的内存错误。
我们绝对建议您阅读上一章以及membugs.c
源代码,以重新熟悉我们将要运行的测试用例。
The membugs program has a total of 13 test cases; we shall not attempt to display the output of all of them within the book; we leave it as an exercise to the reader to try running the program with all test cases under Valgrind and deciphering its output report. It would be of interest to most readers to see the summary table at the end of this section, showing the result of running Valgrind on each of the test cases.
测试用例#1:未初始化的内存访问
$ ./membugs 1
true: x=32568
$
For readability, we remove parts of the output shown as follows and truncate the program pathname.
现在在 Valgrind 的控制下:
$ valgrind ./membugs 1
==19549== Memcheck, a memory error detector
==19549== Copyright (C) 2002-2017, and GNU GPL'd, by Julian Seward et al.
==19549== Using Valgrind-3.13.0 and LibVEX; rerun with -h for copyright info
==19549== Command: ./membugs 1
==19549==
==19549== Conditional jump or move depends on uninitialised value(s)
==19549== at 0x40132C: uninit_var (in <...>/ch3/membugs)
==19549== by 0x401451: process_args (in <...>/ch3/membugs)
==19549== by 0x401574: main (in <...>/ch3/membugs)
==19549==
[...]
==19549== Conditional jump or move depends on uninitialised value(s)
==19549== at 0x4E9101C: vfprintf (in /usr/lib64/libc-2.26.so)
==19549== by 0x4E99255: printf (in /usr/lib64/libc-2.26.so)
==19549== by 0x401357: uninit_var (in <...>/ch3/membugs)
==19549== by 0x401451: process_args (in <...>/ch3/membugs)
==19549== by 0x401574: main (in <...>/ch3/membugs)
==19549==
false: x=0
==19549==
==19549== HEAP SUMMARY:
==19549== in use at exit: 0 bytes in 0 blocks
==19549== total heap usage: 1 allocs, 1 frees, 1,024 bytes allocated
==19549==
==19549== All heap blocks were freed -- no leaks are possible
==19549==
==19549== For counts of detected and suppressed errors, rerun with: -v
==19549== Use --track-origins=yes to see where uninitialised values come from
==19549== ERROR SUMMARY: 6 errors from 6 contexts (suppressed: 0 from 0)
$
显然,Valgrind 发现了未初始化的内存访问错误! 用粗体突出显示的文本清楚地揭示了这一情况。
但是,请注意,尽管 Valgrind 可以向我们显示调用堆栈(包括进程路径名),但它似乎无法向我们显示源代码中出现错误的行号。 不过,等一下。 我们可以通过将 Valgrind 与该程序的启用调试版本一起运行来精确地实现这一点:
$ make membugs_dbg
gcc -g -ggdb -gdwarf-4 -O0 -Wall -Wextra -c membugs.c -o membugs_dbg.o
[...]
membugs.c: In function ‘uninit_var’:
membugs.c:283:5: warning: ‘x’ is used uninitialized in this function [-Wuninitialized]
if (x > MAXVAL)
^
[...]
gcc -g -ggdb -gdwarf-4 -O0 -Wall -Wextra -c ../common.c -o common_dbg.o
gcc -o membugs_dbg membugs_dbg.o common_dbg.o
[...]
Common GCC flags used for debugging
See the gcc(1)
man page for details. Briefly:-g
: Produce sufficient debugging information such that a tool such as the GNU Debugger (GDB) has to debug symbolic information to work with (modern Linux would typically use the DWARF format).
-ggdb
: Use the most expressive format possible for the OS.
-gdwarf-4
: Debug info is in the DWARF- format (ver. 4 is appropriate).
-O0
: Optimization level 0
; good for debugging.
在下面的代码中,我们使用启用调试的二进制可执行文件版本membugs_dbg
重试运行 Valgrind:
$ valgrind --tool=memcheck ./membugs_dbg 1
==20079== Memcheck, a memory error detector
==20079== Copyright (C) 2002-2017, and GNU GPL'd, by Julian Seward et al.
==20079== Using Valgrind-3.13.0 and LibVEX; rerun with -h for copyright info
==20079== Command: ./membugs_dbg 1
==20079==
==20079== Conditional jump or move depends on uninitialised value(s)
==20079== at 0x40132C: uninit_var (membugs.c:283)
==20079== by 0x401451: process_args (membugs.c:326)
==20079== by 0x401574: main (membugs.c:379)
==20079==
==20079== Conditional jump or move depends on uninitialised value(s)
==20079== at 0x4E90DAA: vfprintf (in /usr/lib64/libc-2.26.so)
==20079== by 0x4E99255: printf (in /usr/lib64/libc-2.26.so)
==20079== by 0x401357: uninit_var (membugs.c:286)
==20079== by 0x401451: process_args (membugs.c:326)
==20079== by 0x401574: main (membugs.c:379)
==20079==
==20079== Use of uninitialised value of size 8
==20079== at 0x4E8CD7B: _itoa_word (in /usr/lib64/libc-2.26.so)
==20079== by 0x4E9043D: vfprintf (in /usr/lib64/libc-2.26.so)
==20079== by 0x4E99255: printf (in /usr/lib64/libc-2.26.so)
==20079== by 0x401357: uninit_var (membugs.c:286)
==20079== by 0x401451: process_args (membugs.c:326)
==20079== by 0x401574: main (membugs.c:379)
[...]
==20079==
false: x=0
==20079==
==20079== HEAP SUMMARY:
==20079== in use at exit: 0 bytes in 0 blocks
==20079== total heap usage: 1 allocs, 1 frees, 1,024 bytes allocated
==20079==
==20079== All heap blocks were freed -- no leaks are possible
==20079==
==20079== For counts of detected and suppressed errors, rerun with: -v
==20079== Use --track-origins=yes to see where uninitialised values come from
==20079== ERROR SUMMARY: 6 errors from 6 contexts (suppressed: 0 from 0)
$
像往常一样,以自下而上的方式读取调用堆栈,这样就有意义了!
Important: Please note that, unfortunately, it's quite possible that the precise line numbers shown in the output as follows may not precisely match the line number in the latest version of the source file in the book's GitHub repository.
以下是源代码(这里使用nl
实用程序来显示带有编号的所有行的代码):
$ nl --body-numbering=a membugs.c [...]
278 /* option = 1 : uninitialized var test case */
279 static void uninit_var()
280 {
281 int x;
282
283 if (x) 284 printf("true case: x=%d\n", x);
285 else
286 printf("false case\n");
287 }
[...]
325 case 1:
326 uninit_var();
327 break;
[...]
377 int main(int argc, char **argv)
378 {
379 process_args(argc, argv);
380 exit(EXIT_SUCCESS);
381 }
现在我们可以看到,Valgrind 确实完美地捕捉到了 Buggy 案例。
**测试用例#5:**编译时内存上的读取溢出:
$ valgrind ./membugs_dbg 5
==23024== Memcheck, a memory error detector
==23024== Copyright (C) 2002-2017, and GNU GPL'd, by Julian Seward et al.
==23024== Using Valgrind-3.13.0 and LibVEX; rerun with -h for copyright info
==23024== Command: ./membugs_dbg 5
==23024==
arr = aaaaa����
==23024==
==23024== HEAP SUMMARY:
==23024== in use at exit: 0 bytes in 0 blocks
==23024== total heap usage: 1 allocs, 1 frees, 1,024 bytes allocated
==23024==
==23024== All heap blocks were freed -- no leaks are possible
==23024==
==23024== For counts of detected and suppressed errors, rerun with: -v
==23024== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 0 from 0)
$
你看那个!? Valgrind 无法捕获读取溢出内存错误。 为什么? 这是一个限制:Valgrind 只能检测并捕获动态分配内存上的 UB(错误)。 前面的测试用例使用静态编译时分配的内存。
因此,让我们尝试相同的测试,但这一次使用动态分配的内存;这正是测试用例#6 的设计目的。
**测试用例#6:**动态内存上的读取溢出(为了可读性,我们截断了一些输出):
$ ./membugs_dbg 2>&1 |grep 6
option = 6 : out-of-bounds : read overflow [on dynamic memory]
$ valgrind ./membugs_dbg 6
[...]
==23274== Command: ./membugs_dbg 6
==23274==
==23274== Invalid write of size 1
==23274== at 0x401127: read_overflow_dynmem (membugs.c:215)
==23274== by 0x401483: process_args (membugs.c:341)
==23274== by 0x401574: main (membugs.c:379)
==23274== Address 0x521f045 is 0 bytes after a block of size 5 alloc'd
==23274== at 0x4C2FB6B: malloc (vg_replace_malloc.c:299)
==23274== by 0x4010D9: read_overflow_dynmem (membugs.c:205)
==23274== by 0x401483: process_args (membugs.c:341)
==23274== by 0x401574: main (membugs.c:379)
[...]
==23274== Invalid write of size 1
==23274== at 0x40115E: read_overflow_dynmem (membugs.c:216)
==23274== by 0x401483: process_args (membugs.c:341)
==23274== by 0x401574: main (membugs.c:379)
==23274== Address 0x521f04a is 5 bytes after a block of size 5 alloc'd
==23274== at 0x4C2FB6B: malloc (vg_replace_malloc.c:299)
==23274== by 0x4010D9: read_overflow_dynmem (membugs.c:205)
==23274== by 0x401483: process_args (membugs.c:341)
==23274== by 0x401574: main (membugs.c:379)
==23274==
==23274== Invalid read of size 1
==23274== at 0x4C32B94: strlen (vg_replace_strmem.c:458)
==23274== by 0x4E91955: vfprintf (in /usr/lib64/libc-2.26.so)
==23274== by 0x4E99255: printf (in /usr/lib64/libc-2.26.so)
==23274== by 0x401176: read_overflow_dynmem (membugs.c:217)
==23274== by 0x401483: process_args (membugs.c:341)
==23274== by 0x401574: main (membugs.c:379)
==23274== Address 0x521f045 is 0 bytes after a block of size 5 alloc'd
==23274== at 0x4C2FB6B: malloc (vg_replace_malloc.c:299)
==23274== by 0x4010D9: read_overflow_dynmem (membugs.c:205)
==23274== by 0x401483: process_args (membugs.c:341)
==23274== by 0x401574: main (membugs.c:379)
[...]
arr = aaaaaSecreT
==23274== Conditional jump or move depends on uninitialised value(s)
==23274== at 0x4E90DAA: vfprintf (in /usr/lib64/libc-2.26.so)
==23274== by 0x4E99255: printf (in /usr/lib64/libc-2.26.so)
==23274== by 0x401195: read_overflow_dynmem (membugs.c:220)
==23274== by 0x401483: process_args (membugs.c:341)
==23274== by 0x401574: main (membugs.c:379)
==23274==
==23274== Use of uninitialised value of size 8
==23274== at 0x4E8CD7B: _itoa_word (in /usr/lib64/libc-2.26.so)
==23274== by 0x4E9043D: vfprintf (in /usr/lib64/libc-2.26.so)
==23274== by 0x4E99255: printf (in /usr/lib64/libc-2.26.so)
==23274== by 0x401195: read_overflow_dynmem (membugs.c:220)
==23274== by 0x401483: process_args (membugs.c:341)
==23274== by 0x401574: main (membugs.c:379)
[...]
==23274== ERROR SUMMARY: 31 errors from 17 contexts (suppressed: 0 from 0)
$
这一次,由于精确的调用堆栈位置揭示了源代码中的确切位置(正如我们用-g
编译的那样),因此捕获了大量错误。
**测试用例#8:**UAF(释放后使用):
$ ./membugs_dbg 2>&1 |grep 8
option = 8 : UAF (use-after-free) test case
$
A (partial) screenshot of the action when Valgrind catches the UAF bugs
瓦尔格林德确实抓住了 UAF!
**测试用例#8:**UAR(退货后使用):
$ ./membugs_dbg 9
res: (null)
$ valgrind ./membugs_dbg 9
==7594== Memcheck, a memory error detector
==7594== Copyright (C) 2002-2017, and GNU GPL'd, by Julian Seward et al.
==7594== Using Valgrind-3.13.0 and LibVEX; rerun with -h for copyright info
==7594== Command: ./membugs_dbg 9
==7594==
res: (null)
==7594==
==7594== HEAP SUMMARY:
==7594== in use at exit: 0 bytes in 0 blocks
==7594== total heap usage: 1 allocs, 1 frees, 1,024 bytes allocated
==7594==
==7594== All heap blocks were freed -- no leaks are possible
==7594==
==7594== For counts of detected and suppressed errors, rerun with: -v
==7594== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 0 from 0)
$
哎呀! Valgrind 没有捕捉到 UAR 的臭虫!
**测试用例#13:**内存泄漏用例#3-lib API 泄漏。我们通过选择 13 作为membugs的参数来运行内存泄漏测试用例#3。 值得注意的是,只有在使用--leak-check=full
选项运行时,Valgrind 才会显示泄漏的来源(通过显示的调用堆栈):
$ valgrind --leak-resolution=high --num-callers=50 --leak-check=full ./membugs_dbg 13
==22849== Memcheck, a memory error detector
==22849== Copyright (C) 2002-2017, and GNU GPL'd, by Julian Seward et al.
==22849== Using Valgrind-3.13.0 and LibVEX; rerun with -h for copyright info
==22849== Command: ./membugs_dbg 13
==22849==
## Leakage test: case 3: "lib" API: runtime cond = 0
mypath = /usr/local/bin:/usr/local/sbin:/usr/bin:/usr/sbin:/sbin:/usr/sbin:/usr/local/sbin:/home/kai/Mentorimg/Sourcery_CodeBench_Lite_for_ARM_GNU_Linux/bin/:/mnt/big/scratchpad/buildroot-2017.08.1/output/host/bin/:/sbin:/usr/sbin:/usr/local/sbin
## Leakage test: case 3: "lib" API: runtime cond = 1
mypath = /usr/local/bin:/usr/local/sbin:/usr/bin:/usr/sbin:/sbin:/usr/sbin:/usr/local/sbin:/home/kai/Mentorimg/Sourcery_CodeBench_Lite_for_ARM_GNU_Linux/bin/:/mnt/big/scratchpad/buildroot-2017.08.1/output/host/bin/:/sbin:/usr/sbin:/usr/local/sbin
==22849==
==22849== HEAP SUMMARY:
==22849== in use at exit: 4,096 bytes in 1 blocks
==22849== total heap usage: 3 allocs, 2 frees, 9,216 bytes allocated
==22849==
==22849== 4,096 bytes in 1 blocks are definitely lost in loss record 1 of 1
==22849== at 0x4C2FB6B: malloc (vg_replace_malloc.c:299)
==22849== by 0x400A0C: silly_getpath (membugs.c:38)
==22849== by 0x400AC6: leakage_case3 (membugs.c:59)
==22849== by 0x40152B: process_args (membugs.c:367)
==22849== by 0x401574: main (membugs.c:379)
==22849==
==22849== LEAK SUMMARY:
==22849== definitely lost: 4,096 bytes in 1 blocks
==22849== indirectly lost: 0 bytes in 0 blocks
==22849== possibly lost: 0 bytes in 0 blocks
==22849== still reachable: 0 bytes in 0 blocks
==22849== suppressed: 0 bytes in 0 blocks
==22849==
==22849== For counts of detected and suppressed errors, rerun with: -v
==22849== ERROR SUMMARY: 1 errors from 1 contexts (suppressed: 0 from 0)
$
Valgrind 手册页建议将--leak-resolution=high
和--num-callers=
设置为 40 或更高。
valgrind(1)
上的手册页涵盖了它提供的许多选项(如日志记录和工具(Memcheck)选项);请看一看,以便更深入地了解该工具的用法。
关于我们的测试用例(合并到我们的membugs
程序中),下面是 Valgrind 的成绩单和内存错误,如下所示:
| 测试用例编号 | 测试用例 | 是否由 Valgrind 检测到? | | 1. | 未初始化的存储器读取(UMR) | 是的,是的。 | | 2 个 | 越界(OOB):写入溢出 [在编译时内存上] | 不是 | | 3. | OOB:写入溢出 [在动态内存上] | 是 | | 4. | OOB:写入下溢 [在动态内存上] | 是 | | 5. | OOB:读取溢出 [在编译时内存上] | 不是 | | 6. | OOB:读取溢出 [在动态内存上] | 是 | | 7. | OOB:读取下溢 [在动态内存上] | 是 | | 8 个 | UAF,也称为悬挂式指针 | 是 | | 9. | UAR,也称为范围后使用(UAS) | 不是 | | 10 个 | 双重免费 | 是 | | 11. | 内存泄漏测试用例 1:简单泄漏 | 是 | | 12 个 | 内存泄漏测试用例 1:泄漏更多(循环中) | 是 | | 13 个 | 内存泄漏测试案例 1:Lib API 泄漏 | 是 |
Valgrind 专业人员*:*
-
捕获动态分配的内存区域 上的常见内存错误(UB)
- 使用未初始化的变量
- 内存访问越界(读/写下溢/溢出错误)
- 释放后使用/返回后使用(超出范围)错误
- 双重免费
- 渗漏 / 泄密 / 泄漏
-
无需修改源代码
-
不需要重新编译
-
不需要特殊的编译器标志
Valgrind CONS:
- 性能:在 Valgrind 下运行时,目标软件的运行速度可能会降低 10 到 30 倍。
- 内存占用:目标程序中的每个分配都需要 Valgrind 进行内存分配(这使得在资源高度受限的嵌入式 Linux 系统上运行 Valgrind 变得困难)。
- 无法捕获静态(编译时)分配的内存区域上的错误。
- 为了查看带有行号信息的调用堆栈,需要使用
-g
标志重新编译/编译。
事实是,Valgrind 仍然是人们对付虫子的强大武器。 有许多现实世界中使用 Valgrind 的项目;请访问http://valgrind.org/gallery/users.html查看长长的列表。
There is always more to learn and explore: Valgrind provides a GDB monitor mode allowing you to do advanced debugging on your program via the GNU debugger (GDB). This is particularly useful for using Valgrind on programs that never terminate (daemons being the classic case).
The third chapter of Valgrind's manual is very helpful in this regard: http://valgrind.org/docs/manual/manual-core-adv.html
Saniizer 是 Google 的一套开源工具;与其他内存调试工具一样,它们可以解决常见的内存错误和 UB 问题,包括 OOB(越界访问:读/写下/溢出)、UAF、UAR、双重释放和内存泄漏。 其中一个工具还处理 C/C++代码中的数据竞争。
一个关键的区别是,杀菌器工具通过编译器将指令插入到代码中。 它们使用一种称为编译时指令插入(CTI)的技术以及影子内存技术。 在撰写本文时,ASAN 是 GCC 版本 4.8 和 LLVM(Clang)版本的一部分并提供支持。 3.1 及以上。
要使用给定的工具,请使用使用情况栏中显示的标志编译程序:
| 消毒器工具(简称) | 目的 | 用法(编译器标志) | Linux 平台[+评论] |
| AddressSaniizer(Asan) | 检测一般内存错误[堆|堆栈|全局缓冲区溢出|溢出、UAF、UAR、初始化顺序错误] | -fsanitize=address
| X86、x86_64、ARM、Aarch64、MIPS、MIPS64、PPC64。 [不能与 Tsan 合并] |
| 内核地址 Saniizer(喀山) | 用于 Linux 内核空间的 ASSAN | -fsanitize=kernel-address
| X86_64[内核版本>=4.0],Aarch64[内核版本>=4.4] |
| 内存卫生机(MSAN) | UMR 检测器 | -fsanitize=memory -fPIE -pie [-fno-omit-frame-pointer]
| 仅 Linux x86_64 |
| ThreadSaniizer(Tsan) | 数据竞争检测器 | -fsanitize=thread
| 仅限 Linux x86_64。 [不能与 ASAN 或 LSAN 标志组合] |
| LeakSaniizer(lsan)
(ASAN 的子集) | 内存泄漏检测仪 | -fsanitize=leak
| Linux x86_64 和 OS X[无法与 Tsan 结合] |
| 未定义行为卫生剂(UBSAN) | UB 探测器 | -fsanitize=undefined
| X86、x86_64、ARM、Aarch64、PPC64、MIPS、MIPS64 |
Additional DocumentationGoogle maintains a GitHub page with documentation for the sanitizer tools:
- https://github.com/google/sanitizers
- https://github.com/google/sanitizers/wiki
- https://github.com/google/sanitizers/wiki/SanitizerCommonFlags
There are links leading to each of the tool's individual wiki (documentation) pages. It's recommended you read them in detail when using a tool (for example, each tool might have specific flags and/or environment variables that the user can make use of).
The man page on gcc(1)
is a rich source of information on the intricacies of the -fsanitize=
sanitizer tool gcc options. Interestingly, most of the sanitizer tools are supported on the Android (>=4.1) platform as well.
The Clang documentation also documents the use of the sanitizer tools: https://clang.llvm.org/docs/index.html.
在本章中,我们重点介绍如何使用 ASAN 工具。
如上表所示,我们需要使用适当的编译器标志编译我们的目标应用 membug。 此外,不使用gcc
作为编译器,建议使用clang
。
clang
is considered a compiler frontend for several programming languages, including C and C++; the backend is the LLVM compiler infrastructure project. More information on Clang is available on its Wikipedia page. You will need to ensure that the Clang package is installed on your Linux box; using your distribution's package manager (apt-get
, dnf
, rpm
) would be the easiest way.
下面这段来自我们的 Membugs 生成文件的代码片段显示了我们如何使用clang
编译用于目标的 Membugs 消毒器:
CC=${CROSS_COMPILE}gcc
CL=${CROSS_COMPILE}clang
CFLAGS=-Wall -UDEBUG
CFLAGS_DBG=-g -ggdb -gdwarf-4 -O0 -Wall -Wextra -DDEBUG
CFLAGS_DBG_ASAN=${CFLAGS_DBG} -fsanitize=address
CFLAGS_DBG_MSAN=${CFLAGS_DBG} -fsanitize=memory
CFLAGS_DBG_UB=${CFLAGS_DBG} -fsanitize=undefined
[...]
#--- Sanitizers (use clang): <foo>_dbg_[asan|ub|msan]
membugs_dbg_asan.o: membugs.c
${CL} ${CFLAGS_DBG_ASAN} -c membugs.c -o membugs_dbg_asan.o
membugs_dbg_asan: membugs_dbg_asan.o common_dbg_asan.o
${CL} ${CFLAGS_DBG_ASAN} -o membugs_dbg_asan membugs_dbg_asan.o common_dbg_asan.o
membugs_dbg_ub.o: membugs.c
${CL} ${CFLAGS_DBG_UB} -c membugs.c -o membugs_dbg_ub.o
membugs_dbg_ub: membugs_dbg_ub.o common_dbg_ub.o
${CL} ${CFLAGS_DBG_UB} -o membugs_dbg_ub membugs_dbg_ub.o common_dbg_ub.o
membugs_dbg_msan.o: membugs.c
${CL} ${CFLAGS_DBG_MSAN} -c membugs.c -o membugs_dbg_msan.o
membugs_dbg_msan: membugs_dbg_msan.o common_dbg_msan.o
${CL} ${CFLAGS_DBG_MSAN} -o membugs_dbg_msan membugs_dbg_msan.o common_dbg_msan.o
[...]
为了唤醒我们的记忆,这里是我们的 membugs 程序的帮助屏幕:
$ ./membugs_dbg_asan
Usage: ./membugs_dbg_asan option [ -h | --help]
option = 1 : uninitialized var test case
option = 2 : out-of-bounds : write overflow [on compile-time memory]
option = 3 : out-of-bounds : write overflow [on dynamic memory]
option = 4 : out-of-bounds : write underflow
option = 5 : out-of-bounds : read overflow [on compile-time memory]
option = 6 : out-of-bounds : read overflow [on dynamic memory]
option = 7 : out-of-bounds : read underflow
option = 8 : UAF (use-after-free) test case
option = 9 : UAR (use-after-return) test case
option = 10 : double-free test case
option = 11 : memory leak test case 1: simple leak
option = 12 : memory leak test case 2: leak more (in a loop)
option = 13 : memory leak test case 3: "lib" API leak
-h | --help : show this help screen
$
The membugs program has a total of 13 test cases; we shall not attempt to display the output of all of them in this book; we leave it as an exercise to the reader to try out building and running the program with all test cases under ASan and deciphering its output report. It would be of interest to readers to see the summary table at the end of this section, showing the result of running ASan on each of the test cases.
**测试用例#1:**UMR
让我们尝试第一个测试用例-未初始化的变量读取测试用例:
$ ./membugs_dbg_asan 1
false case
$
它没有抓住虫子! 是的,我们遇到了 ASAN 的限制:AddressSaniizer 无法在静态(编译时)分配的内存上捕获 UMR。 瓦尔格林德做到了。
嗯,这是由 MSAN 工具负责的;它的具体工作是捕获 UMR 错误。 文档指出,MSAN 只能在动态分配的内存上捕获 UMR。 我们发现它甚至在静态分配的内存上捕获了一个 UMR 错误,我们的简单测试用例使用该内存:
$ ./membugs_dbg_msan 1
==3095==WARNING: MemorySanitizer: use-of-uninitialized-value
#0 0x496eb8 (<...>/ch5/membugs_dbg_msan+0x496eb8)
#1 0x494425 (<...>/ch5/membugs_dbg_msan+0x494425)
#2 0x493f2b (<...>/ch5/membugs_dbg_msan+0x493f2b)
#3 0x7fc32f17ab96 (/lib/x86_64-linux-gnu/libc.so.6+0x21b96)
#4 0x41a8c9 (<...>/ch5/membugs_dbg_msan+0x41a8c9)
SUMMARY: MemorySanitizer: use-of-uninitialized-value (<...>/ch5/membugs_dbg_msan+0x496eb8) Exiting $
它捕捉到了错误;然而,这一次,尽管我们使用了使用-g -ggdb
标志构建的调试二进制可执行文件,但是在堆栈跟踪中缺少通常的filename:line_number
信息。 实际上,在下一个测试用例中演示了一种获取此信息的方法。
现在,没关系:这给了我们一个学习另一种有用的调试技术的机会:objdump(1)
是工具链实用程序之一,在这里可以提供很大的帮助(我们可以使用类似的工具,如:readelf(1)
或gdb(1)
)。 我们将使用objdump(1)
(-d
开关,并通过-S
开关使用源代码)反汇编二进制可执行文件,并在其输出中查找发生 UMR 的地址:
SUMMARY: MemorySanitizer: use-of-uninitialized-value (<...>/ch5/membugs_dbg_msan+0x496eb8)
由于objdump
的输出相当大,我们将其截断,仅显示相关部分:
$ objdump -d -S ./membugs_dbg_msan > tmp
<< Now examine the tmp file >>
$ cat tmp
./membugs_dbg_msan: file format elf64-x86-64
Disassembly of section .init:
000000000041a5b0 <_init>:
41a5b0: 48 83 ec 08 sub $0x8,%rsp
41a5b4: 48 8b 05 ad a9 2a 00 mov 0x2aa9ad(%rip),%rax # 6c4f68 <__gmon_start__>
41a5bb: 48 85 c0 test %rax,%rax
41a5be: 74 02 je 41a5c2 <_init+0x12>
[...]
0000000000496e60 <uninit_var>:
{
496e60: 55 push %rbp
496e61: 48 89 e5 mov %rsp,%rbp
int x; /* static mem */
496e64: 48 83 ec 10 sub $0x10,%rsp
[...]
if (x)
496e7f: 8b 55 fc mov -0x4(%rbp),%edx
496e82: 8b 31 mov (%rcx),%esi
496e84: 89 f7 mov %esi,%edi
[...]
496eaf: e9 00 00 00 00 jmpq 496eb4 <uninit_var+0x54>
496eb4: e8 a7 56 f8 ff callq 41c560 <__msan_warning_noreturn>
496eb9: 8a 45 fb mov -0x5(%rbp),%al
496ebc: a8 01 test $0x1,%al
[...]
与 MSAN 提供的作为第一个0x496eb8
错误点的地址最匹配的输出是0x496eb4
。这很好:只需查看前面的第一行代码就可以了;它是以下行:
if (x)
完美无缺。 那正是 UMR 发生的地方!
**测试用例#2:**写入溢出[在编译时内存上]
我们在 Valgrind 和 Asan 下都运行membugs
程序,只是调用了write_overflow_compilemem()
函数来测试编译时分配的内存块上的越界写溢出内存错误。
**案例 1:**使用 Valgrind
请注意,Valgrind 如何无法捕获越界内存错误:
$ valgrind ./membugs_dbg 2 ==8959== Memcheck, a memory error detector
==8959== Copyright (C) 2002-2017, and GNU GPL'd, by Julian Seward et al.
==8959== Using Valgrind-3.13.0 and LibVEX; rerun with -h for copyright info
==8959== Command: ./membugs_dbg 2
==8959==
==8959==
==8959== HEAP SUMMARY:
==8959== in use at exit: 0 bytes in 0 blocks
==8959== total heap usage: 0 allocs, 0 frees, 0 bytes allocated
==8959==
==8959== All heap blocks were freed -- no leaks are possible
==8959==
==8959== For counts of detected and suppressed errors, rerun with: -v
==8959== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 0 from 0)
$
这是因为 Valgrind 仅限于使用动态分配的内存;它不能检测和使用编译时分配的内存。
情况 2:地址消毒剂
Asan 确实抓住了这个问题:
AddressSanitizer (ASan) catches the OOB write-overflow bug
类似的文本版本如下所示:
$ ./membugs_dbg_asan 2
=================================================================
==25662==ERROR: AddressSanitizer: stack-buffer-overflow on address 0x7fff17e789f4 at pc 0x00000051271d bp 0x7fff17e789b0 sp 0x7fff17e789a8
WRITE of size 4 at 0x7fff17e789f4 thread T0
#0 0x51271c (<...>/membugs_dbg_asan+0x51271c)
#1 0x51244e (<...>/membugs_dbg_asan+0x51244e)
#2 0x512291 (<...>/membugs_dbg_asan+0x512291)
#3 0x7f7e19b2db96 (/lib/x86_64-linux-gnu/libc.so.6+0x21b96)
#4 0x419ea9 (<...>/membugs_dbg_asan+0x419ea9)
Address 0x7fff17e789f4 is located in stack of thread T0 at offset 52 in frame
#0 0x5125ef (/home/seawolf/0tmp/membugs_dbg_asan+0x5125ef)
[...]
SUMMARY: AddressSanitizer: stack-buffer-overflow (/home/seawolf/0tmp/membugs_dbg_asan+0x51271c)
[...]
==25662==ABORTING
$
但是请注意,在堆栈回溯中,没有filename:line# information
。 那太令人失望了。 我们能拿到吗?
是的,的确--诀窍在于确保几件事:
- 使用
-g
开关编译应用(以包含调试符号信息;我们对所有*_DBG 版本都这样做)。 - 除了 Clang 编译器之外,还必须安装名为
llvm-symbolizer
的工具。 安装后,您必须确定其在磁盘上的确切位置,并将该目录添加到路径中。 - 在运行时,必须将
ASAN_OPTIONS
环境变量设置为symbolize=1
值。
在这里,我们重新运行 BUGGY 案例,并使用llvm-symbolizer
:
$ export PATH=$PATH:/usr/lib/llvm-6.0/bin/
$ ASAN_OPTIONS=symbolize=1 ./membugs_dbg_asan 2
=================================================================
==25807==ERROR: AddressSanitizer: stack-buffer-overflow on address 0x7ffd63e80cf4 at pc 0x00000051271d bp 0x7ffd63e80cb0 sp 0x7ffd63e80ca8
WRITE of size 4 at 0x7ffd63e80cf4 thread T0
#0 0x51271c in write_overflow_compilemem <...>/ch5/membugs.c:268:10
#1 0x51244e in process_args <...>/ch5/membugs.c:325:4
#2 0x512291 in main <...>/ch5/membugs.c:375:2
#3 0x7f9823642b96 in __libc_start_main /build/glibc-OTsEL5/glibc-2.27/csu/../csu/libc-start.c:310
#4 0x419ea9 in _start (<...>/membugs_dbg_asan+0x419ea9)
[...]
$
现在,新的filename:line# information
出现了!
显然,ASAN 可以检测分配的和动态分配的内存区的编译时,从而捕获这两种内存型错误。
此外,正如我们所看到的,它显示了一个调用堆栈(当然是从下到上阅读)。 我们可以看到调用链是:
_start --> __libc_start_main --> main --> process_args -->
write_overflow_compilemem
AddressSaniizer 还会显示“错误地址周围的阴影字节:”;在此,我们不会尝试解释用于捕获此类错误的内存跟踪技术;如果感兴趣,请参阅 GitHub 存储库上的进一步阅读部分。
**测试用例#3:**写入溢出(在动态存储器上)
不出所料,Asan 抓住了这个漏洞:
$ ./membugs_dbg_asan 3
=================================================================
==25848==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x602000000018 at pc 0x0000004aaedc bp 0x7ffe64dd2cd0 sp 0x7ffe64dd2480
WRITE of size 10 at 0x602000000018 thread T0
#0 0x4aaedb in __interceptor_strcpy.part.245 (<...>/membugs_dbg_asan+0x4aaedb)
#1 0x5128fd in write_overflow_dynmem <...>/ch5/membugs.c:258:2
#2 0x512458 in process_args <...>/ch5/membugs.c:328:4
#3 0x512291 in main <...>/ch5/membugs.c:375:2
#4 0x7f93abb88b96 in __libc_start_main /build/glibc-OTsEL5/glibc-2.27/csu/../csu/libc-start.c:310
#5 0x419ea9 in _start (<...>/membugs_dbg_asan+0x419ea9)
0x602000000018 is located 0 bytes to the right of 8-byte region [0x602000000010,0x602000000018) allocated by thread T0 here:
#0 0x4d9d60 in malloc (<...>/membugs_dbg_asan+0x4d9d60)
#1 0x512896 in write_overflow_dynmem <...>/ch5/membugs.c:254:9
#2 0x512458 in process_args <...>/ch5/membugs.c:328:4
#3 0x512291 in main <...>/ch5/membugs.c:375:2
#4 0x7f93abb88b96 in __libc_start_main /build/glibc-OTsEL5/glibc-2.27/csu/../csu/libc-start.c:310
[...]
当llvm-symbolizer
数据在路径中时,filename:line# information
会再次出现。
不支持尝试编译杀菌器指令插入(通过-fsanitize=
GCC 开关)和尝试在 Valgrind 上运行二进制可执行文件;当我们尝试此操作时,Valgrind 会报告以下信息:
$ valgrind ./membugs_dbg 3
==8917== Memcheck, a memory error detector
==8917== Copyright (C) 2002-2017, and GNU GPL'd, by Julian Seward et al.
==8917== Using Valgrind-3.13.0 and LibVEX; rerun with -h for copyright info
==8917== Command: ./membugs_dbg 3
==8917==
==8917==ASan runtime does not come first in initial library list; you should either link runtime to your application or manually preload it with LD_PRELOAD.
[...]
**测试用例#8:**UAF(免费后使用)。 看一下下面的代码:
$ ./membugs_dbg_asan 8 uaf():162: arr = 0x615000000080:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
=================================================================
==25883==ERROR: AddressSanitizer: heap-use-after-free on address 0x615000000080 at pc 0x000000444b14 bp 0x7ffde4315390 sp 0x7ffde4314b40
WRITE of size 22 at 0x615000000080 thread T0
#0 0x444b13 in strncpy (<...>/membugs_dbg_asan+0x444b13)
#1 0x513529 in uaf <...>/ch5/membugs.c:172:2
#2 0x512496 in process_args <...>/ch5/membugs.c:344:4
#3 0x512291 in main <...>/ch5/membugs.c:375:2
#4 0x7f4ceea9fb96 in __libc_start_main /build/glibc-OTsEL5/glibc-2.27/csu/../csu/libc-start.c:310
#5 0x419ea9 in _start (<...>/membugs_dbg_asan+0x419ea9)
0x615000000080 is located 0 bytes inside of 512-byte region [0x615000000080,0x615000000280)
freed by thread T0 here:
#0 0x4d9b90 in __interceptor_free.localalias.0 (<...>/membugs_dbg_asan+0x4d9b90)
#1 0x513502 in uaf <...>/ch5/membugs.c:171:2
#2 0x512496 in process_args <...>/ch5/membugs.c:344:4
#3 0x512291 in main <...>/ch5/membugs.c:375:2
#4 0x7f4ceea9fb96 in __libc_start_main /build/glibc-OTsEL5/glibc-2.27/csu/../csu/libc-start.c:310
previously allocated by thread T0 here:
#0 0x4d9d60 in malloc (<...>/membugs_dbg_asan+0x4d9d60)
#1 0x513336 in uaf <...>/ch5/membugs.c:157:8
#2 0x512496 in process_args <...>/ch5/membugs.c:344:4
#3 0x512291 in main <...>/ch5/membugs.c:375:2
#4 0x7f4ceea9fb96 in __libc_start_main /build/glibc-OTsEL5/glibc-2.27/csu/../csu/libc-start.c:310
SUMMARY: AddressSanitizer: heap-use-after-free (<...>/membugs_dbg_asan+0x444b13) in strncpy
[...]
超棒的。 ASAN 不仅报告 UAF 错误,甚至报告缓冲区被分配和释放的确切位置! 强大的东西。
测试用例#9::uar
出于本例的目的,假设我们使用gcc
以通常的方式编译membugs
程序。 运行测试用例:
$ ./membugs_dbg 2>&1 | grep -w 9
option = 9 : UAR (use-after-return) test case
$ ./membugs_dbg_asan 9
res: (null)
$
因此,阿山并没有染上这种危险的 UAR 病毒! 正如我们之前看到的,Valgrind 也没有。 但是,编译器确实会发出警告!
不过,请稍等:杀菌器文档中提到,如果满足以下条件,Asan 确实可以捕捉到这个 UAR 漏洞:
clang
(r191186 以后版本)用来编译代码(不是 GCC)- 将特殊标志
detect_stack_use_after_return
设置为1
因此,我们通过 Clang 重新编译可执行文件(同样,我们假设安装了 Clang 包)。 实际上,我们的 Makefile 确实为所有的membugs_dbg_*
构建使用了clang
。 因此,请确保使用 Clang 作为编译器进行重新构建,然后重试:
$ ASAN_OPTIONS=detect_stack_use_after_return=1 ./membugs_dbg_asan 9
=================================================================
==25925==ERROR: AddressSanitizer: stack-use-after-return on address 0x7f7721a00020 at pc 0x000000445b17 bp 0x7ffdb7c3ba10 sp 0x7ffdb7c3b1c0
READ of size 23 at 0x7f7721a00020 thread T0
#0 0x445b16 in printf_common(void*, char const*, __va_list_tag*) (<...>/membugs_dbg_asan+0x445b16)
#1 0x4465db in vprintf (<...>/membugs_dbg_asan+0x4465db)
#2 0x4466ae in __interceptor_printf (<...>/membugs_dbg_asan+0x4466ae)
#3 0x5124b9 in process_args <...>/ch5/membugs.c:348:4
#4 0x512291 in main <...>/ch5/membugs.c:375:2
#5 0x7f7724e80b96 in __libc_start_main /build/glibc-OTsEL5/glibc-2.27/csu/../csu/libc-start.c:310
#6 0x419ea9 in _start (/home/seawolf/0tmp/membugs_dbg_asan+0x419ea9)
Address 0x7f7721a00020 is located in stack of thread T0 at offset 32 in frame
#0 0x5135ef in uar <...>/ch5/membugs.c:141
This frame has 1 object(s):
[32, 64) 'name' (line 142) <== Memory access at offset 32 is inside this variable
[...]
它确实起作用了。 正如我们在测试用例#1:UMR中所展示的,人们可以进一步利用objdump(1)
来梳理出错误发生的确切位置。 我们把这篇文章留给读者作为练习。
有关 ASAN 如何检测堆栈 UAR 的更多信息,请参见https://github.com/google/sanitizers/wiki/AddressSanitizerUseAfterReturn。
**测试用例#10:**双空闲
这个 bug 的测试用例有点有趣(请参考前面的membugs.c
源代码);我们对指针执行malloc
,free
,然后使用非常大的值(-1UL
,它变成无符号的,因此太大)执行另一个malloc
,这样它肯定会失败。 在错误处理代码中,我们(故意)释放前面已经释放的指针,从而生成双重释放测试用例。 在更简单的伪代码中:
ptr = malloc(n);
strncpy(...);
free(ptr);
bogus = malloc(-1UL); /* will fail */
if (!bogus) {
free(ptr); /* the Bug! */
exit(1);
}
重要的是,这种编码揭示了另一个真正重要的教训:开发人员通常没有对错误处理代码路径给予足够的重视;他们可能会也可能不会编写否定的测试用例来彻底测试它们。 这可能会导致严重的错误!
起初,通过 ASAN 指令插入来运行它并没有达到预期的效果:您将看到,由于非常巨大的malloc
故障,ASAN 实际上中止了流程执行;因此,它没有检测到我们正在寻找的真正错误-双重释放:
$ ./membugs_dbg_asan 10 doublefree(): cond 0
doublefree(): cond 1
==25959==WARNING: AddressSanitizer failed to allocate 0xffffffffffffffff bytes
==25959==AddressSanitizer's allocator is terminating the process instead of returning 0
==25959==If you don't like this behavior set allocator_may_return_null=1
==25959==AddressSanitizer CHECK failed: /build/llvm-toolchain-6.0-QjOn7h/llvm-toolchain-6.0-6.0/projects/compiler-rt/lib/sanitizer_common/sanitizer_allocator.cc:225 "((0)) != (0)" (0x0, 0x0)
#0 0x4e2eb5 in __asan::AsanCheckFailed(char const*, int, char const*, unsigned long long, unsigned long long) (<...>/membugs_dbg_asan+0x4e2eb5)
#1 0x500765 in __sanitizer::CheckFailed(char const*, int, char const*, unsigned long long, unsigned long long) (<...>/membugs_dbg_asan+0x500765)
#2 0x4e92a6 in __sanitizer::ReportAllocatorCannotReturnNull() (<...>/membugs_dbg_asan+0x4e92a6)
#3 0x4e92e6 in __sanitizer::ReturnNullOrDieOnFailure::OnBadRequest() (<...>/membugs_dbg_asan+0x4e92e6)
#4 0x424e66 in __asan::asan_malloc(unsigned long, __sanitizer::BufferedStackTrace*) (<...>/membugs_dbg_asan+0x424e66)
#5 0x4d9d3b in malloc (<...>/membugs_dbg_asan+0x4d9d3b)
#6 0x513938 in doublefree <...>/ch5/membugs.c:129:11
#7 0x5124d2 in process_args <...>/ch5/membugs.c:352:4
#8 0x512291 in main <...>/ch5/membugs.c:375:2
#9 0x7f8a7deccb96 in __libc_start_main /build/glibc-OTsEL5/glibc-2.27/csu/../csu/libc-start.c:310
#10 0x419ea9 in _start (/home/seawolf/0tmp/membugs_dbg_asan+0x419ea9)
$
是的,但是,请注意前面的输出行,它说:
[...] If you don't like this behavior set allocator_may_return_null=1 [...]
我们怎么告诉阿山这件事? 环境变量ASAN_OPTIONS
使传递运行时选项成为可能;查找它们(回想一下,我们已经提供了指向 Saniizer 工具集的文档链接),我们这样使用它(可以同时传递多个选项,用:
分隔选项;为了好玩,我们还打开了详细程度选项,但修剪了输出):
$ ASAN_OPTIONS=verbosity=1:allocator_may_return_null=1 ./membugs_dbg_asan 10
==26026==AddressSanitizer: libc interceptors initialized
[...]
SHADOW_OFFSET: 0x7fff8000
==26026==Installed the sigaction for signal 11
==26026==Installed the sigaction for signal 7
==26026==Installed the sigaction for signal 8
==26026==T0: stack [0x7fffdf206000,0x7fffdfa06000) size 0x800000; local=0x7fffdfa039a8
==26026==AddressSanitizer Init done
doublefree(): cond 0
doublefree(): cond 1
==26026==WARNING: AddressSanitizer failed to allocate 0xffffffffffffffff bytes
membugs.c:doublefree:132: malloc failed
=================================================================
==26026==ERROR: AddressSanitizer: attempting double-free on 0x615000000300 in thread T0:
#0 0x4d9b90 in __interceptor_free.localalias.0 (<...>/membugs_dbg_asan+0x4d9b90)
#1 0x5139b0 in doublefree <...>/membugs.c:133:4
#2 0x5124d2 in process_args <...>/ch5/membugs.c:352:4
#3 0x512291 in main <...>/ch5/membugs.c:375:2
#4 0x7fd41e565b96 in __libc_start_main /build/glibc-OTsEL5/glibc-2.27/csu/../csu/libc-start.c:310
#5 0x419ea9 in _start (/home/seawolf/0tmp/membugs_dbg_asan+0x419ea9)
0x615000000300 is located 0 bytes inside of 512-byte region [0x615000000300,0x615000000500) freed by thread T0 here:
#0 0x4d9b90 in __interceptor_free.localalias.0 (<...>/membugs_dbg_asan+0x4d9b90)
#1 0x51391f in doublefree <...>/ch5/membugs.c:126:2
#2 0x5124d2 in process_args <...>/ch5/membugs.c:352:4
#3 0x512291 in main <...>/ch5/membugs.c:375:2
#4 0x7fd41e565b96 in __libc_start_main /build/glibc-OTsEL5/glibc-2.27/csu/../csu/libc-start.c:310
previously allocated by thread T0 here:
#0 0x4d9d60 in malloc (<...>/membugs_dbg_asan+0x4d9d60)
#1 0x51389d in doublefree <...>/ch5/membugs.c:122:8
#2 0x5124d2 in process_args <...>/ch5/membugs.c:352:4
#3 0x512291 in main <...>/ch5/membugs.c:375:2
#4 0x7fd41e565b96 in __libc_start_main /build/glibc-OTsEL5/glibc-2.27/csu/../csu/libc-start.c:310
SUMMARY: AddressSanitizer: double-free (<...>/membugs_dbg_asan+0x4d9b90) in __interceptor_free.localalias.0
==26026==ABORTING
$
这一次,即使遇到分配故障,ASAN 仍会继续运行,因此找到了真正的 bug--双重释放。
**测试用例#11:**内存泄漏测试用例 1-简单泄漏。 请参阅以下代码:
$ ./membugs_dbg_asan 11
leakage_case1(): will now leak 32 bytes (0 MB)
leakage_case1(): will now leak 1048576 bytes (1 MB)
=================================================================
==26054==ERROR: LeakSanitizer: detected memory leaks
Direct leak of 1048576 byte(s) in 1 object(s) allocated from:
#0 0x4d9d60 in malloc (<...>/membugs_dbg_asan+0x4d9d60)
#1 0x513e34 in amleaky <...>/ch5/membugs.c:66:8
#2 0x513a79 in leakage_case1 <...>/ch5/membugs.c:111:2
#3 0x5124ef in process_args <...>/ch5/membugs.c:356:4
#4 0x512291 in main <...>/ch5/membugs.c:375:2
#5 0x7f2dd5884b96 in __libc_start_main /build/glibc-OTsEL5/glibc-2.27/csu/../csu/libc-start.c:310
Direct leak of 32 byte(s) in 1 object(s) allocated from:
#0 0x4d9d60 in malloc (<...>/membugs_dbg_asan+0x4d9d60)
#1 0x513e34 in amleaky <...>/ch5/membugs.c:66:8
#2 0x513a79 in leakage_case1 <...>/ch5/membugs.c:111:2
#3 0x5124e3 in process_args <...>/ch5/membugs.c:355:4
#4 0x512291 in main <...>/ch5/membugs.c:375:2
#5 0x7f2dd5884b96 in __libc_start_main /build/glibc-OTsEL5/glibc-2.27/csu/../csu/libc-start.c:310
SUMMARY: AddressSanitizer: 1048608 byte(s) leaked in 2 allocation(s).
$
它确实找到了漏洞,并准确定位了它。 还要注意的是,LeakSaniizer(LSAN)实际上是 ASAN 的一个子集。
测试用例#13**:**内存泄漏测试用例 3-libAPI 泄漏
下面是一个截图,展示了当 Asan(在引擎盖下,lsan)捕捉到泄漏时的行动:
抓得好!
关于我们的测试用例(包含在我们的membugs
程序中),下面是 Asan 的成绩单:
| 测试用例编号 | 测试用例 | 是否被地址消毒器检测到? | | 1. | UMR | No[1] | | 2 个 | OOB(越界):写入溢出 [在编译时内存上] | 是 | | 3. | OOB(越界):写入溢出 [在动态内存上] | 是 | | 4. | OOB(越界):写入下溢 [在动态内存上] | 是 | | 5. | OOB(越界):读取溢出 [在编译时内存上] | 是 | | 6. | OOB(越界):读取溢出 [在动态内存上] | 是 | | 7. | OOB(越界):读取下溢 [在动态内存上] | 是 | | 8 个 | UAF(释放后使用)也称为悬挂式指针 | 是 | | 9. | UAR 也称为 UAS(范围后使用) | 是的[2] | | 10 个 | 双重免费 | 是 | | 11. | 内存泄漏测试用例 1:简单泄漏 | 是 | | 12 个 | 内存泄漏测试用例 1:泄漏更多(循环中) | 是 | | 13 个 | 内存泄漏测试案例 1:Lib API 泄漏 | 是 |
Table 4: AddressSanitizer and Memory Bugs
[1]内存清洁器(MSAN)正好实现了这一目的-它可以检测 UMR。但是,有两件事需要注意:
- MSAN 仅在动态分配的内存上检测到 UMR
- 成功使用 MSAN 需要使用 Clang 编译器(它不适用于 GCC)
[2]这需要注意的是,代码是用 Clang 编译的,detect_stack_use_after_return=1
标志是通过ASAN_OPTIONS
传递的。
ASAN 的优点:
-
捕获静态(编译时)和动态分配的内存区域 上的常见内存错误(UB)
- 越界(OOB)内存访问(读/写下溢/溢出错误)
- 释放后使用(UAF)错误
- 退货后使用(UAR)错误
- 双重免费
- 渗漏 / 泄密 / 泄漏
-
性能远远优于其他工具(如 Valgrind);最坏的情况下性能下降似乎是原来的 2 倍
-
无需修改源代码
-
完全支持多线程应用
桑康斯:
- ASAN 无法检测到某些类型的错误:
- UMR(如前所述,有一些注意事项,MSAN 可以)
- 未检测到所有 UAF 错误
- IOF(整数下溢/上溢)错误
- 一次使用某个工具;不能总是组合多个消毒器工具(见上表);这意味着通常必须为 ASSAN、TSAN、LSAN 编写单独的测试用例
- 编译器:
- 通常,需要使用 LLVM 前端 Clang 和适当的编译器标志重新编译程序。
- 为了查看带有行号信息的调用堆栈,需要使用
-g
标志重新编译/编译。
在这里,我们合并了前面的两个表。 请参阅下表内存错误-Valgrind 和 Address Saniizer 之间的快速比较:
| 测试用例编号 | 测试用例 | 被 Valgrind 检测到? | 是否被地址消毒器检测到? | | 1. | UMR | 是 | No[1] | | 2 个 | OOB(越界):写入溢出 [在编译时内存上] | 不是 | 是 | | 3. | OOB(越界):写入溢出 [在动态内存上] | 是 | 是 | | 4. | OOB(越界):写入下溢 [在动态内存上] | 是 | 是 | | 5. | OOB(越界):读取溢出 [在编译时内存上] | 不是 | 是 | | 6. | OOB(越界):读取溢出 [在动态内存上] | 是 | 是 | | 7. | OOB(越界):读取下溢 [在动态内存上] | 是 | 是 | | 8 个 | UAF(释放后使用)也称为悬挂式指针 | 是 | 是 | | 9. | UAR(退货后使用),也称为 UAS(范围后使用) | 不是 | 是的[2] | | 10 个 | 双重免费 | 是 | 是 | | 11. | 内存泄漏测试用例 1:简单泄漏 | 是 | 是 | | 12 个 | 内存泄漏测试用例 1:泄漏更多(循环中) | 是 | 是 | | 13 个 | 内存泄漏测试案例 1:Lib API 泄漏 | 是 | 是 |
[1]MSAN 正好实现了这一目的-它确实检测到了 UMR(另请参阅警告)。
它需要注意的是,代码是使用 Clang 编译的,并且通过ASAN_OPTIONS
传递detect_stack_use_after_return=1
标志。
Glibc 有时对程序员很有用,它提供了一种更改 malloc 引擎缺省值的方法,这要归功于它能够传递一些特定的参数。 接口为mallopt(3)
:
#include <malloc.h>
int mallopt(int param, int value);
Please refer to the man page on mallopt(3)
for all the gory details (available at http://man7.org/linux/man-pages/man3/mallopt.3.html).
作为一个有趣的示例,可以调整的参数之一是**M_MMAP_THRESHOLD
**;回想一下,在前面的第 5 章,Linux 内存问题中,我们讨论了这样一个事实:在现代的 glibc 上,malloc 并不总是从堆段获得内存块。 如果分配请求的大小大于或等于MMAP_THRESHOLD
,则通过功能强大的mmap(2)
系统调用(它设置所请求大小的虚拟地址空间的任意区域)在幕后处理该请求。 MMAP_THRESHOLD
的默认值为 128KB;可以使用mallopt(3)
通过M_MMAP_THRESHOLD
参数更改该值!
再说一次,这并不意味着你应该改变它;只是说你可以改变它。 默认值经过仔细计算,可能最适合大多数应用工作负载。
另一个有用的参数是M_CHECK_ACTION
;该参数确定当检测到内存错误(例如,写溢出或双重释放)时 glibc 如何反应。 还要注意的是,该实现不会检测所有类型的内存错误(例如,泄漏不会被注意到)。
*在运行时,glibc 解释参数值的这三个最低有效位(LSB),以确定如何反应:
- 位 0:如果设置,则向
stderr
打印一行错误信息,提供有关原因的详细信息;错误行格式为:
*** glibc detected *** <program-name>: <function where error was detected> : <error description> : <address>
- 位 1:如果设置,则在打印错误消息后调用
abort(3)
,导致进程终止。 根据库的版本,还可以打印堆栈跟踪和进程内存映射的相关部分(通过 proc)。 - 位 2:如果设置,如果设置位 0,则简化错误信息格式。
从 Glibc Ver 来的。 2.3.4,M_CHECK_ACTION
默认值为 3(表示二进制 011,之前为 1)。
Setting M_CHECK_ACTION
to a nonzero value can be very useful as it will cause a buggy process to crash at the point the bug is hit, and display useful diagnostics. If it were zero, the process would probably enter an undefined state (UB) and crash at some arbitrary point in the future, making debugging a lot harder.
作为快速计算器,下面是M_CHECK_ACTION
的一些有用值及其含义:
- 1(001b):打印详细的错误消息,但继续执行(进程现在在 ub!)。
- 3(011b):打印详细的错误消息、调用堆栈、内存映射和中止执行[默认]。
- 5(101b):打印一条简单的错误消息并继续执行(进程现在在 UB!)。
- 7(111B):打印一条简单的错误消息、调用堆栈、内存映射和中止执行。
mallopt(3)
上的手册页很有帮助地提供了一个使用M_CHECK_ACTION
的 C 程序示例。
一个有用的特性:系统允许我们通过环境变量方便地调优一些分配参数,而不是以编程方式使用mallopt(3)
API。 从调试和测试的角度来看,可能最有用的是,MALLOC_CHECK_
变量是与前面描述的M_CHECK_ACTION
参数相对应的环境变量;因此,我们只需设置该值,运行应用,然后自己查看结果!
下面是几个示例,使用我们常用的 membugs 应用检查一些测试用例:
**测试用例#10:**双空闲,MALLOC_CHECK_
设置:
$ MALLOC_CHECK_=1 ./membugs_dbg 10
doublefree(): cond 0
doublefree(): cond 1
membugs.c:doublefree:134: malloc failed
*** Error in `./membugs_dbg': free(): invalid pointer: 0x00005565f9f6b420 ***
$ MALLOC_CHECK_=3 ./membugs_dbg 10
doublefree(): cond 0
doublefree(): cond 1
membugs.c:doublefree:134: malloc failed
*** Error in `./membugs_dbg': free(): invalid pointer: 0x0000562f5da95420 ***
Aborted
$ MALLOC_CHECK_=5 ./membugs_dbg 10
doublefree(): cond 0
doublefree(): cond 1
membugs.c:doublefree:134: malloc failed
$ MALLOC_CHECK_=7 ./membugs_dbg 10
doublefree(): cond 0
doublefree(): cond 1
membugs.c:doublefree:134: malloc failed
$
请注意,当MALLOC_CHECK_
的值为 1 时,将打印错误消息,但进程不会中止;这就是环境变量的值设置为3
时发生的情况。
**测试用例#7:**越界(读取下溢),设置为MALLOC_CHECK_
:
$ MALLOC_CHECK_=3 ./membugs_dbg 7
read_underflow(): cond 0
dest: abcd56789
read_underflow(): cond 1
dest: xabcd56789
*** Error in `./membugs_dbg': free(): invalid pointer: 0x0000562ce36d9420 ***
Aborted
$
**测试 c****ASE#11:**内存泄漏测试用例 1-使用MALLOC_CHECK_
设置的简单泄漏:
$ MALLOC_CHECK_=3 ./membugs_dbg 11
leakage_case1(): will now leak 32 bytes (0 MB)
leakage_case1(): will now leak 1048576 bytes (1 MB)
$
请注意,泄漏错误测试用例是如何没有被检测到的。
The preceding examples were executed on an Ubuntu 17.10 x86_64 box; for some reason, interpretation of MALLOC_CHECK_
on a Fedora 27 box did not seem to work as advertised.
我们已经介绍了一些强大的内存调试工具和技术,但归根结底,这些工具本身是不够的。 今天的开发人员必须保持警惕--还有一些要点需要简要提及,这些要点将作为本章的补充。
要记住使用动态分析工具(我们使用 Valgrind 的 Memcheck 工具和 ASAN/MSAN 进行了介绍)的一个关键点是,只有在测试用例上运行工具时实现完整的代码覆盖,才能真正帮助您完成工作!
这一点怎么强调都不为过。 如果代码中有错误的部分没有实际运行,那么在程序上运行奇妙的工具或编译器工具(如消毒器)有什么用呢? 这些虫子仍然处于休眠状态,没有被捕获。 作为开发人员和测试人员,我们必须严格要求自己编写严格的测试用例,以实际执行完整的代码覆盖,以便所有代码-包括库中的项目代码-实际上都通过这些强大的工具进行测试。
这并不容易:记住,任何值得做的事都值得做好。
面对用 C/C++编写的复杂软件项目中如此多的 UB 潜力,相关的开发人员可能会问,我们该怎么办?
Source: https://blog.regehr.org/archives/1520. Here is a snippet from the excellent blog article, Undefined Behavior in 2017, by Cuoq and Regehr. What is the modern C or C++ developer to do?
- 使用简单的 UB 工具-这些工具通常只需调整 Makefile 即可启用,例如编译器警告、ASAN 和 UBSAN。 尽早并经常使用这些方法,(关键是)根据他们的发现采取行动。
- 熟悉硬 UB 工具(如 TIS 解释器等通常需要更多精力才能运行的工具),并在适当的时候使用它们。
- 投资于基础广泛的测试(跟踪代码覆盖,使用模糊器),以便从动态 UB 检测工具中获得最大收益。
- 执行 UB 感知的代码审查:建立一种文化,让我们集体诊断潜在的危险补丁,并在它们登陆之前修复它们。
- 了解 C 和 C++标准中的实际内容,因为这些是编译器编写人员要遵循的标准。 避免重复令人厌烦的格言,比如 C 是一种可移植的汇编语言,相信程序员。
有很多malloc
API 帮助器例程。 这些在调试困难的场景时很有用;了解可用的内容是个好主意。
在 Ubuntu Linux 系统上,我们与 man 检查关键字:malloc
是否匹配:
$ man -k malloc
__after_morecore_hook (3) - malloc debugging variables
__free_hook (3) - malloc debugging variables
__malloc_hook (3) - malloc debugging variables
__malloc_initialize_hook (3) - malloc debugging variables
__memalign_hook (3) - malloc debugging variables
__realloc_hook (3) - malloc debugging variables
malloc (3) - allocate and free dynamic memory
malloc_get_state (3) - record and restore state of malloc implementation
malloc_hook (3) - malloc debugging variables
malloc_info (3) - export malloc state to a stream
malloc_set_state (3) - record and restore state of malloc implementation
malloc_stats (3) - print memory allocation statistics
malloc_trim (3) - release free memory from the top of the heap
malloc_usable_size (3) - obtain size of block of memory allocated from heap
mtrace (1) - interpret the malloc trace log
mtrace (3) - malloc tracing
muntrace (3) - malloc tracing
$
其中相当多的malloc
API(提醒:圆括号中的数字 3,(3),暗示这是一个库例程)处理 malloc 钩子的概念。 基本思想:可以将库malloc(3)
、realloc(3)
、memalign(3)
和free(3)
API 替换为自己的hook
函数,该函数将在应用调用 API 时被调用。
不过,我们不会再深入研究这方面的问题,何乐而不为呢? 最近版本的 glibc 记录了这样一个事实,即这些钩子函数是:
- 非 MT-SAFE(在第 16 章,第三部分中介绍了使用 Pthreads 多线程)
- Glibc ver 中已弃用。 2.24 以后的版本
最后,这可能是显而易见的,但我们更愿意明确指出这一点:人们必须意识到,使用这些工具只在测试环境中服务;它们并不是要在生产中使用! 一些研究揭示了在生产环境中运行 ASAN 时可利用的安全漏洞;请参阅 GitHub 存储库中的进一步阅读部分。
在本章中,我们尝试向读者展示几个关键点、工具和技术;其中包括:
- 人类会犯错误;对于内存非托管语言(C、C++)尤其如此。
- 对于非常重要的代码库,确实需要强大的内存调试工具。
- 我们详细介绍了其中两个同类最好的动态分析工具:
- 瓦尔格林德(氏)Memcheck
- 消毒剂(主要是 Asan)
- Glibc 允许通过
mallopt(3)
API 以及环境变量对 malloc 代码进行一些调整。 - 在构建测试用例时确保完整的代码覆盖率对项目的成功绝对至关重要。
The next chapter is related to the essentials aspects of file I/O which is essential for a component reader to know. It introduces you to performing efficient file I/O on the Linux platform. We would request the readers to go through this chapter which is available here: https://www.packtpub.com/sites/default/files/downloads/File_IO_Essentials.pdf. We highly recoomend the readers to read Open at the system call layer, The file descriptor and I/O – the read/write system calls which can help in easy understanding the next chapter that is, Chapter 7, Process Credentials.*