(十三)SB示例 升级版的lookup
1. 升级版的lookup
1.1 创建自动化脚本
这个项目的starter
目录中包含了两个Python脚本,这将使您在创建LLDB脚本内容时的生活更轻松。具体如下:
- generate_new_script.py:接受一个名字,然后帮我们搭建一个脚本的基础骨架。
- lldbinit.py:此脚本将遍历位于同一目录中的所有脚本(以.py结尾的文件),并尝试将它们加载到LLDB中。此外,如果存在扩展名为txt的文件,LLDB将尝试通过命令导入来加载这些文件的内容。
把这两个文件都放在本章的starter
文件夹中,并将它们粘贴到~/lldb/
目录中。进入到~/.lldbinit
文件并添加以下代码行:
command script import ~/lldb/lldbinit.py
创建lookup命令
~> lldb
(lldb) reload_script
如果没有错误的话,我们就可以开始了。
(lldb) __generate_script lookup
Opening "/Users/ycpeng/lldb/lookup.py"...
(lldb) reload_script
(lldb) lookup
Hello! the lookup command is working!
实现lookup命令
前面我们看到lookup
命令背后的基础相当简单。“秘密”就是使用SBTarget
的findGlobalFunctions
。在那之后,只需要进行格式化输出。
我们将继续使用Allocator项目。打开项目,在iPhone模拟器上构建并运行。运行之后,暂停应用程序并启动LLDB。
这个FindGlobalFunctions
需要哪些参数?
(lldb) script help(lldb.SBTarget.FindGlobalFunctions)
Help on function FindGlobalFunctions in module lldb:
FindGlobalFunctions(self, name, max_matches, matchtype)
FindGlobalFunctions(SBTarget self, str const * name, uint32_t max_matches, lldb::MatchType matchtype) -> SBSymbolContextList
因为它是一个Python类,所以可以忽略第一个自参数。名为name
的str参数将是我们查询字符串。最大匹配将指定您想要的最大匹配数量。如果使用0
,它将返回所有可用的匹配项。matchType
参数是一个lldb Python枚举,可以指定执行不同类型的搜索,例如regex
或non-regex
。
因为regex
搜索实际上是唯一的方法,所以我们将使用LLDB枚举值lldb.eMatchTypeRegex。
其他枚举值可以在这里找到:https://lldb.llvm.org/python_reference/_lldb%27-module.html#eMatchTypeRegex
是时候在lookup.py
脚本中实现它了。打开~/lldb/lookup.py
。在handle_command
末尾找到以下代码:
# Uncomment if you are expecting at least one argument
# clean_command = shlex.split(args[0])[0]
result.AppendMessage('Hello! the lookup command is working!')
替换为
#1
clean_command = shlex.split(args[0])[0]
#2
target = debugger.GetSelectedTarget()
#3
contextlist = target.FindGlobalFunctions(clean_command, 0, lldb.eMatchTypeRegex)
#4
result.AppendMessage(str(contextlist))
- 获取传递给脚本的命令的干净版本。
- 通过
SBDebugger
获取SBTarget
的实例。 - 使用
FindGlobalFunctions API
,传入clean_command
。提供的0
表示结果数没有上限,并使用eMatchTypeRegex
匹配类型进行正则表达式搜索。 - 将
contextlist
转换为Python str,然后将其附加到SBCommandReturnObject
。
(lldb) lookup DSObjectiveCObject
Module: file = "/Users/ycpeng/Library/Developer/Xcode/DerivedData/Allocator-fnbctbueesgtzzfgksqcimloegwm/Build/Products/Debug-iphonesimulator/Allocator.app/Allocator", arch = "x86_64"
CompileUnit: id = {0x00000000}, file = "/Users/ycpeng/Desktop/Books/Apple_Debugging_and_Reverse_Engineering_v3.0/25-Script Bridging with SBValue & Language Contexts/starter/Allocator/Allocator/DSObjectiveCObject.m", language = "unknown"
Function: id = {0x100000268}, name = "-[DSObjectiveCObject setLastName:]", range = [0x0000000100002150-0x0000000100002181)
FuncType: id = {0x100000268}, byte-size = 0, decl = DSObjectiveCObject.h:36, compiler_type = "void (NSString *)"
Symbol: id = {0x000000db}, range = [0x0000000100002150-0x0000000100002190), name="-[DSObjectiveCObject setLastName:]"
...
得到的输出比image lookup -rn DSObjectiveCObject
更糟糕。我们用script
研究一下这个执行结果。
(lldb) script k = lldb.target.FindGlobalFunctions('DSObjectiveCObject', 0, lldb.eMatchTypeRegex)
我们可以去查看SBSymbolContextList
的文档,或直接查看它的所有方法。
(lldb) gdocumentation SBSymbolContextList
(lldb) script dir(lldb.SBSymbolContextList)
['Append', 'Clear', 'GetContextAtIndex', 'GetDescription', 'GetSize', 'IsValid', '__bool__', '__class__', '__del__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattr__', '__getattribute__', '__getitem__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__iter__', '__le__', '__len__', '__lt__', '__module__', '__ne__', '__new__', '__nonzero__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__swig_destroy__', '__swig_getmethods__', '__swig_setmethods__', '__weakref__', 'blocks', 'compile_units', 'functions', 'get_block_array', 'get_compile_unit_array', 'get_function_array', 'get_line_entry_array', 'get_module_array', 'get_symbol_array', 'line_entries', 'modules', 'symbols']
我们可以发现两个重要的方法__iter__
和__getitem__
,说明我们可以遍历和使用索引访问它。
//等效于script k.__getitem__(0)
(lldb) script k[0]
<lldb.SBSymbolContext; proxy of <Swig Object of type 'lldb::SBSymbolContext *' at 0x10898b6c0> >
(lldb) script print(k[0])
Module: file = "/Users/ycpeng/Library/Developer/Xcode/DerivedData/Allocator-fnbctbueesgtzzfgksqcimloegwm/Build/Products/Debug-iphonesimulator/Allocator.app/Allocator", arch = "x86_64"
CompileUnit: id = {0x00000000}, file = "/Users/ycpeng/Desktop/Books/Apple_Debugging_and_Reverse_Engineering_v3.0/25-Script Bridging with SBValue & Language Contexts/starter/Allocator/Allocator/DSObjectiveCObject.m", language = "objective-c"
Function: id = {0x100000268}, name = "-[DSObjectiveCObject setLastName:]", range = [0x0000000100002150-0x0000000100002181)
FuncType: id = {0x100000268}, byte-size = 0, decl = DSObjectiveCObject.h:36, compiler_type = "void (NSString *)"
Symbol: id = {0x000000db}, range = [0x0000000100002150-0x0000000100002190), name="-[DSObjectiveCObject setLastName:]"
我们关心的是方法名,如何快速拿到它呢?
(lldb) script print(k[0].symbol.name)
-[DSObjectiveCObject setLastName:]
(lldb) script print(k[0].function.name)
-[DSObjectiveCObject setLastName:]
下面我们将
#3
contextlist = target.FindGlobalFunctions(clean_command, 0, lldb.eMatchTypeRegex)
#4
result.AppendMessage(str(contextlist))
替换为
contextlist = target.FindGlobalFunctions(clean_command, 0, lldb.eMatchTypeRegex)
output = ''
for context in contextlist:
output += context.symbol.name + '\n\n'
result.AppendMessage(output)
并在LLDB中实验一下
(lldb) reload_script
(lldb) lookup DSObjectiveCObject
-[DSObjectiveCObject setLastName:]
-[DSObjectiveCObject .cxx_destruct]
-[DSObjectiveCObject setFirstName:]
-[DSObjectiveCObject eyeColor]
-[DSObjectiveCObject init]
-[DSObjectiveCObject lastName]
-[DSObjectiveCObject setEyeColor:]
-[DSObjectiveCObject firstName]
输出更加干净了。
现在我想看看这些函数在进程中的位置。我想将一个特定的模块所有函数打印出来,最上面是模块名和命中数量和分割的*
。
返回lookup.py
文件,创建两个新函数。
-
generateFunctionDictionary获取
SBBreakpointContextList
并生成一个Python字典列表。这个dict
将包含每个模块的键。对于dict
中的值,将为每个命中的SBSymbolContext
提供一个Python列表。 -
generateOutput解析创建的字典,并从
OptionParser
实例接收到的选项。此方法将返回要打印回控制台的字符串。
首先在lookup.py
脚本中的handle_command
函数下面实现generateModuleDictionary
函数:
def generateModuleDictionary(contextlist):
mdict = {}
for context in contextlist:
#1
key = context.module.file.fullpath
#2
if not key in mdict:
mdict[key] = []
#3
mdict[key].append(context)
return mdict
- 在
SBSymbolContext
中,通过SBModule
、SBFileSpec
、fullPath
获取Python字符串,并将其分配给一个名为key的变量。获取完整路径(而不是SBFileSpec
的basename
属性)很重要,因为可能有多个具有相同basename
的模块。 -
mdict
变量将保存找到的所有符号的列表,并按模块拆分。此字典中的键将是模块名称,值将是在该模块中找到的符号数组。检查字典是否已经包含此模块的列表。如果没有,将为此模块键添加一个空白列表。 - 将
SBSymbolContext
实例添加到此模块的相应列表中。
这个函数下面继续完成generateOutput
:
def generateOutput(mdict, options, target):
#1
output = ''
separator = '*' * 60 + '\n'
#2
for key in mdict:
#3
count = len(mdict[key])
firstItem = mdict[key][0]
#4
moduleName = firstItem.module.file.basename
output += '{0}{1} hits in {2}\n{0}'.format(separator, count, moduleName)
#5
for context in mdict[key]:
query = ''
query += context.symbol.name
query += '\n\n'
output += query
return output
- 输出变量将是包含最终传递给
SBCommandReturnObject
的所有内容的返回字符串。 - 遍历
mdict
字典中找到的所有键。 - 这将获取命中数量和第一个对象(下一步从里面取出模块名)。
- 获取模块名。
- 遍历Python列表中的所有
SBSymbolContext
项,并将名称添加到输出变量中。
扩展handle_command
函数中的代码,以便可以使用刚刚创建的两个新方法。找到:
output = ''
for context in contextlist:
output += context.symbol.name + '\n\n'
替换为:
mdict = generateModuleDictionary(contextlist)
output = generateOutput(mdict, options, target)
试一下:
(lldb) reload_script
(lldb) lookup DSObjectiveCObject
************************************************************
8 hits in Allocator
************************************************************
-[DSObjectiveCObject setLastName:]
-[DSObjectiveCObject .cxx_destruct]
-[DSObjectiveCObject setFirstName:]
-[DSObjectiveCObject eyeColor]
-[DSObjectiveCObject init]
-[DSObjectiveCObject lastName]
-[DSObjectiveCObject setEyeColor:]
-[DSObjectiveCObject firstName]
我们现在可以查看OC中有两个参数的所有方法:
(lldb) lookup initWith(\\w+\:){2,2}\]
1.4 为lookup添加选项
- 向每个查询添加加载地址,展示实际函数在内存中的位置。
- 仅提供模块摘要。不生成函数名,只列出每个模块的命中数。
配置generateOptionParser
:
def generateOptionParser():
usage = "usage: %prog [options] code_to_query"
parser = optparse.OptionParser(usage=usage, prog="lookup")
parser.add_option("-l", "--load_address",
action="store_true",
default=False,
dest="load_address",
help="Show the load addresses for a particular hit")
parser.add_option("-s", "--module_summary",
action="store_true",
default=False,
dest="module_summary",
help="Only show the amount of queries in the module")
return parser
您将首先实现--load_address
。在generateOutput
函数中,找到在SBSymbolContext
上迭代的for循环,并替换为:
for context in mdict[key]:
query = ''
#1
if options.load_address:
#2
start = context.symbol.addr.GetLoadAddress(target)
end = context.symbol.end_addr.GetLoadAddress(target)
#3
startHex = '0x' + format(start, '012x')
endHex = '0x' + format(end, '012x')
query += '[{}-{}]\n'.format(startHex, endHex)
query += context.symbol.name
query += '\n\n'
output += query
- 判断是否需要加载地址信息。
- 符号的
addr
和end_addr
中通过GetLoadAddress
获取到加载地址。 - 使用Python format函数对使用Python long表示开始和结束地址进行格式化。
实验一下:
(lldb) reload_script
(lldb) lookup -l DSObjectiveCObject
************************************************************
8 hits in Allocator
************************************************************
[0x0001056de150-0x0001056de190]
-[DSObjectiveCObject setLastName:]
[0x0001056de190-0x0001056de1e9]
-[DSObjectiveCObject .cxx_destruct]
[0x0001056de0f0-0x0001056de130]
-[DSObjectiveCObject setFirstName:]
[0x0001056de070-0x0001056de090]
-[DSObjectiveCObject eyeColor]
[0x0001056ddf60-0x0001056de070]
-[DSObjectiveCObject init]
[0x0001056de130-0x0001056de150]
-[DSObjectiveCObject lastName]
[0x0001056de090-0x0001056de0d0]
-[DSObjectiveCObject setEyeColor:]
[0x0001056de0d0-0x0001056de0f0]
-[DSObjectiveCObject firstName]
然后在generateOutput
在moduleName = firstItem.module.file.basename
后面添加:
if options.module_summary:
output += '{} hits in {}\n'.format(count, moduleName)
continue
实验一下:
(lldb) reload_script
(lldb) lookup -s viewWillAppear
2 hits in Allocator
1 hits in DocumentManager
54 hits in UIKitCore
4 hits in ShareSheet
6 hits in AVKit
2 hits in WebKit
1 hits in PDFKit
1 hits in GLKit
10 hits in MapKit
27 hits in ContactsUI
5 hits in OnBoardingKit
2 hits in AccessibilityUIUtilities
20 hits in Preferences