objective - 在LLDB中的调用

来源:互联网 发布:windows任务栏不见了 编辑:程序博客网 时间:2024/04/26 14:48

Printing Objective-C Invocations in LLDB
objective - 在LLDB中的调用
March 22, 2015
2015年3月22日
INTRODUCTION
介绍
A while ago Facebook open-sourced Chisel, a collection of commands and functions to assist in debugging iOS apps in LLDB. For a collection of tips and tricks on using LLDB in general (including Chisel tips), read my objc. io article on debugging: “Dancing in the Debugger - A Waltz with LLDB” and if you have some time you should read the entire objc. io edition on debugging. It’s really great.
不久前Facebook对Chisel开放源码,Chisel 是在LLDB中协助调试iOS应用程序的一系列命令和函数的集合。想要全面了解使用LLDB的技巧(包括使用Chisel的技巧),请阅读我关于调试的objc。Io系列文章:“在调试器中灵活舞蹈——一支同LLDB的华尔兹”,如果你有时间的话,你应该阅读objc。Io的整个版本。因为它实在是太棒了。
There is one command that I find particularly fun and interesting yet haven’t had the chance to write about, until this post.
有一个命令,我觉得特别有趣,但直到这篇文章,我才终于有机会写一写它。
The command is pinvocation (for print invocation) which is most useful for debugging Objective-C methods in Apple’s code (or anything where you don’t have access to the source and thus symbols). pinvocation finds the values for self and _cmd, packages them up into an NSInvocation and then prints it all out for you, including the arguments. Here’s an example of it in use:
这个命令就是pinvocation (。。。),这是在Apple的代码中(或者任何你没有访问源和符号)最有用的调试objective - c方法,。Pinvocation可以发现self和_cmd的值,打包成一个NSInvocation然后显示出来给你,包括参数。这里有一个关于其使用的例子:
这里写图片描述

