【iOS/Mac OS】程序崩在objc_msgSend(),怎么办?
程序崩在objc_msgSend(),怎么办?
最可能的原因是,当你向一个已经释放的对象发送消息时,或者虽然指针是正确的,却被别的对象破坏了内容(比如内存越界),再或者使用了悬摆指针(dangling pointer)。偶尔的时候也会是因为内存错误导致运行时的数据结构被破坏,但通常问题还是在接收者本身。
无论用Debugger还是通过崩溃日志(crash log),都可以得到远比backtrace(调用堆栈)多的信息。
接收者和selector寄存器(Receiver and selector registers)
objc_msgSend() 会在CPU寄存器中存储接收者对象和selector,这些值可以用来帮助分析问题。
在不同的架构下使用的寄存器会有所不同。下表是针对Mac OS X Leopard (Snow Leopard也应该是相同的)。
objc_msgSend
objc_msgSend_fpret objc_msgSend_stret
receiver SEL receiver SEL
i386 eax* ecx eax* ecx
x86_64 rdi rsi rsi rdx
ppc r3 r4 r4 r5
ppc64 r3 r4 r4 r5
arm r0 r1 r1 r2
* i386中的注释: 接收者对象在大多数崩溃中都是存在eax中的。如果调用的路径过长,eax有可能保存的是其它值。
解读接收者和非法地址(Interpreting the receiver and invalid address)
你可以通过使用接收者地址和非法地址来制造崩溃的情况,以此学习一些技巧。在崩溃日志中,接收者地址使用上表所列的寄存器存储在Thread State中,而非法地址则列在日志顶部位置(通常显示如 KERN_PROTECTION_FAILURE at <invalid address>)。在Debugger控制台中, 非法地址会在程序停止时输出,此时也可以使用上表所列的寄存器来输出接收者的地址。
Program received signal EXC_BAD_ACCESS, Could not access memory.
Reason: KERN_PROTECTION_FAILURE at address: 0x00000001
0x00090ec4 in objc_msgSend ()
(gdb) p/x $eax
$1 = 0x1
这个测试程序崩在[(id)1 release]。
通常是有两种情况中的一项造成的:接收者的地址是错误的(此时非法地址也是一样的,或者偏移16或32字节),或者接收者的地址是合理的,而非法地址是接收者的isa指针。后者通常是因为你正尝试使用一个已经释放的对象。
既要针对性的查找这些值,也要查找它们附近的值。在一些架构中,一个非法的isa会导致程序崩在isa+16或isa+32位置。
未对齐,不能被16整除(Not divisble by 16 - misaligned)
malloc() 返回的是16字节对齐的内存块。如果你的接收者地址没有按16字节对齐,那么它很可能不是一个正确的对象指针。
前两位和后两位都被置位 - malloc的free list
当一个块被释放后,内存分配器会写入free list指针。如果你随后使用被释放的对象,它的isa指针的前两位和后两位都会被置位。
所有位被取反 - GC的free list
和上面的malloc的free list相像,GC的free list会使得地址值看起来是错误的,但~address(取反操作)的值却看起是合理的。
0xa1b1c1d3 - CF container
CoreFoundation containers 使用这个值来表示已经删除或清空的项目。
ASCII 文本
或许是一个已经释放的对象被重新分配为一个字串,亦或者将一个已经释放的字串重新分配为一个对象,也可能是内存越界的原因。使用asciify命令将不同字节对齐模式下的字串输出出来,以便于查看。下面是一个看似URL相关的字串:
% asciify 0x2e777777
###.www###
###www.###
追查selector(Interrogating the selector)
编译优化会使调用堆栈中指向第二段的调用点(call site)可能并不是真正导致崩溃的调用。它可能已经调用成功了,而是这个方法的一个尾部调用(tail call)导致了崩溃。正是因为尾部调用(tail call)的优化会导致其中间调用帧会被忽略而没有显示在调用堆栈中。我们这里可以使用selector寄存器来确认真正的崩溃调用。
一个selector会指向一个唯一的C字串。未来有可能在新系统改变,不过现在可以很方便的用来调试。如果你的程序崩在debugger中,打开Debugger控制台,使用上表中列出的selector寄存器执行如下指令:
(gdb) x/s $ecx
0xa1029: "release"
Snow Leopard系统提供的崩溃日志已经添加了selector的名字:
Application Specific Information:
objc_msgSend() selector name: release
除此之外,要想只从崩溃日志中得到selector是很困难的。直到Snow Leopard,你可以使用下面的方法:
1. 从崩溃日志的Thread State,对照前面表中所列的selector寄存器值,如:
ecx: 0x000a1029
2.从崩溃日志的Binary Images位置, 找到某个镜像(image)包含了这个地址。它常常要么是程序本身,要么就是 libobjc.A.dylib. 如果没有找到对应的image,就放弃吧。
0x8b000 - 0x106ff7 libobjc.A.dylib ??? (???) <9b5973b7fa88f9aab7885530c7b278dd> /usr/lib/libobjc.A.dylib
3.找到同崩溃日志中所列的镜像匹配的程序文件。可以使用UUID确认它们的一致性。
% dwarfdump -u /usr/lib/libobjc.A.dylib
UUID: 26650299-C6EA-B1C8-52D6-072AC874D400 (ppc) /usr/lib/libobjc.A.dylib
UUID: 9B5973B7-FA88-F9AA-B788-5530C7B278DD (i386) /usr/lib/libobjc.A.dylib
UUID: D2A4E8E1-3C1C-E0D9-2249-125B6DD621F8 (x86_64) /usr/lib/libobjc.A.dylib
同时确保系统版本的一致性。
4.计算SEL在镜像中的偏移地址。
0xa1029 - 0x8b000 = 0x16029
5.打印镜像(image)中指定偏移地址处的C字串。记住指定正确的架构。
% otool -v -arch i386 -s __TEXT __cstring /usr/lib/libobjc.A.dylib | grep 16029
00016029 release
补充:移动开发 , IOS ,