苹果调试和逆向工程进阶 - 笔记(一)

引言

写这本书的目的

每个开发人员都应该研究调试代码的艺术。 但是,有些人会从本书中获得更多。 本书是为:

  1. 希望更好地使用 LLDB 进行调试的开发人员

  2. 希望使用 LLDB 构建复杂调试命令的开发人员

  3. 希望深入了解 Swift 和 Objective-C 内部的开发人员

  4. 有兴趣了解:通过逆向工程,他们可以做些什么的开发人员

  5. 对现代主动逆向工程策略感兴趣的开发人员

  6. 希望在发现有关其计算机或软件问题的答案时有所帮助的开发人员

自定义 LLDB 脚本 repo:

https://github.com/DerekSelander/LLDB

这些脚本将有助于您的调试 / 逆向工程,并为您自己的 LLDB 脚本提供新颖的想法。

第一节:开始 LLDB 命令

到本节结束时,您将能够使用调试器来执行调试所需的大多数基本任务,以及创建自己的简单自定义命令

第 1 章:入门

想知道为什么命令是 po? po 代表打印对象。 还有 p,它只是打印 RDI 的内容。 po 通常更有用,因为它提供了 NSObject 的描述或 debugDescription 方法(如果可用)。

如果您想将调试提升到一个新的水平,汇编(Assembly)是一项重要的技能。

它可以让您深入了解 Apple 的代码 - 即使您没有任何源代码可供阅读。 它将使您更好地了解 Swift 编译器团队如何使用 Swift 在 Objective-C 中跳出,并且它将使您更好地了解 Apple 设备上的一切是如何工作的。

如果可以,我将始终选择在调试器中使用 Objective-C,因为使用 Objective-C LLDB 比使用 Swift 更稳定。

第 2 章:Help 和 Apropos

help将转储 (dump) 所有可用的命令,包括从〜/ .lldbinit 加载的自定义命令。

(lldb) help breakpoint

apropos命令可以为您执行此操作; 这有点像使用搜索引擎在网络上找到一些东西。

(lldb) apropos swift

第 3 章:使用 LLDB Attaching

LLDB“Attaching”的短语实际上有点误导。 名为 debugserver(位于 Xcode.app/Contents/SharedFrameworks/LLDB.framework/Resources/)的程序负责附加 (attaching) 到目标进程。

如果它是远程进程,例如在远程设备上运行的 iOS,watchOS 或 tvOS 应用程序,则会在该远程设备上启动远程调试服务器。 LLDB 的工作是启动,连接和协调调试服务器,以处理调试应用程序时的所有交互。

附加到现有进程

lldb -n Xcode

附加到未来的进程

lldb -n Finder -w

launch 可选的参数

这告诉 LLDB 使用/bin/ls(文件列表命令)作为目标可执行文件。

lldb -f /bin/ls

使用 process 的选项 -w 更改工作目录

(lldb) process launch -w /Applications

直接传递参数给 程序,也就是/bin/ls

(lldb) process launch -- /Applications

等价于

$ ls /Applications

-X选项可扩展您提供的任何 shell 参数,例如代字号。

(lldb) process launch -X true -- ~/Desktop

runprocess launch -X true —的缩写,所以

run ~/Desktop

stdin 也有一个选项-i,用来处理标准的输入输出。

(lldb) target delete
(lldb) target create /usr/bin/wc
$ echo "hello world" > /tmp/wc_input.txt
(lldb) process launch -i /tmp/wc_input.txt
Process 24511 launched: '/usr/bin/wc' (x86_64)
1 2 12
Process 24511 exited with status = 0 (0x00000000)

等同于

$ wc < /tmp/wc_input.txt
1 2 12

第 4 章:在代码中停止

无论您是在技术堆栈中使用 Swift,Objective-C,C ++,C 还是完全不同的语言,您都需要学习如何创建断点。 可以轻松地在 Xcode 中单击侧面板以使用 GUI 创建断点,但 LLDB 控制台可以让您更好地控制断点。

Signals(信号)

Unix 信号是进程间通信的基本形式。

例如,其中一个信号 SIGSTOP 可用于保存状态并暂停执行进程,而其对应的 SIGCONT 则被发送到程序以恢复执行。调试器可以使用这两个信号暂停并继续执行程序。

Xcode 断点

符号断点(Symbolic breakpoints)是 Xcode 的一个很好的调试功能。它们允许您在应用程序中的某个符号上设置断点。例如[NSObject init],它引用 NSObject 实例的 init 方法。

您将学习如何在第 10 章“汇编,寄存器和调用约定”中正确使用和操作寄存器,但是现? 在,只需知道 arg1 与​$rdi寄存器同义,并且可以被认为是持有实例的调用 init 时的类。