As of this post Chisel only supports pinvocation on 32-bit x86, which means it’s only available on 32-bit Mac OS X and the 32-bit iOS Simulator. There is no reason that the command can’t be implemented for other architectures, but it’d be a bit more involved.
目前为止,Chisel只支持32位x86的 pinvocation,这意味着它只能在32位Mac OS X和32位iOS模拟器使用。这并不是说命令不能在其他体系结构中执行,但它会稍微复杂一些。
X86 CALLING CONVENTION
X86调用协定
The crux of pinvocation is its ability to locate the arguments for the call and of all the architectures that you’ll encounter in the world of iOS (32- and 64-bit x86 and arm) 32-bit x86 has the simplest and easiest calling convention to remember and work with (which is why it’s the only one currently implemented for pinvocation). The calling convention for x86 is pretty simply:
pinvocation的核心功能是它可以记录关于调用的和你在IOS系统()中会遇到的所有架构的参数,32位x86有着最容易记住和使用的调用协定(这就是为什么它是目前唯一能被pinvocation执行的)。x86的调用协定很简单:
All arguments are on the stack.
所有的参数都在堆栈上。
Yep, that’s it!
是的,就是这样!
Here’s an example of the instructions used for calling the function objc_msgSend.
这里有一个例子说明如何调用objc_msgSend函数。
这里写图片描述
This might look a bit intimidating but don’t worry, we’ll go through it.
这可能看起来有点令人生畏,但不要担心,我们会经历的。
Note: the disassembler writes registers as %reg but I am going to write them as regsincethatithowyourefertoaregisterinLLDB.:reg,因为在LLDB中我们是这样定义它的。
The first thing to notice in the instructions is the use of esp,thestackpointerregisterwhichcontainstheaddressofthe"topofthestack".Inx86thestackactuallystartswithsmalleraddressandgrowsdownward(meaningthatesp really keeps track of the bottom of the stack hehe).
在指令中首先要注意的是esp使,x86(esp确实可以记录堆栈的底部)。
The x86 mov* commands are all of the form mov source destination. The first line is moving the contents of the xmm1 register (one of the floating point registers) onto the stack, 16 bytes in. Interpret the line
x86 mov *命令遵循mov源目的地的形式。第一行是将xmm1寄存器(浮点寄存器中的一个)的内容移动并压入堆栈,16字节。将
movsd %xmm1, 16(%esp)
movsd % xmm1,16(% esp)
as
译为
*(%esp + 16) = %xmm1;
*(% esp + 16)= % xmm1;
If you look at this snippet there aren’t any commands that move stack pointer. You might expect to see an argument placed on the stack, the stack pointer moved, another argument placed on the stack, etc. This would be quite wasteful, so instead the stack pointer is actually moved once at the start of a function to create enough room for the maximum stack-space the function uses and then at the end of the function it puts the stack pointer back to where it was at the start of a function (this is called popping a stack frame).
如果你发现这段代码并不是移动堆栈指针的命令,你可能期望会在堆栈找到一组参数。因为每移动一次堆栈指针,就会在堆栈出现一组相应的参数。但这其实很浪费,所以堆栈指针实际上只在开始一个函数时被移动一次,来为函数使用所需的最大堆栈空间创造足够的空间,然后在函数结束时,它把堆栈指针拨回到函数开始的地方(这叫做弹出堆栈帧)。
The snippet above loads four values onto the top of the stack: esi,eax, xmm0,andxmm1 (in reality these are actually two 32-bit integer registers and two 64-bit registers, respectively, loading two pointers and four floats onto the stack) and then executing objc_msgSend via the call instruction. The order of the instructions may at first appear to be in the opposite order than those listed, but since the stack grows downward, espishigheronthestackthanesp+4 and thus even though the first instruction loads into esp+16,thatisthelowestpartofthestackusedinthe4movinstructions.:esi, eax,xmm0, xmm1(,3264,),objcmsgSend,,esp是在堆栈上的位置是高于esp+4,使esp + 16,它也会是在4 mov指令中堆栈的最低部分。
The first two arguments to every Objective-C method are self and _cmd and since the arguments in this case are esi,eax, xmm0,andxmm1, as explained above, we can deduce that the receiver of the call (self) is in the esiregister,andtheselector(cmd)isineax. Then the four floating point arguments must reside in xmm0 and xmm1.
每一个objective – c的前两个参数是self和_cmd,因为这种情况下的参数是esi,eax, xmm0,xmm1,如上所述,我们可以推断出, 调用(self)接收器是在esi(cmd)eax缓存器中。四个浮点参数必须位于xmm0和 xmm1中。
This call above is actually to -[UIView setFrame:], so hopefully the arguments (four floats) make sense.
上面这个调用实际上是——(UIView setFrame:),所以参数(4个漂浮)很可能是有意义的。
The last detail of the x86 calling convention that you need to know is that the call instruction pushes the current instruction pointer onto the stack. So, before the call command is executed, the address of the receiver is in *(esp)butafterthecallinstructionisexecutedthereceiverisin(esp + 4), because the stack grew upward four bytes, enough space to hold the previous instruction pointer (remember, we are on a 32-bit architecture!).
你需要知道的x86调用协定的最后细节是,调用指令将当前指令指针压入堆栈。因此,在执行调用命令之前,接收者的地址是in *(esp),in(esp + 4),因为堆栈向上增长四个字节,有足够的空间来存放先前指令指针(记住,我们是在一个32位架构!)。
X86 PREAMBLE
X86序言
In x86 objc_msgSend is always called using the callee save convention. This means that if inside a function you (the callee) want to use a register that you must save its contents off to the side and then when you are done using it you must put the contents back the way you found it. This is often done by pushing the value onto the stack, using the desired register, and the popping the value off the stack and back into the register when you are done. All Objective-C methods in x86 are compiled as callee-save and thus they all start the exact same way, with the same “preamble”.
在x86种, objc_msgSend总是使用拯救公约来调用。这意味着在一个函数中,如果你想要使用一个缓存器,你必须将它的内容保存到一边;当你使用完毕时,你必须把内容恢复到你发现它时的样子。这通常通过将值放入堆栈中,使用所需的缓存器, 当你完成时,从堆栈中取出值再输回到缓存器中。所有x86中的objective – c都被编译为callee-save,因此他们都开始于完全相同的方式,有着相同的“序言”。
A function must first move the stack pointer to make enough stack space for all its work. Before moving the stack pointer espitneedtosaveitsvalue(sothatitcanputitback!).Inx86thisisdonebystoringthecontentsofstackpointerregisterintothebasepointerregister(esp’s contents into ebp).Ohshoot,thatwouldlosethecontentsofthebasepointer!Letsputthebasepointerscontentsonthestackfirstthen.esp需要保存它的数值(这样才可以把它放回去!)。在x86中,这是通过将堆栈指针寄存器的内容存储到基指针寄存器(espebp)来实现的。哦,那将失去基指针的内容!那让我们先把基指针的内容放到堆栈上吧。
Remember that arg0 is at espbeforethecallandthusatthestartofafunctionitsin(esp+4). We can now construct the standard function preamble:
记住,在调用之前,arg0 是在espesp + 4上。我们现在可以构建标准的函数序言了:
Push ebpontothestack(arg0isnowinesp+8 since this moves the stack pointer up yet another 4 bytes).
ebp(arg0esp + 8上,因为这会让移动堆栈指针向上移动4字节)。
Move espintoebp (arg0 is now in esp+8andebp+8).
espebp上(arg0现在在esp+8ebp + 8上)。
Push (callee save) all registers that will be used in the method onto the stack (to save their contents).
(根据callee save)将所有会用到的寄存器压入堆栈 (以保留他们的内容)。
Allocate enough stack space for all calls and operations used within the method (subtract some constant X from espsincethestackgrowsdownward).(esp中减去某个常数X,因为堆栈向下增加记录)。
When a function completes it must then teardown. The epilogue performs the inverse set of actions in the opposite order:
当一个函数完成后,它必须被拆解。收尾程序执行的是顺序相反的一系列相反动作:
Unwind the stack (add X back to esp).(Xesp)。
Pop all the register that were used (in the reverse order they were pushed).
对所有用到的寄存器使用POP 指令(按照与他们被压入堆栈时相反的顺序)。
Pop the top value of the stack back into ebp.POPebp。
Here are the instructions for a real prologue and epilogue pair at the start and end of one example function:
下面是以某一函数为例的一组序言和收尾的指令,分别在函数的开始和结尾:
这里写图片描述
THE PROLOGUE.
序言。
这里写图片描述
THE EPILOGUE.
后记。
Notice that the two, when taken together, return the world exactly back to how it was when the function began.
注意到当这两者连在一起时,会完全返回到函数时开始时的样子。
FUNCTION ARGUMENTS IN LLDB
函数参数在LLDB上
All of the above informations tells us the following information:
上面所有的信息告诉我们以下信息:
Just before executing call (before the function is called) the receiver is in esp.()espJustafterexecutingcallthereceiverisinesp+4 (this is true for the first instruction of the function).
执行调用后,接收器在esp+4()Afterpushingebp the receiver is in esp+8(trueatthepointofthesecondinstructioninthefunction).ebp压入堆栈之后,接收器在esp + 8上(真正的第二个指令的功能)。
Once espismovedintoebp the receiver is available in both esp+8andebp+8 (true at the third instruction).
一旦espebp,接收器在esp+8ebp + 8上(真在第三个指令)。
After espisdecrementedthereceiverisinebp+8 (somewhere early in the function bust not necessarily the fourth instruction).
espebp + 8上(某地在函数危机爆发之初就不一定是第四个指令)。
Sounds complicated, doesn’t it?
听起来很复杂,不是吗?
If we look only right at the start of the function then the receiver is in esp+4,whichissimpleenough.Inthe[UIViewsetFrame:]examplementionedabovetheargumentswouldbeasfollowsattheverystartofthemethod:esp + 4上,这很简单。在上面提到过的[UIView setFrame:]中,参数在开始时会如下面所示:
self = (id )($esp + 4)
_cmd = (SEL )($esp + 8)
frame = (CGRect )(esp+12)Thecastsarenecessarybecauseasfarasweareconcernedthestackisfullofvoidwhichcannotbedereferenced.Alsobeawarethatifthereweremoreargumentsthenextwouldbeinesp + 28 since frame is 16-bytes worth of data (its a CGRect, which is a CGPoint and a CGSize, which contain four 32-bit floats in total).
计算过程是必要的,因为我们知道如果堆栈充满void *,它是不能引用的。也请注意,如果有更多的参数,下一个会在$esp + 28上,因为框架是16字节数据宽度的(它是一个CGRect,也就是一个CGPoint和 一个CGSize,总共包含4个32位浮点数)。
Here’s an example of examining the arguments of a -setFrame: call, with execution paused at the first instruction of a method (which is exactly where a symbolic breakpoint would take you ;).
这里有一个检查setFrame参数的例子,调用,然后使执行停在第一个指令 (这正是一个象征性的断点会带你去的地方;)。
这里写图片描述
FINDING ALL THE ARGUMENTS IN AN OBJECTIVE-C METHOD INVOCATION.
在一个objective - c调用中,找到所有的参数。
NOTICE THAT IN LLDB YOU USE A DOLLAR SIGN TO REFERENCE A REGISTER INSTEAD OF A PERCENT SIGN. :-P
注意, 在LLDB中,使用美元符号而不是百分号的标志来标记一个寄存器。:- p
CONSTRUCTING AN NSINVOCATION FROM A STACK FRAME
构建一个NSINVOCATION堆栈帧
Now that we know where all the arguments are in x86 we can build an NSInvocation from them. Why would we want an NSInvocation? Because it contains all the logic for getting grabbing arguments from registers and the stack and packaging them up nicely for us (which is used for forwarding)! If you want more information on how and why it does that then take a dive down into Core Foundation’s forwarding path.
现在,我们知道所有的参数都在x86的哪个位置,我们可以用它们构建一个NSInvocation。为什么我们要构建NSInvocation呢?因为它包含了从寄存器和堆栈获得抓取参数到将其打包很好地呈现给我们(这可以用于转发!)的所有的逻辑。如果你想要了解关于如何以及为什么它能够实现的更多信息,请到核心基础的转发路径中深入研究。
In that post you will see a private selector on NSInvocation
在帖中,你将看到一个私人NSInvocation选择器
[NSInvocation _invocationWithMethodSignature:methodSignature
[NSInvocation _invocationWithMethodSignature:methodSignature
frame:frameStackPointer];
框架:frameStackPointer];
which is used to build up the invocation that is passed to forwardInvocation:. So let’s use that method! All we need is a method signature and the address on the stack where all the arguments reside.
它用于建立传递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)];
and the stack pointer we need to pass is just esp+4(thatsarg0,thereceiver,andtherestareallbelowit!).Thatsallwe.Fromtherewecanusetheprivatemethod(inthedebugger)toconstructanNSInvocation!esp + 4 (也就是arg0,是接收器,其余的都要低于它!)。这就是我们所要做的。从那里我们可以使用私人方法(在调试器)来构造一个NSInvocation !
PUTTING IT ALL TOGETHER
把它放在一起
From here there is nothing left except to write the logic in Python and load it into LLDB (and have a party afterwards). With Chisel this is really easy. Subclass FBCommand, implement name() to return “pinvocation” and then implement run(arguments, options) to do all of what we described above. Chisel even makes parsing out the arguments and options easy too! :D
至此,只剩下编写Python逻辑,然后把它加载到LLDB(之后你可以放松聚会)。有了Chisel, 这真的很简单。Subclass FBCommand, 运行 name() 以返回到 “pinvocation” ,然后运行 run(arguments, options)来做所有我们上面描述的所有操作。Chisel使得简化分析的参数和选项变得简单!:D
You are welcome to read the implementation of pinvocation if you would like to see it in action.
如果你了解它的实际应用。欢迎您阅读的《实现pinvocation》
OTHER ARCHITECTURES
其他体系结构
The 32-bit x86 architecture pushes ALL arguments onto the stack making it really easy to find all arguments at any point in time during a method (esp+8,afterthefirst2instructions).32x86使(esp + 8,在第一次2指令之后)。
On arm (32- and 64-bit) and x84-64 the arguments are passed in registers according to some complex-ish rules. Since they are in registers they may be moved around during the implementation of a method, making is basically impossible to find them with any amount of certainty. However, at the very start of a method all arguments are available (according to the calling convention of the architecture) and pinvocation could be made to work there. You’d likely want to take even more advantage of the forwarding path (which pushes all arguments onto the stack on other architectures) so that you can still use +[NSInvocation _invocationWithMethodSignature:frame:] which expects an address on the stack that contains all arguments.
在arm(32位和64位)和x84 – 64中, 根据一些complex-ish规则,参数在寄存器中传递。因为它们是在寄存器中,他们可能会在一个方法的实现中四处移动,这基本上使得我们无法确定它们能否被找到。然而,方法一开始时,所有参数方法都是可用的(根据架构的调用协定),并且pinvocation可以运行。您可能想要更多地利用转发路径(它可以在其他体系结构上将所有参数压入堆栈),所以你仍然可以使用+[NSInvocation _invocationWithMethodSignature:框架:]它需要一个包含所有参数的堆栈上的地址
If you don’t believe me, here’s the forwarding path on x86-64 pushing all arguments onto the stack before calling into forwarding (so that it can package up an NSInvocation in the event that it needs to enter the forwardInvocation: branch, also known as the “slow path”).
如果你不相信我,这是x86 – 64的转发路径,它在调用forwarding之前将所有参数压入堆栈 (这样就可以在需要时打包一个NSInvocation,如此来输入forwardInvocation:分支,也被称为“慢路径”)。
这里写图片描述
CORE FOUNDATION’S FORWARDING HANDLER PUSHING EVERY REGISTER ONTO THE STACK BEFORE CALLING INTO FORWARDING.
核心基础的转发处理程序将在调用FORWARDING之前把每个寄存器压入堆栈。

0 0