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.
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
42 sys
.path
.insert(0, os
.path
.join(CHROME_SRC
, 'build', 'android'))
43 from pylib
.symbols
import elf_symbolizer
46 # http://bugs.python.org/issue14315
47 # https://hg.python.org/cpython/rev/6dd5e9556a60#l2.8
49 oldDecodeExtra
= zipfile
.ZipInfo
._decodeExtra
50 def decodeExtra(self
):
55 zipfile
.ZipInfo
._decodeExtra
= decodeExtra
59 """'uname' for constructing prebuilt/<...> and out/host/<...> paths."""
63 if proc
== "i386" or proc
== "x86_64":
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.
75 toolchain_source
= "arm-linux-androideabi-4.9"
76 toolchain_prefix
= "arm-linux-androideabi"
79 toolchain_source
= "aarch64-linux-android-4.9"
80 toolchain_prefix
= "aarch64-linux-android"
83 toolchain_source
= "x86-4.9"
84 toolchain_prefix
= "i686-linux-android"
86 elif ARCH
== "x86_64" or ARCH
== "x64":
87 toolchain_source
= "x86_64-4.9"
88 toolchain_prefix
= "x86_64-linux-android"
91 toolchain_source
= "mipsel-linux-android-4.9"
92 toolchain_prefix
= "mipsel-linux-android"
95 raise Exception("Could not find tool chain")
98 "third_party/android_tools/%s/toolchains/%s/prebuilt/linux-x86_64/bin" %
99 (ndk
, toolchain_source
))
101 return os
.path
.join(CHROME_SRC
,
103 toolchain_prefix
+ "-" + tool
)
106 """Look for the latest available toolchain
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.
122 ("aarch64-linux-android-" + gcc_version
, "aarch64", "aarch64-linux-android")
126 ("arm-linux-androideabi-" + gcc_version
, "arm", "arm-linux-androideabi")
130 ("x86-" + gcc_version
, "x86", "i686-linux-android")
132 elif ARCH
=="x86_64" or ARCH
=="x64":
134 ("x86_64-" + gcc_version
, "x86_64", "x86_64-linux-android")
138 ("mipsel-linux-android-" + gcc_version
, "mips", "mipsel-linux-android")
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")
155 """Returns the path to aapt.
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'))
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.
174 aapt: pathname for the 'aapt' executable.
175 apk_path: pathname of the APK file.
176 package_name: package name to match.
179 True if the package name matches or aapt is None, False otherwise.
182 # Allow false positives
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)
190 return package_name == match.group(1)
193 def PathListJoin(prefix_list, suffix_list):
194 """Returns each prefix in prefix_list joined with each suffix in suffix list.
197 prefix_list: list of path prefixes.
198 suffix_list: list of path suffixes.
201 List of paths each of which joins a prefix with a suffix.
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.
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.
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
')
222 buildtype_list = [ buildtype ]
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)
231 itertools.chain.from_iterable(map(candidate_fun, candidates)))
232 candidates = sorted(candidates, key=os.path.getmtime, reverse=True)
235 def GetCandidateApks():
236 """Returns a list of APKs which could contain the library.
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.
250 apk_filename: name of an APK file.
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)
259 return match.group(1)
261 def GetApkFromLibrary(device_library_path):
262 match = re.match(r'.*/([^
/]*)-[0-9]+(\
/[^
/]*)?\
.apk$
', device_library_path)
265 return match.group(1)
267 def GetMatchingApks(package_name):
268 """Find any APKs which match the package indicated by the device_apk_name.
271 device_apk_name: name of the APK on the device.
274 A list of APK filenames which could contain the desired library.
277 lambda candidate_apk:
278 ApkMatchPackageName(GetAapt(), candidate_apk, package_name),
281 def MapDeviceApkToLibrary(device_apk_name):
282 """Provide a library name which corresponds with device_apk_name.
285 device_apk_name: name of the APK on the device.
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)
297 def GetCandidateLibraries(library_name):
298 """Returns a list of candidate library filenames.
301 library_name: basename of the library to match.
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)
320 logging.debug('TranslateLibPath
: apk
=%s' % apk)
321 mapping = MapDeviceApkToLibrary(apk)
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
:
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.
348 lib: library (or executable) pathname containing symbols
349 addr: string hexidecimal address
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.
372 lib: library (or executable) pathname containing symbols
373 unique_addrs: set of hexidecimal addresses
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.
391 addr_to_line
= CallAddr2LineForSet(lib
, unique_addrs
)
395 if get_detailed_info
:
396 addr_to_objdump
= CallObjdumpForSet(lib
, unique_addrs
)
397 if not addr_to_objdump
:
400 addr_to_objdump
= dict((addr
, ("", 0)) for addr
in unique_addrs
)
403 for addr
in unique_addrs
:
404 source_info
= addr_to_line
.get(addr
)
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
,
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
]
419 class MemoizedForSet(object):
420 def __init__(self
, fn
):
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
)
429 lib_cache
.update((k
, None) for k
in no_cache
)
430 result
= self
.fn(lib
, no_cache
)
432 lib_cache
.update(result
)
434 return dict((k
, lib_cache
[k
]) for k
in unique_addrs
if lib_cache
[k
])
438 def CallAddr2LineForSet(lib
, unique_addrs
):
439 """Look up line and symbol information for a set of addresses.
442 lib: library (or executable) pathname containing symbols
443 unique_addrs: set of string hexidecimal addresses look up.
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.
457 symbols
= SYMBOLS_DIR
+ lib
458 if not os
.path
.splitext(symbols
)[1] in ['', '.so', '.apk']:
461 if not os
.path
.isfile(symbols
):
464 addrs
= sorted(unique_addrs
)
467 def _Callback(sym
, addr
):
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
)
474 records
+= [(sym
.name
, location
)]
476 result
[addr
] = records
478 (label
, platform
, target
) = FindToolchain()
479 symbolizer
= elf_symbolizer
.ELFSymbolizer(
480 elf_file_path
=symbols
,
481 addr2line_path
=ToolPath("addr2line"),
486 symbolizer
.SymbolizeAsync(int(addr
, 16), addr
)
492 """Strips the Thumb bit a program counter address when appropriate.
495 addr: the program counter address
498 The stripped program counter address.
507 def CallObjdumpForSet(lib
, unique_addrs
):
508 """Use objdump to find out the names of the containing functions.
511 lib: library (or executable) pathname containing symbols
512 unique_addrs: set of string hexidecimal addresses to find the functions for.
515 A dictionary of the form {addr: (string symbol, offset)}.
520 symbols
= SYMBOLS_DIR
+ lib
521 if not os
.path
.exists(symbols
):
524 symbols
= SYMBOLS_DIR
+ lib
525 if not os
.path
.exists(symbols
):
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"),
548 "--start-address=" + start_addr_dec
,
549 "--stop-address=" + stop_addr_dec
,
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
557 # Is it a function line like:
558 # 000177b0 <android::IBinder::~IBinder()>:
559 components
= func_regexp
.match(line
)
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
)
568 current_symbol
= components
.group(1)
569 offset
= components
.group(2)
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
)
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
)
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
):
600 return "%s+%d" % (symbol
, offset
)