还有 Swift 错误断点,它通过在 swift_willThrow 方法上创建断点来随时停止 Swift 抛出错误。如果您正在处理任何容易出错的 API,这是一个很好的选择,因为它可以让您快速诊断情况,而不会对代码的正确性做出错误的假设。

LLDB 断点语法

image 命令是一个很好的工具,可以帮助内省对设置断点至关重要的细节。

(lldb) image lookup -n "-[UIViewController viewDidLoad]"
(lldb) image lookup -rn test
Objective-C properties
@interface TestClass : NSObject 
@property (nonatomic, strong) NSString *name;
@end
(lldb) image lookup -n "-[TestClass name]"
Objective-C properties and dot notation
TestClass *a = [[TestClass alloc] init];

// Both equivalent for setters
[a setName:@"hello, world"];
a.name = @"hello, world";

// Both equivalent for getters
NSString *b;
b = [a name]; // b = @"hello, world"
b = a.name; // b = @"hello, world"

重要的是要知道您是在处理 Objective-C 代码并尝试使用点表示法在 setter 和 getter 属性上创建断点。

Swift properties
class SwiftTestClass: NSObject { 
var name: String!
}

In the LLDB console, type the following:

(lldb) image lookup -rn Signals.SwiftTestClass.name.setter

You’ll get output similar to below:

1 match found in /Users/derekselander/Library/Developer/Xcode/ DerivedData/Signals-atqcdyprrotlrvdanihoufkwzyqh/Build/Products/Debugiphonesimulator/Signals.app/Signals:

Address: Signals[0x000000010000cc70] (Signals.__TEXT.__text + 44816)

