在WWDC2015上,除了Swift2.0之外,还有一个振奋人心的消息:你可以在Xcode7上直接使用Clang的地址消毒器(AddressSanitizer)。在本文中我们将讨论这个特性详细说明,它是如何工作的,以及如何使用它。这是KonstantinGonikman提出的一个话题。C中异常危险的情况C在很多方面都是一种很棒的编程语言。事实上,在它发明40多年后,它仍然很强大。这足以说明它的伟大。这不是我学习的第一种(也不是第二种)编程语言,但这是我第一次真正揭开计算机工作原理的神秘面纱。此外,它是我至今仍在使用的唯一语言。然而,C也是一种非常危险的编程语言,编码世界中的许多痛苦都源于它。它制造了许多其他编程语言根本无法表达的怪异错误。内存安全是一个主要问题。C中根本没有内存安全。像下面这样的代码将被正常编译并可能正常运行:char*ptr=malloc(5);ptr[12]=0;这段代码只申请了一个5字节的数组空间,而是通过指向第13字节的指针数据写入的。在这个地址,隐藏的数据损坏可能发生也可能不发生(例如,在Apple平台上,malloc函数总是分配至少16字节,即使你请求少于16字节的空间,所以这段代码在Apple上工作正常平台,但不要依赖系统的这个功能)。这个错误代码可能是无害的,也可能是致命的。更“聪明”的语言会在操作时跟踪数组的大小并验证下标的有效性。相同的Java代码将更可靠地抛出异常。有了异常机制,调试这些“神奇”的问题就容易多了。例如,如果一个变量应该是4,但它的实际值为5,我们就知道修改变量值的某段代码有问题(这样至少我们可以专注于程序调试而不是盯着编译,因为它通常没有错误)。但是使用C语言,我们根本不能做假设。bug可能是某段代码“故意”修改了变量值,也可能是某段代码使用了“坏指针”无意中修改了变量值。整个行业已经开始着手解决这个问题。例如,Clang的静态代码分析可以发现代码中特定类型的内存安全问题。像Valgrind这样的程序可以在运行时检测不安全的内存访问。AddressSanitizer是另一种解决方案。它使用了一种有利有弊的新方法。但它仍然是查找代码问题的强大工具。内存访问验证许多这些工具通过在运行时验证内存访问的有效性来发现问题。其理论依据是:在访问内存时,通过将访问的内存与程序实际分配的内存进行比较,来验证内存访问的有效性,从而在bug出现时发现,直到副作用出现时才被发现.理想情况下,每个指针都包含有关数据大小及其指向的内存位置的信息,因此每次内存访问都可以根据这些信息进行验证。C编译器在设计之初没有加入校验功能的原因并没有具体说明。但是附加到指针的元数据使程序与标准C编译器编译的代码不兼容。这意味着系统库不能简单使用,这不可避免地严重限制了使用该系统的检测代码。Valgrind解决上述问题的方法是在模拟器上运行整个程序。这样,标准C编译器生成的二进制文件可以直接运行,无需任何额外修改。然后在程序运行时对其进行分析,检查程序处理的每个内存块。这样,它可以高效运行所有程序,包括系统库,无需任何修改。这样做的代价是变得很慢,所以在一些对效率要求很高的程序中并不实用。此外,这种方法需要深入理解平台系统调用的含义,只有这样才能正确跟踪内存变化状态。因此,必然需要针对特定??主机系统进行深度集成。多年来,Valgrind一直没有明确的Mac支持计划。在撰写本文时,它不支持Mac10.10。保护性内存分配受益于CPU的内置内存检查工具。它取代了标准的malloc函数。使用时,每个分配的内存的末尾都被标记为不可读或不可写。当程序试图访问后面的内存时,会发生错误。这种方法有一个缺点:硬件内存保护不够精确。内存只能在页面尺度上被标记为可读或不可读,而在现代操作系统中,页面至少有4kB的空间。这意味着每次内存分配至少需要8kB内存:一页内存用于存储数据,另一页用于限制越界内存访问。即使只分配了几个字节的内存,这也是必需的。此外,这种方法还导致未被发现的小规模交叉。为了存储对标准malloc内存的保护,需要分配16字节范围内的内存。因此,如果分配的内存大小不是16字节的整数倍,剩下的几个字节就不会被保护。内存清理器机制试图以更精细的粒度处理内存约束。从本质上讲,这样的内存分配保护机制速度较慢,但??更实用。由于硬件级别的内存保护不能用于跟踪受限内存,因此必须使用软件来实现。由于不能通过指针传递额外的数据,因此必须通过某种“全局表”来跟踪内存。该表需要能够被快速读取和修改。MemorySanitizer使用一种简单但巧妙的方法:它在进程的内存空间中保留一个固定区域,称为“影子内存区域”。用内存消毒剂的说法,标记为受限的内存被称为“中毒”内存。“影子内存区”会记录哪些内存字节是中毒的。通过一个简单的公式,可以将进程中的内存空间映射到“影子内存区”,即:每8个字节的普通内存块映射到一个字节的影子内存。在影子内存上,跟踪这个8字节的“毒状态”。每8个字节的内存映射8位(1字节)的影子内存,我们很自然地认为每个字节内存的“中毒状态”只能通过影子内存上的一个位来标记。然而,实际情况是内存清理器在跟踪内存状态时使用每个字节的整数值来记录。它假设所有“中毒内存”块都是连续的,并且顺序是从后到前,所以一个字节的影子内存可以用来表示一个正常内存块中“中毒”内存的数量。例如:0表示所有内存正常;1表示最后一个字节有问题;2表示最后两个字节有问题,以此类推;7表示这些字节有问题。如果所有8个字节都“中毒”,则此值为负数。这样就可以在访问内存的时候进行检查。分配内存的起始位置一般不会太近,所以假设“中毒”的内存是连续的,而且是从后往前,这不会造成什么问题。通过这种表结构,AddressSanitizer会在程序中生成额外的代码来检查每个使用指针的读写操作,并在内存中毒的情况下抛出错误。此功能集成在编译器中,而不仅仅是在外部库和运行时环境中,这带来了许多好处:可以可靠地识别每个指针访问,并在机器代码中添加适当的内存检查。.编译器集成还支持一些巧妙的技巧,例如除了在堆上分配的内存之外,还跟踪受保护的局部和全局变量。局部和全局内存的分配存在一些间隙,如果内存“中毒”,这些间隙可能会导致溢出。此时protectedmemoryallocation就无能为力了,Valgrind也在苦苦应对。编译器集成功能也有其缺点。详细地说,地址清理器无法捕获系统库中的错误内存访问。当然,它是与系统库“兼容”的。使用系统库时,可以打开内存清理器功能。例如,您可以构建一个链接Cocoa的程序并正常运行它。但它不会捕获Cocoa进行的错误内存访问,也不会检测代码调用Cocoa时分配的内存。内存清理器也可用于捕获“释放后使用”错误。内存释放后,会被标记为“中毒”,无法再访问。“释放后使用”错误在重用内存时非常有害,因为那样你会破坏不相关的数据。memorysanitizer会将新释放的内存放入一个recoveryqueue中,内存会在一段时间内不可用,避免在重用时出现此类错误。自然地,为每个指针访问添加检查并不便宜。这取决于代码的作用,因为不同类型的代码访问指针内容的方式不同。内存检查平均会减慢2~5倍左右的速度。这个开销相当大,但还不足以让程序无法使用。如何使用?在Xcode7上使用AddressSanitizer很简单。从命令行编译时,需要将-fsanitize=address参数添加到clang命令调用中。下面是一个测试程序:编译,运行AddressSanitizer:程序立即崩溃,并输出大量内容:这包含了很多信息,对于实际场景中跟踪问题会有很大的帮助。它不仅显示了错误内存的写入位置,而且还标识了内存最初分配的位置。此外,还有其他附加信息。在Xcode中使用memorysanitizer更容易:编辑方案,单击Diagnostics选项卡,然后选中“EnableAddressSanitizer”选项。然后就可以正常构建运行了,然后就可以看到很多诊断信息了。附加功能:未定义的行为消毒剂错误的内存访问只是C语言中许多“有趣”的歧义行为之一。Clang还提供了其他消毒剂,可以捕获许多不明确的行为。下面是一个示例程序:#include#includeintmain(intargc,char**argv){intvalue=1;for(intx=0;x
