[原]调试没有符号的 iOS 应用
说明:
这里的调试是指使用 lldb 远程调试 iOS 应用
设置断点是指在 ObjC 方法上设置断点
使用场景:
1、调试被 strip 了的 iOS 应用
2、调试被 strip 了的 iOS 系统 dylib
在调试时没有符号的 iOS 应用时,设置断点非常不方便:
1、App:在没有开启 ASLR 时,需要首先找到方法的地址,然后针对地址设置断点
2、Dylib:在没有开启 ASLR 时,需要找到dylib的基地址,然后计算偏移
如果开启了 ASLR,设置断点会更麻烦。
一直想解决这个问题,曾经想过的方法:
首先,ObjC 语言是一个相对动态的语言,所以使用class-dump这样的工具,可以 dump 出类信息,函数地址。
另外,DWARF 格式是有公开标准的,
因此,可以通过将 class-dump 的输出信息转换成 DWARF,在调试时动态加载符号。
这个方法我不是第一想到,这个帖子中有详细说明:http://stackoverflow.com/questions/17554070/import-class-dump-info-into-gdb
但是照这个方法进行操作后,发现对 iOS 应用没效果,而且过程繁琐。
后来想,ObjC是通过在C语言之上封装了薄薄的一层(消息特性)而形成的,
所有 ObjC 的方法调用最终会转换为 C 方法调用,
因此,可以通过在对应的 C 函数上设置断点来解决断点设置问题,
而如何得到 C 函数的地址,就依赖于 ObjC 的运行时方法了,主要涉及:
1、object_getClass
2、NSSelectorFromString
3、class_respondsToSelector
4、class_getMethodImplementation
在解决了在什么位置设置断点的问题后,
接下来需要解决如果在 lldb 中方便的设置断点。
lldb 集成了 Python 脚本引擎,参考:http://lldb.llvm.org/python-reference.html
因此我们可以通过Python脚本扩展 lldb 的调试命令,主要用到如下几个函数:
1、lldb.debugger
2、lldb.debugger.GetSelectedTarget()
3、lldb.debugger.GetSelectedTarget().GetProcess()
4、lldb.debugger.GetSelectedTarget().GetProcess().GetSelectedThread()
5、lldb.debugger.GetSelectedTarget().GetProcess().GetSelectedThread().GetSelectedFrame()
6、lldb.frame.EvaluateExpression
7、lldb.debugger.HandleCommand
脚本配置方法:
方法一:在调试控制台执行:command script import bt_objc.py的文件路径
方法二:将如上命令加入到 ~/.lldbinit,如果文件不存在则可以自己动手创建
脚本内容:
1 #!/usr/bin/python 2 3 ''' 4 Author: 5 Proteas 6 Date: 7 2014-03-05 8 Purpose: 9 set breakpoint without symbols, for examle: stripped macho 10 Usage: 11 add the following line to ~/.lldbinit 12 command script import ~/.lldb/bt_objc.py 13 ''' 14 15 import lldb 16 import commands 17 import shlex 18 import optparse 19 import re 20 21 def __lldb_init_module (debugger, dict): 22 debugger.HandleCommand('command script add -f bt_objc.bt_objc bt_objc') 23 print 'The "bt_objc" command has been installed' 24 25 def create_command_arguments(command): 26 return shlex.split(command) 27 28 def is_command_valid(args): 29 "" 30 if len(args) == 0: 31 return False 32 33 arg = args[0] 34 if len(arg) == 0: 35 return False 36 37 ret = re.match('^[+-]\[.+ .+\]$', arg) # TODO: more strict 38 if not ret: 39 return False 40 41 return True 42 43 def get_class_name(arg): 44 match = re.search('(?<=\[)[^\[].*[^ ](?= +)', arg) # TODO: more strict 45 if match: 46 return match.group(0) 47 else: 48 return None 49 50 def get_method_name(arg): 51 match = re.search('(?<= )[^ ].*[^\]](?=\]+)', arg) # TODO: more strict 52 if match: 53 return match.group(0) 54 else: 55 return None 56 57 def is_class_method(arg): 58 if len(arg) == 0: 59 return False 60 61 if arg[0] == '+': 62 return True 63 else: 64 return False 65 66 def get_selected_frame(): 67 debugger = lldb.debugger 68 target = debugger.GetSelectedTarget() 69 process = target.GetProcess() 70 thread = process.GetSelectedThread() 71 frame = thread.GetSelectedFrame() 72 73 return frame 74 75 def get_class_method_address(class_name, method_name): 76 frame = get_selected_frame(); 77 class_addr = frame.EvaluateExpression("(Class)object_getClass((Class)NSClassFromString(@\"%s\"))" % class_name).GetValueAsUnsigned() 78 if class_addr == 0: 79 return 0 80 81 sel_addr = frame.EvaluateExpression("(SEL)NSSelectorFromString(@\"%s\")" % method_name).GetValueAsUnsigned() 82 has_method = frame.EvaluateExpression("(BOOL)class_respondsToSelector(%d, %d)" % (class_addr, sel_addr)).GetValueAsUnsigned() 83 if not has_method: 84 return 0 85 86 method_addr = frame.EvaluateExpression('(void *)class_getMethodImplementation(%d, %d)' % (class_addr, sel_addr)) 87 88 return method_addr.GetValueAsUnsigned() 89 90 def get_instance_method_address(class_name, method_name): 91 frame = get_selected_frame(); 92 class_addr = frame.EvaluateExpression("(Class)NSClassFromString(@\"%s\")" % class_name).GetValueAsUnsigned() 93 if class_addr == 0: 94 return 0 95 96 sel_addr = frame.EvaluateExpression("(SEL)NSSelectorFromString(@\"%s\")" % method_name).GetValueAsUnsigned() 97 has_method = frame.EvaluateExpression("(BOOL)class_respondsToSelector(%d, %d)" % (class_addr, sel_addr)).GetValueAsUnsigned() 98 if not has_method: 99 return 0 100 101 method_addr = frame.EvaluateExpression('(void *)class_getMethodImplementation(%d, %d)' % (class_addr, sel_addr)) 102 103 return method_addr.GetValueAsUnsigned() 104 105 def bt_objc(debugger, command, result, dict): 106 args = create_command_arguments(command) 107 108 if not is_command_valid(args): 109 print 'please specify the param, for example: "-[UIView initWithFrame:]"' 110 return 111 112 arg = args[0] 113 class_name = get_class_name(arg) 114 method_name = get_method_name(arg) 115 116 address = 0 117 if is_class_method(arg): 118 address = get_class_method_address(class_name, method_name) 119 else: 120 address = get_instance_method_address(class_name, method_name) 121 122 if address: 123 lldb.debugger.HandleCommand ('breakpoint set --address %x' % address) 124 else: 125 print "fail, please check the arguments"
如上脚本也可以从这个链接下载:https://raw.github.com/Proteas/lldb-scripts/master/bt_objc.py
脚本配置完毕后,可以通过如下命令设置断点:
bt_objc "-[UIView initWithFrame:]"