XCode 4 的调试定位技巧

给跪了,相当好用:

http://superwanc.diandian.com/post/2013-09-12/40052870537



经常有朋友会问Crash的问题。Crash最多的无非就两种,一种就是signal SIGABRT,大概的意思就是发送Message出现问题,信号迷失了。这种的Crash其实是很好定位,Crash了后直接看Console里出的最后日志,比如这段:

 

 

2012-03-28 19:26:33.055 TableViewMenuDemo[3916:f803] *** Terminating app due to uncaught exception ‘NSInvalidArgumentException’, reason: ‘-[__NSArrayI replaceObjectAtIndex:withObject:]: unrecognized selector sent to instance 0x6a3f3d0′


*** First throw call stack:

 

找到reason字段,那就是原因,说NSArray 调用replaceObjectAtIndex:withObject:

 

这个方法是NSMutableArry的,NSArray并没有该方法。信号迷失掉了,所以Crash了。0x6a3f3d0 是出问题的内存地址,查下内存地址或搜下调用方法就比较好定位了。

 

 

 

Xcode4上还有个更好用的定位方法,就是设置一个Exception Breakpoint就好了。

 

看图在工程的左边栏中选择Breakpoint Navigator,点击下面的+号添加一个Exception Breakpoint,然后再运行试试,Crash后,是不是直接定位到了那行代码了?再根据Console里的日志进行定位修改就好了。

 

下面说说另一种Crash, EXC_BAD_ACCESS,这个比较头疼,因为Crash的时候,可能是比较早之前的某个变量释放了,现在访问时出问题。Console里也没显示什么日志。

 

这里光加入Exception Breakpoint是不够的了,XCode4还个可爱的地方,打开Scheme选项选择EditScheme。

 

 

 

 

然后按图勾上Enable Zombie Objects和Malloc Stack那两项,记住一般只有在定位EXC_BAD_ACCESS时候才勾选,别有事没事都勾上。

 

这样重新跑一下,如果是到Exception Breakpoint处停止了,可以在Console中输入:c(continue)按回车继续跑,直到Crash。看下Console是不是有跟SIGABRT类似的错误信息日志了,后面定位什么的你懂的。

 

如果还没有日志,在Console中输入po$eax$eax标志出错的地方,适用模拟器,真机用$r0(话说EXC_BAD_ACCESS这种错误模拟器定位就行),还可以输入比如:po[$eaxname]po[$eaxreason]等指令查看错误其他信息(注意方括号后没分号的)。然后,就没有然后了。

 

还要补充点,程序开发过程就要多关注左边栏中Issue Navigator里的警告信息,Xcode4不仅会警告,还多数给出解决建议,能避免后面不必要的Crash。

 

 

 

如果程序Crash,第一时间关注下debug Navigator里的执行信息,将滑块拉向右边可以看到更多调用信息,根据这个能大致设想是调用什么方法或进行什么操作时Crash的。

 

 

 

 

 

 

 

 

 

 

来源:DEVDIV博客-History

 

 

 

iOS内存错误EXC_BAD_ACCESS的解决方法

 

发布:mdxy-dxy 字体:[增加 减小] 类型:转载

 

iOS开发,最郁闷的莫过于程序毫无征兆地就崩溃了,用bt命令打出调用栈,给出的是一堆系统EXC_BAD_ACCESS的信息,根本没办法定位问题出现在哪里

 

 

 

 

 

 

iOS开发,最郁闷的莫过于程序毫无征兆地就崩溃了,用bt命令打出调用栈,给出的是一堆系统EXC_BAD_ACCESS的信息,根本没办法定位问题出现在哪里。 首先说一下 EXC_BAD_ACCESS 这个错误,可以这么说,90%的错误来源在于对一个已经释放的对象进行release操作。举一个简单的例子来说明吧,首先看一段Java代码:

复制代码代码如下:


