]>
Commit | Line | Data |
---|---|---|
11fdf7f2 TL |
1 | #!/usr/bin/env python3 |
2 | # | |
3 | # This file is open source software, licensed to you under the terms | |
4 | # of the Apache License, Version 2.0 (the "License"). See the NOTICE file | |
5 | # distributed with this work for additional information regarding copyright | |
6 | # ownership. You may not use this file except in compliance with the License. | |
7 | # | |
8 | # You may obtain a copy of the License at | |
9 | # | |
10 | # http://www.apache.org/licenses/LICENSE-2.0 | |
11 | # | |
12 | # Unless required by applicable law or agreed to in writing, | |
13 | # software distributed under the License is distributed on an | |
14 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY | |
15 | # KIND, either express or implied. See the License for the | |
16 | # specific language governing permissions and limitations | |
17 | # under the License. | |
18 | # | |
19 | # Copyright (C) 2017 ScyllaDB | |
20 | ||
21 | import argparse | |
22 | import collections | |
23 | import re | |
24 | import sys | |
25 | import subprocess | |
26 | ||
27 | class Addr2Line: | |
28 | def __init__(self, binary): | |
29 | self._binary = binary | |
30 | ||
31 | # Print warning if binary has no debug info according to `file`. | |
32 | # Note: no message is printed for system errors as they will be | |
33 | # printed also by addr2line later on. | |
34 | output = subprocess.check_output(["file", self._binary]) | |
35 | s = output.decode("utf-8") | |
36 | if s.find('ELF') >= 0 and s.find('debug_info', len(self._binary)) < 0: | |
37 | print('{}'.format(s)) | |
38 | ||
39 | self._addr2line = subprocess.Popen(["addr2line", "-Cfpia", "-e", self._binary], stdin=subprocess.PIPE, stdout=subprocess.PIPE, universal_newlines=True) | |
40 | ||
41 | # If a library doesn't exist in a particular path, addr2line | |
42 | # will just exit. We need to be robust against that. We | |
43 | # can't just wait on self._addr2line since there is no | |
44 | # guarantee on what timeout is sufficient. | |
45 | self._addr2line.stdin.write('\n') | |
46 | self._addr2line.stdin.flush() | |
47 | res = self._addr2line.stdout.readline() | |
48 | self._missing = res == '' | |
49 | ||
50 | def _read_resolved_address(self): | |
51 | res = self._addr2line.stdout.readline() | |
52 | # remove the address | |
53 | res = res.split(': ', 1)[1] | |
54 | dummy = '0x0000000000000000: ?? ??:0\n' | |
55 | line = '' | |
56 | while line != dummy: | |
57 | res += line | |
58 | line = self._addr2line.stdout.readline() | |
59 | return res | |
60 | ||
61 | def __call__(self, address): | |
62 | if self._missing: | |
63 | return " ".join([self._binary, address, '\n']) | |
64 | # print two lines to force addr2line to output a dummy | |
65 | # line which we can look for in _read_address | |
66 | self._addr2line.stdin.write(address + '\n\n') | |
67 | self._addr2line.stdin.flush() | |
68 | return self._read_resolved_address() | |
69 | ||
70 | class BacktraceResolver(object): | |
9f95a23c | 71 | object_address_re = re.compile('^(.*?)\W(((/[^/]+)+)\+)?(0x[0-9a-f]+)\W*$') |
11fdf7f2 | 72 | |
9f95a23c | 73 | def __init__(self, executable, before_lines, context_re, verbose): |
11fdf7f2 TL |
74 | self._executable = executable |
75 | self._current_backtrace = [] | |
9f95a23c | 76 | self._prefix = None |
11fdf7f2 TL |
77 | self._before_lines = before_lines |
78 | self._before_lines_queue = collections.deque(maxlen=before_lines) | |
79 | self._i = 0 | |
80 | self._known_backtraces = {} | |
9f95a23c TL |
81 | if context_re is not None: |
82 | self._context_re = re.compile(context_re) | |
83 | else: | |
84 | self._context_re = None | |
11fdf7f2 TL |
85 | self._verbose = verbose |
86 | self._known_modules = {self._executable: Addr2Line(self._executable)} | |
87 | ||
88 | def _get_resolver_for_module(self, module): | |
89 | if not module in self._known_modules: | |
90 | self._known_modules[module] = Addr2Line(module) | |
91 | return self._known_modules[module] | |
92 | ||
93 | def __enter__(self): | |
94 | return self | |
95 | ||
96 | def __exit__(self, type, value, tb): | |
97 | self._print_current_backtrace() | |
98 | ||
99 | def _print_resolved_address(self, module, address): | |
100 | resolved_address = self._get_resolver_for_module(module)(address) | |
101 | if self._verbose: | |
102 | resolved_address = '{{{}}} {}: {}'.format(module, address, resolved_address) | |
103 | sys.stdout.write(resolved_address) | |
104 | ||
9f95a23c TL |
105 | def _backtrace_context_matches(self): |
106 | if self._context_re is None: | |
107 | return True | |
108 | ||
109 | if any(map(lambda x: self._context_re.search(x) is not None, self._before_lines_queue)): | |
110 | return True | |
111 | ||
112 | if (not self._prefix is None) and self._context_re.search(self._prefix): | |
113 | return True | |
114 | ||
115 | return False | |
116 | ||
11fdf7f2 TL |
117 | def _print_current_backtrace(self): |
118 | if len(self._current_backtrace) == 0: | |
119 | return | |
120 | ||
9f95a23c TL |
121 | if not self._backtrace_context_matches(): |
122 | self._current_backtrace = [] | |
123 | return | |
124 | ||
11fdf7f2 TL |
125 | for line in self._before_lines_queue: |
126 | sys.stdout.write(line) | |
127 | ||
9f95a23c TL |
128 | if not self._prefix is None: |
129 | print(self._prefix) | |
130 | self._prefix = None | |
131 | ||
11fdf7f2 TL |
132 | backtrace = "".join(map(str, self._current_backtrace)) |
133 | if backtrace in self._known_backtraces: | |
134 | print("[Backtrace #{}] Already seen, not resolving again.".format(self._known_backtraces[backtrace])) | |
135 | print("") # To separate traces with an empty line | |
136 | self._current_backtrace = [] | |
137 | return | |
138 | ||
139 | self._known_backtraces[backtrace] = self._i | |
140 | ||
141 | print("[Backtrace #{}]".format(self._i)) | |
142 | ||
143 | for module, addr in self._current_backtrace: | |
144 | self._print_resolved_address(module, addr) | |
145 | ||
146 | print("") # To separate traces with an empty line | |
147 | ||
148 | self._current_backtrace = [] | |
149 | self._i += 1 | |
150 | ||
151 | def __call__(self, line): | |
152 | match = re.match(self.object_address_re, line) | |
153 | ||
154 | if match: | |
9f95a23c TL |
155 | prefix, _, object_path, _, addr = match.groups() |
156 | ||
157 | if len(self._current_backtrace) == 0: | |
158 | self._prefix = prefix; | |
11fdf7f2 TL |
159 | |
160 | if object_path: | |
161 | self._current_backtrace.append((object_path, addr)) | |
162 | else: | |
163 | self._current_backtrace.append((self._executable, addr)) | |
164 | else: | |
165 | self._print_current_backtrace() | |
166 | if self._before_lines > 0: | |
167 | self._before_lines_queue.append(line) | |
168 | elif self._before_lines < 0: | |
169 | sys.stdout.write(line) # line already has a trailing newline | |
170 | else: | |
171 | pass # when == 0 no non-backtrace lines are printed | |
172 | ||
173 | ||
174 | class StdinBacktraceIterator(object): | |
175 | """ | |
176 | Read stdin char-by-char and stop when when user pressed Ctrl+D or the | |
177 | Enter twice. Altough reading char-by-char is slow this won't be a | |
178 | problem here as backtraces shouldn't be huge. | |
179 | """ | |
180 | def __iter__(self): | |
181 | linefeeds = 0 | |
182 | lines = [] | |
183 | line = [] | |
184 | ||
185 | while True: | |
186 | char = sys.stdin.read(1) | |
187 | ||
188 | if char == '\n': | |
189 | linefeeds += 1 | |
190 | ||
191 | if len(line) > 0: | |
192 | lines.append(''.join(line)) | |
193 | line = [] | |
194 | else: | |
195 | line.append(char) | |
196 | linefeeds = 0 | |
197 | ||
198 | if char == '' or linefeeds > 1: | |
199 | break | |
200 | ||
201 | return iter(lines) | |
202 | ||
203 | ||
204 | description='Massage and pass addresses to the real addr2line for symbol lookup.' | |
205 | epilog=''' | |
206 | There are three operational modes: | |
207 | 1) If -f is specified input will be read from FILE | |
208 | 2) If -f is omitted and there are ADDRESS args they will be read as input | |
209 | 3) If -f is omitted and there are no ADDRESS args input will be read from stdin | |
210 | ''' | |
211 | ||
212 | cmdline_parser = argparse.ArgumentParser( | |
213 | description=description, | |
214 | epilog=epilog, | |
215 | formatter_class=argparse.RawDescriptionHelpFormatter, | |
216 | ) | |
217 | ||
218 | cmdline_parser.add_argument( | |
219 | '-e', | |
220 | '--executable', | |
221 | type=str, | |
222 | required=True, | |
223 | metavar='EXECUTABLE', | |
224 | dest='executable', | |
225 | help='The executable where the addresses originate from') | |
226 | ||
227 | cmdline_parser.add_argument( | |
228 | '-f', | |
229 | '--file', | |
230 | type=str, | |
231 | required=False, | |
232 | metavar='FILE', | |
233 | dest='file', | |
234 | help='The file containing the addresses (one per line)') | |
235 | ||
236 | cmdline_parser.add_argument( | |
237 | '-b', | |
238 | '--before', | |
239 | type=int, | |
240 | metavar='BEFORE', | |
241 | default=1, | |
242 | help='Non-backtrace lines to print before resolved backtraces for context.' | |
9f95a23c TL |
243 | ' Set to 0 to print only resolved backtraces.' |
244 | ' Set to -1 to print all non-backtrace lines. Default is 1.') | |
245 | ||
246 | cmdline_parser.add_argument( | |
247 | '-m', | |
248 | '--match', | |
249 | type=str, | |
250 | metavar='MATCH', | |
251 | help='Only resolve backtraces whose non-backtrace lines match the regular-expression.' | |
252 | ' The amount of non-backtrace lines considered can be set with --before.' | |
253 | ' By default no matching is performed.') | |
11fdf7f2 TL |
254 | |
255 | cmdline_parser.add_argument( | |
256 | '-v', | |
257 | '--verbose', | |
258 | action='store_true', | |
259 | default=False, | |
260 | help='Make resolved backtraces verbose, prepend to each line the module' | |
261 | ' it originates from, as well as the address being resolved') | |
262 | ||
263 | cmdline_parser.add_argument( | |
264 | 'addresses', | |
265 | type=str, | |
266 | metavar='ADDRESS', | |
267 | nargs='*', | |
268 | help='Addresses to parse') | |
269 | ||
270 | args = cmdline_parser.parse_args() | |
271 | ||
272 | if args.addresses and args.file: | |
273 | print("Cannot use both -f and ADDRESS") | |
274 | cmdline_parser.print_help() | |
275 | ||
276 | ||
277 | if args.file: | |
278 | lines = open(args.file, 'r') | |
279 | elif args.addresses: | |
280 | lines = args.addresses | |
281 | else: | |
282 | if sys.stdin.isatty(): | |
283 | lines = StdinBacktraceIterator() | |
284 | else: | |
285 | lines = sys.stdin | |
286 | ||
9f95a23c | 287 | with BacktraceResolver(args.executable, args.before, args.match, args.verbose) as resolve: |
11fdf7f2 TL |
288 | for line in lines: |
289 | resolve(line) |