Summary: Signals`Signals.SwiftTestClass.name.setter : Swift.ImplicitlyUnwrappedOptional<Swift.String> at SwiftTestClass.swift: 28

使用以下正则表达式查询同时搜索 name 属性的 SwiftTestClass setter 和 getter:

(lldb) image lookup -rn Signals.SwiftTestClass.name

创建断点

有几种不同的方法可以创建断点。最基本的方法是只输入字母 b,后跟断点名称。这在 Objective-C 和 C 中相当容易,因为名称简短且易于键入(例如 - [NSObject init]或 - [UIView setAlpha:])。输入 C ++ 和 Swift 非常棘手,因为编译器会将您的方法转换为具有相当长名称的符号。

(lldb) b -[UIViewController viewDidLoad]

与许多速记命令一样,b 是另一个更长的 LLDB 命令的缩写。

正则表达式断点和范围

rb 命令将扩展到 rbreak(假设您没有任何以“rb”开头的其他 LLDB 命令)。

(lldb) rb SwiftTestClass.name.setter

这将在包含短语 name.setter 的任何内容上生成断点:

(lldb) rb name\.setter

在 UIViewController 的每个 Objective-C 实例方法上创建一个断点:

(lldb) rb '\-\[UIViewController\ '

删除所有断点:

(lldb) breakpoint delete

在断点中的 UIViewController 之后,这提供了带有一个或多个字母数字字符后跟空格的可选括号。

(lldb) rb '\-\[UIViewController(\(\w+\))?\ '

使用正则表达式断点可以使用单个表达式捕获各种断点。

您可以使用 -f 选项将断点的范围限制为特定文件。

如果您正在调试 DetailViewController.swift,这将非常有用。它将在此文件中的所有属性 getter / setter,块 / 闭包,扩展 / 类别和函数 / 方法上设置断点。 -f 称为范围限制。

(lldb) rb . -f DetailViewController.swift

使用 -s 可以限制此共享库中设置断点。

(lldb) rb . -s UIKit

-o 选项为此提供了解决方案。它创造了所谓的“一次性”断点。当这些断点命中时,断点将被删除。所以它只会打一次。

(lldb) breakpoint delete 
(lldb) rb . -s UIKit -o
其他很酷的断点选项

-L 选项允许您按源代码的语言进行过滤。因此,如果您只想在 Signals 应用程序的 Commons 模块中使用 Swift 代码,则可以执行以下操作:

(lldb) breakpoint set -L swift -r . -s Commons

这将在 Commons 模块中的每个 Swift 方法上设置断点。

如果你想在 Swift 语句 if let 寻找一些有趣的东西,如果让它完全忘记你的应用程序在哪里,该怎么办?您可以使用源正则表达式断点来帮助确定感兴趣的位置!像这样:

(lldb) breakpoint set -A -p "if let"

这将在包含 if let 的每个源代码位置创建一个断点。 -A 选项表示搜索项目已知的所有源文件。

如果想进一限制文件范围:

(lldb) breakpoint set -p "if let" -f MasterViewController.swift -f DetailViewController.swift

这将获取所有源文件(-A),但只过滤那些属于 Signals 可执行文件(使用 -s Signals 选项)的文件。

再来一个很酷的断点选项示例?好的。每当 viewDidLoad 被命中时,将创建一个打印 UIViewController 的断点,但是将通过 LLDB 控制台而不是符号断点窗口来执行此操作。然后将这个断点导出到文件中,这样就可以通过使用断点读取和断点写入命令展现给同事。

(lldb) breakpoint delete // 清理断点
(lldb) breakpoint set -n "-[UIViewController viewDidLoad]" -C "po $arg1" -G1 // 打印所有符合条件的实例对象
(lldb) breakpoint write -f /tmp/br.json // 断点写入文件
(lldb) platform shell cat /tmp/br.json //shell 读取文件
(lldb) breakpoint delete // 清理断点
(lldb) breakpoint read -f /tmp/br.json // 还能从文件中导入断点
修改和移除断点

断点会从 1 开始分配 ID

(lldb) b main
(lldb) breakpoint list 1
(lldb) breakpoint list 1 -b // 简洁:没有位置(location)
(lldb) breakpoint list 1 3 // 多个
(lldb) breakpoint list 1-3 // 范围
(lldb) breakpoint delete 1 // 删除
(lldb) breakpoint delete 1.1 // 仅删除第一个子断点

第 5 章:表达式

格式化 p 和 po

po 通常用于 Swift 和 Objective-C 代码中以打印出感兴趣的项目。这可以是对象中的实例变量,对象的本地引用或寄存器,如本书前面所述。它甚至可以是一个任意的内存引用 - 只要该地址有一个对象!

po 实际上是expression -O -- 的简写表达式。 -O 参数用于打印对象的描述。

(lldb) help po
Evaluate an expression on the current thread. Displays any returned
value with formatting controlled by the type's author. Expects'raw'
input (see'help raw-input'.)

Syntax: po <expr>

Command Options Usage:
po <expr>

'po'is an abbreviation for'expression -O --'

po 经常被忽视的兄弟 p, 是另一个省略 -O 选项的缩写,expression -- 。打印出的 p 的格式更依赖于 LLDB 类型系统。

(lldb) help p
Evaluate an expression on the current thread. Displays any returned
value with LLDB's default formatting. Expects 'raw' input (see 'help
raw-input'.)

Syntax: p <expr>

Command Options Usage:
p <expr>

'p' is an abbreviation for 'expression --'

例如:

override var description: String { 
return "Yay! debugging" + super.description
}

override var debugDescription: String {
return "debugDescription:" + super.debugDescription
}

override func viewDidLoad() {
super.viewDidLoad()
print("\(self)")
}

在 viewDidLoad 中添加 Xcode 断点:

(lldb) po self

输出:

debugDescription: Yay! debugging <Signals.MasterViewController: 0x7fb71fd04080>

Swift 和 Objective-C 调试上下(文 contexts)

重要的是要注意调试程序时有两个调试上下文:非 Swift 调试上下文和 Swift 上下文。默认情况下,当您停止使用 Objective-C 代码时,LLDB 将使用非 Swift(Objective-C)调试上下文,而如果您在 Swift 代码中停止,则 LLDB 将使用 Swift 上下文。听起来合乎逻辑,对吧?

但是你也可以指定上下文:

(lldb) expression -l objc -O -- [UIApplication sharedApplication]

在这里,您告诉 LLDB 使用 Objective-C 的 objc 语言。如有必要,您还可以使用 objc + + 作为 Objective-C ++。

用户定义变量

如前所述,LLDB 将在打印对象时代表您自动创建局部变量。您也可以创建自己的变量。

但是记得名字前面加$

(lldb) po id $test = [NSObject new] 
(lldb) po $test
<NSObject: 0x60000001d190>
(lldb) expression -l swift -O -- $test // 切换至 Swift 上下文,同样可用
<NSObject: 0x60000001d190>
(lldb) expression -l swift -O -- $test.description // 但是不能期待一切都正常
error: <EXPR>:3:1: error: use of unresolved identifier '$test' $test.description ^~~~~

这是一个正在积极开发的领域,Objective-C 和 Swift 之间通过 LLDB 的桥梁可能会随着时间的推移而有所改善。

在 Xcoode 创建符号断点:

Signals.MasterContainerViewController.viewDidLoad() -> ()

然后

(lldb) p self
(lldb) po $R0.title //$R0 是上一步打印出来的 self 的变量的名字
(lldb) expression -l swift -- $R0.title = "new title" // 还可以修改

通过键入 continue 或按 Xcode 中的播放按钮来恢复应用程序。就会发现标题已经更新为 ’new title‘

类型格式化(Type formatting)

LLDB 的一个不错的选择是能够格式化基本数据类型的输出。这使得 LLDB 成为了解编译器如何格式化基本 C 类型的绝佳工具。当你正在探索汇编部分时,这是必须知道的,你将在本书后面做。

(lldb) expression -G x -- 10 // 指定十进制格式化输出
(lldb) p/x 10 // 十进制
(lldb) p/t 10 // 二进制
(lldb) p/d 'D' // 十进制
-G <gdb-format> (--gdb-format <gdb-format>)
Specify a format using a GDB format specifier string.

输出格式的完整列表如下(取自https://sourceware.org/gdb/ onlinedocs / gdb / Output-Formats.html):

  • x:十六进制
  • d:十进制
  • u:无符号十进制
  • o:八进制
  • t:二进制
  • a:地址
  • c:字符常量
  • f:浮点型
  • s:字符串

如果这些格式不够,您可以使用 LLDB 的额外格式化程序,但您将无法使用 GDB 格式化语法。

(lldb) expression -f Y -- 1430672467
(int) $0 = 53 54 46 55 STFU
-f <format> (--format <format>)
Specify a format to be used for display.

LLDB 具有以下格式化程序(摘自http://lldb.llvm.org/varformats.html):

  • B:布尔值

  • b:二进制

  • y:字节

  • Y:ASCII 字节

  • c:字符

  • C:可打印字符

  • F:复杂的浮点型(包含实部和虚部)

  • s:c-string

  • i:十进制

  • E:枚举

  • x:十六进制

  • f:浮点型

  • o:八进制

  • O:MacOS OSType

  • U:UTF-16

  • u:无符号十进制

  • p:指针

第 6 章:线程,帧和步进(Thread, Frame & Stepping Around)

您已经学习了如何创建断点,如何打印和修改值,以及如何在调试器中暂停时执行代码。但到目前为止,您已经处于高度干燥的状态,如何在调试器中移动并检查数据之外的数据。现在是时候了!

在本章中,您将学习如何在 LLDB 当前暂停时将调试器移入和移出功能。

这是一项关键技能,因为您经常需要在输入或退出代码片段时随时检查值。

(栈的第一课)Stack 101

当计算机程序执行时,它将值存储在堆和栈中。两者都有其优点。作为高级调试器,您需要充分了解这些工作原理。现在,让我们简要介绍一下这个栈。

栈是 LIFO(后进先出)队列,用于存储对当前正在执行的代码的引用。这种 LIFO 排序意味着最近添加的任何内容都会被删除。想想一栈盘子。在顶部添加一个盘子,它将是你首先取下的盘子。

栈指针指向栈的当前顶部。在板块类比中,栈指针指向顶板,告诉您从哪里取下一块板,或者在哪里放下一块板。

image-20190628093457893

在此图中,高地址显示在顶部(0xFFFFFFFF),低地址显示在底部(0x00000000),显示栈将向下增长。

一些插图喜欢在底部具有高地址以与板类比匹配,因为栈将显示为向上增长。但是,我相信任何展示栈的图表都应显示从高地址向下增长,因为这会在以后讨论栈指针的偏移时引起较少的麻烦。

检查栈的帧

这里我将使用 iPhone X Simulator。

添加断点:

Signals.MasterViewController.viewWillAppear(Swift.Bool) -> ()

image-20190628094640418

在 Debug Navigator 面板中,将显示栈跟踪,显示栈帧列表,第一个是viewWillAppear(_ :)。接下来是 Swift / Objective-C 桥接方法,@objc MasterViewController.viewWillAppear(Bool) ->():。这个方法是自动生成的,所以 Objective-C 可以进入 Swift 代码。

之后,有一些来自 UIKit 的 Objective-C 代码栈帧。深入挖掘一下,你会看到一些属于 CoreAnimation 的 C ++ 代码。更深入的是,你会看到一些方法都包含属于 CoreFoundation 的名称 CFRunLoop。最后,最重要的是,main 函数。

下面进入 LLDB console:

(lldb) thread backtrace

也可以使用bt

(lldb) help bt
Show the current thread's call stack. Any numeric argument displays at
most that many frames. The argument 'all' displays all threads. Expects
'raw' input (see 'help raw-input'.)

Syntax: bt [<digit> | all]

'bt' is an abbreviation for '_regexp-bt'
(lldb) help thread backtrace
Show thread call stacks. Defaults to the current thread, thread indexes
can be specified as arguments.
Use the thread-index "all" to see all threads.
Use the thread-index "unique" to see threads grouped by unique call stacks.

Syntax: thread backtrace <cmd-options>

Command Options Usage:
thread backtrace [-c <count>] [-s <frame-index>] [-e <boolean>]

-c <count> (--count <count>)
How many frames to display (-1 for all)

-e <boolean> (--extended <boolean>)
Show the extended backtrace, if available

-s <frame-index> (--start <frame-index>)
Frame in which to start the backtrace
(lldb) bt
* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 1.1
* frame #0: 0x0000000107044301 Signals`MasterViewController.viewWillAppear(animated=false, self=0x00007f9a26e0a670) at MasterViewController.swift:54:5
frame #1: 0x0000000107044993 Signals`@objc MasterViewController.viewWillAppear(_:) at <compiler-generated>:0
frame #2: 0x0000000110fc9437 UIKitCore`-[UIViewController _setViewAppearState:isAnimating:] + 687
frame #3: 0x0000000110fc9995 UIKitCore`__52-[UIViewController _setViewAppearState:isAnimating:]_block_invoke + 265
frame #4: 0x000000010957b33a CoreFoundation`-[__NSSingleObjectArrayI enumerateObjectsWithOptions:usingBlock:] + 58
frame #5: 0x0000000110fc9729 UIKitCore`-[UIViewController _setViewAppearState:isAnimating:] + 1441
frame #6: 0x0000000110fc9ba2 UIKitCore`-[UIViewController __viewWillAppear:] + 131
frame #7: 0x0000000110f275c9 UIKitCore`-[UINavigationController _startTransition:fromViewController:toViewController:] + 868
frame #8: 0x0000000110f283b5 UIKitCore`-[UINavigationController _startDeferredTransitionIfNeeded:] + 896
frame #9: 0x0000000110f296a7 UIKitCore`-[UINavigationController __viewWillLayoutSubviews] + 150
frame #10: 0x0000000110f0a38d UIKitCore`-[UILayoutContainerView layoutSubviews] + 217
frame #11: 0x0000000111a939c1 UIKitCore`-[UIView(CALayerDelegate) layoutSublayersOfLayer:] + 1417
frame #12: 0x000000010ea1feae QuartzCore`-[CALayer layoutSublayers] + 173
frame #13: 0x000000010ea24b88 QuartzCore`CA::Layer::layout_if_needed(CA::Transaction*) + 396
frame #14: 0x000000010ea30ee4 QuartzCore`CA::Layer::layout_and_display_if_needed(CA::Transaction*) + 72
frame #15: 0x000000010e9a03aa QuartzCore`CA::Context::commit_transaction(CA::Transaction*) + 328
frame #16: 0x000000010e9d7584 QuartzCore`CA::Transaction::commit() + 608
frame #17: 0x00000001115deccb UIKitCore`__34-[UIApplication _firstCommitBlock]_block_invoke_2 + 128
frame #18: 0x0000000109592aec CoreFoundation`__CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__ + 12
frame #19: 0x00000001095922b0 CoreFoundation`__CFRunLoopDoBlocks + 336
frame #20: 0x000000010958cb34 CoreFoundation`__CFRunLoopRun + 1252
frame #21: 0x000000010958c302 CoreFoundation`CFRunLoopRunSpecific + 626
frame #22: 0x000000010e8e22fe GraphicsServices`GSEventRunModal + 65
frame #23: 0x00000001115c5ba2 UIKitCore`UIApplicationMain + 140
frame #24: 0x000000010704b88b Signals`main at AppDelegate.swift:32:7
frame #25: 0x000000010af06541 libdyld.dylib`start + 1
(lldb) frame info
frame #0: 0x0000000107044301 Signals`MasterViewController.viewWillAppear(animated=false, self=0x00007f9a26e0a670) at MasterViewController.swift:54:5