public class Test{ 
public static void main(String[] args){ 
String s = "This is a test string"; 
s = s.substring(s.indexOf("a"),(s.length())); 
System.out.println(s); 


}



通常这样的崩溃出现,原因一般就是:调用了已经释放的内存空间,或者说重复释放了某个地址空间。而怎样定位到这个地址呢,可以通过编辑xcode的scheme,添加如下标记位,让系统把错误地址打印出来,如图:

 

(通过Product->Scheme->Edit Scheme进入下面编辑页面,选中Arguments tab,增加标计位NSZombieEnabled设为YES)

这样,但崩溃出现,系统会出现以下提示信息:

2013-06-23 00:45:20.479 *** -[__NSArrayM addObject:]: message sent to deallocated instance 0x7179910

可见崩溃原因是内存地址0x7179910被重复释放了。

Objective-C 这段代码有三个致命问题:1、内存泄露;2、错误释放;3、造成 EXC_BAD_ACCESS 错误。

如果崩溃是发生在当前调用栈,通过上面的做法,系统就会把崩溃原因定位到具体代码中。但是,如果崩溃不在当前调用栈,系统就仅仅只能把崩溃地址告诉我们,而没办法定位到具体代码,这样我们也没法去修改错误。这时就可以修改scheme,让xcode记录每个地址alloc的历史,这样我们就可以用命令把这个地址还原出来。如图:(跟设置NSZombieEnabled一样,添加MallocStackLoggingNoCompact,并且设置为YES)

这样,当出现崩溃原因是message sent to deallocated instance 0x7179910,我们可以使用以下命令,把内存地址还原:

info malloc-history 0x7179910

如图,这个命令能具体把这个地址在哪一行代码生成还原出来。

(需要注意的是,因为这个命令只支持gdb,所以必须把控制台的输出改成gdb,并且有点遗憾的是,只支持模拟器,不支持真机调试)

(同样是通过Product->Scheme->Edit Scheme进入上面编辑页面,选中Info tab)

这样,好好检查一下那一行的代码,应该就很容易找出问题所在了。

----

所谓磨刀不误砍柴工,这里菜鸟我在研究怎么运用xcode处理常见的调试问题,把今后遇到的问题慢慢整理总结下来,以便以后遇到问题能够快速的解决。

调试前,先在xcode中添加环境变量,如下三个

NSDebugEnabled  NSZombieEnabledMallocStackLogging
都先把它们设置为YES

 

话说你还可以添加接下来这个环境变量

  MallocStackLoggingNoCompact
同样置为YES

 

ok,调试前准备的东东就这些,当然,gdb得晓得在哪里设置开启。Gdb会暂停我们的程序,但依然使之驻留在内存中,让我们有机会做调试

第一个:

 

 2003-03-18 13:01:38.644 autoreleasebug[3939] *** *** Selector 'release'
  sent to dealloced instance 0xa4e10 of class NSConcreteData.
大概指的是对象被释放了两次,对已经autorelease的对象进行release会让程序crash掉

 

如果你处于gdb模式中(gdb即可在Console中打开,也可在terminal终端打开,在终端输入gdb即可进入gdb模式),可以在gdb中输入

shell malloc_history 3939 0xa4e10

回车查看堆分配状态


message sent to deallocated instance :报错

常常程式一長,哪邊就不小心多release了一次
這時候編譯器就只會告訴你:BAD_ACCESS,然後程式就死了
剛開始會google到去Argument加個NSZombieEnabled YES
會多吐一點東西讓你把bug除掉
今天遇到加了這個後error message變:
[CALayer release]:message sent to deallocated instance 0x4dd650
1.在Argument裡面加入這三個參數:
NSZombieEnabled YES
MallocStackLogging YES
MallocStackLoggingNoCompact Yes
第一項可監控deallocated的記憶體,給更多的錯誤訊息
第二項可開啟MallocStack,就知道記憶體在程式運行中被配置的歷史
第三項可以更清楚顯示指定的MallocStack狀況(一開始沒加看到快脫窗還是看不懂)
2.跑程式(建議用模擬器),開console,這時候可以注意到一開始會出現類似下列訊息:
myproject(11779) malloc: stack logs being written into /tmp/stack-logs.11779.myproject.81hXWV
表示gdb開始有在紀錄
3.讓程式跑到出錯
如果有做步驟一,應該就會看到message sent to deallocated instance的錯誤訊息
複製後面跟的位址
4.在(gdb)後面下指令info malloc-history 0x4dd650(剛剛得到的位址)
如果gdb說找不到指令,可改用shell 11779 malloc_history(11779為程式的pid)
建議在模擬器跑的原因是因為程式跑在裝置的OS上,pid是裝置給予的,要存取好像會有點問題
今天在這卡關卡了一陣
5.如果上述步驟順利的話就會看到一串比較像程式碼的東西,應該也就看得出bug了

 

第二个:此处不描述问题,gdb中常用指令

gdb中,

1.使用backtrace命令,简写bt,用来查看当前进程的函数调用栈情况,以此回溯到我们自己所写的方法,有时可以看到出错在哪一行;(真怀恋在vs中的编程,找问题哪须这么麻烦)。

2.使用list命令,简写l,回到栈列表,会将当前栈里的程序代码罗列出来,方便问题查找;

3.使用break命令,简写b,设置断点,格式:b filename:line   即在哪个文件的哪一行设置断点

如:b test.m:10

4.使用next命令,简写n,单步调试

5.使用continue,简写c,跳出当前断点继续执行

6.使用回车键,将继续按照上条指令执行

7.使用print,简写p,可打印表达式和变量的值,在print命令后追加/format可以格式化输出。/format是一个gdb的格式化字符串,比较有用的格式化字符有 x:十进制数; c:字符; a:地址

8.使用print-object,简写为po,用来输出obj-c中的对象。它的工作原理是,向被调用的对象发送名为debugDescription的消息。它和常见的description消息很像

9.使用x命令,格式:x/format address。其中address很简单,它通常是指向一块内存的表达式。但是format的语法就有点复杂了。它由三个部分组成:第一个是要显示的块的数量;第二个是显示格式(如x代表16进制,d代表十进制,c代表字符);第三个是每个块的大小。值得注意的是第三部分,即块大小是用字符对应的。用b, h, w,  g 分别表示1, 2, 4, 8 bytes。举例来说,用十六进制方式,打印从ptr开始的4个4-byte块应该这样写:

(gdb) x/4xw ptr

10.使用set命令,设置变量的值,set x=0

/

断点

我们可以在程序的某个位置设置断点,这样当程序运行到那里的时候就会暂停,而把控制权转移给调试器。就像之前提到的,我们用break命令来设置断点。下面详细地列出了如何设置断点的目标:

SymbolName: 为断点指定一个函数名。这样断点就会设置在该函数上。

file.c:1234: 把断点设置在指定文件的一行。

-[ClassName method:name:]: 把断点设置在objc的方法上。用+代表类方法。

*0xdeadbeef: 在内存的指定位置设置断点。这不是很常用,一般在没有源码的调试时使用。

断点可以用enable命令和disable命令来切换到使用和停用状态,也可以通过delete命令彻底删除。想要查看现有断点的话,使用info breakpoints命令(可以简写成info b,或是i b)。

另外,我们也可以用if命令,把断点升级成条件断点。顾名思义,条件断点只会在设定的条件成真时起作用。举例来说,下面的语句为MyMethod添加了一个条件断点,它只在参数等于5的时候有效:

