Skip to content

Latest commit

 

History

History
156 lines (96 loc) · 11.6 KB

1.md

File metadata and controls

156 lines (96 loc) · 11.6 KB

##objective - 在LLDB中的调用

不久前Facebook对Chisel开放源码,Chisel 是在LLDB中协助调试iOS应用程序的一系列命令和函数的集合。想要全面了解使用LLDB的技巧(包括使用Chisel的技巧),请阅读我关于调试的objc。Io系列文章:“在调试器中灵活舞蹈——一支同LLDB的华尔兹”,如果你有时间的话,你应该阅读objc。Io的整个版本。因为它实在是太棒了。

有一个命令,我觉得特别有趣,但直到这篇文章,我才终于有机会写一写它。

这个命令就是pinvocation (。。。),这是在Apple的代码中(或者任何你没有访问源和符号)最有用的调试objective - c方法,。Pinvocation可以发现self和_cmd的值,打包成一个NSInvocation然后显示出来给你,包括参数。这里有一个关于其使用的例子: 这里写图片描述

目前为止,Chisel只支持32位x86的 pinvocation,这意味着它只能在32位Mac OS X和32位iOS模拟器使用。这并不是说命令不能在其他体系结构中执行,但它会稍微复杂一些。 X86 CALLING CONVENTION X86调用协定

pinvocation的核心功能是它可以记录关于调用的和你在IOS系统()中会遇到的所有架构的参数,32位x86有着最容易记住和使用的调用协定(这就是为什么它是目前唯一能被pinvocation执行的)。x86的调用协定很简单:

所有的参数都在堆栈上。

是的,就是这样!

这里有一个例子说明如何调用objc_msgSend函数。 这里写图片描述

这可能看起来有点令人生畏,但不要担心,我们会经历的。

注意:反汇编程序会把寄存器写成% reg,但我要把它写成$reg,因为在LLDB中我们是这样定义它的。

在指令中首先要注意的是$esp的使用,也就是包含堆栈开头地址的堆栈指针寄存器。在x86中,堆栈实际上开始于小地址,向下增加记录(这意味着$esp确实可以记录堆栈的底部)。

x86 mov *命令遵循mov源目的地的形式。第一行是将xmm1寄存器(浮点寄存器中的一个)的内容移动并压入堆栈,16字节。将 movsd %xmm1, 16(%esp) movsd % xmm1,16(% esp) as 译为

  • (%esp + 16) = %xmm1;
  • (% esp + 16)= % xmm1;

如果你发现这段代码并不是移动堆栈指针的命令,你可能期望会在堆栈找到一组参数。因为每移动一次堆栈指针,就会在堆栈出现一组相应的参数。但这其实很浪费,所以堆栈指针实际上只在开始一个函数时被移动一次,来为函数使用所需的最大堆栈空间创造足够的空间,然后在函数结束时,它把堆栈指针拨回到函数开始的地方(这叫做弹出堆栈帧)。

上面的代码片段将四个值加载到堆栈的顶部: $esi, $eax, $xmm0, $xmm1(在现实中,这些实际上是两个32位整数寄存器和两个64位的寄存器,分别将两个指针和四个浮动压入堆栈),然后通过调用执行objc_msgSend指令。指令的顺序可能起初似乎与已显示部分顺序相反,但由于堆栈向下记录数据, $esp是在堆栈上的位置是高于$esp + 4的,因此即使第一个指令加载到$esp + 16,它也会是在4 mov指令中堆栈的最低部分。

每一个objective – c的前两个参数是self和_cmd,因为这种情况下的参数是$esi, $eax, $xmm0, $xmm1,如上所述,我们可以推断出, 调用(self)接收器是在$esi缓存器中,选择器(_cmd)在$eax缓存器中。四个浮点参数必须位于xmm0和 xmm1中。

上面这个调用实际上是——(UIView setFrame:),所以参数(4个漂浮)很可能是有意义的。

你需要知道的x86调用协定的最后细节是,调用指令将当前指令指针压入堆栈。因此,在执行调用命令之前,接收者的地址是in *($esp),但是在执行调用指令后,地址就成为 in *($esp + 4),因为堆栈向上增长四个字节,有足够的空间来存放先前指令指针(记住,我们是在一个32位架构!)。 X86 PREAMBLE X86序言

在x86种, objc_msgSend总是使用拯救公约来调用。这意味着在一个函数中,如果你想要使用一个缓存器,你必须将它的内容保存到一边;当你使用完毕时,你必须把内容恢复到你发现它时的样子。这通常通过将值放入堆栈中,使用所需的缓存器, 当你完成时,从堆栈中取出值再输回到缓存器中。所有x86中的objective – c都被编译为callee-save,因此他们都开始于完全相同的方式,有着相同的“序言”。

一个函数必须先移动堆栈指针为其所有工作腾出足够的堆栈空间。在移动堆栈指针之前,$esp需要保存它的数值(这样才可以把它放回去!)。在x86中,这是通过将堆栈指针寄存器的内容存储到基指针寄存器($esp的内容到$ebp)来实现的。哦,那将失去基指针的内容!那让我们先把基指针的内容放到堆栈上吧。

记住,在调用之前,arg0 是在$esp上的,因此在一个函数开始时,它在$esp + 4上。我们现在可以构建标准的函数序言了:

把$ebp压入堆栈(arg0现在$esp + 8上,因为这会让移动堆栈指针向上移动4字节)。 Move $esp into $ebp (arg0 is now in $esp+8 and $ebp+8). 将$esp移动到$ebp上(arg0现在在$esp + 8 和$ebp + 8上)。

