Resolve rip-relative addresses from otool

March 8 2020 by Jeff Johnson

You can use /usr/bin/otool -tV from the command-line to disassemble Mach-O files. If you don't know what Mach-O's are, they're like Krusty-O's only tastier and healthier. One problem with otool, however, is that it doesn't resolve rip-relative addresses. In the x86_64 (64-bit Intel) architecture, rip is the register that contains the instruction pointer. A rip-relative address is an address specified by an offset, either positive or negative, from the instruction pointer. For example, 0x99eac(%rip) is an rip-relative address in this disassembly:

00000000000016ec	cmpq	$-0x1, 0x99eac(%rip)
00000000000016f4	jne	0x16fe

The instruction pointer actually points at the next instruction, so the rip-relative address resolves to 0x99eac + 0x16f4 = 0x9b5a0. Unfortunately, otool doesn't print this value. It's tedious to manually resolve each rip-relative address, and it's impossible to search the otool output for a specific address if that address is rip-relative. Thus, otool is not as helpful as it could be.

Fortunately, I am as helpful as I could be! I've written a little command-line tool to parse the output from otool and resolve the rip-relative addresses. The source code is below. It's Objective-C, because that's how I roll. Disclaimer: my tool, which I call "riptool", only works with x86_64 files. Some people say that the Intel architecture is dying, but I think that the rumors of its death have been greatly exaggerated. Anyway, here's what the disassembly looks like from riptool:

00000000000016ec	cmpq	$-0x1, 0x99eac(%rip) [0x9b5a0]
00000000000016f4	jne	0x16fe

The source code is not released under any software license, because licenses are for lawyers, and IANAL. This blog post is for programmers. Here's the deal: if I like you, then I won't sue you for using the code. I probably like you, or at least don't dislike you. If you have lawyers, and your lawyers are worried that I dislike you, then contact me for a license. If the price is right, I can learn to like almost anybody.

// Copyright 2020 Jeff Johnson. All rights reserved.

#import <Foundation/Foundation.h>

int main(int argc, const char* argv[]) {
	@autoreleasepool {
		if (argc != 2) {
			printf("Usage: %s <object file>\n", argv[0]);
			return EXIT_FAILURE;
		}
		NSString* path = [NSString stringWithUTF8String:argv[1]];
		if (path == nil) {
			printf("invalid path: %s\n", argv[1]);
			return EXIT_FAILURE;
		}
		
		NSTask* task = [[NSTask alloc] init];
		[task setLaunchPath:@"/usr/bin/otool"];
		[task setArguments:@[@"-tV", path]];
		NSPipe* pipe = [NSPipe pipe];
		[task setStandardOutput:pipe];
		NSFileHandle* fileHandle = [pipe fileHandleForReading];
		NSError* error = nil;
		if (![task launchAndReturnError:&error]) {
			NSLog(@"launch error: %@", error);
			return EXIT_FAILURE;
		}
		NSData* data = [fileHandle readDataToEndOfFile];
		if ([data length] == 0) {
			NSLog(@"no output");
			return EXIT_FAILURE;
		}
		NSString* string = [[NSString alloc] initWithData:data encoding:NSMacOSRomanStringEncoding];
		if (string == nil) {
			NSLog(@"not NSMacOSRomanStringEncoding: %@", data);
			return EXIT_FAILURE;
		}
		
		__block unsigned long long offset = 0;
		__block BOOL isNegative = NO;
		__block NSMutableArray* previousLines = [NSMutableArray array];
		[string enumerateLinesUsingBlock:^(NSString* line, BOOL* stop) {
			if ([previousLines count] > 0) {
				if ([line length] > 16) {
					NSScanner* scanner = [NSScanner scannerWithString:line];
					[scanner setCharactersToBeSkipped:nil];
					unsigned long long address = 0;
					if ([scanner scanHexLongLong:&address] && address > 0) {
						if (isNegative) {
							address -= offset;
							isNegative = NO;
						} else {
							address += offset;
						}
						printf(" [0x%llx]", address);
						for (NSString* previous in previousLines) {
							printf("%s\n", [previous UTF8String]);
						}
						[previousLines removeAllObjects];
						offset = 0;
					} else {
						[previousLines addObject:line];
						return;
					}
				} else {
					[previousLines addObject:line];
					return;
				}
			}
			
			NSRange ripRange = [line rangeOfString:@"(%rip)" options:0];
			NSUInteger ripLocation = ripRange.location;
			if (ripLocation != NSNotFound && ripLocation > (20 + 3)) {
				NSRange hexRange = [line rangeOfString:@"0x" options:NSBackwardsSearch range:NSMakeRange(0, ripLocation)];
				NSUInteger hexLocation = hexRange.location;
				if (hexLocation != NSNotFound && hexLocation > 20) {
					NSString* offsetString = [line substringWithRange:NSMakeRange(hexLocation, ripLocation - hexLocation)];
					NSScanner* scanner = [NSScanner scannerWithString:offsetString];
					[scanner setCharactersToBeSkipped:nil];
					if ([scanner scanHexLongLong:&offset] && offset > 0) {
						if ([[line substringWithRange:NSMakeRange(hexLocation - 1, 1)] isEqualToString:@"-"]) {
							isNegative = YES;
						}
						NSUInteger splitIndex = ripLocation + ripRange.length;
						NSString* prefix = [line substringToIndex:splitIndex];
						NSString* suffix = [line substringFromIndex:splitIndex];
						printf("%s", [prefix UTF8String]);
						[previousLines addObject:suffix];
						return;
					}
				}
			}
			
			printf("%s\n", [line UTF8String]);
		}];
	}
	return EXIT_SUCCESS;
}
Jeff Johnson (My apps, PayPal.Me)