   (gdb) b -[Class myMethod:] if parameter == 5

最后,在断点上可以附加gdb命令。这样,当断点中断时,附带的命令会自动执行。附加命令使用commands breakpointnumber。这时gdb就会进入断点指令输入状态。断点指令就是一个以end结尾的标准gdb指令序列。举个例子,我们想在每次NSLog被调用时输出栈信息:

   (gdb) b NSLog

   Breakpoint 4 at 0x7fff87beaa62

   (gdb) commands

   Type commands for when breakpoint 4 is hit, one per line.

   End with a line saying just "end".

   >bt

   >end

这很好理解,只有一点需要提一下:如果commands命令是作用在刚设置的断点上的话,那么就可以省略断点序号。

有些时候,我们希望调试器输出一些信息,但是并不想中断程序运行。这实际上也可以通过追加指令实现。我们只需要在指令的最后增加continue指令就行了。在下面的例子里,我们在断点中断后打印栈信息和参数信息,随后继续运行:

   (gdb) b -[Class myMethod:]

   Breakpoint 5 at 0x7fff864f1404

   (gdb) commands

   Type commands for when breakpoint 5 is hit, one per line.

   End with a line saying just "end".

   >bt

   >p parameter

   >continue

   >end

最后一个奇特的运用是return命令。它和c中的同名命令一样,都用来跳出当前函数。如果设置了参数,这参数会作为函数的返回值。

比如说,我们可以用这个技巧屏蔽掉NSLog函数:

   (gdb) commands

   Type commands for when breakpoint 6 is hit, one per line.

   End with a line saying just "end".

   >return

   >continue

   >end

有一点需要提醒:虽然上述的技巧很有用,但同时它会带来副作用。例如上面屏蔽NSLog的技巧会严重拖慢程序的运行速度。因为每次断点中断,都会使控制权转移到debugger一边,然后运行命令。这些跨进程的操作很耗时间。

有时候也许看不出来,但当执行的断点变多,或是你在诸如objc_msgSend这样的方法上添加了条件断点,那么也许你的程序会一直运行到天荒地老。

无源码时的参数

有时我们需要在没有代码的地方调试。比如说,我们在用xcode调试时,经常会发现程序在Cocoa框架里的某个地方崩溃了。我们需要找到到底是在哪里出错了。这种时候,一个可行的方法就是查看崩溃处的参数,看看到底发生了什么。

ARM的存储很简单,参数只是按顺序被存储在$r0, $r1, $r2, $r3寄存器里。记住,在所有通过寄存器传递参数的体系结构里(i386不是),只有在函数开头的一小段里,寄存器里存的才是参数。因为在程序进行的过程中,它们随时都可能被其他变量替换掉。

举例来说,我们可以打印出传给NSLog的参数:

