說明:
這裡的調試是指使用 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\"))" % "FigPluginView").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:]"