如您所见,此输出与 Debug Navigator 中找到的内容相匹配。那么,如果您只是从 Debug Navigator 中看到所有内容,为什么这甚至很重要?好吧,使用 LLDB 控制台可以对您想要查看的信息进行细致的控制。此外,您将制作自定义 LLDB 脚本,其中这些命令将变得非常有用。知道 Xcode 从哪里获取信息也很好,对吧?

然后:

(lldb) frame select 1
frame #1: 0x0000000107044993 Signals`@objc MasterViewController.viewWillAppear(_:) at <compiler-generated>:0

记下汇编中的绿线。在该行之前是负责执行 viewWillAppear(_:)callq指令,您在之前设置了断点。

不要让汇编太模糊你的眼睛。你还没有走出汇编树林……

步进

掌握 LLDB 时,您可以在程序暂停时执行的三个最重要的导航操作围绕着逐步执行程序。通过 LLDB,您可以跳过(step over),单步执行(step in)或退出(step out)代码。

跳过
(lldb) run //'run' is an abbreviation for 'process launch -X true --'
(lldb) next //'next' is an abbreviation for 'thread step-over'
单步执行
(lldb) step //'step' is an abbreviation for 'thread step-in'
退出
(lldb) finish //'finish' is an abbreviation for 'thread step-out'

