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

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, 6 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 import gdb
6 import glob
7 import itertools
8 import os
9 import os.path
10 import re
ppi 2015/06/24 15:14:07 Unused.
etiennej 2015/06/29 15:41:42 Done.
11 import shutil
12 import socket
ppi 2015/06/24 15:14:08 Unused?
etiennej 2015/06/29 15:41:42 Done.
13 import struct
ppi 2015/06/24 15:14:08 Unused.
etiennej 2015/06/29 15:41:42 Done.
14 import subprocess
15 import sys
16 import tempfile
17 import time
18
19
20 sys.path.append(os.path.dirname(os.path.abspath(__file__)))
21 import config
22 from remote_file_connection import RemoteFileConnection
23
24
25 def _GetDirAbove(dirname):
26 """Returns the directory "above" this file containing |dirname|."""
27 path = os.path.abspath(__file__)
28 while True:
29 path, _ = os.path.split(path)
30 assert path
31 if dirname in os.listdir(path):
32 return path
33
34
35 def gdb_execute(command):
36 """Execute a GDB command."""
ppi 2015/06/24 15:14:08 s/Execute/Executes/
etiennej 2015/06/29 15:41:42 Done.
37 return gdb.execute(command, to_string=True)
38
39
40 class Mapping(object):
41 def __init__(self, line):
42 self.start = int(line[0], 16)
43 self.end = int(line[1], 16)
44 self.size = int(line[2], 16)
45 self.offset = int(line[3], 16)
46 self.filename = line[4]
47
48
49 def get_mapped_files():
50 mappings = [Mapping(x) for x in
51 [x.split() for x in
52 gdb_execute("info proc map").split('\n')]
53 if len(x) == 5 and x[4][0] == '/']
54 res = {}
55 for m in mappings:
56 libname = m.filename[m.filename.rfind('/') + 1:]
57 res[libname] = res.get(libname, []) + [m]
58 return res.values()
59
60
61 class DebugSession(object):
62 def __init__(self,
63 build_directory=os.path.join(
64 _GetDirAbove('out'), 'out', 'android_Debug'),
65 package_name='org.chromium.mojo.shell',
66 adb='adb',
67 third_party_dir=os.path.join(
68 _GetDirAbove('third_party'), 'third_party')):
69 self.build_directory = build_directory
70 self.package_name = package_name
71 self.adb = adb
72 self.remote_file_cache = os.path.join(os.getenv('HOME'), '.mojosymbols')
73
74 sys.path.append(os.path.join(third_party_dir, 'pyelftools'))
ppi 2015/06/24 15:14:08 devtools are meant to be consumed in other reposit
etiennej 2015/06/29 15:41:42 Done.
75 import elftools.elf.elffile as elffile
76 self._elffile = elffile
77
78 self.libraries = self._find_libraries(build_directory)
79 self.rfc = RemoteFileConnection('localhost', 10000)
80 if not os.path.exists(self.remote_file_cache):
81 os.makedirs(self.remote_file_cache)
82 self.done_mapping = set()
83 self._downloaded_files = []
84
85 def __del__(self):
86 gdbserver_pid = self._get_pid('gdbserver')
87 if gdbserver_pid is not None:
88 subprocess.check_call([self.adb, 'shell', 'kill', gdbserver_pid])
89 remote_file_reader_pid = self._get_pid('remote_file_reader')
90 if remote_file_reader_pid is not None:
91 subprocess.check_call([self.adb, 'shell', 'kill', remote_file_reader_pid])
92
93 def _find_libraries(self, lib_dir):
94 res = {}
95 for fn in glob.glob('%s/*.so' % lib_dir):
96 with open(fn, 'r') as f:
97 s = self._get_signature(f)
98 if s is not None:
99 res[s] = fn
100 return res
101
102
ppi 2015/06/24 15:14:08 Single blank lines between class members, please.
etiennej 2015/06/29 15:41:42 Done.
103 def _get_signature(self, file_object):
104 """Compute a unique signature of a library file."""
105 elf = self._elffile.ELFFile(file_object)
106 text_section = elf.get_section_by_name('.text')
107 file_object.seek(text_section['sh_offset'])
108 data = file_object.read(min(4096, text_section['sh_size']))
109 def combine((i, c)):
110 return i ^ ord(c)
111 result = 16 * [ 0 ]
ppi 2015/06/24 15:14:08 Remove spaces around "0".
etiennej 2015/06/29 15:41:42 Done.
112 for i in xrange(0, len(data), len(result)):
113 result = map(combine,
114 itertools.izip_longest(result,
115 data[i:i+len(result)],
ppi 2015/06/24 15:14:08 Add spaces around "+".
etiennej 2015/06/29 15:41:42 Done.
116 fillvalue='\0'))
117 return ''.join(["%02x" % x for x in result])
118
119 def _associate_symbols(self, mapping, local_file):
120 with open(local_file, "r") as f:
121 elf = self._elffile.ELFFile(f)
122 s = elf.get_section_by_name(".text")
123 text_address = mapping[0].start + s['sh_offset']
124 gdb_execute("add-symbol-file %s 0x%x" % (local_file, text_address))
125
126 def _download_file(self, remote):
127 """Download a remote file through GDB connection.
ppi 2015/06/24 15:14:08 s/Download/Downloads/
etiennej 2015/06/29 15:41:42 Done.
128
129 Returns the filename
ppi 2015/06/24 15:14:08 The format is: """ Returns: What is being return
etiennej 2015/06/29 15:41:42 Done.
130 """
131 temp_file = tempfile.NamedTemporaryFile()
132 gdb_execute("remote get %s %s" % (remote, temp_file.name))
133 # This allows the deletion of temporary files on disk when the debugging
134 # session terminates.
135 self._downloaded_files.append(temp_file)
136 return temp_file.name
137
138 def _download_and_associate_symbol(self, mapping):
139 self._associate_symbols(mapping, self._download_file(mapping[0].filename))
140
141 def _find_mapping_for_address(self, mappings, address):
142 for m in mappings:
143 for s in m:
144 if address >= s.start and address <= s.end:
145 return m
146 return None
147
148 def _try_to_map(self, mapping):
149 remote_file = mapping[0].filename
150 if remote_file in self.done_mapping:
151 return False
152 self.done_mapping.add(remote_file)
153 self.rfc.open(remote_file)
154 s = self._get_signature(self.rfc)
155 if s is not None:
156 if s in self.libraries:
157 self._associate_symbols(mapping, self.libraries[s])
158 else:
159 local_file = os.path.join(self.remote_file_cache, s)
160 if not os.path.exists(local_file):
161 tmp_output = self._download_file(remote_file)
162 shutil.move(tmp_output, local_file)
163 self._associate_symbols(mapping, local_file)
164 return True
165 return False
166
167 def _update_symbols(self):
168 """Update the mapping between symbols as seen from GDB and local library
ppi 2015/06/24 15:14:08 s/Update/Updates/
etiennej 2015/06/29 15:41:42 Done.
169 files."""
170 mapped_files = get_mapped_files()
171 gdb_execute("info threads")
172 nb_threads = len(gdb_execute("info threads").split("\n")) - 2
173 # Map all symbols in /data/
174 for m in mapped_files:
175 filename = m[0].filename
176 if ((filename.startswith('/data/data/') or
177 filename.startswith('/data/app')) and
178 not filename.endswith('.apk') and
179 not filename.endswith('.dex')):
180 print 'Pre-mapping: %s' % m[0].filename
181 self._try_to_map(m)
182 for i in xrange(nb_threads):
183 try:
184 gdb_execute("thread %d" % (i + 1))
185 frame = gdb.newest_frame()
186 while frame and frame.is_valid():
187 if frame.name() is None:
188 m = self._find_mapping_for_address(mapped_files, frame.pc())
189 if m is not None and self._try_to_map(m):
190 # Force gdb to recompute its frames.
191 gdb_execute("info threads")
192 frame = gdb.newest_frame()
193 assert frame.is_valid()
194 if (frame.older() is not None and
195 frame.older().is_valid() and
196 frame.older().pc() != frame.pc()):
197 frame = frame.older()
198 else:
199 frame = None
200 except gdb.error as _:
201 import traceback
202 traceback.print_exc()
203
204
ppi 2015/06/24 15:14:08 Just one blank line.
etiennej 2015/06/29 15:41:42 Done.
205 def _get_pid(self, application):
206 """Get the PID of an application running on a device."""
ppi 2015/06/24 15:14:08 s/Get/Gets/
etiennej 2015/06/29 15:41:42 Done.
207 output = subprocess.check_output([self.adb, 'shell', 'ps'])
208 for line in output.split('\n'):
209 elements = line.split()
210 if len(elements) > 0 and elements[-1] == application:
211 return elements[1]
212 return None
213
214 def start_debug(self):
215 """Start a debugging session."""
ppi 2015/06/24 15:14:07 s/Start/Starts/
etiennej 2015/06/29 15:41:42 Done.
216 gdbserver_pid = self._get_pid('gdbserver')
217 if gdbserver_pid is not None:
218 subprocess.check_call([self.adb, 'shell', 'kill', gdbserver_pid])
219 shell_pid = self._get_pid(self.package_name)
220 if shell_pid is None:
221 raise Exception('Unable to find a running mojo shell.')
222 subprocess.check_call([self.adb, 'forward', 'tcp:9999', 'tcp:9999'])
223 subprocess.Popen(
224 [self.adb, 'shell', 'gdbserver', '--attach', ':9999', shell_pid])
225 time.sleep(0.1)
226
227 read_process = subprocess.Popen(
228 [self.adb, 'shell', config.REMOTE_FILE_READER_DEVICE_PATH],
229 stdout=subprocess.PIPE)
230 port = int(read_process.stdout.readline())
231 subprocess.check_call([self.adb, 'forward', 'tcp:10000', 'tcp:%d' % port])
232 self.rfc.connect()
233
234 gdb_execute('target remote localhost:9999')
235
236 self._update_symbols()
237 def on_stop(_):
238 self._update_symbols()
239 gdb.events.stop.connect(on_stop)
240
ppi 2015/06/24 15:14:08 Remove the trailing blank lines.
etiennej 2015/06/29 15:41:42 Done.
241
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698