Spring aware接口的作用是什么
472
2022-10-07
“内存调试技术”了解一下(内存条安装后怎么调试)
内存问题在 C/C++ 程序中十分常见,比如缓冲区溢出,使用已经释放的堆内存,内存泄露等。 程序大了以后,查找起来又特别的难。即使我们在写程序时非常的仔细小心,代码一多,还是难以保证没有问题。 内存问题除了造成程序崩溃引发意外,也很容易被当做漏洞利用,给程序安全带来隐患。诸多工具尝试通过静态代码分析或运行时动态检测来发现内存问题。 Mozilla 甚至因为内存问题专门发明了一个新的编程语言 Rust,一定程度上回避了程序员的失误,但不能完全解决。 无意间看到一篇讲解 AddressSanitizer 的论文 [1],介绍了几种动态检测技术,分析了多种工具的原理和优缺点,在此整理分享。
一、影子内存(shadow memory)
Shadow Memory 姑且直译为影子内存。 为了说明影子内存,我们把程序正常运行使用的内存叫做 常规内存。 影子内存技术,就是使用额外的内存来存管理常规内存的分配和使用,这些额外的内存对于被检测程序不可见,因此叫影子内存。 每块常规内存都有对应的影子内存。 常规内存分配和释放的时候,在对应的影子内存里记录该常规内存的属性信息,比如是否可访问,是否已经被释放。在每次访问常规内存之前,都先检查对应的影子内存,看看该常规内存是否可访问。 为了快速找到常规内存对应的影子内存,通常使用某种映射算法,实现常规内存地址到影子内存地址的映射。 一种是查表,一种是用比例+偏移来直接映射。查表就是事先设置一个表,里面保存者常规内存和影子内存的对应关系。不多叙述。以下介绍一下比例+偏移的方式。
比例+偏移的映射算法
0, 表示所有 8 个字节都可以访问; k, (1<=k<=7) 表示前 k 个字节可以访问; 负数, 表示整个的 8 个字节都不可访问。可以使用不同的负数,表示不同的内存区域,比如堆内存,栈内存,全局变量的内存,已经释放的内存。
直接映射的代表性的例子是 TaintTrace 和 LIFT。TaintTrace 按照 1:1 映射。缺点就是无法处理内存需求特别大的被检测程序 ,如果被检测程序使用了一半以上的地址空间,那就没有足够的地址空间来容纳影子内存了。相比来说,LIFT 使用 8:1 的比例设置影子内存。 间接映射的代表是 valgrind 和 Dr.Memory。他们设置多个影子内存段,然后配合查表法来完成映射。
二、插桩(instrumentation)
Instrumentation,指用仪器在系统的某些节点进行测量或干预。 这里指在程序的代码里,插入一些测量或是控制用的额外代码。这些额外代码,通常用于 shadow memory 的管理和检测。 Instrumentation 可以编译时完成,编译器生成代码时直接在原来的程序代码里插入一些额外的代码,也可以在编译后完成,修改程序的二进制代码,在里面插入一些额外代码。 在 8 字节对齐的环境里,程序访问一个 8 字节的常规内存时,可以插入以下代码来完成 shadow memory 的检测: 回顾影子内存的编码:0 表示可访问
ShadowAddr = (Addr >> 3) + Offset; if (*ShadowAddr != 0) ReportAndCrash(Addr);
如果程序访问的是长度为 1 或 2 或 4 字节的常规内存,稍微复杂一点,需要比较影子内存里的 k 值和常规内存地址的后 3 位: (回顾影子内存的编码:0 表示可访问,k 表示前 k 字节可访问,负数表示 8 个字节都不能访问)
ShadowAddr = (Addr >> 3) + Offset; k = *ShadowAddr; if (k != 0 && ((Addr & 7) + AccessSize > k)) ReportAndCrash(Addr);
以下用 AddressSanitizer 的例子来说明 instrumentation。分别是 x64 环境里的 8 字节和 4 字节访问。 原本的函数是这样——
void foo(T *a) { *a = 0x1234; }
8 字节访问:
clang -O2 -faddress-sanitizer a.c -c -DT=long
插入代码以后是这样——
push %rax
mov %rdi,%rax # %rdx是指针a
shr $0x3,%rax
mov $0x100000000000,%rcx
or %rax,%rcx # 取得a的影子内存地址
cmpb $0x0,(%rcx) # 判断影子内存的值是否为0(0表示可访问)
jne 23
4 字节访问:
clang -O2 -faddress-sanitizer a.c -c -DT=int
插入代码以后是这样——
push %rax
mov %rdi,%rax # %rdx是指针a
shr $0x3,%rax
mov $0x100000000000,%rcx
or %rax,%rcx
mov (%rcx),%al # 取得影子内存的值
test %al,%al
je 27
三、专用版内存函数
使用专用版本的内存分配和释放函数,替换系统的内存分配和释放函数,由此提供额外的内存管理功能,检测内存的异常使用,同时又不改变原来程序的流程。 这里又分两类:
利用 CPU 的内存页保护功能以 Electric Fence, Duma, GuardMalloc, Page Heap 为代表的工具,使用 CPU 的内存页保护功能:CPU 访问一个不可访问的内存页的时候,会触发异常。该类工具实现的内存分配函数,除了正常的分配内存,还在后面紧接着分配一个不可访问的内存页。程序如果访问内存越界,就访问到了后面的内存页,触发异常。这种办法的缺点是,如果程序需要分配很多的内存,会导致分配很多后面的不可访问的内存页,分配内存次数多,就会运行的很慢。并且这种方式无法检测到轻微的越界,比如分配了 5 个字节的内存,访问了第 6 个字节,因为内存对齐的原因,访问第 6 个字节的内存不会触发异常。 使用填充区和填充标记DieHarder, Dmalloc 为代表,分配内存时,在被分配内存的前后,额外分配内存,并填充特殊的值,释放内存的时候,在被释放的内存里也填充特殊值。如果程序读到了这些特殊值,就表示程序访问内存越界了。这种方法的缺点是,无法及时检测到越界访问行为,只能在运行结束时分析特殊值是否被读取或改写来计算总结,这会导致一定的概率检测不到错误。
上面两者方法都只能用来检测堆内存上的问题。StackGuard 和 Propolice 利用同样的原理,在栈上面也填充一些特殊值,在程序返回的时候检测是否被改写,来发现问题。 实际的内存检测工具,往往多种技术并用,在细节上,算法上有所差异,导致工具的性能和准确度各有千秋。通常检测质量高的,效率比较低;效率高的,质量又会低。有的工具,会吃掉数倍甚至数十倍的内存,cpu 效率也降低到 1/10 的量级。AddressSanitizer 在多种工具的基础上,各取所长,显著提高质量和效率,综合只有 73% 的降低。 在 clang 和 gcc 中都实现了 AddressSanitizer。只需要编译的时候添加上 -fsanitize=address -fno-omit-frame-pointer 即可。该论文中提到,利用 AddressSanitizer 在 Chromium 浏览器中找到了 300 多个之前没有发现的 bug。效果拔群,值得推荐。(陈国 | 天存信息)
Ref.
K. Serebryany, D. Bruening, A. Potapenko, D. Vyukov - AddressSanitizer: A Fast Address Sanity Checker
版权声明:本文内容由网络用户投稿,版权归原作者所有,本站不拥有其著作权,亦不承担相应法律责任。如果您发现本站中有涉嫌抄袭或描述失实的内容,请联系我们jiasou666@gmail.com 处理,核实后本网站将在24小时内删除侵权内容。
发表评论
暂时没有评论,来抢沙发吧~