请记住,只需按 Enter 键,LLDB 将执行您输入的最后一个命令。

Xcode 界面中步进

虽然使用控制台可以获得更加精细的控制,但 Xcode 已经为 LLDB 控制台上方的按钮提供了这些选项。

检查栈中的数据

frame 命令的一个非常有趣的选项是 frame variable 子命令。

此命令将获取可执行文件头中的调试符号信息(如果您的应用程序被剥离,则为 dYSM …稍后会详细介绍)并转储该特定栈帧的信息。由于调试信息,frame variable 命令可以使用适当的选项轻松告诉您函数中所有变量的范围以及程序中的任何全局变量。

(lldb) frame variable
(lldb) frame variable -F self // 以平面格式打印结果
-F (--flat)
Display results in a flat format that uses expression paths for each variable or member.

image-20190701112507258

提供有关 Apple 私有 API 的 ivars 的更多信息,而不是 Variables View。

第 7 章:镜像 (Image)

现在是时候探索通过 LLDB 的权力找到感兴趣代码的最佳工具之一。在本章中,您将深入了解 image 命令。

image命令是 target modules 子命令的别名。image命令专门查询 module 信息; 也就是说,代码在进程中加载和执行。Module 可以包含许多东西,包括主要的可执行文件 (main executables),框架(framework) 或插件 (plugin)。但是,这些 module 中的大多数通常以动态库(dynamic libraries) 的形式出现。动态库的示例包括适用于 iOS 的 UIKit 或适用于 macOS 的 AppKit。

