1#!/usr/bin/python 2# vim:sw=4:ts=4:et: 3# This Source Code Form is subject to the terms of the Mozilla Public 4# License, v. 2.0. If a copy of the MPL was not distributed with this 5# file, You can obtain one at http://mozilla.org/MPL/2.0/. 6 7# This script uses |atos| to post-process the entries produced by 8# NS_FormatCodeAddress(), which on Mac often lack a file name and a line 9# number. 10 11import subprocess 12import sys 13import re 14import os 15import pty 16import termios 17 18class unbufferedLineConverter: 19 """ 20 Wrap a child process that responds to each line of input with one line of 21 output. Uses pty to trick the child into providing unbuffered output. 22 """ 23 def __init__(self, command, args = []): 24 pid, fd = pty.fork() 25 if pid == 0: 26 # We're the child. Transfer control to command. 27 os.execvp(command, [command] + args) 28 else: 29 # Disable echoing. 30 attr = termios.tcgetattr(fd) 31 attr[3] = attr[3] & ~termios.ECHO 32 termios.tcsetattr(fd, termios.TCSANOW, attr) 33 # Set up a file()-like interface to the child process 34 self.r = os.fdopen(fd, "r", 1) 35 self.w = os.fdopen(os.dup(fd), "w", 1) 36 def convert(self, line): 37 self.w.write(line + "\n") 38 return self.r.readline().rstrip("\r\n") 39 @staticmethod 40 def test(): 41 assert unbufferedLineConverter("rev").convert("123") == "321" 42 assert unbufferedLineConverter("cut", ["-c3"]).convert("abcde") == "c" 43 print "Pass" 44 45def separate_debug_file_for(file): 46 return None 47 48address_adjustments = {} 49def address_adjustment(file): 50 if not file in address_adjustments: 51 result = None 52 otool = subprocess.Popen(["otool", "-l", file], stdout=subprocess.PIPE) 53 while True: 54 line = otool.stdout.readline() 55 if line == "": 56 break 57 if line == " segname __TEXT\n": 58 line = otool.stdout.readline() 59 if not line.startswith(" vmaddr "): 60 raise StandardError("unexpected otool output") 61 result = int(line[10:], 16) 62 break 63 otool.stdout.close() 64 65 if result is None: 66 raise StandardError("unexpected otool output") 67 68 address_adjustments[file] = result 69 70 return address_adjustments[file] 71 72atoses = {} 73def addressToSymbol(file, address): 74 converter = None 75 if not file in atoses: 76 debug_file = separate_debug_file_for(file) or file 77 converter = unbufferedLineConverter('/usr/bin/xcrun', ['atos', '-arch', 'x86_64', '-o', debug_file]) 78 atoses[file] = converter 79 else: 80 converter = atoses[file] 81 return converter.convert("0x%X" % address) 82 83cxxfilt_proc = None 84def cxxfilt(sym): 85 if cxxfilt_proc is None: 86 # --no-strip-underscores because atos already stripped the underscore 87 globals()["cxxfilt_proc"] = subprocess.Popen(['c++filt', 88 '--no-strip-underscores', 89 '--format', 'gnu-v3'], 90 stdin=subprocess.PIPE, 91 stdout=subprocess.PIPE) 92 cxxfilt_proc.stdin.write(sym + "\n") 93 return cxxfilt_proc.stdout.readline().rstrip("\n") 94 95# Matches lines produced by NS_FormatCodeAddress(). 96line_re = re.compile("^(.*#\d+: )(.+)\[(.+) \+(0x[0-9A-Fa-f]+)\](.*)$") 97atos_name_re = re.compile("^(.+) \(in ([^)]+)\) \((.+)\)$") 98 99def fixSymbols(line): 100 result = line_re.match(line) 101 if result is not None: 102 (before, fn, file, address, after) = result.groups() 103 address = int(address, 16) 104 105 if os.path.exists(file) and os.path.isfile(file): 106 address += address_adjustment(file) 107 info = addressToSymbol(file, address) 108 109 # atos output seems to have three forms: 110 # address 111 # address (in foo.dylib) 112 # symbol (in foo.dylib) (file:line) 113 name_result = atos_name_re.match(info) 114 if name_result is not None: 115 # Print the first two forms as-is, and transform the third 116 (name, library, fileline) = name_result.groups() 117 # atos demangles, but occasionally it fails. cxxfilt can mop 118 # up the remaining cases(!), which will begin with '_Z'. 119 if (name.startswith("_Z")): 120 name = cxxfilt(name) 121 info = "%s (%s, in %s)" % (name, fileline, library) 122 123 nl = '\n' if line[-1] == '\n' else '' 124 return before + info + after + nl 125 else: 126 sys.stderr.write("Warning: File \"" + file + "\" does not exist.\n") 127 return line 128 else: 129 return line 130 131if __name__ == "__main__": 132 for line in sys.stdin: 133 sys.stdout.write(fixSymbols(line)) 134