# 汇编寄存器的规则
在本章中,您将了解到 CPU 使用的寄存器,并研究和修改传入函数的参数。您还将了解常见的苹果计算机架构,以及如何在函数中使用它们的寄存器。这就是所谓的架构调用约定。
了解汇编如何工作以及特定架构的调用约定如何工作是一项极其重要的技能。它可以让您观察没有源代码的函数参数,并允许您修改传入函数的参数。此外,有时转到底层汇编层面会更好,因为您的源代码可能对您不知道的变量有不同的或未知的名称。 例如,假设您想知道函数调用的第二个参数,但是我并不知道参数的名称是什么。汇编的知识会帮助你来观察这些函数中的参数。
# 汇编 101
等等,所以到底什么是汇编?来看一个场景:您是否曾经打了一个断点,但是中断到没有源代码的地方?然后看到看到大量内存地址和可怕的简短命令?你是不是缩成一团,悄悄地对自己说你再也不会看这些密集的东西了?嗯… 这就是所谓的汇编! 这里有一张 Xcode 中的断点调试图,它展示了模拟器中函数的汇编。
通过上图可以看出汇编代码可以被分成几个不同的部分。汇编指令中的每一行都包含一个操作码,可以认为是对计算机来说非常简单的指令。那么操作码是什么样子的呢?操作码是在计算机上执行一项简单任务的指令。例如下面的程序集片段:
代码语言:javascript复制pushq %rbx
subq $0x228, %rsp
movq %rdi, %rbx
在这个汇编模块中,您可以看到三种操作码: pushq
、 subq
和 movq
可以将操作码项看作要执行的操作。操作码后面的内容是源标签和目标标签。也就是说,这些是操作码所作用的对象。在上面的示例中,有几个寄存器,显示为 rbx
、 rsp
、 rdi
和 rbp
。前面的 %
告诉您这是一个寄存器。
此外,您还可以找到一个十六进制的数字常量,如 0x228。这个常数之前的美元符号告诉你它是一个绝对值。现在不需要知道这段代码在做什么,因为您首先需要了解每个符号的含义。然后你会学到更多关于操作码的知识,并在以后的章节中编写你自己的程序。
注意:在上面的示例中,请注意,在寄存器和常量之前有一堆%和 $。 这就是反汇编程序格式化程序集的展示方式。 但是可以通过两种主要方式展示汇编。 第一个是
英特尔程序集
,第二个是AT&T程序集
。默认情况下,Apple 的反汇编程序工具都会以 AT&T 格式显示,就如上例所示。 尽管这是一种很好的格式,但在眼睛上可能会有些困难。 在下一章中,您将把汇编格式更改为 Intel,并且从那以后将完全使用 Intel 汇编语法。
# x86_64 vs ARM64
作为 Apple 平台的开发人员,学习汇编时要处理两种主要架构:x86_64 架构和 ARM64 架构。 x86_64 是最可能在 macOS 计算机上使用的体系结构,除非您运行的是 “古老” 的 Macintosh。x86_64 是 64 位体系结构,这意味着每个地址最多可以容纳 64 个 1 或 0。 另外,较旧的 Mac 使用 32 位架构,但是 Apple 在 2010 年底停止生产 32 位 Mac。 在 macOS 下运行的程序可能是 64 位兼容的,包括 Simulator 上的程序。 话虽如此,即使您的 macOS 是 x86_64,它仍然可以运行 32 位程序。如果对使用的硬件架构有疑问,可以在终端中运行以下命令来获取计算机的硬件架构:
- uname -m
在能耗要求很高的移动设备(如 iPhone)上使用 ARM64 体系结构。ARM 强调节能功能,因此它减少了一组操作码,有助于简化复杂的汇编指令,从而降低了能耗。 这对您来说是个好消息,因为关于 ARM 的体系结构你需要学习的并不多。这是与之前显示的方法相同的屏幕截图,除了这次是在 iPhone 7 上的 ARM64 程序集中:
您现在可能无法区分这两种架构,但是您很快就会知道它们就像手背一样.
Apple 最初在其许多 iOS 设备中都提供了 32 位 ARM 处理器,但此后便转移到了 64 位 ARM 处理器。 32 位 iOS 设备几乎已过时,因为 Apple 已通过各种 iOS 版本逐步淘汰了它们。 例如,iPhone 5 是最终的 32 位 iOS 设备,iOS 11 不支持该设备。支持 iOS 11 的 “最低” iPhone 是 64 位设备 iPhone 5s。近年来,32 位设备已出现在其他 Apple 产品中。 Apple Watch 的前两代是 32 位设备,但是第三代是 64 位设备。 此外,在较新的 macOS 设备上发现的 Apple Touch Bar(无疑是花哨的)也使用 32 位架构。
由于最好专注于您将来的需求,因此本书将主要关注两种架构的 64 位汇编。 此外,您将首先开始学习 x86_64 程序集,然后过渡到学习 ARM64 程序集,以免感到困惑。 好吧,不要太困惑。
# x86_64 寄存器调用规则
您的 CPU 使用一组寄存器来操纵正在运行的程序中的数据。这些是存储的基础,就像计算机中的 RAM 一样。但是,它们位于 CPU 本身上,因此 CPU 的这些部分可以快速访问这些寄存器。效率非常高,大多数指令涉及一个或多个寄存器,并执行一些操作,例如将寄存器的内容写入内存,将存储器的内容读取到寄存器或对两个寄存器执行算术运算例如 加,减等。
在 x64 中(从现在开始,x64 是 x86_64 的缩写),机器使用 16 个通用寄存器来操纵数据。 这些寄存器是 RAX,RBX,RCX,RDX,RDI,RSI,RSP,RBP 和 R8 至 R15。这些名称对您现在意义不大,但是您很快就会发现每个寄存器的重要性。“在 x64 中调用函数时,寄存器的方式和使用遵循非常特定的规则。这决定了函数的参数应该去哪里以及函数完成时函数的返回值应该在哪里。这很重要,因此可以将一个编译器编译的代码与另一个编译器编译的代码一起使用。
比如你看下面这个行代码:
代码语言:javascript复制NSString *name = @"Zoltan";
NSLog(@"Hello world, I am %@. I'm %d, and I live in %@.", name, 30, @"my father's basement");
NSLog 函数调用中传递了四个参数。 其中一些值按原样传递,而一个参数存储在局部变量中,然后在函数中作为参数引用。 但是,通过汇编查看代码时,计算机并不关心变量的名称 (name); 它只关心该变量在内存中的位置。
在 x64 汇编中调用函数时,以下寄存器用作参数。 尝试将它们提交到内存中,因为将来您会经常使用它们:
代码语言:javascript复制第一个参数:RDI
第二个参数:RSI
第三个参数:RDX
第四个参数:RCX
第五个参数:R8
第六个参数:R9
如果有六个以上的参数,则使用程序的堆栈将其他参数传递给该函数。
回到简单的 Objective-C 代码,上面的 OC 代码可以像下面这样的伪代码流程在寄存器中传递:
代码语言:javascript复制RDI = @"Hello world, I am %@. I'm %d, and I live in %@.";
RSI = @"Zoltan";
RDX = 30;
RCX = @"my father's basement";
NSLog(RDI, RSI, RDX, RCX);
NSLog 调用完成后,指定的寄存器将包含如上所述的适当值。但是,一旦函数序言(准备堆栈和寄存器的函数的开始部分)完成执行,这些寄存器中的值就可能改变。生成的程序集可能会覆盖存储在这些寄存器中的值,或者在代码不再需要这些引用时丢弃这些引用。 这意味着,一旦离开函数的开始,就不能再假定这些寄存器将保存您要观察的期望值,除非您实际查看汇编代码看看它在做什么。 使用此调用规则的浏览寄存器会严重影响您的调试(和断点)策略。必须在函数调用开始时停止以查看或修改参数,而不必实际进入程序集。
# Objective-C 和寄存器
如上一节所述,寄存器使用特定的调用规则。 您也可以将该知识其应用于其他语言。
当 Objective-C 执行方法时,将执行一个名为 objc_msgSend
的特殊 C 函数。 这些功能实际上有几种不同的类型,但稍后会介绍更多。 这是 Objective-C 动态消息分发的核心。 作为第一个参数,objc_msgSend 获取在其上发送消息的对象的引用。 随后是一个选择器,它只是一个 char *,用于指定在对象上调用的方法的名称。 最后,如果选择器指定应有参数,则 objc_msgSend 在函数中采用可变数量的参数。
让我们来看一个在 iOS 环境中的具体示例:
id UIApplicationClass = [UIApplication class];
objc_msgSend(UIApplicationClass, "sharedApplication");
第一个参数是对 UIApplication 类的引用,其后是 sharedApplication 选择器 其实就是需要被调用的方法。 判断是否有参数的一种简单方法是简单地检查 Objective-C 选择器中的冒号。 每个冒号将代表一个方法中的参数。 我们下面来看一个 OC 的方法:
代码语言:javascript复制NSString *helloWorldString = [@"Can't Sleep; "stringByAppendingString:@"Clowns will eat me"];
最终编译会变成:
代码语言:javascript复制NSString *helloWorldString;
helloWorldString = objc_msgSend(@"Can't Sleep; ", "stringByAppendingString:", @"Clowns will eat me");
第一个参数是 NSString 的实例(@“Can't Sleep;”),后跟方法选择器,然后是方法调用的参数。
# 将理论付诸实践
在本节中,您将使用本章资源包中提供的名为 Registers 的项目。 通过 Xcode 打开该项目,然后运行。
这是一个非常简单的应用程序,仅显示 64 位寄存器里面的内容。 需要注意的是,该应用程序不会实时的显示寄存器的值; 它只能在特定的函数调用期间显示寄存器的值。 这意味着您不会看到这些寄存器的值有太多更改,因为在调用获取寄存器值的函数时它们可能具有相同(或相似)的值。 现在,您已经了解了 Registers macOS 应用程序背后的功能,为 NSViewController 的 viewDidLoad 方法创建一个符号断点。 记住,因为您正在使用 Mac 应用程序,所以请使用 “NS” 代替 “ UI”。
生成并重新运行该应用程序。触发断点后在 LLDB 控制台中键入以下内容:
- (lldb) register read
这将列出处于暂停执行状态的所有主要寄存器。 但是输出了太多信息。 您应该有选择地打印出寄存器,并将其视为 Objective-C 对象。如果您还记得的话,-[NSViewController viewDidLoad] 将被转换为以下程序集伪代码:
代码语言:javascript复制RDI = UIViewControllerInstance
RSI = "viewDidLoad"
objc_msgSend(RDI, RSI)
考虑到 x64 调用规则,并且知道 objc_msgSend 的工作方式,您可以找到要加载的特定 NSViewController。 在 LLDB 控制台中键入以下内容:
- (lldb) po $rdi
然后你会看到如下输出:
代码语言:javascript复制<Registers.ViewController: 0x6080000c13b0>
它输出了 RDI 寄存器中的 NSViewController 引用,您现在知道该引用是该方法的第一个参数的位置。
在 LLDB 中,为寄存器加上 $ 字符很重要,因此 LLDB 知道您需要的是寄存器的值,而不是源代码中与范围相关的变量。 是的,这与您在刚刚反汇编视图中看到的汇编不同! 烦人吧?
注:善于观察的你可能已经注意到了在 OC 代码中打断点,在 LLDB 的回溯内看不到 objc_msgSend 的影子。这是因为 objc_msgSend 方法簇执行了 jmp。意思就是说 objc_msgSend 扮演了中转的角色,一但 OC 代码开始执行,所有的关于 objc_msgSend 的栈中的回溯都将消失。这是一种叫做尾递归调用的优化。因为 mesgsend 开始执行证明之前的栈帧已经清空了。
尝试打印出 RSI 寄存器,不出意外的话应该是方法名。 在 LLDB 控制台中输入以下内容:
- (lldb) po $rsi
结果你会输出这个
代码语言:javascript复制140735181830794
为什么是这样? Objective-C 选择器基本上只是一个 char *。 这意味着,像所有 C 类型一样,LLDB 不知道如何格式化此数据。 因此,您必须将此引用显式转换为所需的数据类型。 将 RSI 寄存器强制转换为正确的类型使用如下指令
- po (char *)rsi或者po(SEL)rsi 或者 po (SEL)rsi或者po(SEL)rsi
便可以得到方法名字
代码语言:javascript复制"viewDidLoad"
现在,该探讨带有参数的 Objective-C 方法了。 由于您已经断点了 viewDidLoad,因此可以放心地假设 NSView 实例已加载。 感兴趣的方法是 mouseUp:由 NSView 的父类 NSResponder 实现的选择器。 在 LLDB 中,在 NSResponder 的 mouseUp:选择器上创建一个断点,然后继续执行。 如果您不记得该怎么做,则需要以下命令:
- (lldb) b -[NSResponder mouseUp:]
- (lldb) continue
现在,点击应用程序窗口。 确保单击 NSScrollView 的外部,因为 NSScrollView 它会拦截您的单击,并且不会命中 -[NSResponder mouseUp:] 断点。
点击后,LLDB 就会在 mouseUp:断点处停止。 通过在 LLDB 控制台中键入以下内容,打印出 NSResponder 的引用:
- (lldb) po $rdi
会出现如下的输出
代码语言:javascript复制<NSView: 0x608000120140>
但是,该方法是带参数的! 在 LLDB 控制台中输入以下内容:
- (lldb) po $rdx
输出
代码语言:javascript复制“NSEvent: type=LMouseUp loc=(351.672,137.914) time=175929.4 flags=0 win=0x6100001e0400 winNum=8622 ctxt=0x0 evNum=10956 click=1 buttonNumber=0 pressure=0 deviceID:0x300000014400000 subtype=NSEventSubtypeTouch”
也可以看到参数类型
- po [$rdx class]
NSEvent
太酷了,是吗?有时使用寄存器和断点很有用,以便获得内存中还存在的对象的引用。例如,如果您想将前部 NSWindow 更改为红色,但是在代码中没有对该视图的引用,又不想重新编译任何代码怎么办? 您只需创建一个断点就可以轻松调试,从寄存器中获取引用并根据需要操纵该对象的实例。 您现在将尝试将主窗口更改为红色。”
注:尽管 NSResponder 实现了 mouseDown: 方法,但 NSWindow 重写了它。你可以输出所有实现了 mouseDown: 的类,你就可以看出这个方法被那些类重写了,而不用去看源码。输出所有实现了 mouseDown: 方法的 OC 类的命令是:<u>image lookup -rn ' mouseDown:</u>'
首先使用 LLDB 控制台删除所有以前的断点:
- breakpoint delete
像如下输出
代码语言:javascript复制(lldb) breakpoint delete
About to delete all breakpoints, do you want to do that?: [Y/n] Y
All breakpoints removed. (3 breakpoints)
(lldb)
然后在 LLDB 控制台中键入以下内容:
- (lldb) breakpoint set -o -S "-[NSWindow mouseDown:]
- (lldb) continue
这句话的作用是设置一个单发断点,只会触发一次,然后点击应用程序触发断点,在 LLDB 控制台中键入以下内容:
- (lldb) po [$rdi setBackgroundColor:[NSColor redColor]]
- (lldb) continue
之后就可以看到效果
# Swift 和寄存器
在 Swift 中探索寄存器时,您将遇到两个问题,这使汇编调试比 Objective-C 困难。
- 首先,在 Swift 调试上下文内寄存器不可用。意味着你不得不获取到任何你想要的数据,并使用 OC 调试上下文打印出传入 Swift 函数的寄存器。记住你可以使用 expression -l objc -O 命令,或者使用在书中第八章(“Persisting and Customizing Commands”)的 cpo 命令。幸运的是,register read 命令依然是可以使用的。
- 其次,Swift 相对于 OC 并不是动态的。事实上,有时候最好假设 Swift 像 C 语言一样。如果知道了一个内存地址,你应该显示地强转为你想要的类型。不然 Swift 调试器没有任何线索去解释内存地址。
话虽这么说,但是 Swift 使用了相同的寄存器调用规则。 但是有一个非常重要的区别。 当 Swift 调用一个函数时,它__不需要使用 objc_msgSend__,除非您当然标记了使用动态方法。 这意味着当 Swift 调用函数时,先前分配给选择器的 RSI 寄存器实际上就是函数的第二个参数。好了,足够的理论 - 是时候将其付诸实践了。
在 Registers 项目中,导航到 ViewController.swift 并将以下函数添加到该类:
代码语言:javascript复制func executeLotsOfArguments(one: Int, two: Int, three: Int,
four: Int, five: Int, six: Int,
seven: Int, eight: Int, nine: Int,
ten: Int) {
print("arguments are: (one), (two), (three), (four), (five), (six), (seven), (eight), (nine), (ten)")
}
现在,在 viewDidLoad 中,使用适当的参数调用此函数:
代码语言:javascript复制self.executeLotsOfArguments(one: 1, two: 2, three: 3, four: 4, five: 5, six: 6, seven: 7, eight: 8, nine: 9, ten: 10)
在与 executeLotsOfArguments 声明相同的行上放置一个断点,以便调试器将在函数的开始处停止。 这很重要,否则如果函数已经开始执行,则寄存器可能会被破坏。然后删除您在 -[NSViewController viewDidLoad] 上设置的符号断点。生成并运行该应用程序,然后等待 executeLotsOfArguments 断点停止执行。我们先列出所有的寄存器。 在 LLDB 中,键入以下内容:
- register read -f d
这将列出所有的寄存器,并使用 - f d 选项以十进制显示格式。 输出将类似于以下内容:
代码语言:javascript复制(lldb) register read -f d
General Purpose Registers:
rax = 10
rbx = 7
rcx = 4
rdx = 3
rdi = 1
rsi = 2
rbp = 140732785005232
rsp = 140732785004720
r8 = 5
r9 = 6
r10 = 9
r11 = 8
r12 = 140668105198352
r13 = 105553138827696
r14 = 104
r15 = 8
rip = 4430734802 Registers`Registers.ViewController.executeLotsOfArguments(one: Swift.Int, two: Swift.Int, three: Swift.Int, four: Swift.Int, five: Swift.Int, six: Swift.Int, seven: Swift.Int, eight: Swift.Int, nine: Swift.Int, ten: Swift.Int) -> () 178 at ViewController.swift:68:15
rflags = 514
cs = 43
fs = 0
gs = 0
(lldb)
如您所见,寄存器遵循 x64 调用规则。 RDI,RSI,RDX,RCX,R8 和 R9 保留您的前六个参数。
注意:关于 LLDB,我一直没有告诉您的是,LLDB 可以以argX形式来引用寄存器,其中X是参数号。还记得RDI是第一个参数,而RSI是第二个参数吗?在LLDB中,可以通过arg{X}形式来引用寄存器,其中X是参数号。还记得RDI是第一个参数,而RSI是第二个参数吗?在LLDB中,可以通过argX形式来引用寄存器,其中X是参数号。还记得RDI是第一个参数,而RSI是第二个参数吗?在LLDB中,可以通过 arg1 引用第一个参数 (RDI)。随着示例的进行,您可以使用 $arg2 引用第二个参数 (RSI),以此类推。这些方便值也可以在 ARM64 调用约定中使用,即使 ARM64 使用不同的寄存器。您应该记住寄存器调用规则,以便本书尽量减少使用这些寄存器辅助变量。
您可能还会注意到其他参数存储在其他一些其他寄存器中。 确实如此,但这只是为其余参数设置堆栈的代码中的剩余部分。 请记住,第六个参数之后的参数将进入堆栈。
# RAX,用于返回的寄存器
等等 -- 还有呢!到这里,你已经了解了函数中六个寄存器是如何调用的,但是返回值呢?
幸运的是,只有一个指定的寄存器用于返回值:RAX。回到 executeLotsOfArguments 函数并改变函数的返回值,像这样:
代码语言:javascript复制func executeLotsOfArguments(one: Int, two: Int, three: Int,
four: Int, five: Int, six: Int,
seven: Int, eight: Int, nine: Int,
ten: Int) -> String {
print("arguments are: (one), (two), (three), (four), (five), (six), (seven), (eight), (nine), (ten)")
return "Mom, what happened to the cat?"
}
然后 viewdidlaod 中调用
代码语言:javascript复制let _ = self.executeLotsOfArguments(one: 1, two: 2, three: 3, four: 4, five: 5, six: 6, seven: 7, eight: 8, nine: 9, ten: 10)
在 executeLotsOfArguments 中的某处创建一个断点。 再次生成并运行,然后等待函数停止执行。 接下来,在 LLDB 控制台中键入以下内容:
- (lldb) finish
命令会结束完成函数的执行并停住调试器。这时,函数返回值会在 RAX 内。输入如下命令:
- (lldb) register read rax
你将会看见如下输出
代码语言:javascript复制rax = 0x0000000100003760 "Mom, what happened to the cat?
了解 RAX 中的返回值非常重要,因为它将构成您将在后面的部分中编写的调试脚本的基础。
# 改变寄存器值
为了巩固您对寄存器的理解,您将在一个已编译的应用程序中修改寄存器。 关闭 Xcode 和 Registers 项目。 打开终端窗口,然后启动 iPhone X Simulator。 通过键入以下内容来执行此操作:
- xcrun simctl list | grep "iPhone X”
Phone: iPhone 12 Pro Max (16A6D554-3C10-4A67-9039-31B8BE33871F) (Shutdown)
UDID 就是你要找的。使用它并通过如下命令打开 iOS 模拟器(替换其中的 UDID 部分):
- open /Applications/Xcode.app/Contents/Developer/Applications/Simulator.app --args -CurrentDeviceUDID 16A6D554-3C10-4A67-9039-31B8BE33871F
保证模拟器已经启动而且在主屏幕上。你可以通过按下 Command Shift H 键回到主屏幕。一旦模拟器准备好了,回到终端窗口将 LLDB 绑定到 SpringBoard 程序上。
- lldb -n SpringBoard
这样会将 LLDB 绑定到正在模拟器上运行的 SpringBoard 实例上!SpringBoard 就是在 iOS 上控制主屏幕的程序。
一旦绑定,输入如下命令:
- (lldb) p/x @"Yay! Debugging 可以看到类似如下的输出:
(__NSCFString *) $3 = 0x0000618000644080 @"Yay! Debugging!"
注意下刚刚创建的这个 NSString 实例,因为很快你会用到它。现在,给 UILabel 的 setText: 方法设置一个断点:
- (lldb) b -[UILabel setText:]
- (lldb) breakpoint command add
LLDB 会吐出一些输出且进入多行编辑模式。这个命令让你在刚刚打的断点处添加多个额外要执行的命令。输入如下,使用刚才的 NSString 地址替换下面的内存地址:
代码语言:javascript复制> po $rdx = 0x0000618000644080
> continue
> DONE
回去重新看下你刚刚做的。你在 UILabel 的 setText: 方法上添加了一个断点。一旦遇到该方法,你就会用一个叫 Yay! Debugging! 的 NSString 实例替换 RDX--- 第三个参数。
使用 continue 命令让调试器继续执行:
- (lldb) continue
看看 SpringBoard 模拟器程序什么发生了改变。从下往上扫带出控制中心,观察改变的地方:
尽管这似乎是一个很酷的花招编程技巧,但它却展示了通过有限的汇编和寄存器的知识能够在程序内产生你之前没见过的大的变化。
从调试的角度来看,这也很有用,因为您可以快速直观地验证 -[UILabel setText:] 在 SpringBoard 应用程序中的执行位置,并运行断点条件以查找设置特定 UILabel 文本的确切代码行。
# 寄存器和 SDK
了解寄存器的工作方式以及应用程序的功能可以快速帮助您找到感兴趣的项目。举个简单的例子:通常,我会遇到一个 UIButton,并想知道 IBAction 和接收器,当我点击该按钮时会发生什么。在最高断点处,我可能会发疯…… 认识我自己,我通常在 UIView 或 UIViewController(也许是 UITableViewCell?)中包含 IBAction,并且通常使用某种名称为 “tapped” 的方法。 因此,也许下面的 LLDB 命令会起作用?
- (lldb) rb View(Controller|Cell)?s(?i).*tapped
但是我错误地假设同事 / 其他开发人员正在使用与我相同的命名约定;这个想法行不通。相反,我知道,每当执行 IBAction 方法时,它都必须经过 UIApplication 单例,在该单例中,它将遍历响应者链来找到合适的接收者。为此,将调用 UIControl 的 - sendAction:to:forEvent:方法。 我可以在此方法上设置一个断点,并探索 sendAction:和 to:参数以查找 IBAction 正在执行的代码。 这个想法可以应用到您拥有和没有源代码的应用程序中。我经常发现,即使在我确实有源代码的应用程序中,使用此方法也更快,然后在应用程序中看到数千个 IBAction。
... 但仅出于演示目的,让我们将其应用于 iOS Maps 应用程序。我对右上方按钮的名称和接收者感到好奇,该按钮可以直接定位用户的具体位置。
通过 LLDB 附加到 Maps 应用程序并为 -[UIControl sendAction:to:forEvent:] 设置断点后,很容易找到 UIButton 的名称和接收者。 sendAction:参数(RDX)将使用选择器,而 to:参数将是 IBAction 的接收器(RCX)。
用寄存器知识和轻按 UIButton 查找代码,这有多酷?
# 下一步
好的,学了这么长时间,来休息下,看看你学到了什么:
架构(X86)定义了一个调用规则,该规则规定了函数参数及其返回值的存储位置。
- 在 Objective-C 中,RDI 寄存器是调用 NSObject 的引用,RSI 是选择器,RDX 是第一个参数,依此类推。
- 在 Swift 中,RDI 是第一个参数,RSI 是第二个参数,依此类推,前提是 Swift 方法未使用动态分配。
- RAX 寄存器用于函数中的返回值,无论您使用的是 Objective-C 还是 Swift。
您可以利用寄存器做很多事情。尝试浏览您没有源代码的应用;将为解决棘手的调试问题奠定良好的基础。
尝试附加到 iOS Simulator 上的应用程序,并使用程序集,智能断点和断点命令绘制出 UIViewController 的生命周期。