I am on M1 (Xcode Version 15.4 (15F31d), MacOS 14.5 (23F79), Simulator IOS 17.4) and as far as I remember printing common usage register was possible. I am not sure why it stopped to work rid, rsi and etc (arg1 arg2 seems like still working).
You have be careful here. While objc_msgSend
is declared as a variadic function, it doesn’t follow variadic calling conventions. Rather, it’s a single function that you can call with any Objective-C method arguments. It only cares about id
and SEL
[1]. This isn’t something you can describe in C.
To see this in action, consider this:
@interface Test : NSObject
@end
@implementation Test
- (void)myMethodArg1:(NSInteger)arg1 arg2:(NSInteger)arg2 {
NSLog(@"%zd", arg1);
NSLog(@"%zd", arg2);
}
@end
static void callTestObj(Test * testObj) {
[testObj myMethodArg1:42 arg2:666];
}
static void callPrintf(void) {
printf("%zd %zd", (NSInteger) 42, (NSInteger) 666);
}
If you disassemble the callTestObj
function you see this:
(lldb) disas -n callTestObj
…
0x100003e24 <+40>: ldr x1, [sp]
0x100003e28 <+44>: ldur x0, [x29, #-0x8]
0x100003e2c <+48>: mov x2, #0x2a ; =42
0x100003e30 <+52>: mov x3, #0x29a ; =666
0x100003e34 <+56>: bl 0x100003f20 ; objc_msgSend$myMethodArg1:arg2:
…
Note how 42 and 666 are passed in registers.
OTOH, the callPrintf
code shows this:
(lldb) disas -n callPrintf
xxot`callPrintf:
…
0x100003e5c <+12>: mov x9, sp
0x100003e60 <+16>: mov x8, #0x2a ; =42
0x100003e64 <+20>: str x8, [x9]
0x100003e68 <+24>: mov x8, #0x29a ; =666
0x100003e6c <+28>: str x8, [x9, #0x8]
0x100003e70 <+32>: adrp x0, 0
0x100003e74 <+36>: add x0, x0, #0xf58 ; "%zd %zd"
0x100003e78 <+40>: bl 0x100003eac ; symbol stub for: printf
…
At +12 you see x9
being set up as a frame pointer and then you see the 42 and 66 stored into that.
This is why, if you call objc_msgSend
directly, you have to cast it to the correct function pointer before calling it. If you try to call it as a variadic function, arguments end up in the wrong place.
Some time arg1, arg2 doesnt work correctly for some frames
One thing to watch out for here is the function prologue. If you just set a normal breakpoint on the function then LLDB sets it after the prologue. It’s easy for that prologue to corrupt those argument registers. So, when debugging at the assembly level, it’s best to set your breakpoint on the first instruction of the function.
Share and Enjoy
—
Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"
[1] And the function result, which can cause problems and is why you end up with the _stret
and _fpret
variants.