mirror of
https://github.com/flutter/flutter.git
synced 2026-02-20 02:29:02 +08:00
274 lines
9.8 KiB
Python
274 lines
9.8 KiB
Python
# Copyright 2015 The Chromium Authors. All rights reserved.
|
|
# Use of this source code is governed by a BSD-style license that can be
|
|
# found in the LICENSE file.
|
|
|
|
"""
|
|
Manages a debugging session with GDB.
|
|
|
|
This module is meant to be imported from inside GDB. Once loaded, the
|
|
|DebugSession| attaches GDB to a running Mojo Shell process on an Android
|
|
device using a remote gdbserver.
|
|
|
|
At startup and each time the execution stops, |DebugSession| associates
|
|
debugging symbols for every frame. For more information, see |DebugSession|
|
|
documentation.
|
|
"""
|
|
|
|
import gdb
|
|
import glob
|
|
import itertools
|
|
import logging
|
|
import os
|
|
import os.path
|
|
import shutil
|
|
import subprocess
|
|
import sys
|
|
import tempfile
|
|
import traceback
|
|
import urllib2
|
|
|
|
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
|
import android_gdb.config as config
|
|
from android_gdb.remote_file_connection import RemoteFileConnection
|
|
from android_gdb.signatures import get_signature
|
|
|
|
|
|
logging.getLogger().setLevel(logging.INFO)
|
|
|
|
|
|
def _gdb_execute(command):
|
|
"""Executes a GDB command."""
|
|
return gdb.execute(command, to_string=True)
|
|
|
|
|
|
class Mapping(object):
|
|
"""Represents a mapped memory region."""
|
|
def __init__(self, line):
|
|
self.start = int(line[0], 16)
|
|
self.end = int(line[1], 16)
|
|
self.size = int(line[2], 16)
|
|
self.offset = int(line[3], 16)
|
|
self.filename = line[4]
|
|
|
|
|
|
def _get_mapped_files():
|
|
"""Retrieves all the files mapped into the debugged process memory.
|
|
|
|
Returns:
|
|
List of mapped memory regions grouped by files.
|
|
"""
|
|
# info proc map returns a space-separated table with the following fields:
|
|
# start address, end address, size, offset, file path.
|
|
mappings = [Mapping(x) for x in
|
|
[x.split() for x in
|
|
_gdb_execute("info proc map").split('\n')]
|
|
if len(x) == 5 and x[4][0] == '/']
|
|
res = {}
|
|
for m in mappings:
|
|
libname = m.filename[m.filename.rfind('/') + 1:]
|
|
res[libname] = res.get(libname, []) + [m]
|
|
return res.values()
|
|
|
|
|
|
class DebugSession(object):
|
|
def __init__(self, build_directory, package_name, pyelftools_dir=None,
|
|
adb='adb'):
|
|
self._build_directory = build_directory
|
|
if not os.path.exists(self._build_directory):
|
|
logging.fatal("Please pass a valid build directory")
|
|
sys.exit(1)
|
|
self._package_name = package_name
|
|
self._adb = adb
|
|
self._remote_file_cache = os.path.join(os.getenv('HOME'), '.mojosymbols')
|
|
|
|
if pyelftools_dir != None:
|
|
sys.path.append(pyelftools_dir)
|
|
try:
|
|
import elftools.elf.elffile as elffile
|
|
except ImportError:
|
|
logging.fatal("Unable to find elftools module; please install it "
|
|
"(for exmple, using 'pip install elftools')")
|
|
sys.exit(1)
|
|
|
|
self._elffile_module = elffile
|
|
|
|
self._libraries = self._find_libraries(build_directory)
|
|
self._rfc = RemoteFileConnection('localhost', 10000)
|
|
self._remote_file_reader_process = None
|
|
if not os.path.exists(self._remote_file_cache):
|
|
os.makedirs(self._remote_file_cache)
|
|
self._done_mapping = set()
|
|
self._downloaded_files = []
|
|
|
|
def __del__(self):
|
|
# Note that, per python interpreter documentation, __del__ is not
|
|
# guaranteed to be called when the interpreter (GDB, in our case) quits.
|
|
# Also, most (all?) globals are no longer available at this time (launching
|
|
# a subprocess does not work).
|
|
self.stop()
|
|
|
|
def stop(self, _unused_return_value=None):
|
|
if self._remote_file_reader_process != None:
|
|
self._remote_file_reader_process.kill()
|
|
|
|
def _find_libraries(self, lib_dir):
|
|
"""Finds all libraries in |lib_dir| and key them by their signatures.
|
|
"""
|
|
res = {}
|
|
for fn in glob.glob('%s/*.so' % lib_dir):
|
|
with open(fn, 'r') as f:
|
|
s = get_signature(f, self._elffile_module)
|
|
if s is not None:
|
|
res[s] = fn
|
|
return res
|
|
|
|
def _associate_symbols(self, mapping, local_file):
|
|
with open(local_file, "r") as f:
|
|
elf = self._elffile_module.ELFFile(f)
|
|
s = elf.get_section_by_name(".text")
|
|
text_address = mapping[0].start + s['sh_offset']
|
|
_gdb_execute("add-symbol-file %s 0x%x" % (local_file, text_address))
|
|
|
|
def _download_file(self, signature, remote):
|
|
"""Downloads a remote file either from the cloud or through GDB connection.
|
|
|
|
Returns:
|
|
The filename of the downloaded file
|
|
"""
|
|
temp_file = tempfile.NamedTemporaryFile()
|
|
logging.info("Trying to download symbols from the cloud.")
|
|
symbols_url = "http://storage.googleapis.com/mojo/symbols/%s" % signature
|
|
try:
|
|
symbol_file = urllib2.urlopen(symbols_url)
|
|
try:
|
|
with open(temp_file.name, "w") as dst:
|
|
shutil.copyfileobj(symbol_file, dst)
|
|
logging.info("Getting symbols for %s at %s." % (remote, symbols_url))
|
|
# This allows the deletion of temporary files on disk when the
|
|
# debugging session terminates.
|
|
self._downloaded_files.append(temp_file)
|
|
return temp_file.name
|
|
finally:
|
|
symbol_file.close()
|
|
except urllib2.HTTPError:
|
|
pass
|
|
logging.info("Downloading file %s" % remote)
|
|
_gdb_execute("remote get %s %s" % (remote, temp_file.name))
|
|
# This allows the deletion of temporary files on disk when the debugging
|
|
# session terminates.
|
|
self._downloaded_files.append(temp_file)
|
|
return temp_file.name
|
|
|
|
def _find_mapping_for_address(self, mappings, address):
|
|
"""Returns the list of all mappings of the file occupying the |address|
|
|
memory address.
|
|
"""
|
|
for file_mappings in mappings:
|
|
for mapping in file_mappings:
|
|
if address >= mapping.start and address <= mapping.end:
|
|
return file_mappings
|
|
return None
|
|
|
|
def _try_to_map(self, mapping):
|
|
remote_file = mapping[0].filename
|
|
if remote_file in self._done_mapping:
|
|
return False
|
|
self._done_mapping.add(remote_file)
|
|
self._rfc.open(remote_file)
|
|
signature = get_signature(self._rfc, self._elffile_module)
|
|
if signature is not None:
|
|
if signature in self._libraries:
|
|
self._associate_symbols(mapping, self._libraries[signature])
|
|
else:
|
|
# This library file is not known locally. Download it from the device or
|
|
# the cloud and put it in cache so, if it got symbols, we can see them.
|
|
local_file = os.path.join(self._remote_file_cache, signature)
|
|
if not os.path.exists(local_file):
|
|
tmp_output = self._download_file(signature, remote_file)
|
|
shutil.move(tmp_output, local_file)
|
|
self._associate_symbols(mapping, local_file)
|
|
return True
|
|
return False
|
|
|
|
def _update_symbols(self):
|
|
"""Updates the mapping between symbols as seen from GDB and local library
|
|
files."""
|
|
logging.info("Updating symbols")
|
|
mapped_files = _get_mapped_files()
|
|
_gdb_execute("info threads")
|
|
nb_threads = len(_gdb_execute("info threads").split("\n")) - 2
|
|
# Map all symbols from native libraries packages with the APK.
|
|
for file_mappings in mapped_files:
|
|
filename = file_mappings[0].filename
|
|
if ((filename.startswith('/data/data/') or
|
|
filename.startswith('/data/app')) and
|
|
not filename.endswith('.apk') and
|
|
not filename.endswith('.dex')):
|
|
logging.info('Pre-mapping: %s' % file_mappings[0].filename)
|
|
self._try_to_map(file_mappings)
|
|
for i in xrange(nb_threads):
|
|
try:
|
|
_gdb_execute("thread %d" % (i + 1))
|
|
frame = gdb.newest_frame()
|
|
while frame and frame.is_valid():
|
|
if frame.name() is None:
|
|
m = self._find_mapping_for_address(mapped_files, frame.pc())
|
|
if m is not None and self._try_to_map(m):
|
|
# Force gdb to recompute its frames.
|
|
_gdb_execute("info threads")
|
|
frame = gdb.newest_frame()
|
|
assert frame.is_valid()
|
|
if (frame.older() is not None and
|
|
frame.older().is_valid() and
|
|
frame.older().pc() != frame.pc()):
|
|
frame = frame.older()
|
|
else:
|
|
frame = None
|
|
except gdb.error:
|
|
traceback.print_exc()
|
|
|
|
def _get_device_application_pid(self, application):
|
|
"""Gets the PID of an application running on a device."""
|
|
output = subprocess.check_output([self._adb, 'shell', 'ps'])
|
|
for line in output.split('\n'):
|
|
elements = line.split()
|
|
if len(elements) > 0 and elements[-1] == application:
|
|
return elements[1]
|
|
return None
|
|
|
|
def start(self):
|
|
"""Starts a debugging session."""
|
|
gdbserver_pid = self._get_device_application_pid('gdbserver')
|
|
if gdbserver_pid is not None:
|
|
subprocess.check_call([self._adb, 'shell', 'kill', gdbserver_pid])
|
|
shell_pid = self._get_device_application_pid(self._package_name)
|
|
if shell_pid is None:
|
|
raise Exception('Unable to find a running mojo shell.')
|
|
subprocess.check_call([self._adb, 'forward', 'tcp:9999', 'tcp:9999'])
|
|
subprocess.Popen(
|
|
[self._adb, 'shell', 'gdbserver', '--attach', ':9999', shell_pid],
|
|
# os.setpgrp ensures signals passed to this file (such as SIGINT) are
|
|
# not propagated to child processes.
|
|
preexec_fn = os.setpgrp)
|
|
|
|
# Kill stray remote reader processes. See __del__ comment for more info.
|
|
remote_file_reader_pid = self._get_device_application_pid(
|
|
config.REMOTE_FILE_READER_DEVICE_PATH)
|
|
if remote_file_reader_pid is not None:
|
|
subprocess.check_call([self._adb, 'shell', 'kill',
|
|
remote_file_reader_pid])
|
|
self._remote_file_reader_process = subprocess.Popen(
|
|
[self._adb, 'shell', config.REMOTE_FILE_READER_DEVICE_PATH],
|
|
stdout=subprocess.PIPE, preexec_fn = os.setpgrp)
|
|
port = int(self._remote_file_reader_process.stdout.readline())
|
|
subprocess.check_call([self._adb, 'forward', 'tcp:10000', 'tcp:%d' % port])
|
|
self._rfc.connect()
|
|
|
|
_gdb_execute('target remote localhost:9999')
|
|
|
|
self._update_symbols()
|
|
def on_stop(_):
|
|
self._update_symbols()
|
|
gdb.events.stop.connect(on_stop)
|
|
gdb.events.exited.connect(self.stop)
|