gdbclient: support various PT_INTERP values

gdb looks for an executable's dynamic linker using the PT_INTERP setting
from the executable. That value can be various things:
 - /system/bin/linker[64]
 - /system/bin/linker_asan[64]
 - /system/bin/bootstrap/linker[64]

Currently, only the bootstrap linker is available in the sysroot/symbols
directory. The ordinary and ASAN linkers are symlinks on the target and
are missing from the sysroot (aka symbols) directory.

Use the executable's PT_INTERP value to find the symbolized linker binary
and add it to the solib search path. If necessary, copy or pull a linker
binary.

Test: gdbclient.py -r ls
  "info sharedlib" shows $OUT/symbols/apex/com.android.runtime.debug/bin/linker64

Test: gdbclient.py -r /data/nativetest64/bionic-unit-tests/bionic-unit-tests
  "info sharedlib" shows $OUT/symbols/system/bin/bootstrap/linker64

Test: m asan_test
  gdbclient.py -r /data/nativetest64/asan_test/asan_test
  "info sharedlib" shows /tmp/gdbclient-linker-HunVs9/linker_asan64

Bug: http://b/134183407
Change-Id: I7f79943dcd9ec762d1aaf21178bb6ab3eff40617
diff --git a/python-packages/gdbrunner/__init__.py b/python-packages/gdbrunner/__init__.py
index 17b9833..dbdcfed 100644
--- a/python-packages/gdbrunner/__init__.py
+++ b/python-packages/gdbrunner/__init__.py
@@ -20,6 +20,7 @@
 import argparse
 import atexit
 import os
+import re
 import subprocess
 import sys
 import tempfile
@@ -318,6 +319,16 @@
         raise RuntimeError("unknown architecture: 0x{:x}".format(e_machine))
 
 
