# # Copyright (C) 2012 The Android Open Source Project # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # # # GDB plugin to allow debugging of apps on remote Android systems using gdbserver. # # To use this plugin, source this file from a Python-enabled GDB client, then use: # load-android-app <app-source-dir> to tell GDB about the app you are debugging # run-android-app to start the app in a running state # start-android-app to start the app in a paused state # attach-android-ap to attach to an existing (running) instance of app # set-android-device to select a target (only if multiple devices are attached) import fnmatch import gdb import os import shutil import subprocess import tempfile import time be_verbose = False enable_renderscript_dumps = True local_symbols_library_directory = os.path.join(os.getenv('ANDROID_PRODUCT_OUT', 'out'), 'symbols', 'system', 'lib') local_library_directory = os.path.join(os.getenv('ANDROID_PRODUCT_OUT', 'out'), 'system', 'lib') # ADB - Basic ADB wrapper, far from complete # DebugAppInfo - App configuration struct, as far as GDB cares # StartAndroidApp - Implementation of GDB start (for android apps) # RunAndroidApp - Implementation of GDB run (for android apps) # AttachAndroidApp - GDB command to attach to an existing android app process # AndroidStatus - app status query command (not needed, mostly harmless) # LoadAndroidApp - Sets the package and intent names for an app def _interesting_libs(): return ['libc', 'libbcc', 'libRS', 'libandroid_runtime', 'libdvm'] # In python 2.6, subprocess.check_output does not exist, so it is implemented here def check_output(*popenargs, **kwargs): p = subprocess.Popen(stdout=subprocess.PIPE, stderr=subprocess.STDOUT, *popenargs, **kwargs) out, err = p.communicate() retcode = p.poll() if retcode != 0: c = kwargs.get("args") if c is None: c = popenargs[0] e = subprocess.CalledProcessError(retcode, c) e.output = str(out) + str(err) raise e return out class DebugAppInfo: """Stores information from an app manifest""" def __init__(self): self.name = None self.intent = None def get_name(self): return self.name def get_intent(self): return self.intent def get_data_directory(self): return self.data_directory def get_gdbserver_path(self): return os.path.join(self.data_directory, "lib", "gdbserver") def set_info(self, name, intent, data_directory): self.name = name self.intent = intent self.data_directory = data_directory def unset_info(): self.name = None self.intent = None self.data_directory = None class ADB: """ Python class implementing a basic ADB wrapper for useful commands. Uses subprocess to invoke adb. """ def __init__(self, device=None, verbose=False): self.verbose = verbose self.current_device = device self.temp_libdir = None self.background_processes = [] self.android_build_top = os.getenv('ANDROID_BUILD_TOP', None) if not self.android_build_top: raise gdb.GdbError("Unable to read ANDROID_BUILD_TOP. " \ + "Is your environment setup correct?") self.adb_path = os.path.join(self.android_build_top, 'out', 'host', 'linux-x86', 'bin', 'adb') if not self.current_device: devices = self.devices() if len(devices) == 1: self.set_current_device(devices[0]) return else: msg = "" if len(devices) == 0: msg = "No devices detected. Please connect a device and " else: msg = "Too many devices (" + ", ".join(devices) + ") detected. " \ + "Please " print "Warning: " + msg + " use the set-android-device command." def _prepare_adb_args(self, args): largs = list(args) # Prepare serial number option from current_device if self.current_device and len(self.current_device) > 0: largs.insert(0, self.current_device) largs.insert(0, "-s") largs.insert(0, self.adb_path) return largs def _background_adb(self, *args): largs = self._prepare_adb_args(args) p = None try: if self.verbose: print "### " + str(largs) p = subprocess.Popen(largs) self.background_processes.append(p) except CalledProcessError, e: raise gdb.GdbError("Error starting background adb " + str(largs)) except: raise gdb.GdbError("Unknown error starting background adb " + str(largs)) return p def _call_adb(self, *args): output = "" largs = self._prepare_adb_args(args) try: if self.verbose: print "### " + str(largs) output = check_output(largs) except subprocess.CalledProcessError, e: raise gdb.GdbError("Error starting adb " + str(largs)) except Exception as e: raise gdb.GdbError("Unknown error starting adb " + str(largs)) return output def _shell(self, *args): args = ["shell"] + list(args) return self._call_adb(*args) def _background_shell(self, *args): args = ["shell"] + list(args) return self._background_adb(*args) def _cleanup_background_processes(self): for handle in self.background_processes: try: handle.terminate() except OSError, e: # Background process died already pass def _cleanup_temp(self): if self.temp_libdir: shutil.rmtree(self.temp_libdir) self.temp_libdir = None def __del__(self): self._cleanup_temp() self._cleanup_background_processes() def _get_local_libs(self): ret = [] for lib in _interesting_libs(): lib_path = os.path.join(local_library_directory, lib + ".so") if not os.path.exists(lib_path) and self.verbose: print "Warning: unable to find expected library " \ + lib_path + "." ret.append(lib_path) return ret def _check_remote_libs_match_local_libs(self): ret = [] all_remote_libs = self._shell("ls", "/system/lib/*.so").split() local_libs = self._get_local_libs() self.temp_libdir = tempfile.mkdtemp() for lib in _interesting_libs(): lib += ".so" for remote_lib in all_remote_libs: if lib in remote_lib: # Pull lib from device and compute hash tmp_path = os.path.join(self.temp_libdir, lib) self.pull(remote_lib, tmp_path) remote_hash = self._md5sum(tmp_path) # Find local lib and compute hash built_library = filter(lambda l: lib in l, local_libs)[0] built_hash = self._md5sum(built_library) # Alert user if library mismatch is detected if built_hash != remote_hash: self._cleanup_temp() raise gdb.GdbError("Library mismatch between:\n" \ + "\t(" + remote_hash + ") " + tmp_path + " (from target) and\n " \ + "\t(" + built_hash + ") " + built_library + " (on host)\n" \ + "The target is running a different build than the host." \ + " This situation is not debuggable.") self._cleanup_temp() def _md5sum(self, file): try: return check_output(["md5sum", file]).strip().split()[0] except subprocess.CalledProcessError, e: raise gdb.GdbError("Error invoking md5sum commandline utility") # Returns the list of serial numbers of connected devices def devices(self): ret = [] raw_output = self._call_adb("devices").split() if len(raw_output) < 5: return None else: for serial_num_index in range(4, len(raw_output), 2): ret.append(raw_output[serial_num_index]) return ret def set_current_device(self, serial): if self.current_device == str(serial): print "Current device already is: " + str(serial) return # TODO: this function should probably check the serial is valid. self.current_device = str(serial) api_version = self.getprop("ro.build.version.sdk") if api_version < 15: print "Warning: untested API version. Upgrade to 15 or higher" # Verify the local libraries loaded by GDB are identical to those # sitting on the device actually executing. Alert the user if # this is happening self._check_remote_libs_match_local_libs() # adb getprop [property] # if property is not None, returns the given property, otherwise # returns all properties. def getprop(self, property=None): if property == None: # get all the props return self._call_adb(*["shell", "getprop"]).split('\n') else: return str(self._call_adb(*["shell", "getprop", str(property)]).split('\n')[0]) # adb push def push(self, source, destination): self._call_adb(*["push", source, destination]) # adb forward <source> <destination> def forward(self, source, destination): self._call_adb(*["forward", source, destination]) # Returns true if filename exists on Android fs, false otherwise def exists(self, filename): raw_listing = self._shell(*["ls", filename]) return "No such file or directory" not in raw_listing # adb pull <remote_path> <local_path> def pull(self, remote_path, local_path): self._call_adb(*["pull", remote_path, local_path]) #wrapper for adb shell ps. leave process_name=None for list of all processes #Otherwise, returns triple with process name, pid and owner, def get_process_info(self, process_name=None): ret = [] raw_output = self._shell("ps") for raw_line in raw_output.splitlines()[1:]: line = raw_line.split() name = line[-1] if process_name == None or name == process_name: user = line[0] pid = line[1] if process_name != None: return (pid, user) else: ret.append((pid, user)) # No match in target process if process_name != None: return (None, None) return ret def kill_by_pid(self, pid): self._shell(*["kill", "-9", pid]) def kill_by_name(self, process_name): (pid, user) = self.get_process_info(process_name) while pid != None: self.kill_by_pid(pid) (pid, user) = self.get_process_info(process_name) class AndroidStatus(gdb.Command): """Implements the android-status gdb command.""" def __init__(self, adb, name="android-status", cat=gdb.COMMAND_OBSCURE, verbose=False): super (AndroidStatus, self).__init__(name, cat) self.verbose = verbose self.adb = adb def _update_status(self, process_name, gdbserver_process_name): self._check_app_is_loaded() # Update app status (self.pid, self.owner_user) = \ self.adb.get_process_info(process_name) self.running = self.pid != None # Update gdbserver status (self.gdbserver_pid, self.gdbserver_user) = \ self.adb.get_process_info(gdbserver_process_name) self.gdbserver_running = self.gdbserver_pid != None # Print results if self.verbose: print "--==Android GDB Plugin Status Update==--" print "\tinferior name: " + process_name print "\trunning: " + str(self.running) print "\tpid: " + str(self.pid) print "\tgdbserver running: " + str(self.gdbserver_running) print "\tgdbserver pid: " + str(self.gdbserver_pid) print "\tgdbserver user: " + str(self.gdbserver_user) def _check_app_is_loaded(self): if not currentAppInfo.get_name(): raise gdb.GdbError("Error: no app loaded. Try load-android-app.") def invoke(self, arg, from_tty): self._check_app_is_loaded() self._update_status(currentAppInfo.get_name(), currentAppInfo.get_gdbserver_path()) # TODO: maybe print something if verbose is off class StartAndroidApp (AndroidStatus): """Implements the 'start-android-app' gdb command.""" def _update_status(self): AndroidStatus._update_status(self, self.process_name, \ self.gdbserver_path) # Calls adb shell ps every retry_delay seconds and returns # the pid when process_name show up in output, or return 0 # after num_retries attempts. num_retries=0 means retry # indefinitely. def _wait_for_process(self, process_name, retry_delay=1, num_retries=10): """ This function is a hack and should not be required""" (pid, user) = self.adb.get_process_info(process_name) retries_left = num_retries while pid == None and retries_left != 0: (pid, user) = self.adb.get_process_info(process_name) time.sleep(retry_delay) retries_left -= 1 return pid def _gdbcmd(self, cmd, from_tty=False): if self.verbose: print '### GDB Command: ' + str(cmd) gdb.execute(cmd, from_tty) # Remove scratch directory if any def _cleanup_temp(self): if self.temp_dir: shutil.rmtree(self.temp_dir) self.temp_dir = None def _cleanup_jdb(self): if self.jdb_handle: try: self.jdb_handle.terminate() except OSError, e: # JDB process has likely died pass self.jdb_handle = None def _load_local_libs(self): for lib in _interesting_libs(): self._gdbcmd("shar " + lib) def __del__(self): self._cleanup_temp() self._cleanup_jdb() def __init__ (self, adb, name="start-android-app", cat=gdb.COMMAND_RUNNING, verbose=False): super (StartAndroidApp, self).__init__(adb, name, cat, verbose) self.adb = adb self.jdb_handle = None # TODO: handle possibility that port 8700 is in use (may help with # Eclipse problems) self.jdwp_port = 8700 # Port for gdbserver self.gdbserver_port = 5039 self.temp_dir = None def start_process(self, start_running=False): #TODO: implement libbcc cache removal if needed args = ["am", "start"] # If we are to start running, we can take advantage of am's -W flag to wait # for the process to start before returning. That way, we don't have to # emulate the behaviour (poorly) through the sleep-loop below. if not start_running: args.append("-D") else: args.append("-W") args.append(self.process_name + "/" + self.intent) am_output = self.adb._shell(*args) if "Error:" in am_output: raise gdb.GdbError("Cannot start app. Activity Manager returned:\n"\ + am_output) # Gotta wait until the process starts if we can't use -W if not start_running: self.pid = self._wait_for_process(self.process_name) if not self.pid: raise gdb.GdbError("Unable to detect running app remotely." \ + "Is " + self.process_name + " installed correctly?") if self.verbose: print "--==Android App Started: " + self.process_name \ + " (pid=" + self.pid + ")==--" # Forward port for java debugger to Dalvik self.adb.forward("tcp:" + str(self.jdwp_port), \ "jdwp:" + str(self.pid)) def start_gdbserver(self): # TODO: adjust for architecture... gdbserver_local_path = os.path.join(os.getenv('ANDROID_BUILD_TOP'), 'prebuilt', 'android-arm', 'gdbserver', 'gdbserver') if not self.adb.exists(self.gdbserver_path): # Install gdbserver try: self.adb.push(gdbserver_local_path, self.gdbserver_path) except gdb.GdbError, e: print "Unable to push gdbserver to device. Try re-installing app." raise e self.adb._background_shell(*[self.gdbserver_path, "--attach", ":" + str(self.gdbserver_port), self.pid]) self._wait_for_process(self.gdbserver_path) self._update_status() if self.verbose: print "--==Remote gdbserver Started " \ + " (pid=" + str(self.gdbserver_pid) \ + " port=" + str(self.gdbserver_port) + ") ==--" # Forward port for gdbserver self.adb.forward("tcp:" + str(self.gdbserver_port), \ "tcp:" + str(5039)) def attach_gdb(self, from_tty): self._gdbcmd("target remote :" + str(self.gdbserver_port), False) if self.verbose: print "--==GDB Plugin requested attach (port=" \ + str(self.gdbserver_port) + ")==-" # If GDB has no file set, things start breaking...so grab the same # binary the NDK grabs from the filesystem and continue self._cleanup_temp() self.temp_dir = tempfile.mkdtemp() self.gdb_inferior = os.path.join(self.temp_dir, 'app_process') self.adb.pull("/system/bin/app_process", self.gdb_inferior) self._gdbcmd('file ' + self.gdb_inferior) def start_jdb(self, port): # Kill if running self._cleanup_jdb() # Start the java debugger args = ["jdb", "-connect", "com.sun.jdi.SocketAttach:hostname=localhost,port=" + str(port)] if self.verbose: self.jdb_handle = subprocess.Popen(args, \ stdin=subprocess.PIPE) else: # Unix-only bit here.. self.jdb_handle = subprocess.Popen(args, \ stdin=subprocess.PIPE, stderr=subprocess.STDOUT, stdout=open('/dev/null', 'w')) def invoke (self, arg, from_tty): # TODO: self._check_app_is_installed() self._check_app_is_loaded() self.intent = currentAppInfo.get_intent() self.process_name = currentAppInfo.get_name() self.data_directory = currentAppInfo.get_data_directory() self.gdbserver_path = currentAppInfo.get_gdbserver_path() self._update_status() if self.gdbserver_running: self.adb.kill_by_name(self.gdbserver_path) if self.verbose: print "--==Killed gdbserver process (pid=" \ + str(self.gdbserver_pid) + ")==--" self._update_status() if self.running: self.adb.kill_by_name(self.process_name) if self.verbose: print "--==Killed app process (pid=" + str(self.pid) + ")==--" self._update_status() self.start_process() # Start remote gdbserver self.start_gdbserver() # Attach the gdb self.attach_gdb(from_tty) # Load symbolic libraries self._load_local_libs() # Set the debug output directory (for JIT debugging) if enable_renderscript_dumps: self._gdbcmd('set gDebugDumpDirectory="' + self.data_directory + '"') # Start app # unblock the gdb by connecting with jdb self.start_jdb(self.jdwp_port) class RunAndroidApp(StartAndroidApp): """Implements the run-android-app gdb command.""" def __init__(self, adb, name="run-android-app", cat=gdb.COMMAND_RUNNING, verbose=False): super (RunAndroidApp, self).__init__(adb, name, cat, verbose) def invoke(self, arg, from_tty): StartAndroidApp.invoke(self, arg, from_tty) self._gdbcmd("continue") class AttachAndroidApp(StartAndroidApp): """Implements the attach-android-app gdb command.""" def __init__(self, adb, name="attach-android-app", cat=gdb.COMMAND_RUNNING, verbose=False): super (AttachAndroidApp, self).__init__(adb, name, cat, verbose) def invoke(self, arg, from_tty): # TODO: self._check_app_is_installed() self._check_app_is_loaded() self.intent = currentAppInfo.get_intent() self.process_name = currentAppInfo.get_name() self.data_directory = currentAppInfo.get_data_directory() self.gdbserver_path = currentAppInfo.get_gdbserver_path() self._update_status() if self.gdbserver_running: self.adb.kill_by_name(self.gdbserver_path) if self.verbose: print "--==Killed gdbserver process (pid=" \ + str(self.gdbserver_pid) + ")==--" self._update_status() # Start remote gdbserver self.start_gdbserver() # Attach the gdb self.attach_gdb(from_tty) # Load symbolic libraries self._load_local_libs() # Set the debug output directory (for JIT debugging) if enable_renderscript_dumps: self._gdbcmd('set gDebugDumpDirectory="' + self.data_directory + '"') class LoadApp(AndroidStatus): """ Implements the load-android-app gbd command. """ def _awk_script_path(self, script_name): if os.path.exists(script_name): return script_name script_root = os.path.join(os.getenv('ANDROID_BUILD_TOP'), \ 'ndk', 'build', 'awk') path_in_root = os.path.join(script_root, script_name) if os.path.exists(path_in_root): return path_in_root raise gdb.GdbError("Unable to find awk script " \ + str(script_name) + " in " + path_in_root) def _awk(self, script, command): args = ["awk", "-f", self._awk_script_path(script), str(command)] if self.verbose: print "### awk command: " + str(args) awk_output = "" try: awk_output = check_output(args) except subprocess.CalledProcessError, e: raise gdb.GdbError("### Error in subprocess awk " + str(args)) except: print "### Random error calling awk " + str(args) return awk_output.rstrip() def __init__(self, adb, name="load-android-app", cat=gdb.COMMAND_RUNNING, verbose=False): super (LoadApp, self).__init__(adb, name, cat, verbose) self.manifest_name = "AndroidManifest.xml" self.verbose = verbose self.adb = adb self.temp_libdir = None def _find_manifests(self, path): manifests = [] for root, dirnames, filenames in os.walk(path): for filename in fnmatch.filter(filenames, self.manifest_name): manifests.append(os.path.join(root, filename)) return manifests def _usage(self): return "Usage: load-android-app [<path-to-AndroidManifest.xml>" \ + " | <package-name> <intent-name>]" def invoke(self, arg, from_tty): package_name = '' launchable = '' args = arg.strip('"').split() if len(args) == 2: package_name = args[0] launchable = args[1] elif len(args) == 1: if os.path.isfile(args[0]) and os.path.basename(args[0]) == self.manifest_name: self.manifest_path = args[0] elif os.path.isdir(args[0]): manifests = self._find_manifests(args[0]) if len(manifests) == 0: raise gdb.GdbError(self.manifest_name + " not found in: " \ + args[0] + "\n" + self._usage()) elif len(manifests) > 1: raise gdb.GdbError("Ambiguous argument! Found too many " \ + self.manifest_name + " files found:\n" + "\n".join(manifests)) else: self.manifest_path = manifests[0] else: raise gdb.GdbError("Invalid path: " + args[0] + "\n" + self._usage()) package_name = self._awk("extract-package-name.awk", self.manifest_path) launchable = self._awk("extract-launchable.awk", self.manifest_path) else: raise gdb.GdbError(self._usage()) data_directory = self.adb._shell("run-as", package_name, "/system/bin/sh", "-c", "pwd").rstrip() if not data_directory \ or len(data_directory) == 0 \ or not self.adb.exists(data_directory): data_directory = os.path.join('/data', 'data', package_name) print "Warning: unable to read data directory for package " \ + package_name + ". Meh, defaulting to " + data_directory currentAppInfo.set_info(package_name, launchable, data_directory) if self.verbose: print "--==Android App Loaded==--" print "\tname=" + currentAppInfo.get_name() print "\tintent=" + currentAppInfo.get_intent() # TODO: Check status of app on device class SetAndroidDevice (gdb.Command): def __init__(self, adb, name="set-android-device", cat=gdb.COMMAND_RUNNING, verbose=False): super (SetAndroidDevice, self).__init__(name, cat) self.verbose = verbose self.adb = adb def _usage(self): return "Usage: set-android-device <serial>" def invoke(self, arg, from_tty): if not arg or len(arg) == 0: raise gdb.GdbError(self._usage) serial = str(arg) devices = adb.devices() if serial in devices: adb.set_current_device(serial) else: raise gdb.GdbError("Invalid serial. Serial numbers of connected " \ + "device(s): \n" + "\n".join(devices)) # Global initialization def initOnce(adb): # Try to speed up startup by skipping most android shared objects gdb.execute("set auto-solib-add 0", False); # Set shared object search path gdb.execute("set solib-search-path " + local_symbols_library_directory, False) # Global instance of the object containing the info for current app currentAppInfo = DebugAppInfo () # Global instance of ADB helper adb = ADB(verbose=be_verbose) # Perform global initialization initOnce(adb) # Command registration StartAndroidApp (adb, "start-android-app", gdb.COMMAND_RUNNING, be_verbose) RunAndroidApp (adb, "run-android-app", gdb.COMMAND_RUNNING, be_verbose) AndroidStatus (adb, "android-status", gdb.COMMAND_OBSCURE, be_verbose) LoadApp (adb, "load-android-app", gdb.COMMAND_RUNNING, be_verbose) SetAndroidDevice (adb, "set-android-device", gdb.COMMAND_RUNNING, be_verbose) AttachAndroidApp (adb, "attach-android-app", gdb.COMMAND_RUNNING, be_verbose)