Monday, September 7, 2009

Introducing Subjective-C, an objc_msgSend[_[st|fp]ret]? logger.

Time ago I logged calls to objc_msgSend to understand how to construct UIKBKeyboards. But that logger is known to cause problems due to asserting the arguments use less than 1024 bytes. I needed to log calls again for issue 312, but the old buggy behavior leads me to rewriting it more reliably.

The result is the dynamic library called Subjective-C. It has the following new features:
  • Stack-safe. No arguments will be lost due to this logger.
  • Call tree construction.
  • Filtering.

along with the old features:
  • Print and format all arguments, and the return value.


Note:
  • Due to licensing, only the ARM version is released, although the x86 version works perfectly.
  • If your product depends on Subjective-C (why?), please note that it is GPLv3.
  • (No, it won't help even if I BSD everything.)


Sample output




+[UIScroller _registerForNotifications]
+[NSString alloc] {
+[NSString allocWithZone:] (0x0)
}
+[NSBundle mainBundle] {
-[NSRecursiveLock lock] <0x1007540>
-[NSRecursiveLock unlock] <0x1007540>
} = <NSBundle 0x100db50>
-[NSBundle bundleIdentifier] <0x100db50> {
-[NSBundle infoDictionary] <0x100db50> {
-[NSBundle _cfBundle] <0x100db50> = 0x1009d60
} = <NSCFDictionary 0x100af10>
-[NSCFDictionary objectForKey:] <0x100af10> (@"CFBundleIdentifier") = @"com.yourcompany.Untitled4"
} = @"com.yourcompany.Untitled4"
-[NSPlaceholderString initWithFormat:] <0x100cf70> (@"%@.UIKit.migserver") {
-[NSPlaceholderString initWithFormat:locale:arguments:] <0x100cf70> (@"%@.UIKit.migserver", nil, "∞≠") {
-[NSCFString respondsToSelector:] <0x100adb0> (@selector(descriptionWithLocale:)) {
-[NSCFString class] <0x100adb0> = NSCFString
+[NSCFString resolveInstanceMethod:] (@selector(descriptionWithLocale:)) = NO
} = NO
-[NSCFString description] <0x100adb0> = /*self*/ @"com.yourcompany.Untitled4"
} = @"com.yourcompany.Untitled4.UIKit.migserver"
} = @"com.yourcompany.Untitled4.UIKit.migserver"




API



You can find the header file in subjc.h.

Most of the case you just need to call two functions:
  • void SubjC_start(FILE* f, size_t maximum_depth, bool print_arguments, bool print_return_value)
    • Starts logging all objective-C calls. The result will be written to the file f, up to a maximum call-tree depth maximum_depth. You may use the boolean parameters print_arguments and print_return_value to control whether to format the arguments and return values. Not doing so may save some time.
  • void SubjC_end();
    • Stops logging. This function must be called at the same level as SubjC_start.


The SubjC_initialize() function does the initialization things. It will be called automatically in SubjC_start if not done, so you don't need to explicitly call it.

The rest are to control whether a (sub)message can be logged. If a message is filtered, all calls spawned by
this message
will not be logged. For example, if the selector copy is filtered, then in the log you will only see
-[XXSomeClass copy] <0xfedcba98> = <XXSomeClass 0x12345678>
instead of
-[XXSomeClass copy] = {
-[XXSomeClass copyWithZone:] <0xfedcba98> (0x0) = <XXSomeClass 0x12345678>
} = <XXSomeClass 0x12345678>


Known issues



  • Subjective-C is not thread-safe. I repeat: Subjective-C is not thread-safe. If there are ≥1 threads that uses Objective-C during logging you're almost surely doomed to crash. OK it won't crash now by changing the global lr stack to thread-local, but it's still not completely thread-safe.
  • Variadic arguments can never be logged.
  • +initialize messages cannot be logged. This is because Subjective-C cannot work if the class is not initialized, so it implicitly initialize the class before logging.


How it works



All Objective-C calls (except the manually cached ones) will go through objc_msgSend, objc_msgSend_stret, and in x86, objc_msgSend_fpret. Therefore, if we replace these functions with our custom one the every message can be logged.

The tricky part is to call the original function after logging is finished. GCC has __builtin_apply_args and friends to construct the calls, but it assumes you know the maximum bytes of the arguments, because the approach taken by these is to copy the content of stack. This leads to the buggy behavior in the old logger.

To avoid this, the new logger was written in assembly. The approach (and the name) was inspired by saurik's Aspective-C (thanks for informing this work). In the new logger, every action before calling the original function was done without modifying the stack pointer and the content above it, nor the registers.

That means, in ARM, the registers r0-r3 cannot be modified, push/pop cannot be used, and the only free register is r12. Fortunately, heap memory can still be accessed, and well-defined functions will not affect anything except r0-r3, r12 and lr, so the only necessary step to perform is
  • Load the address of an array of 32-bit integers
  • Store r0-r3 in there
  • Call any functions we like
  • Load the address of that array again
  • Restore r0-r3 from there.
  • Call original objc_msgSend
  • Print the returned value and return.

But to return, the link register (lr) need to persist. And it needs to persist throughout the whole replaced function, which in the middle a self-call may happen. This means the lr must be stored on a stack:
  • Load the address of an array of 32-bit integers
  • Store r0-r3 in there
  • Push lr onto a custom stack
  • Call any functions we like
  • Load the address of that array again
  • Restore r0-r3 from there.
  • Call original objc_msgSend
  • Pop lr from the custom stack
  • Print the returned value and return.


The situation is easier on x86. Every argument, and the return address must be on the stack. Therefore the "Store r0-r3" steps can be ignored. Nevertheless, we need to replace the return address to ensure the original objc_msgSend return to our place, so the custom stack is still needed.

Compiling Subjective-C



To compile Subjective-C you need the following:


subjc.mf contains the Makefile for ARM, and subjc.x86 for x86.

2 comments:

  1. That's awesome!

    ReplyDelete
  2. KennyTM, why no news about GriP? Sorry for being such a pain, but i really love it, it´s my fav jb app

    ReplyDelete