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

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

Powered by Google App Engine
This is Rietveld 408576698