   Breakpoint 2, 0x00007fff87beaa62 in NSLog ()

   (gdb) po $rdi

   Hello, world!

这里有个很常见的技巧:如果我们想给NSLog添加断点来巡查崩溃,就可以根据输出内容设置一下判断,让debugger不至于在每次NSLog时都中断:

   (gdb) break NSLog if (char)[$rdi hasPrefix: @"crashing"]

记住,方法的前两个参数是self和_cmd。所以我们的参数应该从$rdx(x86_64)或$rd2(ARM)开始计算。

异常

异常会被运行时方法objc_exception_throw抛出。在这个方法里设置断点是很重要的。原因有两点:

1. 抛出异常,通常是程序出现严重错误的信号。

2. 被抛出的异常通常会被对应的代码捕获。如果你不在这里设置断点的话,就只能获得异常被捕获之后的信息,而不知道它到底是在哪里被抛出的。

如果你设置了断点,程序就会在异常被抛出的时候停止。这样你就有机会查看栈信息,知道具体是哪里抛出了异常。

为异常设置断点的方法也很简单,因为要抛出的异常是objc_exception_throw方法的唯一一个参数,所以我们可以用上一小节提到的方法来完成它。

第三个:线程

现在,多线程代码随处可见。知道如何调试多线程程序也越来越重要。以下一段代码启动了几个后台运行的线程:

   dispatch_apply(3, dispatch_get_global_queue(0, 0), ^(size_t x){

       sleep(100);

   });

运行debugger,在程序睡眠的时候用Control-C杀掉它:

  (gdb) run

   Starting program: /Users/mikeash/shell/a.out

   Reading symbols for shared libraries .+++........................ done

   ^C

   Program received signal SIGINT, Interrupt.

   0x00007fff88c6ff8a in __semwait_signal ()

   (gdb) bt

   #0 0x00007fff88c6ff8a in __semwait_signal ()

   #1 0x00007fff88c6fe19 in nanosleep ()

   #2 0x00007fff88cbcdf0 in sleep ()

   #3 0x0000000100000ea7 in __main_block_invoke_1 (.block_descriptor=0x1000010a0, x=0) at test.m:12

   #4 0x00007fff88cbbbc8 in _dispatch_apply2 ()

   #5 0x00007fff88cb31e5 in dispatch_apply_f ()

   #6 0x0000000100000e6a in main (argc=1, argv=0x7fff5fbff628) at test.m:11


和我们想的一样,我们输出了一个线程的信息。但是,另外两个后台运行的线程在哪里?我们可以用info threads命令获取所有线程的列表:


   (gdb) info threads

     3 "com.apple.root.default-priorit" 0x00007fff88c6ff8a in __semwait_signal ()

     2 "com.apple.root.default-priorit" 0x00007fff88c6ff8a in __semwait_signal ()

   * 1 "com.apple.root.default-priorit" 0x00007fff88c6ff8a in __semwait_signal ()


线程1前面有个星号,这表示它是现在活动中的线程。现在我们切换到线程2:


   (gdb) thread 2

   [Switching to thread 2 (process 4794), "com.apple.root.default-priority"]

   0x00007fff88c6ff8a in __semwait_signal ()

   (gdb) bt

   #0 0x00007fff88c6ff8a in __semwait_signal ()

   #1 0x00007fff88c6fe19 in nanosleep ()

   #2 0x00007fff88cbcdf0 in sleep ()

   #3 0x0000000100000ea7 in __main_block_invoke_1 (.block_descriptor=0x1000010a0, x=1) at test.m:12

   #4 0x00007fff88cbbbc8 in _dispatch_apply2 ()

   #5 0x00007fff88c4f7f1 in _dispatch_worker_thread2 ()

   #6 0x00007fff88c4f128 in _pthread_wqthread ()

