Switch global error menu icon to vectorized MD asset
[chromium-blink-merge.git] / third_party / android_platform / development / scripts / symbol.py
blobb72253d63134446653e9db89dc97ddd6a8518394
1 #!/usr/bin/python
3 # Copyright (C) 2013 The Android Open Source Project
5 # Licensed under the Apache License, Version 2.0 (the "License");
6 # you may not use this file except in compliance with the License.
7 # You may obtain a copy of the License at
9 # http://www.apache.org/licenses/LICENSE-2.0
11 # Unless required by applicable law or agreed to in writing, software
12 # distributed under the License is distributed on an "AS IS" BASIS,
13 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 # See the License for the specific language governing permissions and
15 # limitations under the License.
17 """Module for looking up symbolic debugging information.
19 The information can include symbol names, offsets, and source locations.
20 """
22 import glob
23 import itertools
24 import logging
25 import os
26 import re
27 import struct
28 import subprocess
29 import sys
30 import zipfile
32 CHROME_SRC = os.path.join(os.path.realpath(os.path.dirname(__file__)),
33 os.pardir, os.pardir, os.pardir, os.pardir)
34 ANDROID_BUILD_TOP = CHROME_SRC
35 SYMBOLS_DIR = CHROME_SRC
36 CHROME_SYMBOLS_DIR = CHROME_SRC
38 ARCH = "arm"
40 TOOLCHAIN_INFO = None
42 sys.path.insert(0, os.path.join(CHROME_SRC, 'build', 'android'))
43 from pylib.symbols import elf_symbolizer
45 # See:
46 # http://bugs.python.org/issue14315
47 # https://hg.python.org/cpython/rev/6dd5e9556a60#l2.8
48 def PatchZipFile():
49 oldDecodeExtra = zipfile.ZipInfo._decodeExtra
50 def decodeExtra(self):
51 try:
52 oldDecodeExtra(self)
53 except struct.error:
54 pass
55 zipfile.ZipInfo._decodeExtra = decodeExtra
56 PatchZipFile()
58 def Uname():
59 """'uname' for constructing prebuilt/<...> and out/host/<...> paths."""
60 uname = os.uname()[0]
61 if uname == "Darwin":
62 proc = os.uname()[-1]
63 if proc == "i386" or proc == "x86_64":
64 return "darwin-x86"
65 return "darwin-ppc"
66 if uname == "Linux":
67 return "linux-x86"
68 return uname
70 def ToolPath(tool, toolchain_info=None):
71 """Return a full qualified path to the specified tool"""
72 # ToolPath looks for the tools in the completely incorrect directory.
73 # This looks in the checked in android_tools.
74 if ARCH == "arm":
75 toolchain_source = "arm-linux-androideabi-4.9"
76 toolchain_prefix = "arm-linux-androideabi"
77 ndk = "ndk"
78 elif ARCH == "arm64":
79 toolchain_source = "aarch64-linux-android-4.9"
80 toolchain_prefix = "aarch64-linux-android"
81 ndk = "ndk"
82 elif ARCH == "x86":
83 toolchain_source = "x86-4.9"
84 toolchain_prefix = "i686-linux-android"
85 ndk = "ndk"
86 elif ARCH == "x86_64" or ARCH == "x64":
87 toolchain_source = "x86_64-4.9"
88 toolchain_prefix = "x86_64-linux-android"
89 ndk = "ndk"
90 elif ARCH == "mips":
91 toolchain_source = "mipsel-linux-android-4.9"
92 toolchain_prefix = "mipsel-linux-android"
93 ndk = "ndk"
94 else:
95 raise Exception("Could not find tool chain")
97 toolchain_subdir = (
98 "third_party/android_tools/%s/toolchains/%s/prebuilt/linux-x86_64/bin" %
99 (ndk, toolchain_source))
101 return os.path.join(CHROME_SRC,
102 toolchain_subdir,
103 toolchain_prefix + "-" + tool)
105 def FindToolchain():
106 """Look for the latest available toolchain
108 Args:
109 None
111 Returns:
112 A pair of strings containing toolchain label and target prefix.
114 global TOOLCHAIN_INFO
115 if TOOLCHAIN_INFO is not None:
116 return TOOLCHAIN_INFO
118 ## Known toolchains, newer ones in the front.
119 gcc_version = "4.9"
120 if ARCH == "arm64":
121 known_toolchains = [
122 ("aarch64-linux-android-" + gcc_version, "aarch64", "aarch64-linux-android")
124 elif ARCH == "arm":
125 known_toolchains = [
126 ("arm-linux-androideabi-" + gcc_version, "arm", "arm-linux-androideabi")
128 elif ARCH =="x86":
129 known_toolchains = [
130 ("x86-" + gcc_version, "x86", "i686-linux-android")
132 elif ARCH =="x86_64" or ARCH =="x64":
133 known_toolchains = [
134 ("x86_64-" + gcc_version, "x86_64", "x86_64-linux-android")
136 elif ARCH == "mips":
137 known_toolchains = [
138 ("mipsel-linux-android-" + gcc_version, "mips", "mipsel-linux-android")
140 else:
141 known_toolchains = []
143 logging.debug('FindToolcahin: known_toolchains=%s' % known_toolchains)
144 # Look for addr2line to check for valid toolchain path.
145 for (label, platform, target) in known_toolchains:
146 toolchain_info = (label, platform, target);
147 if os.path.exists(ToolPath("addr2line", toolchain_info)):
148 TOOLCHAIN_INFO = toolchain_info
149 print "Using toolchain from :" + ToolPath("", TOOLCHAIN_INFO)
150 return toolchain_info
152 raise Exception("Could not find tool chain")
154 def GetAapt():
155 """Returns the path to aapt.
157 Args:
158 None
160 Returns:
161 the pathname of the 'aapt' executable.
163 sdk_home = os.path.join('third_party', 'android_tools', 'sdk')
164 sdk_home = os.environ.get('SDK_HOME', sdk_home)
165 aapt_exe = glob.glob(os.path.join(sdk_home, 'build-tools', '*', 'aapt'))
166 if not aapt_exe:
167 return None
168 return sorted(aapt_exe, key=os.path.getmtime, reverse=True)[0]
170 def ApkMatchPackageName(aapt, apk_path, package_name):
171 """Returns true the APK's package name matches package_name.
173 Args:
174 aapt: pathname for the 'aapt' executable.
175 apk_path: pathname of the APK file.
176 package_name: package name to match.
178 Returns:
179 True if the package name matches or aapt is None, False otherwise.
181 if not aapt:
182 # Allow false positives
183 return True
184 aapt_output = subprocess.check_output(
185 [aapt, 'dump', 'badging', apk_path]).split('\n')
186 package_name_re = re.compile(r'package: .*name=\'(\S*)\'')
187 for line in aapt_output:
188 match = package_name_re.match(line)
189 if match:
190 return package_name == match.group(1)
191 return False
193 def PathListJoin(prefix_list, suffix_list):
194 """Returns each prefix in prefix_list joined with each suffix in suffix list.
196 Args:
197 prefix_list: list of path prefixes.
198 suffix_list: list of path suffixes.
200 Returns:
201 List of paths each of which joins a prefix with a suffix.
203 return [
204 os.path.join(prefix, suffix)
205 for prefix in prefix_list for suffix in suffix_list ]
207 def GetCandidates(dirs, filepart, candidate_fun):
208 """Returns a list of candidate filenames.
210 Args:
211 dirs: a list of the directory part of the pathname.
212 filepart: the file part of the pathname.
213 candidate_fun: a function to apply to each candidate, returns a list.
215 Returns:
216 A list of candidate files ordered by modification time, newest first.
218 out_dir = os.environ.get('CHROMIUM_OUT_DIR', 'out')
219 out_dir = os.path.join(CHROME_SYMBOLS_DIR, out_dir)
220 buildtype = os.environ.get('BUILDTYPE')
221 if buildtype:
222 buildtype_list = [ buildtype ]
223 else:
224 buildtype_list = [ 'Debug', 'Release' ]
226 candidates = PathListJoin([out_dir], buildtype_list) + [CHROME_SYMBOLS_DIR]
227 candidates = PathListJoin(candidates, dirs)
228 candidates = PathListJoin(candidates, [filepart])
229 logging.debug('GetCandidates: prefiltered candidates = %s' % candidates)
230 candidates = list(
231 itertools.chain.from_iterable(map(candidate_fun, candidates)))
232 candidates = sorted(candidates, key=os.path.getmtime, reverse=True)
233 return candidates
235 def GetCandidateApks():
236 """Returns a list of APKs which could contain the library.
238 Args:
239 None
241 Returns:
242 list of APK filename which could contain the library.
244 return GetCandidates(['apks'], '*.apk', glob.glob)
246 def GetCrazyLib(apk_filename):
247 """Returns the name of the first crazy library from this APK.
249 Args:
250 apk_filename: name of an APK file.
252 Returns:
253 Name of the first library which would be crazy loaded from this APK.
255 zip_file = zipfile.ZipFile(apk_filename, 'r')
256 for filename in zip_file.namelist():
257 match = re.match('lib/[^/]*/crazy.(lib.*[.]so)', filename)
258 if match:
259 return match.group(1)
261 def GetApkFromLibrary(device_library_path):
262 match = re.match(r'.*/([^/]*)-[0-9]+(\/[^/]*)?\.apk$', device_library_path)
263 if not match:
264 return None
265 return match.group(1)
267 def GetMatchingApks(package_name):
268 """Find any APKs which match the package indicated by the device_apk_name.
270 Args:
271 device_apk_name: name of the APK on the device.
273 Returns:
274 A list of APK filenames which could contain the desired library.
276 return filter(
277 lambda candidate_apk:
278 ApkMatchPackageName(GetAapt(), candidate_apk, package_name),
279 GetCandidateApks())
281 def MapDeviceApkToLibrary(device_apk_name):
282 """Provide a library name which corresponds with device_apk_name.
284 Args:
285 device_apk_name: name of the APK on the device.
287 Returns:
288 Name of the library which corresponds to that APK.
290 matching_apks = GetMatchingApks(device_apk_name)
291 logging.debug('MapDeviceApkToLibrary: matching_apks=%s' % matching_apks)
292 for matching_apk in matching_apks:
293 crazy_lib = GetCrazyLib(matching_apk)
294 if crazy_lib:
295 return crazy_lib
297 def GetCandidateLibraries(library_name):
298 """Returns a list of candidate library filenames.
300 Args:
301 library_name: basename of the library to match.
303 Returns:
304 A list of matching library filenames for library_name.
306 return GetCandidates(
307 ['lib', 'lib.target', '.'], library_name,
308 lambda filename: filter(os.path.exists, [filename]))
310 def TranslateLibPath(lib):
311 # The filename in the stack trace maybe an APK name rather than a library
312 # name. This happens when the library was loaded directly from inside the
313 # APK. If this is the case we try to figure out the library name by looking
314 # for a matching APK file and finding the name of the library in contains.
315 # The name of the APK file on the device is of the form
316 # <package_name>-<number>.apk. The APK file on the host may have any name
317 # so we look at the APK badging to see if the package name matches.
318 apk = GetApkFromLibrary(lib)
319 if apk is not None:
320 logging.debug('TranslateLibPath: apk=%s' % apk)
321 mapping = MapDeviceApkToLibrary(apk)
322 if mapping:
323 lib = mapping
325 # SymbolInformation(lib, addr) receives lib as the path from symbols
326 # root to the symbols file. This needs to be translated to point to the
327 # correct .so path. If the user doesn't explicitly specify which directory to
328 # use, then use the most recently updated one in one of the known directories.
329 # If the .so is not found somewhere in CHROME_SYMBOLS_DIR, leave it
330 # untranslated in case it is an Android symbol in SYMBOLS_DIR.
331 library_name = os.path.basename(lib)
333 logging.debug('TranslateLibPath: lib=%s library_name=%s' % (lib, library_name))
335 candidate_libraries = GetCandidateLibraries(library_name)
336 logging.debug('TranslateLibPath: candidate_libraries=%s' % candidate_libraries)
337 if not candidate_libraries:
338 return lib
340 library_path = os.path.relpath(candidate_libraries[0], SYMBOLS_DIR)
341 logging.debug('TranslateLibPath: library_path=%s' % library_path)
342 return '/' + library_path
344 def SymbolInformation(lib, addr, get_detailed_info):
345 """Look up symbol information about an address.
347 Args:
348 lib: library (or executable) pathname containing symbols
349 addr: string hexidecimal address
351 Returns:
352 A list of the form [(source_symbol, source_location,
353 object_symbol_with_offset)].
355 If the function has been inlined then the list may contain
356 more than one element with the symbols for the most deeply
357 nested inlined location appearing first. The list is
358 always non-empty, even if no information is available.
360 Usually you want to display the source_location and
361 object_symbol_with_offset from the last element in the list.
363 lib = TranslateLibPath(lib)
364 info = SymbolInformationForSet(lib, set([addr]), get_detailed_info)
365 return (info and info.get(addr)) or [(None, None, None)]
368 def SymbolInformationForSet(lib, unique_addrs, get_detailed_info):
369 """Look up symbol information for a set of addresses from the given library.
371 Args:
372 lib: library (or executable) pathname containing symbols
373 unique_addrs: set of hexidecimal addresses
375 Returns:
376 A dictionary of the form {addr: [(source_symbol, source_location,
377 object_symbol_with_offset)]} where each address has a list of
378 associated symbols and locations. The list is always non-empty.
380 If the function has been inlined then the list may contain
381 more than one element with the symbols for the most deeply
382 nested inlined location appearing first. The list is
383 always non-empty, even if no information is available.
385 Usually you want to display the source_location and
386 object_symbol_with_offset from the last element in the list.
388 if not lib:
389 return None
391 addr_to_line = CallAddr2LineForSet(lib, unique_addrs)
392 if not addr_to_line:
393 return None
395 if get_detailed_info:
396 addr_to_objdump = CallObjdumpForSet(lib, unique_addrs)
397 if not addr_to_objdump:
398 return None
399 else:
400 addr_to_objdump = dict((addr, ("", 0)) for addr in unique_addrs)
402 result = {}
403 for addr in unique_addrs:
404 source_info = addr_to_line.get(addr)
405 if not source_info:
406 source_info = [(None, None)]
407 if addr in addr_to_objdump:
408 (object_symbol, object_offset) = addr_to_objdump.get(addr)
409 object_symbol_with_offset = FormatSymbolWithOffset(object_symbol,
410 object_offset)
411 else:
412 object_symbol_with_offset = None
413 result[addr] = [(source_symbol, source_location, object_symbol_with_offset)
414 for (source_symbol, source_location) in source_info]
416 return result
419 class MemoizedForSet(object):
420 def __init__(self, fn):
421 self.fn = fn
422 self.cache = {}
424 def __call__(self, lib, unique_addrs):
425 lib_cache = self.cache.setdefault(lib, {})
427 no_cache = filter(lambda x: x not in lib_cache, unique_addrs)
428 if no_cache:
429 lib_cache.update((k, None) for k in no_cache)
430 result = self.fn(lib, no_cache)
431 if result:
432 lib_cache.update(result)
434 return dict((k, lib_cache[k]) for k in unique_addrs if lib_cache[k])
437 @MemoizedForSet
438 def CallAddr2LineForSet(lib, unique_addrs):
439 """Look up line and symbol information for a set of addresses.
441 Args:
442 lib: library (or executable) pathname containing symbols
443 unique_addrs: set of string hexidecimal addresses look up.
445 Returns:
446 A dictionary of the form {addr: [(symbol, file:line)]} where
447 each address has a list of associated symbols and locations
448 or an empty list if no symbol information was found.
450 If the function has been inlined then the list may contain
451 more than one element with the symbols for the most deeply
452 nested inlined location appearing first.
454 if not lib:
455 return None
457 symbols = SYMBOLS_DIR + lib
458 if not os.path.splitext(symbols)[1] in ['', '.so', '.apk']:
459 return None
461 if not os.path.isfile(symbols):
462 return None
464 addrs = sorted(unique_addrs)
465 result = {}
467 def _Callback(sym, addr):
468 records = []
469 while sym: # Traverse all the inlines following the |inlined_by| chain.
470 if sym.source_path and sym.source_line:
471 location = '%s:%d' % (sym.source_path, sym.source_line)
472 else:
473 location = None
474 records += [(sym.name, location)]
475 sym = sym.inlined_by
476 result[addr] = records
478 (label, platform, target) = FindToolchain()
479 symbolizer = elf_symbolizer.ELFSymbolizer(
480 elf_file_path=symbols,
481 addr2line_path=ToolPath("addr2line"),
482 callback=_Callback,
483 inlines=True)
485 for addr in addrs:
486 symbolizer.SymbolizeAsync(int(addr, 16), addr)
487 symbolizer.Join()
488 return result
491 def StripPC(addr):
492 """Strips the Thumb bit a program counter address when appropriate.
494 Args:
495 addr: the program counter address
497 Returns:
498 The stripped program counter address.
500 global ARCH
502 if ARCH == "arm":
503 return addr & ~1
504 return addr
506 @MemoizedForSet
507 def CallObjdumpForSet(lib, unique_addrs):
508 """Use objdump to find out the names of the containing functions.
510 Args:
511 lib: library (or executable) pathname containing symbols
512 unique_addrs: set of string hexidecimal addresses to find the functions for.
514 Returns:
515 A dictionary of the form {addr: (string symbol, offset)}.
517 if not lib:
518 return None
520 symbols = SYMBOLS_DIR + lib
521 if not os.path.exists(symbols):
522 return None
524 symbols = SYMBOLS_DIR + lib
525 if not os.path.exists(symbols):
526 return None
528 result = {}
530 # Function lines look like:
531 # 000177b0 <android::IBinder::~IBinder()+0x2c>:
532 # We pull out the address and function first. Then we check for an optional
533 # offset. This is tricky due to functions that look like "operator+(..)+0x2c"
534 func_regexp = re.compile("(^[a-f0-9]*) \<(.*)\>:$")
535 offset_regexp = re.compile("(.*)\+0x([a-f0-9]*)")
537 # A disassembly line looks like:
538 # 177b2: b510 push {r4, lr}
539 asm_regexp = re.compile("(^[ a-f0-9]*):[ a-f0-0]*.*$")
541 for target_addr in unique_addrs:
542 start_addr_dec = str(StripPC(int(target_addr, 16)))
543 stop_addr_dec = str(StripPC(int(target_addr, 16)) + 8)
544 cmd = [ToolPath("objdump"),
545 "--section=.text",
546 "--demangle",
547 "--disassemble",
548 "--start-address=" + start_addr_dec,
549 "--stop-address=" + stop_addr_dec,
550 symbols]
552 current_symbol = None # The current function symbol in the disassembly.
553 current_symbol_addr = 0 # The address of the current function.
555 stream = subprocess.Popen(cmd, stdout=subprocess.PIPE).stdout
556 for line in stream:
557 # Is it a function line like:
558 # 000177b0 <android::IBinder::~IBinder()>:
559 components = func_regexp.match(line)
560 if components:
561 # This is a new function, so record the current function and its address.
562 current_symbol_addr = int(components.group(1), 16)
563 current_symbol = components.group(2)
565 # Does it have an optional offset like: "foo(..)+0x2c"?
566 components = offset_regexp.match(current_symbol)
567 if components:
568 current_symbol = components.group(1)
569 offset = components.group(2)
570 if offset:
571 current_symbol_addr -= int(offset, 16)
573 # Is it an disassembly line like:
574 # 177b2: b510 push {r4, lr}
575 components = asm_regexp.match(line)
576 if components:
577 addr = components.group(1)
578 i_addr = int(addr, 16)
579 i_target = StripPC(int(target_addr, 16))
580 if i_addr == i_target:
581 result[target_addr] = (current_symbol, i_target - current_symbol_addr)
582 stream.close()
584 return result
587 def CallCppFilt(mangled_symbol):
588 cmd = [ToolPath("c++filt")]
589 process = subprocess.Popen(cmd, stdin=subprocess.PIPE, stdout=subprocess.PIPE)
590 process.stdin.write(mangled_symbol)
591 process.stdin.write("\n")
592 process.stdin.close()
593 demangled_symbol = process.stdout.readline().strip()
594 process.stdout.close()
595 return demangled_symbol
597 def FormatSymbolWithOffset(symbol, offset):
598 if offset == 0:
599 return symbol
600 return "%s+%d" % (symbol, offset)