OLD | NEW |
(Empty) | |
| 1 #!/usr/bin/python |
| 2 # |
| 3 # Copyright (C) 2013 The Android Open Source Project |
| 4 # |
| 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 |
| 8 # |
| 9 # http://www.apache.org/licenses/LICENSE-2.0 |
| 10 # |
| 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. |
| 16 |
| 17 """Module for looking up symbolic debugging information. |
| 18 |
| 19 The information can include symbol names, offsets, and source locations. |
| 20 """ |
| 21 |
| 22 import glob |
| 23 import itertools |
| 24 import os |
| 25 import re |
| 26 import subprocess |
| 27 import zipfile |
| 28 |
| 29 CHROME_SRC = os.path.join(os.path.realpath(os.path.dirname(__file__)), |
| 30 os.pardir, os.pardir, os.pardir, os.pardir) |
| 31 ANDROID_BUILD_TOP = CHROME_SRC |
| 32 SYMBOLS_DIR = CHROME_SRC |
| 33 CHROME_SYMBOLS_DIR = CHROME_SRC |
| 34 |
| 35 ARCH = "arm" |
| 36 |
| 37 TOOLCHAIN_INFO = None |
| 38 |
| 39 def Uname(): |
| 40 """'uname' for constructing prebuilt/<...> and out/host/<...> paths.""" |
| 41 uname = os.uname()[0] |
| 42 if uname == "Darwin": |
| 43 proc = os.uname()[-1] |
| 44 if proc == "i386" or proc == "x86_64": |
| 45 return "darwin-x86" |
| 46 return "darwin-ppc" |
| 47 if uname == "Linux": |
| 48 return "linux-x86" |
| 49 return uname |
| 50 |
| 51 def ToolPath(tool, toolchain_info=None): |
| 52 """Return a full qualified path to the specified tool""" |
| 53 # ToolPath looks for the tools in the completely incorrect directory. |
| 54 # This looks in the checked in android_tools. |
| 55 if ARCH == "arm": |
| 56 toolchain_source = "arm-linux-androideabi-4.9" |
| 57 toolchain_prefix = "arm-linux-androideabi" |
| 58 ndk = "ndk" |
| 59 elif ARCH == "arm64": |
| 60 toolchain_source = "aarch64-linux-android-4.9" |
| 61 toolchain_prefix = "aarch64-linux-android" |
| 62 ndk = "ndk" |
| 63 elif ARCH == "x86": |
| 64 toolchain_source = "x86-4.9" |
| 65 toolchain_prefix = "i686-linux-android" |
| 66 ndk = "ndk" |
| 67 elif ARCH == "x86_64": |
| 68 toolchain_source = "x86_64-4.9" |
| 69 toolchain_prefix = "x86_64-linux-android" |
| 70 ndk = "ndk" |
| 71 elif ARCH == "mips": |
| 72 toolchain_source = "mipsel-linux-android-4.9" |
| 73 toolchain_prefix = "mipsel-linux-android" |
| 74 ndk = "ndk" |
| 75 else: |
| 76 raise Exception("Could not find tool chain") |
| 77 |
| 78 toolchain_subdir = ( |
| 79 "third_party/android_tools/%s/toolchains/%s/prebuilt/linux-x86_64/bin" % |
| 80 (ndk, toolchain_source)) |
| 81 |
| 82 return os.path.join(CHROME_SRC, |
| 83 toolchain_subdir, |
| 84 toolchain_prefix + "-" + tool) |
| 85 |
| 86 def FindToolchain(): |
| 87 """Look for the latest available toolchain |
| 88 |
| 89 Args: |
| 90 None |
| 91 |
| 92 Returns: |
| 93 A pair of strings containing toolchain label and target prefix. |
| 94 """ |
| 95 global TOOLCHAIN_INFO |
| 96 if TOOLCHAIN_INFO is not None: |
| 97 return TOOLCHAIN_INFO |
| 98 |
| 99 ## Known toolchains, newer ones in the front. |
| 100 gcc_version = "4.9" |
| 101 if ARCH == "arm64": |
| 102 known_toolchains = [ |
| 103 ("aarch64-linux-android-" + gcc_version, "aarch64", "aarch64-linux-android
") |
| 104 ] |
| 105 elif ARCH == "arm": |
| 106 known_toolchains = [ |
| 107 ("arm-linux-androideabi-" + gcc_version, "arm", "arm-linux-androideabi") |
| 108 ] |
| 109 elif ARCH =="x86": |
| 110 known_toolchains = [ |
| 111 ("x86-" + gcc_version, "x86", "i686-linux-android") |
| 112 ] |
| 113 elif ARCH =="x86_64": |
| 114 known_toolchains = [ |
| 115 ("x86_64-" + gcc_version, "x86_64", "x86_64-linux-android") |
| 116 ] |
| 117 elif ARCH == "mips": |
| 118 known_toolchains = [ |
| 119 ("mipsel-linux-android-" + gcc_version, "mips", "mipsel-linux-android") |
| 120 ] |
| 121 else: |
| 122 known_toolchains = [] |
| 123 |
| 124 # Look for addr2line to check for valid toolchain path. |
| 125 for (label, platform, target) in known_toolchains: |
| 126 toolchain_info = (label, platform, target); |
| 127 if os.path.exists(ToolPath("addr2line", toolchain_info)): |
| 128 TOOLCHAIN_INFO = toolchain_info |
| 129 print "Using toolchain from :" + ToolPath("", TOOLCHAIN_INFO) |
| 130 return toolchain_info |
| 131 |
| 132 raise Exception("Could not find tool chain") |
| 133 |
| 134 def GetAapt(): |
| 135 """Returns the path to aapt. |
| 136 |
| 137 Args: |
| 138 None |
| 139 |
| 140 Returns: |
| 141 the pathname of the 'aapt' executable. |
| 142 """ |
| 143 sdk_home = os.path.join('third_party', 'android_tools', 'sdk') |
| 144 sdk_home = os.environ.get('SDK_HOME', sdk_home) |
| 145 aapt_exe = glob.glob(os.path.join(sdk_home, 'build-tools', '*', 'aapt')) |
| 146 if not aapt_exe: |
| 147 return None |
| 148 return sorted(aapt_exe, key=os.path.getmtime, reverse=True)[0] |
| 149 |
| 150 def ApkMatchPackageName(aapt, apk_path, package_name): |
| 151 """Returns true the APK's package name matches package_name. |
| 152 |
| 153 Args: |
| 154 aapt: pathname for the 'aapt' executable. |
| 155 apk_path: pathname of the APK file. |
| 156 package_name: package name to match. |
| 157 |
| 158 Returns: |
| 159 True if the package name matches or aapt is None, False otherwise. |
| 160 """ |
| 161 if not aapt: |
| 162 # Allow false positives |
| 163 return True |
| 164 aapt_output = subprocess.check_output( |
| 165 [aapt, 'dump', 'badging', apk_path]).split('\n') |
| 166 package_name_re = re.compile(r'package: .*name=\'(\S*)\'') |
| 167 for line in aapt_output: |
| 168 match = package_name_re.match(line) |
| 169 if match: |
| 170 return package_name == match.group(1) |
| 171 return False |
| 172 |
| 173 def PathListJoin(prefix_list, suffix_list): |
| 174 """Returns each prefix in prefix_list joined with each suffix in suffix list. |
| 175 |
| 176 Args: |
| 177 prefix_list: list of path prefixes. |
| 178 suffix_list: list of path suffixes. |
| 179 |
| 180 Returns: |
| 181 List of paths each of which joins a prefix with a suffix. |
| 182 """ |
| 183 return [ |
| 184 os.path.join(prefix, suffix) |
| 185 for prefix in prefix_list for suffix in suffix_list ] |
| 186 |
| 187 def GetCandidates(dirs, filepart, candidate_fun): |
| 188 """Returns a list of candidate filenames. |
| 189 |
| 190 Args: |
| 191 dirs: a list of the directory part of the pathname. |
| 192 filepart: the file part of the pathname. |
| 193 candidate_fun: a function to apply to each candidate, returns a list. |
| 194 |
| 195 Returns: |
| 196 A list of candidate files ordered by modification time, newest first. |
| 197 """ |
| 198 out_dir = os.environ.get('CHROMIUM_OUT_DIR', 'out') |
| 199 out_dir = os.path.join(CHROME_SYMBOLS_DIR, out_dir) |
| 200 buildtype = os.environ.get('BUILDTYPE') |
| 201 if buildtype: |
| 202 buildtype_list = [ buildtype ] |
| 203 else: |
| 204 buildtype_list = [ 'Debug', 'Release' ] |
| 205 |
| 206 candidates = PathListJoin([out_dir], buildtype_list) + [CHROME_SYMBOLS_DIR] |
| 207 candidates = PathListJoin(candidates, dirs) |
| 208 candidates = PathListJoin(candidates, [filepart]) |
| 209 candidates = list( |
| 210 itertools.chain.from_iterable(map(candidate_fun, candidates))) |
| 211 candidates = sorted(candidates, key=os.path.getmtime, reverse=True) |
| 212 # candidates = ['/usr/local/google/home/qsr/programmes/mojo/src/out/android_De
bug/libmojo_shell.so'] |
| 213 return candidates |
| 214 |
| 215 def GetCandidateApks(): |
| 216 """Returns a list of APKs which could contain the library. |
| 217 |
| 218 Args: |
| 219 None |
| 220 |
| 221 Returns: |
| 222 list of APK filename which could contain the library. |
| 223 """ |
| 224 return GetCandidates(['apks'], '*.apk', glob.glob) |
| 225 |
| 226 def GetCrazyLib(apk_filename): |
| 227 """Returns the name of the first crazy library from this APK. |
| 228 |
| 229 Args: |
| 230 apk_filename: name of an APK file. |
| 231 |
| 232 Returns: |
| 233 Name of the first library which would be crazy loaded from this APK. |
| 234 """ |
| 235 zip_file = zipfile.ZipFile(apk_filename, 'r') |
| 236 for filename in zip_file.namelist(): |
| 237 match = re.match('lib/[^/]*/crazy.(lib.*[.]so)', filename) |
| 238 if match: |
| 239 return match.group(1) |
| 240 |
| 241 def GetMatchingApks(device_apk_name): |
| 242 """Find any APKs which match the package indicated by the device_apk_name. |
| 243 |
| 244 Args: |
| 245 device_apk_name: name of the APK on the device. |
| 246 |
| 247 Returns: |
| 248 A list of APK filenames which could contain the desired library. |
| 249 """ |
| 250 match = re.match('(.*)-[0-9]+[.]apk$', device_apk_name) |
| 251 if not match: |
| 252 return None |
| 253 package_name = match.group(1) |
| 254 return filter( |
| 255 lambda candidate_apk: |
| 256 ApkMatchPackageName(GetAapt(), candidate_apk, package_name), |
| 257 GetCandidateApks()) |
| 258 |
| 259 def MapDeviceApkToLibrary(device_apk_name): |
| 260 """Provide a library name which corresponds with device_apk_name. |
| 261 |
| 262 Args: |
| 263 device_apk_name: name of the APK on the device. |
| 264 |
| 265 Returns: |
| 266 Name of the library which corresponds to that APK. |
| 267 """ |
| 268 matching_apks = GetMatchingApks(device_apk_name) |
| 269 for matching_apk in matching_apks: |
| 270 crazy_lib = GetCrazyLib(matching_apk) |
| 271 if crazy_lib: |
| 272 return crazy_lib |
| 273 |
| 274 def GetCandidateLibraries(library_name): |
| 275 """Returns a list of candidate library filenames. |
| 276 |
| 277 Args: |
| 278 library_name: basename of the library to match. |
| 279 |
| 280 Returns: |
| 281 A list of matching library filenames for library_name. |
| 282 """ |
| 283 return GetCandidates( |
| 284 ['', 'lib', 'lib.target'], library_name, |
| 285 lambda filename: filter(os.path.exists, [filename])) |
| 286 |
| 287 def TranslateLibPath(lib): |
| 288 # SymbolInformation(lib, addr) receives lib as the path from symbols |
| 289 # root to the symbols file. This needs to be translated to point to the |
| 290 # correct .so path. If the user doesn't explicitly specify which directory to |
| 291 # use, then use the most recently updated one in one of the known directories. |
| 292 # If the .so is not found somewhere in CHROME_SYMBOLS_DIR, leave it |
| 293 # untranslated in case it is an Android symbol in SYMBOLS_DIR. |
| 294 library_name = os.path.basename(lib) |
| 295 |
| 296 # The filename in the stack trace maybe an APK name rather than a library |
| 297 # name. This happens when the library was loaded directly from inside the |
| 298 # APK. If this is the case we try to figure out the library name by looking |
| 299 # for a matching APK file and finding the name of the library in contains. |
| 300 # The name of the APK file on the device is of the form |
| 301 # <package_name>-<number>.apk. The APK file on the host may have any name |
| 302 # so we look at the APK badging to see if the package name matches. |
| 303 if re.search('-[0-9]+[.]apk$', library_name): |
| 304 mapping = MapDeviceApkToLibrary(library_name) |
| 305 if mapping: |
| 306 library_name = mapping |
| 307 |
| 308 candidate_libraries = GetCandidateLibraries(library_name) |
| 309 if not candidate_libraries: |
| 310 return lib |
| 311 |
| 312 library_path = os.path.relpath(candidate_libraries[0], SYMBOLS_DIR) |
| 313 return '/' + library_path |
| 314 |
| 315 def SymbolInformation(lib, addr, get_detailed_info): |
| 316 """Look up symbol information about an address. |
| 317 |
| 318 Args: |
| 319 lib: library (or executable) pathname containing symbols |
| 320 addr: string hexidecimal address |
| 321 |
| 322 Returns: |
| 323 A list of the form [(source_symbol, source_location, |
| 324 object_symbol_with_offset)]. |
| 325 |
| 326 If the function has been inlined then the list may contain |
| 327 more than one element with the symbols for the most deeply |
| 328 nested inlined location appearing first. The list is |
| 329 always non-empty, even if no information is available. |
| 330 |
| 331 Usually you want to display the source_location and |
| 332 object_symbol_with_offset from the last element in the list. |
| 333 """ |
| 334 lib = TranslateLibPath(lib) |
| 335 info = SymbolInformationForSet(lib, set([addr]), get_detailed_info) |
| 336 return (info and info.get(addr)) or [(None, None, None)] |
| 337 |
| 338 |
| 339 def SymbolInformationForSet(lib, unique_addrs, get_detailed_info): |
| 340 """Look up symbol information for a set of addresses from the given library. |
| 341 |
| 342 Args: |
| 343 lib: library (or executable) pathname containing symbols |
| 344 unique_addrs: set of hexidecimal addresses |
| 345 |
| 346 Returns: |
| 347 A dictionary of the form {addr: [(source_symbol, source_location, |
| 348 object_symbol_with_offset)]} where each address has a list of |
| 349 associated symbols and locations. The list is always non-empty. |
| 350 |
| 351 If the function has been inlined then the list may contain |
| 352 more than one element with the symbols for the most deeply |
| 353 nested inlined location appearing first. The list is |
| 354 always non-empty, even if no information is available. |
| 355 |
| 356 Usually you want to display the source_location and |
| 357 object_symbol_with_offset from the last element in the list. |
| 358 """ |
| 359 if not lib: |
| 360 return None |
| 361 |
| 362 addr_to_line = CallAddr2LineForSet(lib, unique_addrs) |
| 363 if not addr_to_line: |
| 364 return None |
| 365 |
| 366 if get_detailed_info: |
| 367 addr_to_objdump = CallObjdumpForSet(lib, unique_addrs) |
| 368 if not addr_to_objdump: |
| 369 return None |
| 370 else: |
| 371 addr_to_objdump = dict((addr, ("", 0)) for addr in unique_addrs) |
| 372 |
| 373 result = {} |
| 374 for addr in unique_addrs: |
| 375 source_info = addr_to_line.get(addr) |
| 376 if not source_info: |
| 377 source_info = [(None, None)] |
| 378 if addr in addr_to_objdump: |
| 379 (object_symbol, object_offset) = addr_to_objdump.get(addr) |
| 380 object_symbol_with_offset = FormatSymbolWithOffset(object_symbol, |
| 381 object_offset) |
| 382 else: |
| 383 object_symbol_with_offset = None |
| 384 result[addr] = [(source_symbol, source_location, object_symbol_with_offset) |
| 385 for (source_symbol, source_location) in source_info] |
| 386 |
| 387 return result |
| 388 |
| 389 |
| 390 class MemoizedForSet(object): |
| 391 def __init__(self, fn): |
| 392 self.fn = fn |
| 393 self.cache = {} |
| 394 |
| 395 def __call__(self, lib, unique_addrs): |
| 396 lib_cache = self.cache.setdefault(lib, {}) |
| 397 |
| 398 no_cache = filter(lambda x: x not in lib_cache, unique_addrs) |
| 399 if no_cache: |
| 400 lib_cache.update((k, None) for k in no_cache) |
| 401 result = self.fn(lib, no_cache) |
| 402 if result: |
| 403 lib_cache.update(result) |
| 404 |
| 405 return dict((k, lib_cache[k]) for k in unique_addrs if lib_cache[k]) |
| 406 |
| 407 |
| 408 @MemoizedForSet |
| 409 def CallAddr2LineForSet(lib, unique_addrs): |
| 410 """Look up line and symbol information for a set of addresses. |
| 411 |
| 412 Args: |
| 413 lib: library (or executable) pathname containing symbols |
| 414 unique_addrs: set of string hexidecimal addresses look up. |
| 415 |
| 416 Returns: |
| 417 A dictionary of the form {addr: [(symbol, file:line)]} where |
| 418 each address has a list of associated symbols and locations |
| 419 or an empty list if no symbol information was found. |
| 420 |
| 421 If the function has been inlined then the list may contain |
| 422 more than one element with the symbols for the most deeply |
| 423 nested inlined location appearing first. |
| 424 """ |
| 425 if not lib: |
| 426 return None |
| 427 |
| 428 |
| 429 symbols = SYMBOLS_DIR + lib |
| 430 if not os.path.isfile(symbols): |
| 431 return None |
| 432 |
| 433 (label, platform, target) = FindToolchain() |
| 434 cmd = [ToolPath("addr2line"), "--functions", "--inlines", |
| 435 "--demangle", "--exe=" + symbols] |
| 436 child = subprocess.Popen(cmd, stdin=subprocess.PIPE, stdout=subprocess.PIPE) |
| 437 |
| 438 result = {} |
| 439 addrs = sorted(unique_addrs) |
| 440 for addr in addrs: |
| 441 child.stdin.write("0x%s\n" % addr) |
| 442 child.stdin.flush() |
| 443 records = [] |
| 444 first = True |
| 445 while True: |
| 446 symbol = child.stdout.readline().strip() |
| 447 if symbol == "??": |
| 448 symbol = None |
| 449 location = child.stdout.readline().strip() |
| 450 if location == "??:0": |
| 451 location = None |
| 452 if symbol is None and location is None: |
| 453 break |
| 454 records.append((symbol, location)) |
| 455 if first: |
| 456 # Write a blank line as a sentinel so we know when to stop |
| 457 # reading inlines from the output. |
| 458 # The blank line will cause addr2line to emit "??\n??:0\n". |
| 459 child.stdin.write("\n") |
| 460 first = False |
| 461 result[addr] = records |
| 462 child.stdin.close() |
| 463 child.stdout.close() |
| 464 return result |
| 465 |
| 466 |
| 467 def StripPC(addr): |
| 468 """Strips the Thumb bit a program counter address when appropriate. |
| 469 |
| 470 Args: |
| 471 addr: the program counter address |
| 472 |
| 473 Returns: |
| 474 The stripped program counter address. |
| 475 """ |
| 476 global ARCH |
| 477 |
| 478 if ARCH == "arm": |
| 479 return addr & ~1 |
| 480 return addr |
| 481 |
| 482 @MemoizedForSet |
| 483 def CallObjdumpForSet(lib, unique_addrs): |
| 484 """Use objdump to find out the names of the containing functions. |
| 485 |
| 486 Args: |
| 487 lib: library (or executable) pathname containing symbols |
| 488 unique_addrs: set of string hexidecimal addresses to find the functions for. |
| 489 |
| 490 Returns: |
| 491 A dictionary of the form {addr: (string symbol, offset)}. |
| 492 """ |
| 493 if not lib: |
| 494 return None |
| 495 |
| 496 symbols = SYMBOLS_DIR + lib |
| 497 if not os.path.exists(symbols): |
| 498 return None |
| 499 |
| 500 symbols = SYMBOLS_DIR + lib |
| 501 if not os.path.exists(symbols): |
| 502 return None |
| 503 |
| 504 result = {} |
| 505 |
| 506 # Function lines look like: |
| 507 # 000177b0 <android::IBinder::~IBinder()+0x2c>: |
| 508 # We pull out the address and function first. Then we check for an optional |
| 509 # offset. This is tricky due to functions that look like "operator+(..)+0x2c" |
| 510 func_regexp = re.compile("(^[a-f0-9]*) \<(.*)\>:$") |
| 511 offset_regexp = re.compile("(.*)\+0x([a-f0-9]*)") |
| 512 |
| 513 # A disassembly line looks like: |
| 514 # 177b2: b510 push {r4, lr} |
| 515 asm_regexp = re.compile("(^[ a-f0-9]*):[ a-f0-0]*.*$") |
| 516 |
| 517 for target_addr in unique_addrs: |
| 518 start_addr_dec = str(StripPC(int(target_addr, 16))) |
| 519 stop_addr_dec = str(StripPC(int(target_addr, 16)) + 8) |
| 520 cmd = [ToolPath("objdump"), |
| 521 "--section=.text", |
| 522 "--demangle", |
| 523 "--disassemble", |
| 524 "--start-address=" + start_addr_dec, |
| 525 "--stop-address=" + stop_addr_dec, |
| 526 symbols] |
| 527 |
| 528 current_symbol = None # The current function symbol in the disassembly. |
| 529 current_symbol_addr = 0 # The address of the current function. |
| 530 |
| 531 stream = subprocess.Popen(cmd, stdout=subprocess.PIPE).stdout |
| 532 for line in stream: |
| 533 # Is it a function line like: |
| 534 # 000177b0 <android::IBinder::~IBinder()>: |
| 535 components = func_regexp.match(line) |
| 536 if components: |
| 537 # This is a new function, so record the current function and its address
. |
| 538 current_symbol_addr = int(components.group(1), 16) |
| 539 current_symbol = components.group(2) |
| 540 |
| 541 # Does it have an optional offset like: "foo(..)+0x2c"? |
| 542 components = offset_regexp.match(current_symbol) |
| 543 if components: |
| 544 current_symbol = components.group(1) |
| 545 offset = components.group(2) |
| 546 if offset: |
| 547 current_symbol_addr -= int(offset, 16) |
| 548 |
| 549 # Is it an disassembly line like: |
| 550 # 177b2: b510 push {r4, lr} |
| 551 components = asm_regexp.match(line) |
| 552 if components: |
| 553 addr = components.group(1) |
| 554 i_addr = int(addr, 16) |
| 555 i_target = StripPC(int(target_addr, 16)) |
| 556 if i_addr == i_target: |
| 557 result[target_addr] = (current_symbol, i_target - current_symbol_addr) |
| 558 stream.close() |
| 559 |
| 560 return result |
| 561 |
| 562 |
| 563 def CallCppFilt(mangled_symbol): |
| 564 cmd = [ToolPath("c++filt")] |
| 565 process = subprocess.Popen(cmd, stdin=subprocess.PIPE, stdout=subprocess.PIPE) |
| 566 process.stdin.write(mangled_symbol) |
| 567 process.stdin.write("\n") |
| 568 process.stdin.close() |
| 569 demangled_symbol = process.stdout.readline().strip() |
| 570 process.stdout.close() |
| 571 return demangled_symbol |
| 572 |
| 573 def FormatSymbolWithOffset(symbol, offset): |
| 574 if offset == 0: |
| 575 return symbol |
| 576 return "%s+%d" % (symbol, offset) |
OLD | NEW |