Module

(lldb) image list 
(lldb) image list Foundation
[0] FD4BF3C6-63C9-3A30-BEEA-D39F6F8EEB13 0x0000000107d70000 /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/Library/CoreSimulator/Profiles/Runtimes/iOS.simruntime/Contents/Resources/RuntimeRoot/System/Library/Frameworks/Foundation.framework/Foundation

这是一种有用的方法,可以找到有关所需 module 的信息。

让我们来探索这个输出。 那里有一些有趣的东西:

  1. 首先打印 module 的 UUID(FD4BF3C6-63C9-3A30-BEEA-D39F6F8EEB13)。UUID 对于搜索符号信息和唯一标识 Foundation module 的版本非常重要。

  2. 在 UUID 之后是加载地址(0x0000000107d70000)。 这标识将 Foundation module 加载到 Signals 可执行文件的进程空间中。

  3. 最后,您拥有 module 在磁盘上所在位置的完整路径。

(lldb) image dump symtab UIKitCore -s address

这将转储 UIKitCore 可用的所有符号表信息。由于 -s address 参数,此命令按私有 UIKitCore 模块中实现函数的地址对输出进行排序。

但是可读性不佳,需要另外一个命令来搜索:

(lldb) image lookup -n "-[UIViewController viewDidLoad]"
1 match found in /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/Library/CoreSimulator/Profiles/Runtimes/iOS.simruntime/Contents/Resources/RuntimeRoot/System/Library/PrivateFrameworks/UIKitCore.framework/UIKitCore:
Address: UIKitCore[0x000000000034adf9] (UIKitCore.__TEXT.__text + 3443497)
Summary: UIKitCore`-[UIViewController viewDidLoad]

这将转储与 UIViewController 的 viewDidLoad 实例方法相关的信息。

如果想用正则来模糊搜索:

(lldb) image lookup -rn UIViewController

但是这还不够,因为结果包含了UIViewControllerBuiltinTransitionViewAnimator,这并不是我们想要的,所改为:

(lldb) image lookup -rn '\[UIViewController\ '
(lldb) image lookup -rn \[UIViewController\s // 等价的

但是如果搜索 Category,命名规则:UIViewController(CategoryName),改进如下:

(lldb) image lookup -rn '\[UIViewController\(\w+\)\ '

当然这些都是参考正则表达式的规则,然后用在实际的需求中。

寻找代码

您可以使用上面的 image lookup 命令找到 UIViewController 方法。您还使用它来寻找第 4 章“在代码中停止”中如何命名 Swift 属性 setter 和 getter。

但是 Block 如何搜索呢?如下面例子

dispatch_once(&onceToken, ^{
sharedSignalHandler = [[UnixSignalHandler alloc] initPrivate];
});

我们先在 Xcode 添加断点,然后:

(lldb) frame info
frame #0: 0x00000001048e1250 Commons`__34+[UnixSignalHandler sharedHandler]_block_invoke(.block_descriptor=0x00000001048e8230) at UnixSignalHandler.m:72:28

可以看到完整的函数名:__34+[UnixSignalHandler sharedHandler]_block_invoke

让我们先试试:

(lldb) image lookup -rn _block_invoke

