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.
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:
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.
x86 Calling Convention
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:
All arguments are on the stack.
Yep, that's it!
Here's an example of the instructions used for calling the function 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 $reg since that it how you refer to a register in LLDB.
The first thing to notice in the instructions is the use of $esp, the stack pointer register which contains the address of the "top of the stack". In x86 the stack actually starts with smaller address and grows downward (meaning that $esp really keeps track of the bottom of the stack hehe).
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
movsd %xmm1, 16(%esp)
*(%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, and $xmm1 (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, $esp is higher on the stack than $esp+4 and thus even though the first instruction loads into $esp+16, that is the lowest part of the stack used in the 4 mov instructions.
The first two arguments to every Objective-C method are self and _cmd and since the arguments in this case are $esi, $eax, $xmm0, and $xmm1, as explained above, we can deduce that the receiver of the call (self) is in the $esi register, and the selector (_cmd) is in $eax. Then the four floating point arguments must reside in xmm0 and xmm1.
This call above is actually to -[UIView setFrame:], so hopefully the arguments (four floats) make sense.
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) but after the call instruction is executed the receiver is in *($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!).
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".
A function must first move the stack pointer to make enough stack space for all its work. Before moving the stack pointer $esp it need to save its value (so that it can put it back!). In x86 this is done by storing the contents of stack pointer register into the base pointer register ($esp's contents into $ebp). Oh shoot, that would lose the contents of the base pointer! Let's put the base pointers contents on the stack first then.
Remember that arg0 is at $esp before the call and thus at the start of a function its in ($esp+4). We can now construct the standard function preamble:
- Push $ebp onto the stack (arg0 is now in $esp+8 since this moves the stack pointer up yet another 4 bytes).
- Move $esp into $ebp (arg0 is now in $esp+8 and $ebp+8).
- Push (callee save) all registers that will be used in the method onto the stack (to save their contents).
- Allocate enough stack space for all calls and operations used within the method (subtract some constant X from $esp since the stack grows downward).
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).
- Pop all the register that were used (in the reverse order they were pushed).
- Pop the top value of the stack back into $ebp.
Here are the instructions for a real prologue and epilogue pair at the start and end of one example function:
Notice that the two, when taken together, return the world exactly back to how it was when the function began.
Function Arguments in 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.
- Just after executing call the receiver is in $esp+4 (this is true for the first instruction of the function).
- After pushing $ebp the receiver is in $esp+8 (true at the point of the second instruction in the function).
- Once $esp is moved into $ebp the receiver is available in both $esp+8 and $ebp+8 (true at the third instruction).
- After $esp is decremented the receiver is in $ebp+8 (somewhere early in the function bust not necessarily the fourth instruction).
Sounds complicated, doesn't it?
If we look only right at the start of the function then the receiver is in $esp+4, which is simple enough. In the -[UIView setFrame:] example mentioned above the arguments would be as follows at the very start of the method:
- self = *(id *)($esp + 4)
- _cmd = *(SEL *)($esp + 8)
- frame = *(CGRect *)($esp + 12)
The casts are necessary because as far as we are concerned the stack is full of void* which cannot be dereferenced. Also be aware that if there were more arguments the next would be in $esp + 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).
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 ;).
Constructing an NSInvocation from a Stack Frame
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.
In that post you will see a private selector on NSInvocation
[NSInvocation _invocationWithMethodSignature:methodSignature frame: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.
So, first we construct the method signature, which is easy. We already know where self and _cmd are.
[*(id *)($esp +4) methodSignatureForSelector:*(SEL *)($esp + 8)];
and the stack pointer we need to pass is just $esp+4 (that's arg0, the receiver, and the rest are all below it!). That's all we. From there we can use the private method (in the debugger) to construct an 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
You are welcome to read the implementation of pinvocation if you would like to see it in action.
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, after the first 2 instructions).
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.
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").