调试方式

来源:互联网 发布:微信公众号授权域名 编辑:程序博客网 时间:2024/05/16 05:36

1、断点

a)、基本断点


Condition
这里可以输入条件表达式,满足条件的时候断点就会生效。例如上面输入a == 50。这个是非常有用的设置,特别在循环体内调试的时候,用着真的是爽。


Ingore
在这里可以设置忽略断点次数。


Action


常用的就是Log Message和Debugger Command

Log Message

在这里填写的东西可以打印到控制台。例如我做了如下设置:

duandian3.png

%B会打印断点的名字,%H会打印断点的调用次数,@@中间可以输入表达式。 上面的设置在控制台的输出如下:

55.png

Debugger Command

这里可以输入调试命令,也就是po(打印对象信息),bt(打印函数栈),expression(表达式)这些调试命令。看图就明白了:

duandian4.png

image 控制台输出如下:

duandian5.png

Options

勾选Automatically continue after evaluating actions之后程序会在断点产生后继续运行。这个属性是相当有用的,可以输入调试信息至于不暂停程序。


b)、其他断点



Exception Breakpoint

Exception Breakpoint是一个非常有用的断点项。正如名字所示,当程序抛出异常的时候就回产生断点。通常程序崩溃会停在崩溃的地方,但有时候并不能准确停在引起异常的地方。比如数组越界!比如我下图所示,会引起数组越界访问。 

duandian7.png

duandian8.png

程序运行的时候就会崩溃。但是崩溃停在了main函数里面,就算看了栈信息也不能马上定位到到底是那个数组越界访问了。为什么崩溃不能停在数组越界哪里?这是因为数组越界访问不一定会导致程序崩溃的,数组越界访问会导致异常抛出,而抛出的异常没有得到处理才会导致程序崩溃。因此最后会导致崩溃停在CoreFoundation框架里面。这个时候就需要设置Exception Breakpoint产生断点来定位错误了。

duandian10.png

duandian11.png

OpenGL ES Error Breakpoint

这个主要是OpenGL ES的断点调试,这个个人没用到过。

Symbolic Breakpoint

Symbolic Breakpoint,符号断点,真的是调试神器啊。当程序运行到特定符号的时候就会产生断点。通过这种方式添加断点,就不需要在源文件中添加,也不需要知道断点设置在文件的第几行。如图: 

duandian12.png

比普通断点多了两个属性Symbol和Module。

Symbol

Symbol的内容,可以有如下几种: 

1. 方法名称:会对所有具有此方法名称的类方法生效。例如 initWithFrame: 。 

2. 特定类的方法:OC类和C++类都适用,例如 ,[UIView initWithFrame:]或者 Shap::draw()。 

3. 函数名称。例如普通C函数。

通过设置Symbol来调试,好用根本停不下来,想怎么断点就怎么断点。


Test Failure Breakpoint

这个类型的断点会在test assertion 失败的时候暂停程序的执行。


2、LLDB

lldb 常用命令 

po(print-object)+ 对象

p(print)+ 基本数据类型 int之类

bt 打印最后一次调用堆栈

expr 动态修改变量


3、Log

a)、使用macro封装的NSLOG

b)、使用封装好的开源库,如NSLogger


4、descriotion

对于一个Person类,如果没有重写description方法,NSLog(@%@,p),输出的是 Person:地址,而我们想要的效果是打印出Person的成员变量,所以我们可以在Person类里重写description方法。description方法,返回值是OC字符串。


5、NSZombieEnabled(僵尸模式)

可以在Xcode的scheme页面中设置NSZombieEnabled环境变量。点击Product——>Edit Scheme打开该页面,然后勾选Enable Zombie Objects 复选框。


NSZombieEnabled变量用来调试与内存有关的问题,跟踪对象的释放过程。启用了NSZombieEnabled的话,它会用一个僵尸来替换默认的dealloc实现,也就是在引用计数降到0时,该僵尸实现会将该对象转换成僵尸对象。僵尸对象的作用是在你向它发送消息时,它会显示一段日志并自动跳入调试器。

          所以,当在应用中启用NSZombie而不是让应用直接崩溃掉时,一个错误的内存访问就会变成一条无法识别的消息发送给僵尸对象。僵尸对象会显示接受到得信息,然后跳入调试器,这样你就可以查看到底是哪里出了问题。