结果太多,添加范围限制:

(lldb) image lookup -rn _block_invoke Commons

现在创建断点:

(lldb) rb appendSignal.*_block_invoke -s Commons

之后我回到终端:

pkill -SIGIO Signals

就会触发刚刚创建的断点:

2019-07-01 14:16:15.922561+0800 Signals[7583:429444] Appending new signal: SIGIO
(lldb) frame variable
(__block_literal_5 *) .block_descriptor = 0x0000600002f1d580
(int) sig = <read memory from 0x41 failed (0 of 4 bytes read)>

(siginfo_t *) siginfo = <read memory from 0x39 failed (0 of 8 bytes read)>

(UnixSignalHandler *const) self = <read memory from 0x31 failed (0 of 8 bytes read)>

(lldb) next
(lldb) frame variable
(__block_literal_5 *) .block_descriptor = 0x0000600002f1d580
(int) sig = 23
(siginfo_t *) siginfo = 0x00007ffeecdd9e78
(UnixSignalHandler *) self = 0x0000600002f091c0
(UnixSignal *) unixSignal = 0x0000000106ca4454

您需要跳过一个语句,因此块执行了一些初始逻辑来设置函数,也称为函数序言(function prologue)。函数序言是与汇编相关的主题,您将在第 II 节中了解。

这实际上非常有趣。首先,您会看到一个 block 的对象,正在调用的。然后有 sig 和 siginfo 参数传递给 Objective-C 方法,在该方法中调用此块。这些如何传递到 block 中?

好吧,当创建一个 block 时,编译器足够聪明,可以确定它正在使用哪些参数。然后它创建一个将这些作为参数的函数。调用块时,调用此函数,并传入相关参数。

(lldb) image lookup -t __block_literal_5
Best match found in /Users/will/Library/Developer/Xcode/DerivedData/Signals-bgjklehkjddsnxcexztwhrufpsbn/Build/Products/Debug-iphonesimulator/Signals.app/Frameworks/Commons.framework/Commons:
id = {0x100000be1}, name = "__block_literal_5", byte-size = 52, decl = UnixSignalHandler.m:127, compiler_type = "struct __block_literal_5 {
void *__isa;
int __flags;
int __reserved;
void (*__FuncPtr)();
__block_descriptor_withcopydispose *__descriptor;
UnixSignalHandler *const self;
siginfo_t *siginfo;
int sig;
}"

这是定义 block 的对象!

正如您所看到的,这几乎与头文件一样好,可以告诉您如何在 block 中导航内存。如果将内存中的引用转换为 __block_literal_5 类型,则可以轻松打印出 block 引用的所有变量。

