Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(12)

Side by Side Diff: mojo/devtools/common/android_gdb/session.py

Issue 1209593002: GDB support for Android in devtools' debugger. (Closed) Base URL: git@github.com:domokit/mojo.git@master
Patch Set: Kill stray remote file reader Created 5 years, 5 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
OLDNEW
(Empty)
1 # Copyright 2015 The Chromium Authors. All rights reserved.
2 # Use of this source code is governed by a BSD-style license that can be
3 # found in the LICENSE file.
ppi 2015/07/15 13:54:18 Add a blank line after the license.
etiennej 2015/07/15 14:43:03 Done.
4 """
5 Manages a debugging session with GDB.
6
7 This module is meant to be imported from inside GDB. Once loaded, the
8 |DebugSession| attaches GDB to a running Mojo Shell process on an Android
9 device using a remote gdbserver.
10
11 At startup and each time the execution stops, |DebugSession| associates
12 debugging symbols for every frame. For more information, see |DebugSession|
13 documentation.
14 """
15
16 import logging
17 import gdb
18 import glob
19 import itertools
20 import os
21 import os.path
22 import shutil
23 import subprocess
24 import sys
25 import tempfile
26 import time
ppi 2015/07/15 13:54:18 Unused?
etiennej 2015/07/15 14:43:04 Done.
27
28 sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
29 import android_gdb.config as config
30 from android_gdb.remote_file_connection import RemoteFileConnection
31
32 logging.getLogger().setLevel(logging.INFO)
33
ppi 2015/07/15 13:54:18 Two blank lines around top-level definitions pleas
etiennej 2015/07/15 14:43:03 Done.
34 def _gdb_execute(command):
35 """Executes a GDB command."""
36 return gdb.execute(command, to_string=True)
37
ppi 2015/07/15 13:54:18 Here too.
etiennej 2015/07/15 14:43:04 Done.
38 def get_signature(file_object, elffile_module):
ppi 2015/07/15 13:54:18 Is it used outside this module? If not, _get_signa
etiennej 2015/07/15 14:43:04 qsr@ wants to use it elsewhere, yes.
39 """Computes a unique signature of a library file.
40
41 We only hash the .text section of the library in order to make the hash
42 resistant to stripping (we want the same hash for the same library with debug
43 symbols kept or stripped).
44 """
45 elf = elffile_module.ELFFile(file_object)
46 text_section = elf.get_section_by_name('.text')
47 file_object.seek(text_section['sh_offset'])
48 data = file_object.read(min(4096, text_section['sh_size']))
49 def combine((i, c)):
50 return i ^ ord(c)
51 result = 16 * [0]
52 for i in xrange(0, len(data), len(result)):
53 result = map(combine,
54 itertools.izip_longest(result,
55 data[i:i + len(result)],
56 fillvalue='\0'))
57 return ''.join(["%02x" % x for x in result])
58
59
60 class Mapping(object):
61 """Mapping represents a mapped memory region."""
ppi 2015/07/15 13:54:18 s/Mapping represents/Represents/
etiennej 2015/07/15 14:43:04 Done.
62 def __init__(self, line):
63 self.start = int(line[0], 16)
64 self.end = int(line[1], 16)
65 self.size = int(line[2], 16)
66 self.offset = int(line[3], 16)
67 self.filename = line[4]
68
69
70 def _get_mapped_files():
71 """Retrieve all the files mapped into the debugged process memory.
ppi 2015/07/15 13:54:18 Retrieves
etiennej 2015/07/15 14:43:04 Done.
72
73 Returns:
74 List of mapped memory regions grouped by files.
ppi 2015/07/15 13:54:18 Just two leading spaces.
etiennej 2015/07/15 14:43:04 Done.
75 """
76 # info proc map returns a space-separated table with the following fields:
77 # start address, end address, size, offset, file path
ppi 2015/07/15 13:54:18 End comment with a period.
etiennej 2015/07/15 14:43:04 Done.
78 mappings = [Mapping(x) for x in
79 [x.split() for x in
80 _gdb_execute("info proc map").split('\n')]
81 if len(x) == 5 and x[4][0] == '/']
82 res = {}
83 for m in mappings:
84 libname = m.filename[m.filename.rfind('/') + 1:]
85 res[libname] = res.get(libname, []) + [m]
86 return res.values()
87
88
89 class DebugSession(object):
ppi 2015/07/15 13:54:18 This now specific to a remote gdb server on the de
etiennej 2015/07/15 14:43:03 Given the amount of android-specific stuff compare
90 def __init__(self, build_directory, package_name, pyelftools_dir=None,
91 adb='adb'):
92 self._build_directory = build_directory
93 if not os.path.exists(self._build_directory):
94 logging.fatal("Please pass a valid build directory")
95 sys.exit(1)
96 self._package_name = package_name
97 self._adb = adb
98 self._remote_file_cache = os.path.join(os.getenv('HOME'), '.mojosymbols')
99
100
ppi 2015/07/15 13:54:18 Too many blank lines.
etiennej 2015/07/15 14:43:04 Done.
101 if pyelftools_dir != None:
102 sys.path.append(pyelftools_dir)
103 try:
104 import elftools.elf.elffile as elffile
105 except ImportError:
106 logging.fatal("Unable to find elftools module; please install it "
107 "(for exmple, using 'pip install elftools')")
108 sys.exit(1)
109
110 self._elffile_module = elffile
111
112 self._libraries = self._find_libraries(build_directory)
113 self._rfc = RemoteFileConnection('localhost', 10000)
114 self._remote_file_reader_process = None
115 if not os.path.exists(self._remote_file_cache):
116 os.makedirs(self._remote_file_cache)
117 self._done_mapping = set()
118 self._downloaded_files = []
119
120 def __del__(self):
121 # Note that, per python interpreter documentation, __del__ is not
122 # guaranteed to be called when the interpreter (GDB, in our case) quits.
ppi 2015/07/15 13:54:18 Should we then install an at-exit handler?
etiennej 2015/07/15 14:43:04 Done.
123 # Also, most (all?) globals are no longer available at this time (launching
124 # a subprocess does not work).
125 if self._remote_file_reader_process != None:
ppi 2015/07/15 13:54:18 maybe "!= None" --> "is not None"
etiennej 2015/07/15 14:43:03 Done.
126 self._remote_file_reader_process.kill()
127
128 def _find_libraries(self, lib_dir):
129 """Finds all libraries in |lib_dir| and key them by their signatures.
130 """
131 res = {}
132 for fn in glob.glob('%s/*.so' % lib_dir):
133 with open(fn, 'r') as f:
134 s = get_signature(f, self._elffile_module)
135 if s is not None:
136 res[s] = fn
137 return res
138
139 def _associate_symbols(self, mapping, local_file):
140 with open(local_file, "r") as f:
141 elf = self._elffile_module.ELFFile(f)
142 s = elf.get_section_by_name(".text")
143 text_address = mapping[0].start + s['sh_offset']
144 _gdb_execute("add-symbol-file %s 0x%x" % (local_file, text_address))
145
146 def _download_file(self, remote):
147 """Downloads a remote file through GDB connection.
148
149 Returns:
150 The filename of the downloaded file
151 """
152 temp_file = tempfile.NamedTemporaryFile()
153 logging.info("Downloading file %s" % remote)
154 _gdb_execute("remote get %s %s" % (remote, temp_file.name))
155 # This allows the deletion of temporary files on disk when the debugging
156 # session terminates.
157 self._downloaded_files.append(temp_file)
158 return temp_file.name
159
160 def _download_and_associate_symbol(self, mapping):
161 self._associate_symbols(mapping, self._download_file(mapping[0].filename))
162
163 def _find_mapping_for_address(self, mappings, address):
164 """Returns the list of all mappings of the file occupying the |address|
165 memory address.
166 """
167 for file_mappings in mappings:
168 for mapping in file_mappings:
169 if address >= mapping.start and address <= mapping.end:
170 return file_mappings
171 return None
172
173 def _try_to_map(self, mapping):
174 remote_file = mapping[0].filename
175 if remote_file in self._done_mapping:
176 return False
177 self._done_mapping.add(remote_file)
178 self._rfc.open(remote_file)
179 signature = get_signature(self._rfc, self._elffile_module)
180 if signature is not None:
181 if signature in self._libraries:
182 self._associate_symbols(mapping, self._libraries[signature])
183 else:
184 # This library file is not known locally. Download it from the device
185 # and put it in cache so, if it got symbols, we can see them.
186 local_file = os.path.join(self._remote_file_cache, signature)
187 if not os.path.exists(local_file):
188 tmp_output = self._download_file(remote_file)
189 shutil.move(tmp_output, local_file)
190 self._associate_symbols(mapping, local_file)
191 return True
192 return False
193
194 def _update_symbols(self):
195 """Updates the mapping between symbols as seen from GDB and local library
196 files."""
197 logging.info("Updating symbols")
198 mapped_files = _get_mapped_files()
199 _gdb_execute("info threads")
200 nb_threads = len(_gdb_execute("info threads").split("\n")) - 2
201 # Map all symbols from native libraries packages with the APK.
202 for file_mappings in mapped_files:
203 filename = file_mappings[0].filename
204 if ((filename.startswith('/data/data/') or
205 filename.startswith('/data/app')) and
206 not filename.endswith('.apk') and
207 not filename.endswith('.dex')):
208 logging.info('Pre-mapping: %s' % file_mappings[0].filename)
209 self._try_to_map(file_mappings)
210 for i in xrange(nb_threads):
211 try:
212 _gdb_execute("thread %d" % (i + 1))
213 frame = gdb.newest_frame()
214 while frame and frame.is_valid():
215 if frame.name() is None:
216 m = self._find_mapping_for_address(mapped_files, frame.pc())
217 if m is not None and self._try_to_map(m):
218 # Force gdb to recompute its frames.
219 _gdb_execute("info threads")
220 frame = gdb.newest_frame()
221 assert frame.is_valid()
222 if (frame.older() is not None and
223 frame.older().is_valid() and
224 frame.older().pc() != frame.pc()):
225 frame = frame.older()
226 else:
227 frame = None
228 except gdb.error as _:
ppi 2015/07/15 13:54:18 do we need "as _"?
etiennej 2015/07/15 14:43:03 Done.
229 import traceback
ppi 2015/07/15 13:54:18 All imports at the top please.
etiennej 2015/07/15 14:43:04 Done.
230 traceback.print_exc()
231
232 def _get_device_application_pid(self, application):
233 """Gets the PID of an application running on a device."""
234 output = subprocess.check_output([self._adb, 'shell', 'ps'])
235 for line in output.split('\n'):
236 elements = line.split()
237 if len(elements) > 0 and elements[-1] == application:
238 return elements[1]
239 return None
240
241 def start(self):
242 """Starts a debugging session."""
243 gdbserver_pid = self._get_device_application_pid('gdbserver')
244 if gdbserver_pid is not None:
245 subprocess.check_call([self._adb, 'shell', 'kill', gdbserver_pid])
246 shell_pid = self._get_device_application_pid(self._package_name)
247 if shell_pid is None:
248 raise Exception('Unable to find a running mojo shell.')
249 subprocess.check_call([self._adb, 'forward', 'tcp:9999', 'tcp:9999'])
250 subprocess.Popen(
251 [self._adb, 'shell', 'gdbserver', '--attach', ':9999', shell_pid],
252 # os.setpgrp ensures signals passed to this file (such as SIGINT) are
253 # not propagated to child processes.
254 preexec_fn = os.setpgrp)
255
256 # Kill stray remote reader processes. See __del__ comment for more info.
257 remote_file_reader_pid = self._get_device_application_pid(
258 config.REMOTE_FILE_READER_DEVICE_PATH)
259 if remote_file_reader_pid is not None:
260 subprocess.check_call([self._adb, 'shell', 'kill',
261 remote_file_reader_pid])
262 self._remote_file_reader_process = subprocess.Popen(
263 [self._adb, 'shell', config.REMOTE_FILE_READER_DEVICE_PATH],
264 stdout=subprocess.PIPE, preexec_fn = os.setpgrp)
265 port = int(self._remote_file_reader_process.stdout.readline())
266 subprocess.check_call([self._adb, 'forward', 'tcp:10000', 'tcp:%d' % port])
267 self._rfc.connect()
268
269 _gdb_execute('target remote localhost:9999')
270
271 self._update_symbols()
272 def on_stop(_):
273 self._update_symbols()
274 gdb.events.stop.connect(on_stop)
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698