(根据callee save)将所有会用到的寄存器压入堆栈 (以保留他们的内容)。

为可能用到的所有调用和操作分配足够的堆栈空间(从$esp中减去某个常数X,因为堆栈向下增加记录)。

当一个函数完成后,它必须被拆解。收尾程序执行的是顺序相反的一系列相反动作:

平仓栈(将X加回到$esp)。

对所有用到的寄存器使用POP 指令(按照与他们被压入堆栈时相反的顺序)。

将堆栈的开头数值POP回到$ebp。

下面是以某一函数为例的一组序言和收尾的指令,分别在函数的开始和结尾: 这里写图片描述 THE PROLOGUE. 序言。 这里写图片描述

##后记

注意到当这两者连在一起时,会完全返回到函数时开始时的样子。

函数参数在LLDB上

上面所有的信息告诉我们以下信息:

执行调用之前(在函数被调用之前)接收器在esp美元。

执行调用后,接收器在$esp + 4上(这对于函数的第一个指令来说是没有问题的)。

在将$ebp压入堆栈之后,接收器在esp + 8上(真正的第二个指令的功能)。

一旦$esp 被移到$ebp,接收器在$esp + 8和$ebp + 8上(真在第三个指令)。

在$esp被递减后,接收器在$ebp + 8上(某地在函数危机爆发之初就不一定是第四个指令)。

听起来很复杂,不是吗?

如果我们关注函数开始时,接收器是在$esp + 4上,这很简单。在上面提到过的[UIView setFrame:]中,参数在开始时会如下面所示: self = *(id *)($esp + 4) _cmd = *(SEL *)($esp + 8) frame = *(CGRect *)($esp + 12)

计算过程是必要的,因为我们知道如果堆栈充满void *,它是不能引用的。也请注意,如果有更多的参数,下一个会在$esp + 28上,因为框架是16字节数据宽度的(它是一个CGRect,也就是一个CGPoint和 一个CGSize,总共包含4个32位浮点数)。

这里有一个检查setFrame参数的例子,调用,然后使执行停在第一个指令 (这正是一个象征性的断点会带你去的地方;)。 这里写图片描述

在一个objective - c调用中,找到所有的参数。

注意, 在LLDB中,使用美元符号而不是百分号的标志来标记一个寄存器。:- p

构建一个NSINVOCATION堆栈帧

现在,我们知道所有的参数都在x86的哪个位置,我们可以用它们构建一个NSInvocation。为什么我们要构建NSInvocation呢?因为它包含了从寄存器和堆栈获得抓取参数到将其打包很好地呈现给我们(这可以用于转发!)的所有的逻辑。如果你想要了解关于如何以及为什么它能够实现的更多信息,请到核心基础的转发路径中深入研究。

在帖中,你将看到一个私人NSInvocation选择器

[NSInvocation _invocationWithMethodSignature:methodSignature [NSInvocation _invocationWithMethodSignature:methodSignature frame:frameStackPointer]; 框架:frameStackPointer];

它用于建立传递forwardInvocation的调用。让我们用这个方法!我们需要的是一个方法签名和存储有所有参数的堆栈上的地址。 So, first we construct the method signature, which is easy. We already know where self and _cmd are. 所以,我们首先构造方法签名,这很容易。我们已经知道self和_cmd在哪里。

[*(id )($esp +4) methodSignatureForSelector:(SEL *)($esp + 8)];

我们需要传递的堆栈指针是$esp + 4 (也就是arg0,是接收器,其余的都要低于它!)。这就是我们所要做的。从那里我们可以使用私人方法(在调试器)来构造一个NSInvocation ! PUTTING IT ALL TOGETHER 把它放在一起

至此,只剩下编写Python逻辑,然后把它加载到LLDB(之后你可以放松聚会)。有了Chisel, 这真的很简单。Subclass FBCommand, 运行 name() 以返回到 "pinvocation" ,然后运行 run(arguments, options)来做所有我们上面描述的所有操作。Chisel使得简化分析的参数和选项变得简单!:D

如果你了解它的实际应用。欢迎您阅读的《实现pinvocation》 OTHER ARCHITECTURES 其他体系结构

32位x86体系结构将所有参数压入堆栈,使它很容易及时找到所有参数 ($esp + 8,在第一次2指令之后)。

在arm(32位和64位)和x84 – 64中, 根据一些complex-ish规则,参数在寄存器中传递。因为它们是在寄存器中,他们可能会在一个方法的实现中四处移动,这基本上使得我们无法确定它们能否被找到。然而,方法一开始时,所有参数方法都是可用的(根据架构的调用协定),并且pinvocation可以运行。您可能想要更多地利用转发路径(它可以在其他体系结构上将所有参数压入堆栈),所以你仍然可以使用+[NSInvocation _invocationWithMethodSignature:框架:]它需要一个包含所有参数的堆栈上的地址

如果你不相信我,这是x86 – 64的转发路径,它在调用__forwarding__之前将所有参数压入堆栈 (这样就可以在需要时打包一个NSInvocation,如此来输入forwardInvocation:分支,也被称为“慢路径”)。 这里写图片描述 CORE FOUNDATION'S FORWARDING HANDLER PUSHING EVERY REGISTER ONTO THE STACK BEFORE CALLING INTO FORWARDING. 核心基础的转发处理程序将在调用__FORWARDING__之前把每个寄存器压入堆栈。