|
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 process the output of nsTraceRefcnt's Mac OS |
|
8 # X stack walking code. This is useful for two things: |
|
9 # (1) Getting line number information out of |
|
10 # |nsTraceRefcnt::WalkTheStack|'s output in debug builds. |
|
11 # (2) Getting function names out of |nsTraceRefcnt::WalkTheStack|'s |
|
12 # output on all builds (where it mostly prints UNKNOWN because only |
|
13 # a handful of symbols are exported from component libraries). |
|
14 # |
|
15 # Use the script by piping output containing stacks (such as raw stacks |
|
16 # or make-tree.pl balance trees) through this script. |
|
17 |
|
18 import subprocess |
|
19 import sys |
|
20 import re |
|
21 import os |
|
22 import pty |
|
23 import termios |
|
24 |
|
25 class unbufferedLineConverter: |
|
26 """ |
|
27 Wrap a child process that responds to each line of input with one line of |
|
28 output. Uses pty to trick the child into providing unbuffered output. |
|
29 """ |
|
30 def __init__(self, command, args = []): |
|
31 pid, fd = pty.fork() |
|
32 if pid == 0: |
|
33 # We're the child. Transfer control to command. |
|
34 os.execvp(command, [command] + args) |
|
35 else: |
|
36 # Disable echoing. |
|
37 attr = termios.tcgetattr(fd) |
|
38 attr[3] = attr[3] & ~termios.ECHO |
|
39 termios.tcsetattr(fd, termios.TCSANOW, attr) |
|
40 # Set up a file()-like interface to the child process |
|
41 self.r = os.fdopen(fd, "r", 1) |
|
42 self.w = os.fdopen(os.dup(fd), "w", 1) |
|
43 def convert(self, line): |
|
44 self.w.write(line + "\n") |
|
45 return self.r.readline().rstrip("\r\n") |
|
46 @staticmethod |
|
47 def test(): |
|
48 assert unbufferedLineConverter("rev").convert("123") == "321" |
|
49 assert unbufferedLineConverter("cut", ["-c3"]).convert("abcde") == "c" |
|
50 print "Pass" |
|
51 |
|
52 def separate_debug_file_for(file): |
|
53 return None |
|
54 |
|
55 address_adjustments = {} |
|
56 def address_adjustment(file): |
|
57 if not file in address_adjustments: |
|
58 result = None |
|
59 otool = subprocess.Popen(["otool", "-l", file], stdout=subprocess.PIPE) |
|
60 while True: |
|
61 line = otool.stdout.readline() |
|
62 if line == "": |
|
63 break |
|
64 if line == " segname __TEXT\n": |
|
65 line = otool.stdout.readline() |
|
66 if not line.startswith(" vmaddr "): |
|
67 raise StandardError("unexpected otool output") |
|
68 result = int(line[10:], 16) |
|
69 break |
|
70 otool.stdout.close() |
|
71 |
|
72 if result is None: |
|
73 raise StandardError("unexpected otool output") |
|
74 |
|
75 address_adjustments[file] = result |
|
76 |
|
77 return address_adjustments[file] |
|
78 |
|
79 atoses = {} |
|
80 def addressToSymbol(file, address): |
|
81 converter = None |
|
82 if not file in atoses: |
|
83 debug_file = separate_debug_file_for(file) or file |
|
84 converter = unbufferedLineConverter('/usr/bin/xcrun', ['atos', '-arch', 'x86_64', '-o', debug_file]) |
|
85 atoses[file] = converter |
|
86 else: |
|
87 converter = atoses[file] |
|
88 return converter.convert("0x%X" % address) |
|
89 |
|
90 cxxfilt_proc = None |
|
91 def cxxfilt(sym): |
|
92 if cxxfilt_proc is None: |
|
93 # --no-strip-underscores because atos already stripped the underscore |
|
94 globals()["cxxfilt_proc"] = subprocess.Popen(['c++filt', |
|
95 '--no-strip-underscores', |
|
96 '--format', 'gnu-v3'], |
|
97 stdin=subprocess.PIPE, |
|
98 stdout=subprocess.PIPE) |
|
99 cxxfilt_proc.stdin.write(sym + "\n") |
|
100 return cxxfilt_proc.stdout.readline().rstrip("\n") |
|
101 |
|
102 line_re = re.compile("^(.*) ?\[([^ ]*) \+(0x[0-9a-fA-F]{1,8})\](.*)$") |
|
103 balance_tree_re = re.compile("^([ \|0-9-]*)") |
|
104 atos_name_re = re.compile("^(.+) \(in ([^)]+)\) \((.+)\)$") |
|
105 |
|
106 def fixSymbols(line): |
|
107 result = line_re.match(line) |
|
108 if result is not None: |
|
109 # before allows preservation of balance trees |
|
110 # after allows preservation of counts |
|
111 (before, file, address, after) = result.groups() |
|
112 address = int(address, 16) |
|
113 |
|
114 if os.path.exists(file) and os.path.isfile(file): |
|
115 address += address_adjustment(file) |
|
116 info = addressToSymbol(file, address) |
|
117 |
|
118 # atos output seems to have three forms: |
|
119 # address |
|
120 # address (in foo.dylib) |
|
121 # symbol (in foo.dylib) (file:line) |
|
122 name_result = atos_name_re.match(info) |
|
123 if name_result is not None: |
|
124 # Print the first two forms as-is, and transform the third |
|
125 (name, library, fileline) = name_result.groups() |
|
126 # atos demangles, but occasionally it fails. cxxfilt can mop |
|
127 # up the remaining cases(!), which will begin with '_Z'. |
|
128 if (name.startswith("_Z")): |
|
129 name = cxxfilt(name) |
|
130 info = "%s (%s, in %s)" % (name, fileline, library) |
|
131 |
|
132 # throw away the bad symbol, but keep balance tree structure |
|
133 before = balance_tree_re.match(before).groups()[0] |
|
134 |
|
135 return before + info + after + "\n" |
|
136 else: |
|
137 sys.stderr.write("Warning: File \"" + file + "\" does not exist.\n") |
|
138 return line |
|
139 else: |
|
140 return line |
|
141 |
|
142 if __name__ == "__main__": |
|
143 for line in sys.stdin: |
|
144 sys.stdout.write(fixSymbols(line)) |