(lldb) po ((__block_literal_5 *)0x0000600002f1d580)
<__NSMallocBlock__: 0x600002f1d580>
(lldb) p/x ((__block_literal_5 *)0x0000600002f1d580)->__FuncPtr
(void (*)()) $1 = 0x0000000103143a80 (Commons`__38-[UnixSignalHandler appendSignal:sig:]_block_invoke_2 at UnixSignalHandler.m:127)
(lldb) image lookup -a 0x0000000103143a80
Address: Commons[0x0000000000001a80] (Commons.__TEXT.__text + 2240)
Summary: Commons`__38-[UnixSignalHandler appendSignal:sig:]_block_invoke_2 at UnixSignalHandler.m:127
(lldb) po ((__block_literal_5 *)0x0000600002f1d580)->sig // 打印出传递给 block 的所有参数
23
(lldb) p *(__block_literal_5 *)0x0000600002f1d580 // 可以使用 p 命令转储完整的结构并解除引用指针
(__block_literal_5) $4 = {
__isa = 0x0000000106d6e170
__flags = -1023410174
__reserved = 0
__FuncPtr = 0x0000000103143a80 (Commons`__38-[UnixSignalHandler appendSignal:sig:]_block_invoke_2 at UnixSignalHandler.m:127)
__descriptor = 0x000000010314a2c0
self = 0x0000600002f091c0
siginfo = 0x00007ffeecdd9e78
sig = 23
}

深入研究(Snooping around)

好的,您已经发现了如何以静态方式检查私有类的实例变量,但是该块内存地址太过诱人而无法置之不理。 尝试将其打印出来并使用动态分析进行探索。

直接打印 block 的内存地址:

(lldb) po 0x0000600002f1d580
<__NSMallocBlock__: 0x600002f1d580>
(lldb) image lookup -rn __NSMallocBlock__ // 没有结果
(lldb) po [__NSMallocBlock__ superclass]
__NSMallocBlock
(lldb) image lookup -rn __NSMallocBlock
5 matches found in /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/Library/CoreSimulator/Profiles/Runtimes/iOS.simruntime/Contents/Resources/RuntimeRoot/System/Library/Frameworks/CoreFoundation.framework/CoreFoundation:
Address: CoreFoundation[0x000000000018b600] (CoreFoundation.__TEXT.__text + 1614448)
Summary: CoreFoundation`-[__NSMallocBlock retain] Address: CoreFoundation[0x000000000018b620] (CoreFoundation.__TEXT.__text + 1614480)
Summary: CoreFoundation`-[__NSMallocBlock release] Address: CoreFoundation[0x000000000018b630] (CoreFoundation.__TEXT.__text + 1614496)
Summary: CoreFoundation`-[__NSMallocBlock retainCount] Address: CoreFoundation[0x000000000018b640] (CoreFoundation.__TEXT.__text + 1614512)
Summary: CoreFoundation`-[__NSMallocBlock _tryRetain] Address: CoreFoundation[0x000000000018b650] (CoreFoundation.__TEXT.__text + 1614528)
Summary: CoreFoundation`-[__NSMallocBlock _isDeallocating]

(lldb) po [__NSMallocBlock superclass]
NSBlock

(lldb) image lookup -rn 'NSBlock\ '
6 matches found in /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/Library/CoreSimulator/Profiles/Runtimes/iOS.simruntime/Contents/Resources/RuntimeRoot/System/Library/Frameworks/CoreFoundation.framework/CoreFoundation:
Address: CoreFoundation[0x000000000018b4c0] (CoreFoundation.__TEXT.__text + 1614128)
Summary: CoreFoundation`+[NSBlock allocWithZone:] Address: CoreFoundation[0x000000000018b4e0] (CoreFoundation.__TEXT.__text + 1614160)
Summary: CoreFoundation`+[NSBlock alloc] Address: CoreFoundation[0x000000000018b500] (CoreFoundation.__TEXT.__text + 1614192)
Summary: CoreFoundation`-[NSBlock copy] Address: CoreFoundation[0x000000000018b510] (CoreFoundation.__TEXT.__text + 1614208)
Summary: CoreFoundation`-[NSBlock copyWithZone:] Address: CoreFoundation[0x000000000018b520] (CoreFoundation.__TEXT.__text + 1614224)
Summary: CoreFoundation`-[NSBlock invoke] Address: CoreFoundation[0x000000000018b530] (CoreFoundation.__TEXT.__text + 1614240)
Summary: CoreFoundation`-[NSBlock performAfterDelay:]

您现在将尝试在 block 上调用此方法。但是,当保留此 block 的引用释放其控制时,您不希望块消失,从而降低 retainCount,并可能释放 block。

有一种简单的方法来保持这个 block - 只需 retain 它!在 LLDB 中键入以下内容,将地址替换为 block 的地址:

(lldb) po id $block = (id)0x0000600002f1d580
(lldb) po [$block retain]
<__NSMallocBlock__: 0x600002f1d580>

(lldb) po [$block invoke]
2019-07-01 16:05:33.855039+0800 Signals[7583:452921] Appending new signal: SIGIO
nil

这表明你已经再次调用了 block!

这种用于探索公共和私有类,然后探索它们实现的方法的方法,是了解程序覆盖范围内的内容的好方法。稍后您将对方法使用相同的发现过程,然后分析这些方法执行的程序集,为您提供原始方法源代码的非常接近的近似值。

Private debugging methods

image lookup 命令可以很好地搜索私有方法以及您在 Apple 开发职业生涯中看到的公共方法。

但是,在调试自己的代码时,有一些隐藏的方法非常有用。

例如,以 _ 开头的方法通常表示自己是一个私有(也可能是重要的!)方法。

让我们尝试在所有以下划线字符开头的模块中搜索任何 Objective-C 方法,并在其中包含单词“description”。

如果您在阅读此行代码时有些迷惑不解,强烈建议您仔细阅读 https://docs.python.org/2/library/re.html 以了解正则表达式查询; 从现在开始,它只会变得更加复杂。

(lldb) image lookup -rn (?i)\ _\w+description\] // 不区分大小写
(lldb) image lookup -rn NSObject\(IvarDescription\)
(lldb) po [[UIApplication sharedApplication] _ivarDescription]
(lldb) image lookup -rn '\[UIStatusBar\ set' // 查找所以 set 开头方法
(lldb) po (BOOL)[[UIStatusBar class] isSubclassOfClass:[UIView class]]

通过地址调用对象:

(lldb) po [[UIApplication sharedApplication] statusBar]
<UIStatusBar_Modern: 0x7f9965e03f00; frame = (0 0; 375 44); autoresize = W+BM; layer = <CALayer: 0x6000025dd640>>

(lldb) po [0x7f9965e03f00 setBackgroundColor:[UIColor purpleColor]]
purpleColor

未完待续