   #7 0x00007fff88c4efc5 in start_wqthread ()


现在我们输出了线程2的信息。然后时线程3……是不是觉得这种方法效率太低了?我们只有3个线程,但如果有300个呢?幸好,gdb提供了thread apply all backtrace命令(简写为t a a bt),用来列出所有线程的详细信息。


Thread 3 (process 4794):

#0 0x00007fff88c6ff8a in __semwait_signal ()

#1 0x00007fff88c6fe19 in nanosleep ()

#2 0x00007fff88cbcdf0 in sleep ()

#3 0x0000000100000ea7 in __main_block_invoke_1 (.block_descriptor=0x1000010a0, x=2) at test.m:12

#4 0x00007fff88cbbbc8 in _dispatch_apply2 ()

#5 0x00007fff88c4f7f1 in _dispatch_worker_thread2 ()

#6 0x00007fff88c4f128 in _pthread_wqthread ()

#7 0x00007fff88c4efc5 in start_wqthread ()


Thread 2 (process 4794):

#0 0x00007fff88c6ff8a in __semwait_signal ()

#1 0x00007fff88c6fe19 in nanosleep ()

#2 0x00007fff88cbcdf0 in sleep ()

#3 0x0000000100000ea7 in __main_block_invoke_1 (.block_descriptor=0x1000010a0, x=1) at test.m:12

#4 0x00007fff88cbbbc8 in _dispatch_apply2 ()

#5 0x00007fff88c4f7f1 in _dispatch_worker_thread2 ()

#6 0x00007fff88c4f128 in _pthread_wqthread ()

#7 0x00007fff88c4efc5 in start_wqthread ()


Thread 1 (process 4794):

#0 0x00007fff88c6ff8a in __semwait_signal ()

#1 0x00007fff88c6fe19 in nanosleep ()

#2 0x00007fff88cbcdf0 in sleep ()

#3 0x0000000100000ea7 in __main_block_invoke_1 (.block_descriptor=0x1000010a0, x=0) at test.m:12

#4 0x00007fff88cbbbc8 in _dispatch_apply2 ()

#5 0x00007fff88cb31e5 in dispatch_apply_f ()

#6 0x0000000100000e6a in main (argc=1, argv=0x7fff5fbff628) at test.m:11


现在我们可以方便地查看整个程序中的线程了。如果想要更彻底地观察某个线程,只需要用thread命令切换到该线程,然后使用各种已经学过的gdb命令。


控制台参数和环境变量

在用gdb调试带参数的程序时会遇到一个疑惑,即程序的参数究竟怎么输入:

   $ gdb /bin/echo hello world

   Excess command line arguments ignored. (world)

   [...]

   /Users/mikeash/shell/hello: No such file or directory

如上,把参数直接缀在后面显然是不对的。因为这样它们会被解释成gdb的参数,而不是要调试程序的参数。运行结果也证明了这一点,gdb把hello和world都解释成了要运行的程序名。


解决方法也很简单,即,在gdb启动之后,执行run命令的同时输入参数:


   (gdb) run hello world

   Starting program: /bin/echo hello world

   Reading symbols for shared libraries +. done

   hello world

环境变量可以在启动gdb之前预先在shell中载入,通常情况下这么做也没有问题。但是,如果你操纵的环境变量会对每个程序都造成严重影响的话,这就不是一个好主意了。在这种情况下,我们用set env命令,做针对于目标程序的修改:

   (gdb) set env DYLD_INSERT_LIBRARIES /gdb/crashes/if/this/is/inserted.dylib

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
提供的源码资源涵盖了Java应用等多个领域,每个领域都包含了丰富的实例和项目。这些源码都是基于各自平台的最新技术和标准编写,确保了在对应环境下能够无缝运行。同时,源码中配备了详细的注释和文档,帮助用户快速理解代码结构和实现逻辑。 适用人群: 适合毕业设计、课程设计作业。这些源码资源特别适合大学生群体。无论你是计算机相关专业的学生,还是对其他领域编程感兴趣的学生,这些资源都能为你提供宝贵的学习和实践机会。通过学习和运行这些源码,你可以掌握各平台开发的基础知识,提升编程能力和项目实战经验。 使用场景及目标: 在学习阶段,你可以利用这些源码资源进行课程实践、课外项目或毕业设计。通过分析和运行源码,你将深入了解各平台开发的技术细节和最佳实践,逐步培养起自己的项目开发和问题解决能力。此外,在求职或创业过程中,具备跨平台开发能力的大学生将更具竞争力。 其他说明: 为了确保源码资源的可运行性和易用性,特别注意了以下几点:首先,每份源码都提供了详细的运行环境和依赖说明,确保用户能够轻松搭建起开发环境;其次,源码中的注释和文档都非常完善,方便用户快速上手和理解代码;最后,我会定期更新这些源码资源,以适应各平台技术的最新发展和市场需求。 所有源码均经过严格测试,可以直接运行,可以放心下载使用。有任何使用问题欢迎随时与博主沟通,第一时间进行解答!

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值