6、Address Sanitizer(地址消毒剂


Address Sanitizer是另外一种解决方案。它使用了一种新的方法,有利有弊。但仍不失为一个查找代码问题的有力工具。

内存访问验证

许多这类工具在运行时验证内存访问的有效性,从而查找到问题。理论依据是:访问内存时,通过比较访问的内存和程序实际分配的内存,验证内存访问的有效性,从而在bug发生时就检测到它们,而不会等到副作用产生时才有所察觉。

理想情况下,每个指针都会包含数据大小和指向内存的位置信息,因此可针对这些验证每次的内存访问。为何C编译器在设计之初没有加入验证的特性,还没有具体的原因。但附加在指针上的元数据会使程序无法兼容标准C编译器编译的代码。这就意味着无法简单使用系统库,必然严重限制了使用该体系检测代码。

Valgrind解决以上问题的方案是:在模拟器上运行整个程序。这样,就可以直接运行标准C编译器生成的二进制文件,而不需要做任何额外的修改。然后在程序运行的时候进行分析,检查程序处理的每一块内存。这样的方式可以使它高效运行所有程序,包括系统的库,而不做任何修改。这样做的代价是速度变得很慢,因此在一些效率要求高的程序中不实用。另外,这种方式需要深度了解某个平台系统调用的含义,

只有这样才能合适地追踪内存改变状态。因而必然需要针对特定宿主系统的深度整合。多年间,Valgrind对Mac的支持无明确计划。截止本文发布之时,它还不支持Mac 10.10。

保护性内存分配得益于CPU内置的内存检查工具。它取代了标准的malloc函数。使用时,每个分配内存结尾的后面会被标记为不可读写。当程序尝试访问后面的内存,会出错。这样的做法有一个弊端:硬件的内存保护精确度不够。内存只能在内存页尺度上被标记为可读或不可读,而在现代操作系统中,内存页至少有4kB空间。这意味着每次内存分配至少都需要占用8kB内存:一页内存用来存储数据,另外一页用来限制越界的内存访问。即使只申请几字节的内存,也需要这样做。另外,这样的做法也导致小规模的越界不会被检测到。为了储存针对标准malloc的内存的保护,需要分配内存到16字节的范围内,因此,若分配的内存大小不是16字节的整数倍,余出的几个字节将不受保护。

内存消毒剂机制尝试在更小的粒度上处理内存受限。在本质上,这样的内存分配保护机制较慢,但却更实用。

追踪受限内存

既然不能使用硬件层面的内存保护,就必须使用软件的手段来实现。因为通过指针无法传递额外数据,跟踪内存必须通过某种“全局表”来完成。这个表需要能被快速的读取和修改。

内存消毒剂使用了一种简单但是很巧妙的方法:它在进程的内存空间上保存了一个固定的区域,叫做“影子内存区”。用内存消毒剂的术语来说,一个被标记为受限的内存被称作“中毒”内存。“影子内存区”会记录哪些内存字节是中毒的。通过一个简单的公式,可以将进程中的内存空间映射到“影子内存区”中,即:每8字节的正常内存块映射到一个字节的影子内存上。在影子内存上,会跟踪这8字节的“中毒状态”。

每8字节的内存映射8位(1字节)的影子内存,我们自然会想到,每字节内存的“中毒状态”只能通过影子内存上的一位来标记的。然而实际情况是,内存消毒剂在跟踪内存状态时,每字节使用一个整型值来记录。它假定所有“中毒内存”块都是连续的,且顺序从后往前,因此可以使用影子内存的一个字节来表示正常内存块中“中毒”的内存数量。例如:0表示所有内存都是正常的;1表示最后一个字节有问题;2表示最后两个字节有问题,依次类推,7表示这几个字节都有问题。若所有8字节都“中毒”,这个值就为负。使用这样的方式,就可以在访问内存的时候进行检查。分配内存的起始位置一般来说不会太过接近,因此,假定“中毒”内存是连续的且从后往前的, 这样不会带来什么问题。

有了这个表结构,地址消毒剂在程序中生成额外的代码来检查每次使用指针的读写操作,并在内存中毒的状态下抛出错误。该特性被集成在编译器中,而不仅仅在外部库和运行环境中存在,这样带来了不少好处:每个指针访问可被可靠地标识,并将合适的内存检查添加到机器码中。

编译器集成还支持一些简洁的技巧, 比如,除了堆(heap)上分配的内存外,可以跟踪保护本地和全局变量。本地和全局内存分配时会产生一些间隔,这些间隔内存若“中毒”可能导致溢出。这一点上,保护式内存分配无能为力,Valgrind也疲于应对。

编译器集成的特性也有其缺点。详细来说,地址消毒剂无法捕捉系统库中的错误内存访问。当然,它和系统库是“兼容”的。当使用系统库的时候,你可以打开内存消毒剂功能。比如,你可以构建一个链接Cocoa的程序,正常运行它。但是它不会捕捉Cocoa造成的错误内存访问,也无法检测你的代码调用Cocoa时分配的内存。

内存消毒剂也能用来捕捉“释放后使用”的错误。内存在释放后都会被标记为“中毒”,之后无法对其再进行访问。“释放后使用”的错误在内存重用时危害不浅,因为那样你会破坏不相关的数据。内存消毒剂会将刚释放的内存放置到一个回收队列中,在一段时间内将无法申请到这些内存,从而在重用时避免这样的错误。自然,为每个指针访问添加检查代价不小。它取决于代码做了什么,因为不同类型的代码访问指针内容的频率各不相同。平均算来,内存检查会降低大概2~5倍的速度,这个开销挺大,但还不至于让程序无法使用。

如何使用?

在Xcode 7上使用Address Sanitizer很简单。当通过命令行编译时,需要给clang命令调用添加-fsanitize=address参数。下面是一个测试程序:
26.png

编译,通过Address Sanitizer运行:
27.png27.png

程序立马crash,输出很多内容:

QQ截图20150729163210.png

这里包含很多信息,真实场景中,这些信息将对跟踪问题产生巨大帮助。它不仅显示了错误内存写入的位置,还标识了内存初始分配的位置。另外,还有其他附加信息。

在Xcode中使用内存消毒剂更简单:编辑scheme,点击Diagnostics标签页,选中"Enable Address Sanitizer"选项。然后就可以正常构建、运行,然后就能查看到大量诊断信息。

附加特性:不明确行为消毒剂

错误的内存访问只是C语言中诸多“有趣”的不明确行为的一种。Clang还提供了其他的消毒剂,使用它可以捕捉许多不明确行为。以下是实例程序:

    #include     #include     int main(int argc, char **argv) {        int value = 1;        for(int x = 0; x < atoi(argv[1]); x++) {            value *= 10;            printf("%d\n", value);        }    }

运行代码:

21.png21.png

结果的最后有些怪异。毫无疑问,有符号整形值溢出是C语言中的不明确行为。若能将这个错误捕捉,而不是产生错误的数据,就再好不过了。不明确行为消毒剂能有所帮助,传递-fsanitize=undefined-trap -fsanitize-undefined-trap-on-error参数来开启它:

22.png

这里并不像地址消毒剂那样输出额外的信息,但是,在出现错误的时候,程序立即停止了执行,而且我们通过调试工具可以很简单地查找问题。
不明确行为消毒剂暂时未集成到Xcode中,但是你可以在工程的build settings中添加compiler flags来使用。

结论

Address Sanitizer是一个伟大的技术,可以帮助我们查找到很多C代码中的问题。它并不完美,不能查找到所有错误,但仍能提供非常有用的诊断信息。在这里,我强烈建议你在自己的代码中尝试使用它,你会发现令你吃惊的结果。


7、静态分析器(Analyzer) 和检查器

在Xcode中有两个工具可以用来清理代码,以减少代码的错误率。静态分析器工具可以对我们的代码提出改进意见,比如检测出没有使用过的对象,没有release对象(针对Core Foundation对象,ARC仍然会有这样的问题)。通过选择Product菜单中的‘Anlayze’可以查看到相关建议。

analyse

检查器是非常强大的一组工具,通过检查器不仅可以从不同的角度检查我们程序对内存的使用情况,文件系统的使用情况(增加、删除、修改等),甚至还提供了自动UI交互的方法。通过选择Product菜单中的‘Profile’可以查看到这些检查器。

选择‘Profile’会打开一个Instrument窗口,这里可以选择一个配置模板进行运行。最常用的模板有zombies(稍后会讨论),activity monitor和leaks。在程序运行时,对内存泄露进行捕捉时,Leaks可能是最有用的一个模板。

Screen-Shot-2012-12-09-at-1.23.38-PM


8、xcode自带的图层查看器



0 0