1#!/usr/bin/env python 2# 3# Copyright (C) 2016 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 18"""utils.py: export utility functions. 19""" 20 21from __future__ import print_function 22import argparse 23import logging 24import os 25import os.path 26import re 27import shutil 28import subprocess 29import sys 30import time 31 32def get_script_dir(): 33 return os.path.dirname(os.path.realpath(__file__)) 34 35def is_windows(): 36 return sys.platform == 'win32' or sys.platform == 'cygwin' 37 38def is_darwin(): 39 return sys.platform == 'darwin' 40 41def get_platform(): 42 if is_windows(): 43 return 'windows' 44 if is_darwin(): 45 return 'darwin' 46 return 'linux' 47 48def is_python3(): 49 return sys.version_info >= (3, 0) 50 51 52def log_debug(msg): 53 logging.debug(msg) 54 55 56def log_info(msg): 57 logging.info(msg) 58 59 60def log_warning(msg): 61 logging.warning(msg) 62 63 64def log_fatal(msg): 65 raise Exception(msg) 66 67def log_exit(msg): 68 sys.exit(msg) 69 70def disable_debug_log(): 71 logging.getLogger().setLevel(logging.WARN) 72 73def str_to_bytes(str_value): 74 if not is_python3(): 75 return str_value 76 # In python 3, str are wide strings whereas the C api expects 8 bit strings, 77 # hence we have to convert. For now using utf-8 as the encoding. 78 return str_value.encode('utf-8') 79 80def bytes_to_str(bytes_value): 81 if not bytes_value: 82 return '' 83 if not is_python3(): 84 return bytes_value 85 return bytes_value.decode('utf-8') 86 87def get_target_binary_path(arch, binary_name): 88 if arch == 'aarch64': 89 arch = 'arm64' 90 arch_dir = os.path.join(get_script_dir(), "bin", "android", arch) 91 if not os.path.isdir(arch_dir): 92 log_fatal("can't find arch directory: %s" % arch_dir) 93 binary_path = os.path.join(arch_dir, binary_name) 94 if not os.path.isfile(binary_path): 95 log_fatal("can't find binary: %s" % binary_path) 96 return binary_path 97 98 99def get_host_binary_path(binary_name): 100 dirname = os.path.join(get_script_dir(), 'bin') 101 if is_windows(): 102 if binary_name.endswith('.so'): 103 binary_name = binary_name[0:-3] + '.dll' 104 elif '.' not in binary_name: 105 binary_name += '.exe' 106 dirname = os.path.join(dirname, 'windows') 107 elif sys.platform == 'darwin': # OSX 108 if binary_name.endswith('.so'): 109 binary_name = binary_name[0:-3] + '.dylib' 110 dirname = os.path.join(dirname, 'darwin') 111 else: 112 dirname = os.path.join(dirname, 'linux') 113 dirname = os.path.join(dirname, 'x86_64' if sys.maxsize > 2 ** 32 else 'x86') 114 binary_path = os.path.join(dirname, binary_name) 115 if not os.path.isfile(binary_path): 116 log_fatal("can't find binary: %s" % binary_path) 117 return binary_path 118 119 120def is_executable_available(executable, option='--help'): 121 """ Run an executable to see if it exists. """ 122 try: 123 subproc = subprocess.Popen([executable, option], stdout=subprocess.PIPE, 124 stderr=subprocess.PIPE) 125 subproc.communicate() 126 return subproc.returncode == 0 127 except OSError: 128 return False 129 130DEFAULT_NDK_PATH = { 131 'darwin': 'Library/Android/sdk/ndk-bundle', 132 'linux': 'Android/Sdk/ndk-bundle', 133 'windows': 'AppData/Local/Android/sdk/ndk-bundle', 134} 135 136EXPECTED_TOOLS = { 137 'adb': { 138 'is_binutils': False, 139 'test_option': 'version', 140 'path_in_ndk': '../platform-tools/adb', 141 }, 142 'readelf': { 143 'is_binutils': True, 144 'accept_tool_without_arch': True, 145 }, 146 'addr2line': { 147 'is_binutils': True, 148 'accept_tool_without_arch': True 149 }, 150 'objdump': { 151 'is_binutils': True, 152 }, 153 'strip': { 154 'is_binutils': True, 155 }, 156} 157 158def _get_binutils_path_in_ndk(toolname, arch, platform): 159 if not arch: 160 arch = 'arm64' 161 if arch == 'arm64': 162 name = 'aarch64-linux-android-' + toolname 163 path = 'toolchains/aarch64-linux-android-4.9/prebuilt/%s-x86_64/bin/%s' % (platform, name) 164 elif arch == 'arm': 165 name = 'arm-linux-androideabi-' + toolname 166 path = 'toolchains/arm-linux-androideabi-4.9/prebuilt/%s-x86_64/bin/%s' % (platform, name) 167 elif arch == 'x86_64': 168 name = 'x86_64-linux-android-' + toolname 169 path = 'toolchains/x86_64-4.9/prebuilt/%s-x86_64/bin/%s' % (platform, name) 170 elif arch == 'x86': 171 name = 'i686-linux-android-' + toolname 172 path = 'toolchains/x86-4.9/prebuilt/%s-x86_64/bin/%s' % (platform, name) 173 else: 174 log_fatal('unexpected arch %s' % arch) 175 return (name, path) 176 177def find_tool_path(toolname, ndk_path=None, arch=None): 178 if toolname not in EXPECTED_TOOLS: 179 return None 180 tool_info = EXPECTED_TOOLS[toolname] 181 is_binutils = tool_info['is_binutils'] 182 test_option = tool_info.get('test_option', '--help') 183 platform = get_platform() 184 if is_binutils: 185 toolname_with_arch, path_in_ndk = _get_binutils_path_in_ndk(toolname, arch, platform) 186 else: 187 toolname_with_arch = toolname 188 path_in_ndk = tool_info['path_in_ndk'] 189 path_in_ndk = path_in_ndk.replace('/', os.sep) 190 191 # 1. Find tool in the given ndk path. 192 if ndk_path: 193 path = os.path.join(ndk_path, path_in_ndk) 194 if is_executable_available(path, test_option): 195 return path 196 197 # 2. Find tool in the ndk directory containing simpleperf scripts. 198 path = os.path.join('..', path_in_ndk) 199 if is_executable_available(path, test_option): 200 return path 201 202 # 3. Find tool in the default ndk installation path. 203 home = os.environ.get('HOMEPATH') if is_windows() else os.environ.get('HOME') 204 if home: 205 default_ndk_path = os.path.join(home, DEFAULT_NDK_PATH[platform].replace('/', os.sep)) 206 path = os.path.join(default_ndk_path, path_in_ndk) 207 if is_executable_available(path, test_option): 208 return path 209 210 # 4. Find tool in $PATH. 211 if is_executable_available(toolname_with_arch, test_option): 212 return toolname_with_arch 213 214 # 5. Find tool without arch in $PATH. 215 if is_binutils and tool_info.get('accept_tool_without_arch'): 216 if is_executable_available(toolname, test_option): 217 return toolname 218 return None 219 220 221class AdbHelper(object): 222 def __init__(self, enable_switch_to_root=True): 223 adb_path = find_tool_path('adb') 224 if not adb_path: 225 log_exit("Can't find adb in PATH environment.") 226 self.adb_path = adb_path 227 self.enable_switch_to_root = enable_switch_to_root 228 229 230 def run(self, adb_args): 231 return self.run_and_return_output(adb_args)[0] 232 233 234 def run_and_return_output(self, adb_args, stdout_file=None, log_output=True): 235 adb_args = [self.adb_path] + adb_args 236 log_debug('run adb cmd: %s' % adb_args) 237 if stdout_file: 238 with open(stdout_file, 'wb') as stdout_fh: 239 returncode = subprocess.call(adb_args, stdout=stdout_fh) 240 stdoutdata = '' 241 else: 242 subproc = subprocess.Popen(adb_args, stdout=subprocess.PIPE) 243 (stdoutdata, _) = subproc.communicate() 244 stdoutdata = bytes_to_str(stdoutdata) 245 returncode = subproc.returncode 246 result = (returncode == 0) 247 if stdoutdata and adb_args[1] != 'push' and adb_args[1] != 'pull': 248 if log_output: 249 log_debug(stdoutdata) 250 log_debug('run adb cmd: %s [result %s]' % (adb_args, result)) 251 return (result, stdoutdata) 252 253 def check_run(self, adb_args): 254 self.check_run_and_return_output(adb_args) 255 256 257 def check_run_and_return_output(self, adb_args, stdout_file=None, log_output=True): 258 result, stdoutdata = self.run_and_return_output(adb_args, stdout_file, log_output) 259 if not result: 260 log_exit('run "adb %s" failed' % adb_args) 261 return stdoutdata 262 263 264 def _unroot(self): 265 result, stdoutdata = self.run_and_return_output(['shell', 'whoami']) 266 if not result: 267 return 268 if 'root' not in stdoutdata: 269 return 270 log_info('unroot adb') 271 self.run(['unroot']) 272 self.run(['wait-for-device']) 273 time.sleep(1) 274 275 276 def switch_to_root(self): 277 if not self.enable_switch_to_root: 278 self._unroot() 279 return False 280 result, stdoutdata = self.run_and_return_output(['shell', 'whoami']) 281 if not result: 282 return False 283 if 'root' in stdoutdata: 284 return True 285 build_type = self.get_property('ro.build.type') 286 if build_type == 'user': 287 return False 288 self.run(['root']) 289 time.sleep(1) 290 self.run(['wait-for-device']) 291 result, stdoutdata = self.run_and_return_output(['shell', 'whoami']) 292 return result and 'root' in stdoutdata 293 294 def get_property(self, name): 295 result, stdoutdata = self.run_and_return_output(['shell', 'getprop', name]) 296 return stdoutdata if result else None 297 298 def set_property(self, name, value): 299 return self.run(['shell', 'setprop', name, value]) 300 301 302 def get_device_arch(self): 303 output = self.check_run_and_return_output(['shell', 'uname', '-m']) 304 if 'aarch64' in output: 305 return 'arm64' 306 if 'arm' in output: 307 return 'arm' 308 if 'x86_64' in output: 309 return 'x86_64' 310 if '86' in output: 311 return 'x86' 312 log_fatal('unsupported architecture: %s' % output.strip()) 313 return '' 314 315 316 def get_android_version(self): 317 build_version = self.get_property('ro.build.version.release') 318 android_version = 0 319 if build_version: 320 if not build_version[0].isdigit(): 321 c = build_version[0].upper() 322 if c.isupper() and c >= 'L': 323 android_version = ord(c) - ord('L') + 5 324 else: 325 strs = build_version.split('.') 326 if strs: 327 android_version = int(strs[0]) 328 return android_version 329 330 331def flatten_arg_list(arg_list): 332 res = [] 333 if arg_list: 334 for items in arg_list: 335 res += items 336 return res 337 338 339def remove(dir_or_file): 340 if os.path.isfile(dir_or_file): 341 os.remove(dir_or_file) 342 elif os.path.isdir(dir_or_file): 343 shutil.rmtree(dir_or_file, ignore_errors=True) 344 345 346def open_report_in_browser(report_path): 347 if is_darwin(): 348 # On darwin 10.12.6, webbrowser can't open browser, so try `open` cmd first. 349 try: 350 subprocess.check_call(['open', report_path]) 351 return 352 except subprocess.CalledProcessError: 353 pass 354 import webbrowser 355 try: 356 # Try to open the report with Chrome 357 browser = webbrowser.get('google-chrome') 358 browser.open(report_path, new=0, autoraise=True) 359 except webbrowser.Error: 360 # webbrowser.get() doesn't work well on darwin/windows. 361 webbrowser.open_new_tab(report_path) 362 363def is_elf_file(path): 364 if os.path.isfile(path): 365 with open(path, 'rb') as fh: 366 data = fh.read(4) 367 if len(data) == 4 and bytes_to_str(data) == '\x7fELF': 368 return True 369 return False 370 371def find_real_dso_path(dso_path_in_record_file, binary_cache_path): 372 """ Given the path of a shared library in perf.data, find its real path in the file system. """ 373 if dso_path_in_record_file[0] != '/' or dso_path_in_record_file == '//anon': 374 return None 375 if binary_cache_path: 376 tmp_path = os.path.join(binary_cache_path, dso_path_in_record_file[1:]) 377 if is_elf_file(tmp_path): 378 return tmp_path 379 if is_elf_file(dso_path_in_record_file): 380 return dso_path_in_record_file 381 return None 382 383 384class Addr2Nearestline(object): 385 """ Use addr2line to convert (dso_path, func_addr, addr) to (source_file, line) pairs. 386 For instructions generated by C++ compilers without a matching statement in source code 387 (like stack corruption check, switch optimization, etc.), addr2line can't generate 388 line information. However, we want to assign the instruction to the nearest line before 389 the instruction (just like objdump -dl). So we use below strategy: 390 Instead of finding the exact line of the instruction in an address, we find the nearest 391 line to the instruction in an address. If an address doesn't have a line info, we find 392 the line info of address - 1. If still no line info, then use address - 2, address - 3, 393 etc. 394 395 The implementation steps are as below: 396 1. Collect all (dso_path, func_addr, addr) requests before converting. This saves the 397 times to call addr2line. 398 2. Convert addrs to (source_file, line) pairs for each dso_path as below: 399 2.1 Check if the dso_path has .debug_line. If not, omit its conversion. 400 2.2 Get arch of the dso_path, and decide the addr_step for it. addr_step is the step we 401 change addr each time. For example, since instructions of arm64 are all 4 bytes long, 402 addr_step for arm64 can be 4. 403 2.3 Use addr2line to find line info for each addr in the dso_path. 404 2.4 For each addr without line info, use addr2line to find line info for 405 range(addr - addr_step, addr - addr_step * 4 - 1, -addr_step). 406 2.5 For each addr without line info, use addr2line to find line info for 407 range(addr - addr_step * 5, addr - addr_step * 128 - 1, -addr_step). 408 (128 is a guess number. A nested switch statement in 409 system/core/demangle/Demangler.cpp has >300 bytes without line info in arm64.) 410 """ 411 class Dso(object): 412 """ Info of a dynamic shared library. 413 addrs: a map from address to Addr object in this dso. 414 """ 415 def __init__(self): 416 self.addrs = {} 417 418 class Addr(object): 419 """ Info of an addr request. 420 func_addr: start_addr of the function containing addr. 421 source_lines: a list of [file_id, line_number] for addr. 422 source_lines[:-1] are all for inlined functions. 423 """ 424 def __init__(self, func_addr): 425 self.func_addr = func_addr 426 self.source_lines = None 427 428 def __init__(self, ndk_path, binary_cache_path, with_function_name): 429 self.addr2line_path = find_tool_path('addr2line', ndk_path) 430 if not self.addr2line_path: 431 log_exit("Can't find addr2line. Please set ndk path with --ndk_path option.") 432 self.readelf = ReadElf(ndk_path) 433 self.dso_map = {} # map from dso_path to Dso. 434 self.binary_cache_path = binary_cache_path 435 self.with_function_name = with_function_name 436 # Saving file names for each addr takes a lot of memory. So we store file ids in Addr, 437 # and provide data structures connecting file id and file name here. 438 self.file_name_to_id = {} 439 self.file_id_to_name = [] 440 self.func_name_to_id = {} 441 self.func_id_to_name = [] 442 443 def add_addr(self, dso_path, func_addr, addr): 444 dso = self.dso_map.get(dso_path) 445 if dso is None: 446 dso = self.dso_map[dso_path] = self.Dso() 447 if addr not in dso.addrs: 448 dso.addrs[addr] = self.Addr(func_addr) 449 450 def convert_addrs_to_lines(self): 451 for dso_path in self.dso_map: 452 self._convert_addrs_in_one_dso(dso_path, self.dso_map[dso_path]) 453 454 def _convert_addrs_in_one_dso(self, dso_path, dso): 455 real_path = find_real_dso_path(dso_path, self.binary_cache_path) 456 if not real_path: 457 if dso_path not in ['//anon', 'unknown', '[kernel.kallsyms]']: 458 log_debug("Can't find dso %s" % dso_path) 459 return 460 461 if not self._check_debug_line_section(real_path): 462 log_debug("file %s doesn't contain .debug_line section." % real_path) 463 return 464 465 addr_step = self._get_addr_step(real_path) 466 self._collect_line_info(dso, real_path, [0]) 467 self._collect_line_info(dso, real_path, range(-addr_step, -addr_step * 4 - 1, -addr_step)) 468 self._collect_line_info(dso, real_path, 469 range(-addr_step * 5, -addr_step * 128 - 1, -addr_step)) 470 471 def _check_debug_line_section(self, real_path): 472 return '.debug_line' in self.readelf.get_sections(real_path) 473 474 def _get_addr_step(self, real_path): 475 arch = self.readelf.get_arch(real_path) 476 if arch == 'arm64': 477 return 4 478 if arch == 'arm': 479 return 2 480 return 1 481 482 def _collect_line_info(self, dso, real_path, addr_shifts): 483 """ Use addr2line to get line info in a dso, with given addr shifts. """ 484 # 1. Collect addrs to send to addr2line. 485 addr_set = set() 486 for addr in dso.addrs: 487 addr_obj = dso.addrs[addr] 488 if addr_obj.source_lines: # already has source line, no need to search. 489 continue 490 for shift in addr_shifts: 491 # The addr after shift shouldn't change to another function. 492 shifted_addr = max(addr + shift, addr_obj.func_addr) 493 addr_set.add(shifted_addr) 494 if shifted_addr == addr_obj.func_addr: 495 break 496 if not addr_set: 497 return 498 addr_request = '\n'.join(['%x' % addr for addr in sorted(addr_set)]) 499 500 # 2. Use addr2line to collect line info. 501 try: 502 option = '-ai' + ('fC' if self.with_function_name else '') 503 subproc = subprocess.Popen([self.addr2line_path, option, '-e', real_path], 504 stdin=subprocess.PIPE, stdout=subprocess.PIPE) 505 (stdoutdata, _) = subproc.communicate(str_to_bytes(addr_request)) 506 stdoutdata = bytes_to_str(stdoutdata) 507 except OSError: 508 return 509 addr_map = {} 510 cur_line_list = None 511 need_function_name = self.with_function_name 512 cur_function_name = None 513 for line in stdoutdata.strip().split('\n'): 514 if line[:2] == '0x': 515 # a new address 516 cur_line_list = addr_map[int(line, 16)] = [] 517 elif need_function_name: 518 cur_function_name = line.strip() 519 need_function_name = False 520 else: 521 need_function_name = self.with_function_name 522 # a file:line. 523 if cur_line_list is None: 524 continue 525 # Handle lines like "C:\Users\...\file:32". 526 items = line.rsplit(':', 1) 527 if len(items) != 2: 528 continue 529 if '?' in line: 530 # if ? in line, it doesn't have a valid line info. 531 # An addr can have a list of (file, line), when the addr belongs to an inlined 532 # function. Sometimes only part of the list has ? mark. In this case, we think 533 # the line info is valid if the first line doesn't have ? mark. 534 if not cur_line_list: 535 cur_line_list = None 536 continue 537 (file_path, line_number) = items 538 line_number = line_number.split()[0] # Remove comments after line number 539 try: 540 line_number = int(line_number) 541 except ValueError: 542 continue 543 file_id = self._get_file_id(file_path) 544 if self.with_function_name: 545 func_id = self._get_func_id(cur_function_name) 546 cur_line_list.append((file_id, line_number, func_id)) 547 else: 548 cur_line_list.append((file_id, line_number)) 549 550 # 3. Fill line info in dso.addrs. 551 for addr in dso.addrs: 552 addr_obj = dso.addrs[addr] 553 if addr_obj.source_lines: 554 continue 555 for shift in addr_shifts: 556 shifted_addr = max(addr + shift, addr_obj.func_addr) 557 lines = addr_map.get(shifted_addr) 558 if lines: 559 addr_obj.source_lines = lines 560 break 561 if shifted_addr == addr_obj.func_addr: 562 break 563 564 def _get_file_id(self, file_path): 565 file_id = self.file_name_to_id.get(file_path) 566 if file_id is None: 567 file_id = self.file_name_to_id[file_path] = len(self.file_id_to_name) 568 self.file_id_to_name.append(file_path) 569 return file_id 570 571 def _get_func_id(self, func_name): 572 func_id = self.func_name_to_id.get(func_name) 573 if func_id is None: 574 func_id = self.func_name_to_id[func_name] = len(self.func_id_to_name) 575 self.func_id_to_name.append(func_name) 576 return func_id 577 578 def get_dso(self, dso_path): 579 return self.dso_map.get(dso_path) 580 581 def get_addr_source(self, dso, addr): 582 source = dso.addrs[addr].source_lines 583 if source is None: 584 return None 585 if self.with_function_name: 586 return [(self.file_id_to_name[file_id], line, self.func_id_to_name[func_id]) 587 for (file_id, line, func_id) in source] 588 return [(self.file_id_to_name[file_id], line) for (file_id, line) in source] 589 590 591class SourceFileSearcher(object): 592 """ Find source file paths in the file system. 593 The file paths reported by addr2line are the paths stored in debug sections 594 of shared libraries. And we need to convert them to file paths in the file 595 system. It is done in below steps: 596 1. Collect all file paths under the provided source_dirs. The suffix of a 597 source file should contain one of below: 598 h: for C/C++ header files. 599 c: for C/C++ source files. 600 java: for Java source files. 601 kt: for Kotlin source files. 602 2. Given an abstract_path reported by addr2line, select the best real path 603 as below: 604 2.1 Find all real paths with the same file name as the abstract path. 605 2.2 Select the real path having the longest common suffix with the abstract path. 606 """ 607 608 SOURCE_FILE_EXTS = {'.h', '.hh', '.H', '.hxx', '.hpp', '.h++', 609 '.c', '.cc', '.C', '.cxx', '.cpp', '.c++', 610 '.java', '.kt'} 611 612 @classmethod 613 def is_source_filename(cls, filename): 614 ext = os.path.splitext(filename)[1] 615 return ext in cls.SOURCE_FILE_EXTS 616 617 def __init__(self, source_dirs): 618 # Map from filename to a list of reversed directory path containing filename. 619 self.filename_to_rparents = {} 620 self._collect_paths(source_dirs) 621 622 def _collect_paths(self, source_dirs): 623 for source_dir in source_dirs: 624 for parent, _, file_names in os.walk(source_dir): 625 rparent = None 626 for file_name in file_names: 627 if self.is_source_filename(file_name): 628 rparents = self.filename_to_rparents.get(file_name) 629 if rparents is None: 630 rparents = self.filename_to_rparents[file_name] = [] 631 if rparent is None: 632 rparent = parent[::-1] 633 rparents.append(rparent) 634 635 def get_real_path(self, abstract_path): 636 abstract_path = abstract_path.replace('/', os.sep) 637 abstract_parent, file_name = os.path.split(abstract_path) 638 abstract_rparent = abstract_parent[::-1] 639 real_rparents = self.filename_to_rparents.get(file_name) 640 if real_rparents is None: 641 return None 642 best_matched_rparent = None 643 best_common_length = -1 644 for real_rparent in real_rparents: 645 length = len(os.path.commonprefix((real_rparent, abstract_rparent))) 646 if length > best_common_length: 647 best_common_length = length 648 best_matched_rparent = real_rparent 649 if best_matched_rparent is None: 650 return None 651 return os.path.join(best_matched_rparent[::-1], file_name) 652 653 654class Objdump(object): 655 """ A wrapper of objdump to disassemble code. """ 656 def __init__(self, ndk_path, binary_cache_path): 657 self.ndk_path = ndk_path 658 self.binary_cache_path = binary_cache_path 659 self.readelf = ReadElf(ndk_path) 660 self.objdump_paths = {} 661 662 def get_dso_info(self, dso_path): 663 real_path = find_real_dso_path(dso_path, self.binary_cache_path) 664 if not real_path: 665 return None 666 arch = self.readelf.get_arch(real_path) 667 if arch == 'unknown': 668 return None 669 return (real_path, arch) 670 671 def disassemble_code(self, dso_info, start_addr, addr_len): 672 """ Disassemble [start_addr, start_addr + addr_len] of dso_path. 673 Return a list of pair (disassemble_code_line, addr). 674 """ 675 real_path, arch = dso_info 676 objdump_path = self.objdump_paths.get(arch) 677 if not objdump_path: 678 objdump_path = find_tool_path('objdump', self.ndk_path, arch) 679 if not objdump_path: 680 log_exit("Can't find objdump. Please set ndk path with --ndk_path option.") 681 self.objdump_paths[arch] = objdump_path 682 683 # 3. Run objdump. 684 args = [objdump_path, '-dlC', '--no-show-raw-insn', 685 '--start-address=0x%x' % start_addr, 686 '--stop-address=0x%x' % (start_addr + addr_len), 687 real_path] 688 try: 689 subproc = subprocess.Popen(args, stdin=subprocess.PIPE, stdout=subprocess.PIPE) 690 (stdoutdata, _) = subproc.communicate() 691 stdoutdata = bytes_to_str(stdoutdata) 692 except OSError: 693 return None 694 695 if not stdoutdata: 696 return None 697 result = [] 698 for line in stdoutdata.split('\n'): 699 line = line.rstrip() # Remove '\r' on Windows. 700 items = line.split(':', 1) 701 try: 702 addr = int(items[0], 16) 703 except ValueError: 704 addr = 0 705 result.append((line, addr)) 706 return result 707 708 709class ReadElf(object): 710 """ A wrapper of readelf. """ 711 def __init__(self, ndk_path): 712 self.readelf_path = find_tool_path('readelf', ndk_path) 713 if not self.readelf_path: 714 log_exit("Can't find readelf. Please set ndk path with --ndk_path option.") 715 716 def get_arch(self, elf_file_path): 717 """ Get arch of an elf file. """ 718 if is_elf_file(elf_file_path): 719 try: 720 output = subprocess.check_output([self.readelf_path, '-h', elf_file_path]) 721 output = bytes_to_str(output) 722 if output.find('AArch64') != -1: 723 return 'arm64' 724 if output.find('ARM') != -1: 725 return 'arm' 726 if output.find('X86-64') != -1: 727 return 'x86_64' 728 if output.find('80386') != -1: 729 return 'x86' 730 except subprocess.CalledProcessError: 731 pass 732 return 'unknown' 733 734 def get_build_id(self, elf_file_path): 735 """ Get build id of an elf file. """ 736 if is_elf_file(elf_file_path): 737 try: 738 output = subprocess.check_output([self.readelf_path, '-n', elf_file_path]) 739 output = bytes_to_str(output) 740 result = re.search(r'Build ID:\s*(\S+)', output) 741 if result: 742 build_id = result.group(1) 743 if len(build_id) < 40: 744 build_id += '0' * (40 - len(build_id)) 745 else: 746 build_id = build_id[:40] 747 build_id = '0x' + build_id 748 return build_id 749 except subprocess.CalledProcessError: 750 pass 751 return "" 752 753 def get_sections(self, elf_file_path): 754 """ Get sections of an elf file. """ 755 section_names = [] 756 if is_elf_file(elf_file_path): 757 try: 758 output = subprocess.check_output([self.readelf_path, '-SW', elf_file_path]) 759 output = bytes_to_str(output) 760 for line in output.split('\n'): 761 # Parse line like:" [ 1] .note.android.ident NOTE 0000000000400190 ...". 762 result = re.search(r'^\s+\[\s*\d+\]\s(.+?)\s', line) 763 if result: 764 section_name = result.group(1).strip() 765 if section_name: 766 section_names.append(section_name) 767 except subprocess.CalledProcessError: 768 pass 769 return section_names 770 771def extant_dir(arg): 772 """ArgumentParser type that only accepts extant directories. 773 774 Args: 775 arg: The string argument given on the command line. 776 Returns: The argument as a realpath. 777 Raises: 778 argparse.ArgumentTypeError: The given path isn't a directory. 779 """ 780 path = os.path.realpath(arg) 781 if not os.path.isdir(path): 782 raise argparse.ArgumentTypeError('{} is not a directory.'.format(path)) 783 return path 784 785def extant_file(arg): 786 """ArgumentParser type that only accepts extant files. 787 788 Args: 789 arg: The string argument given on the command line. 790 Returns: The argument as a realpath. 791 Raises: 792 argparse.ArgumentTypeError: The given path isn't a file. 793 """ 794 path = os.path.realpath(arg) 795 if not os.path.isfile(path): 796 raise argparse.ArgumentTypeError('{} is not a file.'.format(path)) 797 return path 798 799logging.getLogger().setLevel(logging.DEBUG) 800