+def get_binary_interp(binary_path, llvm_readobj_path):
+    args = [llvm_readobj_path, "--elf-output-style=GNU", "-l", binary_path]
+    output = subprocess.check_output(args, universal_newlines=True)
+    m = re.search(r"\[Requesting program interpreter: (.*?)\]\n", output)
+    if m is None:
+        return None
+    else:
+        return m.group(1)
+
+
 def start_gdb(gdb_path, gdb_commands, gdb_flags=None):
     """Start gdb in the background and block until it finishes.
 
diff --git a/scripts/gdbclient.py b/scripts/gdbclient.py
index a96aaa8..e3a0503 100755
--- a/scripts/gdbclient.py
+++ b/scripts/gdbclient.py
@@ -20,14 +20,19 @@
 import json
 import logging
 import os
+import posixpath
 import re
+import shutil
 import subprocess
 import sys
+import tempfile
 import textwrap
 
 # Shared functions across gdbclient.py and ndk-gdb.py.
 import gdbrunner
 
+g_temp_dirs = []
+
 def get_gdbserver_path(root, arch):
     path = "{}/prebuilts/misc/gdbserver/android-{}/gdbserver{}"
     if arch.endswith("64"):
@@ -101,14 +106,62 @@
     return pids[0]
 
 
-def ensure_linker(device, sysroot, is64bit):
-    local_path = os.path.join(sysroot, "system", "bin", "linker")
-    remote_path = "/system/bin/linker"
-    if is64bit:
-        local_path += "64"
-        remote_path += "64"
-    if not os.path.exists(local_path):
-        device.pull(remote_path, local_path)
+def make_temp_dir(prefix):
+    global g_temp_dirs
+    result = tempfile.mkdtemp(prefix='gdbclient-linker-')
+    g_temp_dirs.append(result)
+    return result
+
+
+def ensure_linker(device, sysroot, interp):
+    """Ensure that the device's linker exists on the host.
+
+    PT_INTERP is usually /system/bin/linker[64], but on the device, that file is
+    a symlink to /apex/com.android.runtime/bin/linker[64]. The symbolized linker
+    binary on the host is located in ${sysroot}/apex, not in ${sysroot}/system,
+    so add the ${sysroot}/apex path to the solib search path.
+
+    PT_INTERP will be /system/bin/bootstrap/linker[64] for executables using the
+    non-APEX/bootstrap linker. No search path modification is needed.
+
+    For a tapas build, only an unbundled app is built, and there is no linker in
+    ${sysroot} at all, so copy the linker from the device.
+
+    Returns:
+        A directory to add to the soinfo search path or None if no directory
+        needs to be added.
+    """
+
+    # Static executables have no interpreter.
+    if interp is None:
+        return None
+
+    # gdb will search for the linker using the PT_INTERP path. First try to find
+    # it in the sysroot.
+    local_path = os.path.join(sysroot, interp.lstrip("/"))
+    if os.path.exists(local_path):
+        return None
+
+    # If the linker on the device is a symlink, search for the symlink's target
+    # in the sysroot directory.
+    interp_real, _ = device.shell(["realpath", interp])
+    interp_real = interp_real.strip()
+    local_path = os.path.join(sysroot, interp_real.lstrip("/"))
+    if os.path.exists(local_path):
+        if posixpath.basename(interp) == posixpath.basename(interp_real):
+            # Add the interpreter's directory to the search path.
+            return os.path.dirname(local_path)
+        else:
+            # If PT_INTERP is linker_asan[64], but the sysroot file is
+            # linker[64], then copy the local file to the name gdb expects.
+            result = make_temp_dir('gdbclient-linker-')
+            shutil.copy(local_path, os.path.join(result, posixpath.basename(interp)))
+            return result
+
+    # Pull the system linker.
+    result = make_temp_dir('gdbclient-linker-')
+    device.pull(interp, os.path.join(result, posixpath.basename(interp)))
+    return result
 
 
 def handle_switches(args, sysroot):
@@ -247,7 +300,7 @@
 
     return gdb_commands
 
-def generate_setup_script(gdbpath, sysroot, binary_file, is64bit, port, debugger, connect_timeout=5):
+def generate_setup_script(gdbpath, sysroot, linker_search_dir, binary_file, is64bit, port, debugger, connect_timeout=5):
     # Generate a setup script.
     # TODO: Detect the zygote and run 'art-on' automatically.
     root = os.environ["ANDROID_BUILD_TOP"]
@@ -259,6 +312,8 @@
     vendor_paths = ["", "hw", "egl"]
     solib_search_path += [os.path.join(symbols_dir, x) for x in symbols_paths]
     solib_search_path += [os.path.join(vendor_dir, x) for x in vendor_paths]
+    if linker_search_dir is not None:
+        solib_search_path += [linker_search_dir]
 
     dalvik_gdb_script = os.path.join(root, "development", "scripts", "gdb", "dalvik.gdb")
     if not os.path.exists(dalvik_gdb_script):
@@ -275,7 +330,7 @@
         raise Exception("Unknown debugger type " + debugger)
 
 
-def main():
+def do_main():
     required_env = ["ANDROID_BUILD_TOP",
                     "ANDROID_PRODUCT_OUT", "TARGET_PRODUCT"]
     for env in required_env:
@@ -303,11 +358,21 @@
     binary_file, pid, run_cmd = handle_switches(args, sysroot)
 
     with binary_file:
+        if sys.platform.startswith("linux"):
+            platform_name = "linux-x86"
+        elif sys.platform.startswith("darwin"):
+            platform_name = "darwin-x86"
+        else:
+            sys.exit("Unknown platform: {}".format(sys.platform))
+
         arch = gdbrunner.get_binary_arch(binary_file)
         is64bit = arch.endswith("64")
 
         # Make sure we have the linker
-        ensure_linker(device, sysroot, is64bit)
+        llvm_readobj_path = os.path.join(root, "prebuilts", "clang", "host", platform_name,
+                                         "llvm-binutils-stable", "llvm-readobj")
+        interp = gdbrunner.get_binary_interp(binary_file.name, llvm_readobj_path)
+        linker_search_dir = ensure_linker(device, sysroot, interp)
 
         tracer_pid = get_tracer_pid(device, pid)
         if tracer_pid == 0:
@@ -327,19 +392,12 @@
             gdbrunner.forward_gdbserver_port(device, local=args.port,
                                              remote="tcp:{}".format(args.port))
 
-        # Find where gdb is
-        if sys.platform.startswith("linux"):
-            platform_name = "linux-x86"
-        elif sys.platform.startswith("darwin"):
-            platform_name = "darwin-x86"
-        else:
-            sys.exit("Unknown platform: {}".format(sys.platform))
-
         gdb_path = os.path.join(root, "prebuilts", "gdb", platform_name, "bin",
                                 "gdb")
         # Generate a gdb script.
         setup_commands = generate_setup_script(gdbpath=gdb_path,
                                                sysroot=sysroot,
+                                               linker_search_dir=linker_search_dir,
                                                binary_file=binary_file,
                                                is64bit=is64bit,
                                                port=args.port,
@@ -368,5 +426,15 @@
             print("")
             raw_input("Press enter to shutdown gdbserver")
 
+
+def main():
+    try:
+        do_main()
+    finally:
+        global g_temp_dirs
+        for temp_dir in g_temp_dirs:
+            shutil.rmtree(temp_dir)
+
+
 if __name__ == "__main__":
     main()