• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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