diff --git a/renderdoc/CMakeLists.txt b/renderdoc/CMakeLists.txt index e4a5c86af..915d77289 100644 --- a/renderdoc/CMakeLists.txt +++ b/renderdoc/CMakeLists.txt @@ -95,6 +95,10 @@ set(sources android/android_utils.cpp android/android.h android/android_utils.h + android/jdwp.h + android/jdwp.cpp + android/jdwp_util.cpp + android/jdwp_connection.cpp core/plugins.cpp core/plugins.h core/resource_manager.cpp diff --git a/renderdoc/android/android.cpp b/renderdoc/android/android.cpp index 98bf861fa..1c57c89dd 100644 --- a/renderdoc/android/android.cpp +++ b/renderdoc/android/android.cpp @@ -31,7 +31,7 @@ namespace Android { -void adbForwardPorts(int index, const std::string &deviceID) +void adbForwardPorts(int index, const std::string &deviceID, uint16_t jdwpPort, int pid) { const char *forwardCommand = "forward tcp:%i localabstract:renderdoc_%i"; int offs = RenderDoc_AndroidPortOffset * (index + 1); @@ -40,7 +40,88 @@ void adbForwardPorts(int index, const std::string &deviceID) RenderDoc_RemoteServerPort)); adbExecCommand(deviceID, StringFormat::Fmt(forwardCommand, RenderDoc_FirstTargetControlPort + offs, RenderDoc_FirstTargetControlPort)); + + if(jdwpPort && pid) + adbExecCommand(deviceID, StringFormat::Fmt("forward tcp:%hu jdwp:%i", jdwpPort, pid)); } + +uint16_t GetJdwpPort() +{ + // we loop over a number of ports to try and avoid previous failed attempts from leaving sockets + // open and messing with subsequent attempts + const uint16_t portBase = RenderDoc_FirstTargetControlPort + RenderDoc_AndroidPortOffset * 2; + + static uint16_t portIndex = 0; + + portIndex++; + portIndex %= RenderDoc_AndroidPortOffset; + + return portBase + portIndex; +} + +std::string GetDefaultActivityForPackage(const std::string &deviceID, const std::string &packageName) +{ + Process::ProcessResult activity = + adbExecCommand(deviceID, StringFormat::Fmt("shell cmd package resolve-activity" + " -c android.intent.category.LAUNCHER %s", + packageName.c_str())); + + if(activity.strStdout.empty()) + { + RDCERR("Failed to resolve default activity of APK. STDERR: %s", activity.strStderror.c_str()); + return ""; + } + + std::vector lines; + split(activity.strStdout, lines, '\n'); + + for(std::string &line : lines) + { + line = trim(line); + + if(!strncmp(line.c_str(), "name=", 5)) + { + return line.substr(5); + } + } + + RDCERR("Didn't find default activity in adb output"); + return ""; +} + +int GetCurrentPid(const std::string &deviceID, const std::string &packageName) +{ + // try 5 times, 200ms apart to find the pid + for(int i = 0; i < 5; i++) + { + Process::ProcessResult pidOutput = + adbExecCommand(deviceID, StringFormat::Fmt("shell ps | grep %s", packageName.c_str())); + + std::string output = trim(pidOutput.strStdout); + size_t space = output.find_first_of("\t "); + + if(output.empty() || output.find(packageName) == std::string::npos || space == std::string::npos) + { + Threading::Sleep(200); + continue; + } + + char *pid = &output[space]; + while(*pid == ' ' || *pid == '\t') + pid++; + + char *end = pid; + while(*end >= '0' && *end <= '9') + end++; + + *end = 0; + + return atoi(pid); + } + + return 0; +} + uint32_t StartAndroidPackageForCapture(const char *host, const char *package) { int index = 0; @@ -49,11 +130,31 @@ uint32_t StartAndroidPackageForCapture(const char *host, const char *package) string packageName = basename(string(package)); // Remove leading '/' if any + // adb shell cmd package resolve-activity -c android.intent.category.LAUNCHER com.jake.cube1 + string activityName = GetDefaultActivityForPackage(deviceID, packageName); + + uint16_t jdwpPort = GetJdwpPort(); + + // remove any previous jdwp port forward on this port + adbExecCommand(deviceID, StringFormat::Fmt("forward --remove tcp:%i", jdwpPort)); + // force stop the package if it was running before adbExecCommand(deviceID, "shell am force-stop " + packageName); - adbForwardPorts(index, deviceID); + // enable the vulkan layer (will only be used by vulkan programs) adbExecCommand(deviceID, "shell setprop debug.vulkan.layers VK_LAYER_RENDERDOC_Capture"); - adbExecCommand(deviceID, - "shell monkey -p " + packageName + " -c android.intent.category.LAUNCHER 1"); + // start the activity in this package with debugging enabled and force-stop after starting + adbExecCommand(deviceID, StringFormat::Fmt("shell am start -S -D %s/%s", packageName.c_str(), + activityName.c_str())); + + // adb shell ps | grep $PACKAGE | awk '{print $2}') + int pid = GetCurrentPid(deviceID, packageName); + + adbForwardPorts(index, deviceID, jdwpPort, pid); + + // sleep a little to let the ports initialise + Threading::Sleep(500); + + // use a JDWP connection to inject our libraries + InjectWithJDWP(deviceID, jdwpPort); uint32_t ret = RenderDoc_FirstTargetControlPort + RenderDoc_AndroidPortOffset * (index + 1); uint32_t elapsed = 0, @@ -73,6 +174,10 @@ uint32_t StartAndroidPackageForCapture(const char *host, const char *package) elapsed += 1000; } + // we might open the connection early, when the library is first injected, before the vulkan + // loader completes . + Threading::Sleep(1000); + // Let the app pickup the setprop before we turn it back off for replaying. adbExecCommand(deviceID, "shell setprop debug.vulkan.layers :"); @@ -317,7 +422,7 @@ extern "C" RENDERDOC_API void RENDERDOC_CC RENDERDOC_EnumerateAndroidDevices(rdc ret += StringFormat::Fmt("adb:%d:%s", idx, tokens[0].c_str()); // Forward the ports so we can see if a remoteserver/captured app is already running. - adbForwardPorts(idx, tokens[0]); + adbForwardPorts(idx, tokens[0], 0, 0); idx++; } @@ -344,7 +449,7 @@ extern "C" RENDERDOC_API void RENDERDOC_CC RENDERDOC_StartAndroidRemoteServer(co } adbExecCommand(deviceID, "shell am force-stop org.renderdoc.renderdoccmd"); - adbForwardPorts(index, deviceID); + adbForwardPorts(index, deviceID, 0, 0); adbExecCommand(deviceID, "shell setprop debug.vulkan.layers :"); adbExecCommand( deviceID, diff --git a/renderdoc/android/android.h b/renderdoc/android/android.h index 15aeb3146..356550264 100644 --- a/renderdoc/android/android.h +++ b/renderdoc/android/android.h @@ -35,4 +35,5 @@ uint32_t StartAndroidPackageForCapture(const char *host, const char *package); void ExtractDeviceIDAndIndex(const std::string &hostname, int &index, std::string &deviceID); Process::ProcessResult adbExecCommand(const std::string &deviceID, const std::string &args, const string &workDir = "."); +bool InjectWithJDWP(const std::string &deviceID, uint16_t jdwpport); }; diff --git a/renderdoc/android/jdwp.cpp b/renderdoc/android/jdwp.cpp new file mode 100644 index 000000000..b6b219384 --- /dev/null +++ b/renderdoc/android/jdwp.cpp @@ -0,0 +1,406 @@ +/****************************************************************************** + * The MIT License (MIT) + * + * Copyright (c) 2018 Baldur Karlsson + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + ******************************************************************************/ + +#include "jdwp.h" +#include +#include "core/core.h" +#include "strings/string_utils.h" +#include "android.h" + +namespace JDWP +{ +void InjectVulkanLayerSearchPath(Connection &conn, threadID thread, int32_t slotIdx, + const std::string &libPath) +{ + referenceTypeID stringClass = conn.GetType("Ljava/lang/String;"); + methodID stringConcat = conn.GetMethod(stringClass, "concat"); + + if(conn.IsErrored()) + return; + + if(!stringClass || !stringConcat) + { + RDCERR("Couldn't find java.lang.String (%llu) or java.lang.String.concat() (%llu)", stringClass, + stringConcat); + return; + } + + // get the callstack frames + std::vector stack = conn.GetCallStack(thread); + + if(stack.empty()) + { + RDCERR("Couldn't get callstack!"); + return; + } + + // get the local in the top (current) frame + value librarySearchPath = conn.GetLocalValue(thread, stack[0].id, slotIdx, Tag::Object); + + if(librarySearchPath.tag != Tag::String || librarySearchPath.String == 0) + { + RDCERR("Couldn't get 'String librarySearchPath' local parameter!"); + return; + } + + RDCDEBUG("librarySearchPath is %s", conn.GetString(librarySearchPath.String).c_str()); + + value appendSearch = conn.NewString(thread, ":" + libPath); + + // temp = librarySearchPath.concat(appendSearch); + value temp = conn.InvokeInstance(thread, stringClass, stringConcat, librarySearchPath.String, + {appendSearch}); + + if(temp.tag != Tag::String || temp.String == 0) + { + RDCERR("Failed to concat search path!"); + return; + } + + RDCDEBUG("librarySearchPath is now %s", conn.GetString(temp.String).c_str()); + + // we will have resume the thread above to call concat, invalidating our frames. + // Re-fetch the callstack + stack = conn.GetCallStack(thread); + + if(stack.empty()) + { + RDCERR("Couldn't get callstack!"); + return; + } + + // replace the search path with our modified one + // librarySearchPath = temp; + conn.SetLocalValue(thread, stack[0].id, slotIdx, temp); +} + +bool InjectLibraries(Network::Socket *sock, std::string libPath) +{ + Connection conn(sock); + + // check that the handshake completed successfully + if(conn.IsErrored()) + return false; + + // immediately re-suspend, as connecting will have woken it up + conn.Suspend(); + + conn.SetupIDSizes(); + + if(conn.IsErrored()) + return false; + + // default to arm as a safe bet + std::string abi = "armeabi-v7a"; + + // determine the CPU ABI from android.os.Build.CPU_ABI + referenceTypeID buildClass = conn.GetType("Landroid/os/Build;"); + if(buildClass) + { + fieldID CPU_ABI = conn.GetField(buildClass, "CPU_ABI"); + + if(CPU_ABI) + { + value val = conn.GetFieldValue(buildClass, CPU_ABI); + + if(val.tag == Tag::String) + abi = conn.GetString(val.String); + else + RDCERR("CPU_ABI value was type %u, not string!", (uint32_t)val.tag); + } + else + { + RDCERR("Couldn't find CPU_ABI field in android.os.Build"); + } + } + else + { + RDCERR("Couldn't find android.os.Build"); + } + + // use the ABI to determine where to find our library + if(abi == "armeabi-v7a") + { + libPath += "arm"; + } + else if(abi == "arm64-v8a") + { + libPath += "arm64"; + } + else + { + RDCERR("Unhandled ABI '%s'", abi.c_str()); + return false; + } + + if(conn.IsErrored()) + return false; + + // try to find the vulkan loader class and patch the search path when getClassLoader is called. + // This is an optional step as some devices may not support vulkan and may not have this class, so + // in that case we just skip it. + referenceTypeID vulkanLoaderClass = conn.GetType("Landroid/app/ApplicationLoaders;"); + + if(vulkanLoaderClass) + { + // See: + // https://android.googlesource.com/platform/frameworks/base/+/f9419f0f8524da4980726e06130a80e0fb226763/core/java/android/app/ApplicationLoaders.java + // for the public getClassLoader. + + // look for both signatures in this order, note that the final "String classLoaderName" was + // added recently + + // ClassLoader getClassLoader(String zip, int targetSdkVersion, boolean isBundled, + // String librarySearchPath, String libraryPermittedPath, + // ClassLoader parent); + methodID vulkanLoaderMethod = conn.GetMethod( + vulkanLoaderClass, "getClassLoader", + "(Ljava/lang/String;IZLjava/lang/String;Ljava/lang/String;Ljava/lang/ClassLoader;)" + "Ljava/lang/ClassLoader;"); + + // ClassLoader getClassLoader(String zip, int targetSdkVersion, boolean isBundled, + // String librarySearchPath, String libraryPermittedPath, + // ClassLoader parent, String classLoaderName); + if(vulkanLoaderMethod == 0) + vulkanLoaderMethod = conn.GetMethod( + vulkanLoaderClass, "getClassLoader", + "(Ljava/lang/String;IZLjava/lang/String;Ljava/lang/String;Ljava/lang/ClassLoader;" + "Ljava/lang/String;)Ljava/lang/ClassLoader;"); + + if(vulkanLoaderMethod) + { + int32_t slotIdx = + conn.GetLocalVariable(vulkanLoaderClass, vulkanLoaderMethod, "librarySearchPath"); + + // as a default, use the 4th slot as it's the 4th argument argument (0 is this), if symbols + // weren't available we can't identify the variable by name + if(slotIdx == -1) + slotIdx = 4; + + // wait for the method to get hit - WaitForEvent will resume, watch events, and return + // (re-suspended) when the first event occurs that matches the filter function + Event evData = + conn.WaitForEvent(EventKind::MethodEntry, {{ModifierKind::ClassOnly, vulkanLoaderClass}}, + [vulkanLoaderMethod](const Event &evData) { + return evData.MethodEntry.location.methodID == vulkanLoaderMethod; + }); + + // if we successfully hit the event, try to inject + if(evData.eventKind == EventKind::MethodEntry) + InjectVulkanLayerSearchPath(conn, evData.MethodEntry.thread, slotIdx, libPath); + } + else + { + // we expect if we can get the class, we should find the method. + RDCERR("Couldn't find getClassLoader method in android.app.ApplicationLoaders"); + } + } + else + { + // warning only - it's not a problem if we're capturing GLES + RDCWARN("Couldn't find class android.app.ApplicationLoaders. Vulkan won't be hooked."); + } + + // we get here whether we processed vulkan or not. Now we need to wait for the application to hit + // onCreate() and load our library + + referenceTypeID androidApp = conn.GetType("Landroid/app/Application;"); + + if(androidApp == 0) + { + RDCERR("Couldn't find android.app.Application"); + return false; + } + + methodID appConstruct = conn.GetMethod(androidApp, "", "()V"); + + if(appConstruct == 0) + { + RDCERR("Couldn't find android.app.Application constructor"); + return false; + } + + threadID thread; + + // wait until we hit the constructor of android.app.Application + { + Event evData = conn.WaitForEvent(EventKind::MethodEntry, {{ModifierKind::ClassOnly, androidApp}}, + [appConstruct](const Event &evData) { + return evData.MethodEntry.location.methodID == appConstruct; + }); + + if(evData.eventKind == EventKind::MethodEntry) + thread = evData.MethodEntry.thread; + } + + if(thread == 0) + { + RDCERR("Didn't hit android.app.Application constructor"); + return false; + } + + // get the callstack frames + std::vector stack = conn.GetCallStack(thread); + + if(stack.empty()) + { + RDCERR("Couldn't get callstack!"); + return false; + } + + // get this on the top frame + objectID thisPtr = conn.GetThis(thread, stack[0].id); + + if(thisPtr == 0) + { + RDCERR("Couldn't find this"); + return false; + } + + // get the type for the this object + referenceTypeID thisType = conn.GetType(thisPtr); + + if(thisType == 0) + { + RDCERR("Couldn't find this's class"); + return false; + } + + // call getClass, this will give us the information for the most derived class + methodID getClass = conn.GetMethod(thisType, "getClass", "()Ljava/lang/Class;"); + + if(getClass == 0) + { + RDCERR("Couldn't find this.getClass()"); + return false; + } + + value thisClass = conn.InvokeInstance(thread, thisType, getClass, thisPtr, {}); + + if(thisClass.tag != Tag::ClassObject || thisClass.Object == 0) + { + RDCERR("Failed to call this.getClass()!"); + return false; + } + + // look up onCreate in the most derived class - since we can't guarantee that the base + // application.app.onCreate() will get called. + methodID onCreate = conn.GetMethod(thisClass.Object, "onCreate", "()V"); + + if(onCreate == 0) + { + RDCERR("Couldn't find this.getClass().onCreate()"); + return false; + } + + // wait until we hit the derived onCreate + { + thread = 0; + + Event evData = conn.WaitForEvent( + EventKind::MethodEntry, {{ModifierKind::ClassOnly, thisClass.Object}}, + [onCreate](const Event &evData) { return evData.MethodEntry.location.methodID == onCreate; }); + + if(evData.eventKind == EventKind::MethodEntry) + thread = evData.MethodEntry.thread; + } + + if(thread == 0) + { + RDCERR("Didn't hit android.app.Application.onCreate()"); + return false; + } + + // find java.lang.Runtime + referenceTypeID runtime = conn.GetType("Ljava/lang/Runtime;"); + + if(runtime == 0) + { + RDCERR("Couldn't find java.lang.Runtime"); + return false; + } + + // find both the static Runtime.getRuntime() as well as the instance Runtime.load() + methodID getRuntime = conn.GetMethod(runtime, "getRuntime", "()Ljava/lang/Runtime;"); + methodID load = conn.GetMethod(runtime, "load", "(Ljava/lang/String;)V"); + + if(getRuntime == 0 || load == 0) + { + RDCERR("Couldn't find java.lang.Runtime.getRuntime() %llu or java.lang.Runtime.load() %llu", + getRuntime, load); + return false; + } + + // get the Runtime object via java.lang.Runtime.getRuntime() + value runtimeObject = conn.InvokeStatic(thread, runtime, getRuntime, {}); + + if(runtimeObject.tag != Tag::Object || runtimeObject.Object == 0) + { + RDCERR("Failed to call getClass!"); + return false; + } + + // call Runtime.load() on our library. This will load the library and from then on it's + // responsible for injecting its hooks into GLES on its own. See android_hook.cpp for more + // information on the implementation + value ret = + conn.InvokeInstance(thread, runtime, load, runtimeObject.Object, + {conn.NewString(thread, libPath + "/libVkLayer_GLES_RenderDoc.so")}); + + if(ret.tag != Tag::Void) + { + RDCERR("Failed to call load()!"); + return false; + } + + return true; +} +}; // namespace JDWP + +namespace Android +{ +bool InjectWithJDWP(const std::string &deviceID, uint16_t jdwpport) +{ + Network::Socket *sock = Network::CreateClientSocket("localhost", jdwpport, 500); + + if(sock) + { + std::string apk = adbExecCommand(deviceID, "shell pm path org.renderdoc.renderdoccmd").strStdout; + apk = trim(apk); + apk.resize(apk.size() - 8); + apk.erase(0, 8); + + bool ret = JDWP::InjectLibraries(sock, apk + "lib/"); + delete sock; + + return ret; + } + else + { + RDCERR("Couldn't make JDWP connection"); + } + + return false; +} +}; // namespace Android diff --git a/renderdoc/android/jdwp.h b/renderdoc/android/jdwp.h new file mode 100644 index 000000000..c12f6dbe6 --- /dev/null +++ b/renderdoc/android/jdwp.h @@ -0,0 +1,531 @@ +/****************************************************************************** + * The MIT License (MIT) + * + * Copyright (c) 2018 Baldur Karlsson + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + ******************************************************************************/ + +#pragma once + +#include "serialise/streamio.h" + +namespace JDWP +{ +// enums and structs defined by the JDWP spec. These are defined in: +// https://docs.oracle.com/javase/7/docs/platform/jpda/jdwp/jdwp-protocol.html#JDWP_Tag + +enum class CommandSet : byte +{ + Unknown = 0, + VirtualMachine = 1, + ReferenceType = 2, + ClassType = 3, + ArrayType = 4, + InterfaceType = 5, + Method = 6, + Field = 8, + ObjectReference = 9, + StringReference = 10, + ThreadReference = 11, + ThreadGroupReference = 12, + ArrayReference = 13, + ClassLoaderReference = 14, + EventRequest = 15, + StackFrame = 16, + ClassObjectReference = 17, + Event = 64, +}; + +enum class TypeTag : byte +{ + Class = 1, + Interface = 2, + Arrary = 3, +}; + +enum class Tag : byte +{ + Unknown = '0', + Array = '[', + Byte = 'B', + Char = 'C', + Object = 'L', + Float = 'F', + Double = 'D', + Int = 'I', + Long = 'J', + Short = 'S', + Void = 'V', + Boolean = 'Z', + String = 's', + Thread = 't', + ThreadGroup = 'g', + ClassLoader = 'l', + ClassObject = 'c', +}; + +enum class EventKind : byte +{ + Unknown = 0, + SingleStep = 1, + Breakpoint = 2, + FramePop = 3, + Exception = 4, + UserDefined = 5, + ThreadStart = 6, + ThreadDeath = 7, + ThreadEnd = 7, + ClassPrepare = 8, + ClassUnload = 9, + ClassLoad = 10, + FieldAccess = 20, + FieldModification = 21, + ExceptionCatch = 30, + MethodEntry = 40, + MethodExit = 41, + MethodExitWithReturnValue = 42, + MonitorContendedEnter = 43, + MonitorContendedEntered = 44, + MonitorWait = 45, + MonitorWaited = 46, + VMStart = 90, + VMInit = 90, + VMDeath = 99, + VMDisconnected = 100, +}; + +enum class ModifierKind : byte +{ + Count = 1, + Conditional = 2, + ThreadOnly = 3, + ClassOnly = 4, + ClassMatch = 5, + ClassExclude = 6, + LocationOnly = 7, + ExceptionOnly = 8, + FieldOnly = 9, + Step = 10, + InstanceOnly = 11, + SourceNameMatch = 12, +}; + +enum class SuspendPolicy : byte +{ + None = 0, + EventThread = 1, + All = 2, +}; + +enum class InvokeOptions : int32_t +{ + SingleThreaded = 0x1, + NonVirtual = 0x2, +}; + +enum class ClassStatus : int32_t +{ + Verified = 0x1, + Prepared = 0x2, + Initialized = 0x4, + Error = 0x8, +}; + +enum class Error : uint16_t +{ + None = 0, + InvalidThread = 10, + InvalidThreadGroup = 11, + InvalidPriority = 12, + ThreadNotSuspended = 13, + ThreadSuspended = 14, + ThreadNotAlive = 15, + InvalidObject = 20, + InvalidClass = 21, + ClassNotPrepared = 22, + InvalidMethodid = 23, + InvalidLocation = 24, + InvalidFieldid = 25, + InvalidFrameid = 30, + NoMoreFrames = 31, + OpaqueFrame = 32, + NotCurrentFrame = 33, + TypeMismatch = 34, + InvalidSlot = 35, + Duplicate = 40, + NotFound = 41, + InvalidMonitor = 50, + NotMonitorOwner = 51, + Interrupt = 52, + InvalidClassFormat = 60, + CircularClassDefinition = 61, + FailsVerification = 62, + AddMethodNotImplemented = 63, + SchemaChangeNotImplemented = 64, + InvalidTypestate = 65, + HierarchyChangeNotImplemented = 66, + DeleteMethodNotImplemented = 67, + UnsupportedVersion = 68, + NamesDontMatch = 69, + ClassModifiersChangeNotImplemented = 70, + MethodModifiersChangeNotImplemented = 71, + NotImplemented = 99, + NullPointer = 100, + AbsentInformation = 101, + InvalidEventType = 102, + IllegalArgument = 103, + OutOfMemory = 110, + AccessDenied = 111, + VmDead = 112, + Internal = 113, + UnattachedThread = 115, + InvalidTag = 500, + AlreadyInvoking = 502, + InvalidIndex = 503, + InvalidLength = 504, + InvalidString = 506, + InvalidClassLoader = 507, + InvalidArray = 508, + TransportLoad = 509, + TransportInit = 510, + NativeMethod = 511, + InvalidCount = 512, +}; + +struct CommandData; + +// we abstract the objectID size away, and always treat it as a uint64 (if it's actually 4 bytes, we +// just only read/write the lower 4 bytes). +struct objectID +{ +public: + objectID() = default; + objectID(uint64_t v) { data.u64 = v; } + operator uint64_t() const { return size == 4 ? data.u32 : data.u64; } + static int32_t getSize() { return size; } + static void setSize(int32_t s) + { + RDCASSERT(s == 4 || s == 8); + size = s; + } + + void EndianSwap() + { + if(size == 4) + data.u32 = ::EndianSwap(data.u32); + else + data.u64 = ::EndianSwap(data.u64); + } + +private: + union + { + uint32_t u32; + uint64_t u64; + } data; + static int32_t size; +}; + +// all object types are the same and we don't care too much about type safety, so we just typedef +// them. +typedef objectID threadID; +typedef objectID threadGroupID; +typedef objectID stringID; +typedef objectID classLoaderID; +typedef objectID classObjectID; +typedef objectID arrayID; +typedef objectID referenceTypeID; +typedef objectID classID; +typedef objectID interfaceID; +typedef objectID arrayTypeID; +typedef objectID methodID; +typedef objectID fieldID; +typedef objectID frameID; + +struct taggedObjectID +{ + Tag tag; + objectID id; +}; + +struct value +{ + Tag tag; + + union + { + arrayID Array; + byte Byte; + char Char; + objectID Object; + float Float; + double Double; + int32_t Int; + int64_t Long; + int16_t Short; + bool Bool; + objectID String; + threadID Thread; + threadGroupID ThreadGroup; + classLoaderID ClassLoader; + classObjectID ClassObject; + }; +}; + +struct Location +{ + TypeTag tag; + objectID classID; + objectID methodID; + uint64_t index; +}; + +struct Method +{ + methodID id; + std::string name; + std::string signature; + int32_t modBits; +}; + +struct Field +{ + fieldID id; + std::string name; + std::string signature; + int32_t modBits; +}; + +struct VariableSlot +{ + uint64_t codeIndex; + std::string name; + std::string signature; + uint32_t length; + int32_t slot; +}; + +struct StackFrame +{ + frameID id; + Location location; +}; + +struct EventFilter +{ + ModifierKind modKind; + referenceTypeID ClassOnly; +}; + +struct Event +{ + EventKind eventKind; + int32_t requestID; + + struct + { + threadID thread; + Location location; + } MethodEntry; + + struct + { + threadID thread; + TypeTag refTypeTag; + referenceTypeID typeID; + std::string signature; + union + { + // get around strict aliasing + ClassStatus status; + int32_t statusInt; + }; + } ClassPrepare; +}; + +struct Command +{ + Command(CommandSet s = CommandSet::Unknown, byte c = 0) : commandset(s), command(c) {} + uint32_t Send(StreamWriter &writer); + void Recv(StreamReader &reader); + + // only these need to be publicly writeable + CommandSet commandset; + byte command; + // get the ID assigned/received + uint32_t GetID() { return id; } + // get the error received (only for replies) + Error GetError() { return error; } + // read-write access to the data (encodes and endian swaps) + CommandData GetData(); + +private: + static uint32_t idalloc; + + // automatically calculated + uint32_t length = 0; + uint32_t id = 0; + Error error = Error::None; + std::vector data; +}; + +// a helper class for reading/writing the payload of a packet, with endian swapping. +struct CommandData +{ + CommandData(std::vector &dataStore) : data(dataStore) {} + template + CommandData &Read(T &val) + { + RDCCOMPILE_ASSERT(sizeof(bool) == 1, "bool must be 1 byte for default template"); + ReadBytes(&val, sizeof(T)); + val = EndianSwap(val); + return *this; + } + + template + CommandData &Write(const T &val) + { + T tmp = EndianSwap(val); + WriteBytes(&tmp, sizeof(T)); + return *this; + } + + // called when we've finished reading, to ensure we consumed all the data + void Done() { RDCASSERT(offs == data.size(), offs, data.size()); } +private: + std::vector &data; + size_t offs = 0; + + void ReadBytes(void *bytes, size_t length); + void WriteBytes(const void *bytes, size_t length); +}; + +// overloads for the basic types we want to read and write +template <> +CommandData &CommandData::Read(std::string &str); +template <> +CommandData &CommandData::Write(const std::string &str); +template <> +CommandData &CommandData::Read(objectID &id); +template <> +CommandData &CommandData::Write(const objectID &id); +template <> +CommandData &CommandData::Read(taggedObjectID &id); +template <> +CommandData &CommandData::Write(const taggedObjectID &id); +template <> +CommandData &CommandData::Read(value &val); +template <> +CommandData &CommandData::Write(const value &val); +template <> +CommandData &CommandData::Read(Location &loc); +template <> +CommandData &CommandData::Write(const Location &loc); + +typedef std::function EventFilterFunction; + +// A JDWP connection, with high-level helper functions that implement the protocol underneath +class Connection +{ +public: + Connection(Network::Socket *sock); + ~Connection(); + + bool IsErrored() { return error || writer.IsErrored() || reader.IsErrored(); } + // suspend or resume the whole VM's execution. + void Suspend(); + void Resume(); + + // this must be called first before any of the commands that use IDs. It's separated out because + // depending on the circumstance it might be necessary to suspend the VM first before sending this + // command. + void SetupIDSizes(); + + // get the type handle for a given JNI signature + referenceTypeID GetType(const std::string &signature); + + // get the type handle for an object + referenceTypeID GetType(objectID obj); + + // get a field handle. If signature is empty, it's ignored for matching + fieldID GetField(referenceTypeID type, const std::string &name, const std::string &signature = ""); + + // get the value of a static field + value GetFieldValue(referenceTypeID type, fieldID field); + + // get a method handle. If signature is empty, it's ignored for matching + methodID GetMethod(referenceTypeID type, const std::string &name, + const std::string &signature = ""); + + // get a local variable slot. If signature is empty, it's ignored for matching. Returns -1 if not + // found + int32_t GetLocalVariable(referenceTypeID type, methodID method, const std::string &name, + const std::string &signature = ""); + + // get a thread's stack frames + std::vector GetCallStack(threadID thread); + + // get the this pointer for a given stack frame + objectID GetThis(threadID thread, frameID frame); + + // get the value of a string object + std::string GetString(objectID str); + + // resume the VM and wait for an event to happen, filtered by some built-in filters or a callback. + // Returns the matching event and leaves the VM suspended, or an empty event if there was a + // problem + Event WaitForEvent(EventKind kind, const std::vector &eventFilters, + EventFilterFunction filterCallback); + + // create a new string reference on the given thread + value NewString(threadID thread, const std::string &str); + + // get/set local variables + value GetLocalValue(threadID thread, frameID frame, int32_t slot, Tag tag); + void SetLocalValue(threadID thread, frameID frame, int32_t slot, value val); + + // invoke a static method + value InvokeStatic(threadID thread, classID clazz, methodID method, + const std::vector &arguments, + InvokeOptions options = InvokeOptions::SingleThreaded) + { + // instance detects if object is empty, and invokes as static + return InvokeInstance(thread, clazz, method, 0, arguments, options); + } + // invoke an instance method + value InvokeInstance(threadID thread, classID clazz, methodID method, objectID object, + const std::vector &arguments, + InvokeOptions options = InvokeOptions::SingleThreaded); + +private: + StreamWriter writer; + StreamReader reader; + bool error = false; + + int suspendRefCount = 0; + + bool SendReceive(Command &cmd); + void ReadEvent(CommandData &data, Event &ev); + + classID GetSuper(classID clazz); + std::string GetSignature(referenceTypeID typeID); + std::vector GetMethods(referenceTypeID searchClass); +}; +}; \ No newline at end of file diff --git a/renderdoc/android/jdwp_connection.cpp b/renderdoc/android/jdwp_connection.cpp new file mode 100644 index 000000000..b9b3cd087 --- /dev/null +++ b/renderdoc/android/jdwp_connection.cpp @@ -0,0 +1,586 @@ +/****************************************************************************** + * The MIT License (MIT) + * + * Copyright (c) 2018 Baldur Karlsson + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + ******************************************************************************/ + +#include "jdwp.h" + +namespace JDWP +{ +// short helper functions to read/write vectors - always preceeded by an int length +template +void ReadVector(CommandData &data, std::vector &vec, + std::function process) +{ + int32_t count = 0; + data.Read(count); + + vec.resize((size_t)count); + for(int32_t i = 0; i < count; i++) + process(data, vec[i]); +} + +template +void WriteVector(CommandData &data, const std::vector &vec, + std::function process) +{ + int32_t count = (int32_t)vec.size(); + data.Write(count); + + for(int32_t i = 0; i < count; i++) + process(data, vec[i]); +} + +Connection::Connection(Network::Socket *sock) + : writer(sock, Ownership::Nothing), reader(sock, Ownership::Nothing) +{ + // the first thing we do is write the handshake bytes and expect them immediately echo'd back. + const int handshakeLength = 14; + const char handshake[handshakeLength + 1] = "JDWP-Handshake"; + + writer.Write(handshake, handshakeLength); + writer.Flush(); + + char response[15] = {}; + reader.Read(response, handshakeLength); + + if(memcmp(handshake, response, 14)) + { + RDCERR("handshake failed - received >%s< - expected >%s<", response, handshake); + error = true; + } +} + +Connection::~Connection() +{ + // bail immediately if we're in error state + if(IsErrored()) + return; + + // otherwise, undo any suspends we might have done that are still outstanding, in case a logic + // error made us bail while we had the VM suspended. We copy the refcount since Resume() + // decrements it. + int ref = suspendRefCount; + for(int i = 0; i < ref; i++) + Resume(); +} + +bool Connection::SendReceive(Command &cmd) +{ + // send the command, and recieve the reply back into the same object. + // save the auto-generated ID for this command so we can compare it to the reply - we expect a + // synchronous reply, no other commands in the way. + uint32_t id = cmd.Send(writer); + cmd.Recv(reader); + + if(id != cmd.GetID()) + { + RDCERR("Didn't get matching reply packet for %d/%d", cmd.commandset, cmd.command); + error = true; + return false; + } + + return true; +} + +void Connection::SetupIDSizes() +{ + Command cmd(CommandSet::VirtualMachine, 7); + if(!SendReceive(cmd)) + return; + + int32_t fieldIDSize = 0; + int32_t methodIDSize = 0; + int32_t objectIDSize = 0; + int32_t referenceTypeIDSize = 0; + int32_t frameIDSize = 0; + + cmd.GetData() + .Read(fieldIDSize) + .Read(methodIDSize) + .Read(objectIDSize) + .Read(referenceTypeIDSize) + .Read(frameIDSize) + .Done(); + + // we only support sizes that are all the same + if(fieldIDSize != methodIDSize || fieldIDSize != objectIDSize || + fieldIDSize != referenceTypeIDSize || fieldIDSize != frameIDSize) + { + RDCLOG("Field ID sizes (%d %d %d %d %d) are not the same. Unsupported!", fieldIDSize, + methodIDSize, objectIDSize, referenceTypeIDSize, frameIDSize); + error = true; + return; + } + + // we also only support 4 or 8 bytes + if(fieldIDSize != 4 && fieldIDSize != 8) + { + RDCLOG("Field ID size %d is unsupported!", fieldIDSize); + error = true; + return; + } + + objectID::setSize(fieldIDSize); +} + +void Connection::Suspend() +{ + suspendRefCount++; + + Command cmd(CommandSet::VirtualMachine, 8); + SendReceive(cmd); +} + +void Connection::Resume() +{ + if(suspendRefCount > 0) + suspendRefCount--; + else + RDCERR("Resuming while we are believed to be running! suspend refcount problem"); + + Command cmd(CommandSet::VirtualMachine, 9); + SendReceive(cmd); +} + +referenceTypeID Connection::GetType(const std::string &signature) +{ + Command cmd(CommandSet::VirtualMachine, 2); + cmd.GetData().Write(signature); + + if(!SendReceive(cmd)) + return {}; + + int32_t numTypes = 0; + byte typetag; + referenceTypeID ret; + int32_t status; // unused + + cmd.GetData().Read(numTypes).Read(typetag).Read(ret).Read(status).Done(); + + if(numTypes == 0) + return {}; + else if(numTypes > 1) + RDCWARN("Multiple types found for '%s'", signature.c_str()); + + return ret; +} + +referenceTypeID Connection::GetType(objectID obj) +{ + Command cmd(CommandSet::ObjectReference, 1); + cmd.GetData().Write(obj); + + if(!SendReceive(cmd)) + return {}; + + byte tag = 0; + referenceTypeID ret = {}; + + cmd.GetData().Read(tag).Read(ret).Done(); + return ret; +} + +methodID Connection::GetMethod(referenceTypeID type, const std::string &name, + const std::string &signature) +{ + referenceTypeID searchClass = type; + + while(searchClass != 0) + { + /* + RDCDEBUG("Searching for %s (%s) in '%s'", name.c_str(), signature.c_str(), + GetSignature(searchClass).c_str()); + */ + + std::vector methods = GetMethods(searchClass); + + for(const Method &m : methods) + { + if(m.name == name && (signature == "" || signature == m.signature)) + { + // RDCDEBUG("Found %s (%s)!", m.name.c_str(), m.signature.c_str()); + return m.id; + } + } + + searchClass = GetSuper(searchClass); + } + + return {}; +} + +int32_t Connection::GetLocalVariable(referenceTypeID type, methodID method, const std::string &name, + const std::string &signature) +{ + Command cmd(CommandSet::Method, 2); + cmd.GetData().Write(type).Write(method); + + if(!SendReceive(cmd)) + return -1; + + int32_t argumentCount = 0; + std::vector slots; + + CommandData data = cmd.GetData(); + data.Read(argumentCount); // unused for now + ReadVector(data, slots, [](CommandData &data, VariableSlot &s) { + data.Read(s.codeIndex).Read(s.name).Read(s.signature).Read(s.length).Read(s.slot); + }); + data.Done(); + + for(const VariableSlot &s : slots) + if(s.name == name && (signature == "" || signature == s.signature)) + return s.slot; + + return -1; +} + +fieldID Connection::GetField(referenceTypeID type, const std::string &name, + const std::string &signature) +{ + Command cmd(CommandSet::ReferenceType, 4); + cmd.GetData().Write(type); + + if(!SendReceive(cmd)) + return {}; + + std::vector fields; + CommandData data = cmd.GetData(); + ReadVector(data, fields, [](CommandData &data, Field &f) { + data.Read(f.id).Read(f.name).Read(f.signature).Read(f.modBits); + }); + data.Done(); + + for(const Field &f : fields) + if(f.name == name && (signature == "" || signature == f.signature)) + return f.id; + + return {}; +} + +value Connection::GetFieldValue(referenceTypeID type, fieldID field) +{ + Command cmd(CommandSet::ReferenceType, 6); + cmd.GetData().Write(type).Write(1).Write(field); + + if(!SendReceive(cmd)) + return {}; + + int32_t numVals = 0; + value ret; + + cmd.GetData().Read(numVals).Read(ret).Done(); + + if(numVals != 1) + RDCWARN("Unexpected number of values found in GetValue: %d", numVals); + + return ret; +} + +std::vector Connection::GetCallStack(threadID thread) +{ + Command cmd(CommandSet::ThreadReference, 6); + // always fetch full stack + cmd.GetData().Write(thread).Write(0).Write(-1); + + if(!SendReceive(cmd)) + return {}; + + std::vector ret; + CommandData data = cmd.GetData(); + ReadVector( + data, ret, [](CommandData &data, StackFrame &f) { data.Read(f.id).Read(f.location); }); + data.Done(); + + // simplify error handling, if the stack came back as nonsense then clear it + if(!ret.empty() && ret[0].id == 0) + ret.clear(); + + return ret; +} + +objectID Connection::GetThis(threadID thread, frameID frame) +{ + Command cmd(CommandSet::StackFrame, 3); + cmd.GetData().Write(thread).Write(frame); + + if(!SendReceive(cmd)) + return {}; + + taggedObjectID ret = {}; + cmd.GetData().Read(ret).Done(); + return ret.id; +} + +void Connection::ReadEvent(CommandData &data, Event &ev) +{ + data.Read((byte &)ev.eventKind).Read(ev.requestID); + + switch(ev.eventKind) + { + case EventKind::ClassPrepare: + { + data.Read(ev.ClassPrepare.thread) + .Read((byte &)ev.ClassPrepare.refTypeTag) + .Read(ev.ClassPrepare.typeID) + .Read(ev.ClassPrepare.signature) + .Read(ev.ClassPrepare.statusInt); + break; + } + case EventKind::MethodEntry: + { + data.Read(ev.MethodEntry.thread).Read(ev.MethodEntry.location); + break; + } + default: RDCERR("Unexpected event! Add handling"); + } +} + +Event Connection::WaitForEvent(EventKind kind, const std::vector &eventFilters, + EventFilterFunction filterCallback) +{ + int32_t eventRequestID = 0; + + { + Command cmd(CommandSet::EventRequest, 1); + CommandData data = cmd.GetData(); + + // always suspend all threads + data.Write((byte)kind).Write((byte)SuspendPolicy::All); + + WriteVector(data, eventFilters, [](CommandData &data, const EventFilter &f) { + data.Write((byte)f.modKind); + if(f.modKind == ModifierKind::ClassOnly) + data.Write(f.ClassOnly); + else + RDCERR("Unsupported event filter %d", f.modKind); + }); + + if(!SendReceive(cmd)) + return {}; + + cmd.GetData().Read(eventRequestID).Done(); + } + + if(eventRequestID == 0) + { + RDCERR("Failed to set event"); + error = true; + return {}; + } + + // now resume execution + Resume(); + + // wait for method entry on the method we care about + while(!reader.IsErrored()) + { + SuspendPolicy suspendPolicy; + std::vector events; + + Command msg; + msg.Recv(reader); + + if(msg.commandset != CommandSet::Event || msg.command != 100) + { + RDCERR("Expected event packet, but got %d/%d", msg.commandset, msg.command); + error = true; + return {}; + } + + CommandData data = msg.GetData(); + + data.Read((byte &)suspendPolicy); + ReadVector(data, events, [this](CommandData &data, Event &ev) { ReadEvent(data, ev); }); + data.Done(); + + // event arrived, we're now suspended + if(suspendPolicy != SuspendPolicy::None) + suspendRefCount++; + + for(const Event &event : events) + { + if(event.eventKind == kind && event.requestID == eventRequestID) + { + // check callback filter + if(filterCallback(event)) + { + // stop listening to this event, and leave VM suspended + Command cmd(CommandSet::EventRequest, 2); + cmd.GetData().Write((byte)EventKind::MethodEntry).Write(eventRequestID); + SendReceive(cmd); + + // return the matching event + return event; + } + } + } + + // resume to get the next event + Resume(); + } + + // something went wrong, we stopped receiving events before the found we wanted. + return {}; +} + +value Connection::NewString(threadID thread, const std::string &str) +{ + Command cmd(CommandSet::VirtualMachine, 11); + cmd.GetData().Write(str); + + if(!SendReceive(cmd)) + return {}; + + stringID ret; + cmd.GetData().Read(ret).Done(); + return {Tag::String, ret}; +} + +value Connection::GetLocalValue(threadID thread, frameID frame, int32_t slot, Tag tag) +{ + Command cmd(CommandSet::StackFrame, 1); + // request one value + cmd.GetData().Write(thread).Write(frame).Write(1).Write(slot).Write((byte)tag); + + SendReceive(cmd); + + int32_t numVals = 0; + value ret; + + cmd.GetData().Read(numVals).Read(ret).Done(); + + if(numVals != 1) + RDCWARN("Unexpected number of values found in GetValue: %d", numVals); + + return ret; +} + +void Connection::SetLocalValue(threadID thread, frameID frame, int32_t slot, value val) +{ + Command cmd(CommandSet::StackFrame, 2); + // set one value + cmd.GetData().Write(thread).Write(frame).Write(1).Write(slot).Write(val); + + SendReceive(cmd); +} + +value Connection::InvokeInstance(threadID thread, classID clazz, methodID method, objectID object, + const std::vector &arguments, InvokeOptions options) +{ + Command cmd; + CommandData data = cmd.GetData(); + + // static or instance invoke + if(object == 0) + { + cmd.commandset = CommandSet::ClassType; + cmd.command = 3; + + data.Write(clazz).Write(thread).Write(method); + } + else + { + cmd.commandset = CommandSet::ObjectReference; + cmd.command = 6; + + data.Write(object).Write(thread).Write(clazz).Write(method); + } + + WriteVector(data, arguments, [](CommandData &data, const value &v) { data.Write(v); }); + + data.Write((int32_t)options); + + if(!SendReceive(cmd)) + return {}; + + value returnValue; + taggedObjectID exception; + + cmd.GetData().Read(returnValue).Read(exception).Done(); + + if(exception.id != 0) + { + RDCERR("Exception encountered while invoking method"); + return {}; + } + + return returnValue; +} + +std::string Connection::GetString(objectID str) +{ + Command cmd(CommandSet::StringReference, 1); + cmd.GetData().Write(str); + + if(!SendReceive(cmd)) + return ""; + + std::string ret; + cmd.GetData().Read(ret).Done(); + return ret; +} + +classID Connection::GetSuper(classID clazz) +{ + Command cmd(CommandSet::ClassType, 1); + cmd.GetData().Write(clazz); + + if(!SendReceive(cmd)) + return {}; + + classID ret; + cmd.GetData().Read(ret).Done(); + return ret; +} + +std::string Connection::GetSignature(referenceTypeID typeID) +{ + Command cmd(CommandSet::ReferenceType, 1); + cmd.GetData().Write(typeID); + + if(!SendReceive(cmd)) + return {}; + + std::string ret; + cmd.GetData().Read(ret).Done(); + return ret; +} + +std::vector Connection::GetMethods(referenceTypeID searchClass) +{ + Command cmd(CommandSet::ReferenceType, 5); + cmd.GetData().Write(searchClass); + + if(!SendReceive(cmd)) + return {}; + + std::vector ret; + CommandData data = cmd.GetData(); + ReadVector(data, ret, [](CommandData &data, Method &m) { + data.Read(m.id).Read(m.name).Read(m.signature).Read(m.modBits); + }); + data.Done(); + return ret; +} +}; \ No newline at end of file diff --git a/renderdoc/android/jdwp_util.cpp b/renderdoc/android/jdwp_util.cpp new file mode 100644 index 000000000..8beea5bc1 --- /dev/null +++ b/renderdoc/android/jdwp_util.cpp @@ -0,0 +1,238 @@ +/****************************************************************************** + * The MIT License (MIT) + * + * Copyright (c) 2018 Baldur Karlsson + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + ******************************************************************************/ + +#include "jdwp.h" + +// The handshake and packet spec is defined in: +// https://docs.oracle.com/javase/1.5.0/docs/guide/jpda/jdwp-spec.html +// This gives the overall structure of each packet, plus the format of the 'basic' types like +// objectID, value, location, etc. + +namespace JDWP +{ +uint32_t Command::idalloc = 42; +int32_t objectID::size = 8; + +uint32_t Command::Send(StreamWriter &writer) +{ + id = idalloc++; + length = 11 + (uint32_t)data.size(); + + uint32_t tmp = EndianSwap(length); + writer.Write(tmp); + tmp = EndianSwap(id); + writer.Write(tmp); + + // no need to endian swap these + byte flags = 0; + writer.Write(flags); + writer.Write(commandset); + writer.Write(command); + + // already endian swapped + writer.Write(data.data(), data.size()); + + writer.Flush(); + + return id; +} + +void Command::Recv(StreamReader &reader) +{ + reader.Read(length); + length = EndianSwap(length); + reader.Read(id); + id = EndianSwap(id); + + byte flags = 0; + reader.Read(flags); + if(flags == 0x80) + { + commandset = CommandSet::Unknown; + command = 0; + uint16_t errInt = 0; + reader.Read(errInt); + error = (Error)EndianSwap(errInt); + } + else + { + reader.Read(commandset); + reader.Read(command); + error = Error::None; + } + + data.clear(); + data.resize(length - 11); + reader.Read(&data[0], data.size()); +} + +CommandData Command::GetData() +{ + return CommandData(data); +} + +void CommandData::ReadBytes(void *bytes, size_t length) +{ + // read the data if there's enough, otherwise set to 0s. + if(offs + length <= data.size()) + memcpy(bytes, data.data() + offs, length); + else + memset(bytes, 0, length); + + // always increment the offset so we can see how much we over-read in Done(). + offs += length; +} + +void CommandData::WriteBytes(const void *bytes, size_t length) +{ + const byte *start = (const byte *)bytes; + data.insert(data.end(), start, start + length); +} + +template <> +CommandData &CommandData::Read(std::string &str) +{ + uint32_t length = 0; + Read(length); + str.resize(length); + ReadBytes(&str[0], length); + return *this; +} + +template <> +CommandData &CommandData::Write(const std::string &str) +{ + uint32_t length = (uint32_t)str.size(); + Write(length); + WriteBytes(str.c_str(), str.size()); + return *this; +} + +template <> +CommandData &CommandData::Read(objectID &id) +{ + ReadBytes(&id, objectID::getSize()); + id.EndianSwap(); + return *this; +} + +template <> +CommandData &CommandData::Write(const objectID &id) +{ + objectID tmp = id; + tmp.EndianSwap(); + WriteBytes(&tmp, objectID::getSize()); + return *this; +} + +template <> +CommandData &CommandData::Read(taggedObjectID &id) +{ + Read((byte &)id.tag); + Read(id.id); + return *this; +} + +template <> +CommandData &CommandData::Write(const taggedObjectID &id) +{ + Write((const byte &)id.tag); + Write(id.id); + return *this; +} + +template <> +CommandData &CommandData::Read(value &val) +{ + Read((byte &)val.tag); + switch(val.tag) + { + case Tag::Unknown: break; + case Tag::Array: Read(val.Array); break; + case Tag::Byte: Read(val.Byte); break; + case Tag::Char: Read(val.Char); break; + case Tag::Object: Read(val.Object); break; + case Tag::Float: Read(val.Float); break; + case Tag::Double: Read(val.Double); break; + case Tag::Int: Read(val.Int); break; + case Tag::Long: Read(val.Long); break; + case Tag::Short: Read(val.Short); break; + case Tag::Void: break; + case Tag::Boolean: Read(val.Bool); break; + case Tag::String: Read(val.String); break; + case Tag::Thread: Read(val.Thread); break; + case Tag::ThreadGroup: Read(val.ThreadGroup); break; + case Tag::ClassLoader: Read(val.ClassLoader); break; + case Tag::ClassObject: Read(val.ClassObject); break; + } + return *this; +} + +template <> +CommandData &CommandData::Write(const value &val) +{ + Write((const byte &)val.tag); + switch(val.tag) + { + case Tag::Unknown: break; + case Tag::Array: Write(val.Array); break; + case Tag::Byte: Write(val.Byte); break; + case Tag::Char: Write(val.Char); break; + case Tag::Object: Write(val.Object); break; + case Tag::Float: Write(val.Float); break; + case Tag::Double: Write(val.Double); break; + case Tag::Int: Write(val.Int); break; + case Tag::Long: Write(val.Long); break; + case Tag::Short: Write(val.Short); break; + case Tag::Void: break; + case Tag::Boolean: Write(val.Bool); break; + case Tag::String: Write(val.String); break; + case Tag::Thread: Write(val.Thread); break; + case Tag::ThreadGroup: Write(val.ThreadGroup); break; + case Tag::ClassLoader: Write(val.ClassLoader); break; + case Tag::ClassObject: Write(val.ClassObject); break; + } + return *this; +} + +template <> +CommandData &CommandData::Read(Location &loc) +{ + Read((byte &)loc.tag); + Read(loc.classID); + Read(loc.methodID); + Read(loc.index); + return *this; +} + +template <> +CommandData &CommandData::Write(const Location &loc) +{ + Write((const byte &)loc.tag); + Write(loc.classID); + Write(loc.methodID); + Write(loc.index); + return *this; +} +}; \ No newline at end of file diff --git a/renderdoc/driver/gl/gl_hooks_egl.cpp b/renderdoc/driver/gl/gl_hooks_egl.cpp index 7c9a4b14c..6858c35be 100644 --- a/renderdoc/driver/gl/gl_hooks_egl.cpp +++ b/renderdoc/driver/gl/gl_hooks_egl.cpp @@ -61,7 +61,19 @@ public: return false; if(libName) + { + // register our hooked functions for PLT hooking on android + PosixHookFunctions(); + + // load the libEGL.so library and when loaded call libHooked which initialises GLES capture PosixHookLibrary("libEGL.so", &libHooked); + PosixHookLibrary("libEGL.so.1", NULL); + PosixHookLibrary("libGL.so.1", NULL); + PosixHookLibrary("libGLESv1_CM.so", NULL); + PosixHookLibrary("libGLESv2.so", NULL); + PosixHookLibrary("libGLESv2.so.2", NULL); + PosixHookLibrary("libGLESv3.so", NULL); + } bool success = SetupHooks(); @@ -216,13 +228,6 @@ public: bool PopulateHooks(); } eglhooks; -void EGLHook::libHooked(void *realLib) -{ - libGLdlsymHandle = realLib; - eglhooks.CreateHooks(NULL); - eglhooks.GetDriver()->SetDriverType(RDCDriver::OpenGLES); -} - // everything below here needs to have C linkage extern "C" { @@ -243,6 +248,8 @@ __attribute__((visibility("default"))) EGLContext eglCreateContext(EGLDisplay di EGLContext shareContext, EGLint const *attribList) { + PosixHookReapply(); + EGLint defaultAttribList[] = {0}; const EGLint *attribs = attribList ? attribList : defaultAttribList; @@ -392,7 +399,11 @@ __attribute__((visibility("default"))) __eglMustCastToProperFunctionPointerType if(eglhooks.real.GetProcAddress == NULL) eglhooks.SetupExportedFunctions(); - __eglMustCastToProperFunctionPointerType realFunc = eglhooks.real.GetProcAddress(func); + __eglMustCastToProperFunctionPointerType realFunc = NULL; + { + PosixScopedSuppressHooking suppress; + realFunc = eglhooks.real.GetProcAddress(func); + } if(!strcmp(func, "eglCreateContext")) return (__eglMustCastToProperFunctionPointerType)&eglCreateContext; @@ -416,13 +427,31 @@ __attribute__((visibility("default"))) __eglMustCastToProperFunctionPointerType }; // extern "C" +void EGLHook::libHooked(void *realLib) +{ + libGLdlsymHandle = realLib; + eglhooks.CreateHooks(NULL); + eglhooks.GetDriver()->SetDriverType(RDCDriver::OpenGLES); + + PosixHookFunction("eglCreateContext", (void *)&eglCreateContext); + PosixHookFunction("eglGetDisplay", (void *)&eglGetDisplay); + PosixHookFunction("eglDestroyContext", (void *)&eglDestroyContext); + PosixHookFunction("eglMakeCurrent", (void *)&eglMakeCurrent); + PosixHookFunction("eglSwapBuffers", (void *)&eglSwapBuffers); + PosixHookFunction("eglGetProcAddress", (void *)&eglGetProcAddress); +} + bool EGLHook::PopulateHooks() { SetupHooks(); - return SharedPopulateHooks( - false, // dlsym can return GL symbols during a GLES context - [](const char *funcName) { return (void *)eglGetProcAddress(funcName); }); + // dlsym can return GL symbols during a GLES context + return SharedPopulateHooks(false, [](const char *funcName) { + // on some android devices we need to hook dlsym, but eglGetProcAddress might call dlsym so we + // need to ensure we return the 'real' pointers + PosixScopedSuppressHooking suppress; + return (void *)eglGetProcAddress(funcName); + }); } const GLHookSet &GetRealGLFunctionsEGL() diff --git a/renderdoc/driver/gl/gl_hooks_linux_shared.cpp b/renderdoc/driver/gl/gl_hooks_linux_shared.cpp index 129cfdf6b..498f32ceb 100644 --- a/renderdoc/driver/gl/gl_hooks_linux_shared.cpp +++ b/renderdoc/driver/gl/gl_hooks_linux_shared.cpp @@ -1181,6 +1181,7 @@ bool SharedPopulateHooks(bool dlsymFirst, void *(*lookupFunc)(const char *)) #define HookInit(function) \ if(GL.function == NULL) \ { \ + PosixScopedSuppressHooking suppress; \ if(dlsymFirst) \ GL.function = (CONCAT(function, _hooktype))dlsym(libGLdlsymHandle, STRINGIZE(function)); \ lookupFunc((const char *)STRINGIZE(function)); \ @@ -1204,3 +1205,19 @@ bool SharedPopulateHooks(bool dlsymFirst, void *(*lookupFunc)(const char *)) return true; } + +void PosixHookFunctions() +{ +#undef HookInit +#define HookInit(function) \ + PosixHookFunction(STRINGIZE(function), (void *)&CONCAT(function, _renderdoc_hooked)) +#undef HookExtension +#define HookExtension(funcPtrType, function) \ + PosixHookFunction(STRINGIZE(function), (void *)&CONCAT(function, _renderdoc_hooked)) +#undef HookExtensionAlias +#define HookExtensionAlias(funcPtrType, function, alias) \ + PosixHookFunction(STRINGIZE(alias), (void *)&CONCAT(function, _renderdoc_hooked)) + + DLLExportHooks(); + HookCheckGLExtensions(); +} \ No newline at end of file diff --git a/renderdoc/driver/gl/gl_hooks_linux_shared.h b/renderdoc/driver/gl/gl_hooks_linux_shared.h index 96ab7542f..d9e459e23 100644 --- a/renderdoc/driver/gl/gl_hooks_linux_shared.h +++ b/renderdoc/driver/gl/gl_hooks_linux_shared.h @@ -32,6 +32,7 @@ void CloneDisplay(Display *dpy); void *SharedLookupFuncPtr(const char *func, void *realFunc); bool SharedPopulateHooks(bool dlsymFirst, void *(*lookupFunc)(const char *)); +void PosixHookFunctions(); extern GLHookSet GL; extern WrappedOpenGL *m_GLDriver; diff --git a/renderdoc/hooks/hooks.h b/renderdoc/hooks/hooks.h index 51d1c4091..186ede0f8 100644 --- a/renderdoc/hooks/hooks.h +++ b/renderdoc/hooks/hooks.h @@ -68,7 +68,7 @@ private: #include #define HOOKS_BEGIN() PosixHookInit() -#define HOOKS_END() +#define HOOKS_END() PosixHookApply() #define HOOKS_REMOVE() #define HOOKS_IDENTIFY(identifier) PosixHookDetect(identifier) diff --git a/renderdoc/os/posix/android/android_hook.cpp b/renderdoc/os/posix/android/android_hook.cpp index 1247c21c5..c2afd3d0a 100644 --- a/renderdoc/os/posix/android/android_hook.cpp +++ b/renderdoc/os/posix/android/android_hook.cpp @@ -22,10 +22,55 @@ * THE SOFTWARE. ******************************************************************************/ +#include "3rdparty/plthook/plthook.h" +#include "common/common.h" +#include "common/threading.h" #include "os/posix/posix_hook.h" +#include #include +#include +#include #include +#include +#include +#include +#include + +// uncomment the following to print (very verbose) debugging prints for the android PLT hooking +//#define HOOK_DEBUG_PRINT(...) RDCLOG(__VA_ARGS__) + +#if !defined(HOOK_DEBUG_PRINT) +#define HOOK_DEBUG_PRINT(...) \ + do \ + { \ + } while(0) +#endif + +// from plthook_elf.c +#if defined __x86_64__ || defined __x86_64 +#define R_JUMP_SLOT R_X86_64_JUMP_SLOT +#define Elf_Rel ElfW(Rela) +#define ELF_R_TYPE ELF64_R_TYPE +#define ELF_R_SYM ELF64_R_SYM +#elif defined __i386__ || defined __i386 +#define R_JUMP_SLOT R_386_JMP_SLOT +#define Elf_Rel ElfW(Rel) +#define ELF_R_TYPE ELF32_R_TYPE +#define ELF_R_SYM ELF32_R_SYM +#elif defined __arm__ || defined __arm +#define R_JUMP_SLOT R_ARM_JUMP_SLOT +#define Elf_Rel ElfW(Rel) +#define ELF_R_TYPE ELF32_R_TYPE +#define ELF_R_SYM ELF32_R_SYM +#elif defined __aarch64__ || defined __aarch64 /* ARM64 */ +#define R_JUMP_SLOT R_AARCH64_JUMP_SLOT +#define Elf_Rel ElfW(Rela) +#define ELF_R_TYPE ELF64_R_TYPE +#define ELF_R_SYM ELF64_R_SYM +#else +#error unsupported OS +#endif void PosixHookInit() { @@ -36,7 +81,448 @@ bool PosixHookDetect(const char *identifier) return dlsym(RTLD_DEFAULT, identifier) != NULL; } +class HookingInfo +{ +public: + void AddFunctionHook(const std::string &name, void *hook) + { + SCOPED_LOCK(lock); + funchooks[name] = hook; + } + + void AddLibHook(const std::string &name) + { + SCOPED_LOCK(lock); + libhooks.insert(name); + } + + void *GetFunctionHook(const std::string &name) + { + SCOPED_LOCK(lock); + return funchooks[name]; + } + + bool IsLibHook(const std::string &path) + { + SCOPED_LOCK(lock); + for(const std::string &filename : libhooks) + { + if(path.find(filename) != std::string::npos) + { + HOOK_DEBUG_PRINT("Intercepting and returning ourselves for %s (matches %s)", path.c_str(), + filename.c_str()); + return true; + } + } + + return false; + } + + bool IsLibHook(void *handle) + { + SCOPED_LOCK(lock); + for(const std::string &lib : libhooks) + { + void *libHandle = dlopen(lib.c_str(), RTLD_NOLOAD); + HOOK_DEBUG_PRINT("%s is %p", lib.c_str(), libHandle); + if(libHandle == handle) + return true; + } + + return false; + } + + bool AlreadyHooked(void *handle) + { + SCOPED_LOCK(lock); + bool ret = hooked_handle_already.find(handle) != hooked_handle_already.end(); + hooked_handle_already.insert(handle); + return ret; + } + + bool AlreadyHooked(const std::string &soname) + { + SCOPED_LOCK(lock); + if(hooked_soname_already.find(soname) != hooked_soname_already.end()) + return true; + + // above will be absolute path, allow substring matches + for(const std::string &fn : hooked_soname_already) + if(soname.find(fn) != std::string::npos) + return true; + + hooked_soname_already.insert(soname); + return false; + } + +private: + std::set hooked_soname_already; + std::set hooked_handle_already; + + std::map funchooks; + std::set libhooks; + + Threading::CriticalSection lock; +}; + +HookingInfo &GetHookInfo() +{ + static HookingInfo hookinfo; + return hookinfo; +} + +void PosixHookFunction(const char *name, void *hook) +{ + GetHookInfo().AddFunctionHook(name, hook); +} + void PosixHookLibrary(const char *name, dlopenCallback cb) { - cb(dlopen(name, RTLD_NOW)); + GetHookInfo().AddLibHook(name); + + if(cb) + cb(dlopen(name, RTLD_NOW)); } + +static int dl_iterate_callback(struct dl_phdr_info *info, size_t size, void *data) +{ + if(info->dlpi_name == NULL) + { + HOOK_DEBUG_PRINT("Skipping NULL entry!"); + return 0; + } + std::string soname = info->dlpi_name; + + if(GetHookInfo().AlreadyHooked(soname)) + return 0; + + HOOK_DEBUG_PRINT("Hooking %s", soname.c_str()); + + for(int ph = 0; ph < info->dlpi_phnum; ph++) + { + if(info->dlpi_phdr[ph].p_type != PT_DYNAMIC) + continue; + + ElfW(Dyn) *dynamic = (ElfW(Dyn) *)(info->dlpi_addr + info->dlpi_phdr[ph].p_vaddr); + + ElfW(Sym) *dynsym = NULL; + const char *strtab = NULL; + size_t strtabcount = 0; + Elf_Rel *pltbase = NULL; + ElfW(Sword) pltcount = 0; + + while(dynamic->d_tag != DT_NULL) + { + if(dynamic->d_tag == DT_SYMTAB) + dynsym = (ElfW(Sym) *)(info->dlpi_addr + dynamic->d_un.d_ptr); + else if(dynamic->d_tag == DT_STRTAB) + strtab = (const char *)(info->dlpi_addr + dynamic->d_un.d_ptr); + else if(dynamic->d_tag == DT_STRSZ) + strtabcount = dynamic->d_un.d_val; + else if(dynamic->d_tag == DT_JMPREL) + pltbase = (Elf_Rel *)(info->dlpi_addr + dynamic->d_un.d_ptr); + else if(dynamic->d_tag == DT_PLTRELSZ) + pltcount = dynamic->d_un.d_val / sizeof(Elf_Rel); + + /* + if(dynamic->d_tag == DT_NEEDED) + HOOK_DEBUG_PRINT("NEEDED [%i, %s]", dynamic->d_un.d_val, strtab + dynamic->d_un.d_val); + */ + + dynamic++; + } + + if(!dynsym || !strtab || !pltbase || pltcount == 0 || strtabcount == 0) + { + RDCWARN("Missing required section to hook %s", info->dlpi_name); + continue; + } + + void **relro_base = NULL; + void **relro_end = NULL; + bool relro_failed = false; + + FILE *f = FileIO::fopen(info->dlpi_name, "r"); + + // read the file on disk to get the .relro section + if(f) + { + ElfW(Ehdr) ehdr; + size_t read = FileIO::fread(&ehdr, sizeof(ehdr), 1, f); + + if(read == 1 && ehdr.e_ident[0] == ELFMAG0 && ehdr.e_ident[1] == 'E' && + ehdr.e_ident[2] == 'L' && ehdr.e_ident[3] == 'F') + { + FileIO::fseek64(f, ehdr.e_phoff, SEEK_SET); + for(ElfW(Half) idx = 0; idx < ehdr.e_phnum; idx++) + { + ElfW(Phdr) phdr; + read = FileIO::fread(&phdr, sizeof(phdr), 1, f); + if(read != 1) + { + RDCWARN("Failed reading section"); + break; + } + + if(phdr.p_type == PT_GNU_RELRO) + { + relro_base = (void **)(info->dlpi_addr + phdr.p_vaddr); + relro_end = (void **)(info->dlpi_addr + phdr.p_vaddr + phdr.p_memsz); + } + } + } + else + { + RDCWARN("Didn't get valid ELF header"); + } + + FileIO::fclose(f); + } + else + { + RDCWARN("Couldn't open '%s' to look for relro!", info->dlpi_name); + relro_failed = true; + } + + if(relro_base) + HOOK_DEBUG_PRINT("Got relro %p -> %p", relro_base, relro_end); + HOOK_DEBUG_PRINT("Got %i PLT entries", pltcount); + + int pagesize = sysconf(_SC_PAGE_SIZE); + + for(ElfW(Sword) i = 0; i < pltcount; i++) + { + Elf_Rel *plt = pltbase + i; + if(ELF_R_TYPE(plt->r_info) != R_JUMP_SLOT) + { + HOOK_DEBUG_PRINT("[%i]: Mismatched type %i vs %i", i, ELF_R_TYPE(plt->r_info), R_JUMP_SLOT); + continue; + } + + size_t idx = ELF_R_SYM(plt->r_info); + size_t name = dynsym[idx].st_name; + if(name + 1 > strtabcount) + { + HOOK_DEBUG_PRINT("[%i] name out of boundstoo big section header string table index: %zu", i, + name); + continue; + } + + const char *importname = strtab + name; + void **import = (void **)(info->dlpi_addr + plt->r_offset); + + HOOK_DEBUG_PRINT("[%i] %s at %p (ptr to %p)", i, importname, import, *import); + + void *repl = GetHookInfo().GetFunctionHook(importname); + if(repl) + { + HOOK_DEBUG_PRINT("replacing %s!", importname); + + uintptr_t pagebase = 0; + + if(relro_failed || (relro_base <= import && import <= relro_end)) + { + if(relro_failed) + HOOK_DEBUG_PRINT("Couldn't get relro sections - mapping read/write"); + else + HOOK_DEBUG_PRINT("In relro range - %p <= %p <= %p", relro_base, import, relro_end); + pagebase = uintptr_t(import) & ~(pagesize - 1); + + int ret = mprotect((void *)pagebase, pagesize, PROT_READ | PROT_WRITE); + if(ret != 0) + { + RDCERR("Couldn't read/write the page: %d %d", ret, errno); + return 0; + } + + HOOK_DEBUG_PRINT("Marked page read/write"); + } + else + { + HOOK_DEBUG_PRINT("Not in relro! - %p vs %p vs %p", relro_base, import, relro_end); + } + + *import = repl; + + if(pagebase) + { + if(relro_failed) + { + HOOK_DEBUG_PRINT( + "Couldn't find relro sections - being conservative and leaving read-write"); + } + else + { + HOOK_DEBUG_PRINT("Moving back to read-only"); + mprotect((void *)pagebase, pagesize, PROT_READ); + } + } + + HOOK_DEBUG_PRINT("[%i*] %s at %p (ptr to %p)", i, importname, import, *import); + } + } + } + + return 0; +} + +extern "C" __attribute__((visibility("default"))) void *hooked_dlopen(const char *filename, int flag); +extern "C" __attribute__((visibility("default"))) void *hooked_dlsym(void *handle, + const char *symbol); +extern "C" __attribute__((visibility("default"))) void *hooked_android_dlopen_ext( + const char *__filename, int __flags, const android_dlextinfo *__info); + +// android has a special dlopen that passes the caller address in. +typedef void *(*pfn__loader_dlopen)(const char *filename, int flags, const void *caller_addr); + +pfn__loader_dlopen loader_dlopen = NULL; +uint64_t suppressTLS = 0; + +void PosixHookApply() +{ + RDCLOG("Applying hooks"); + + suppressTLS = Threading::AllocateTLSSlot(); + + // blacklist hooking certain system libraries or ourselves + GetHookInfo().AlreadyHooked("libVkLayer_GLES_RenderDoc.so"); + GetHookInfo().AlreadyHooked("libc.so"); + GetHookInfo().AlreadyHooked("libvndksupport.so"); + + GetHookInfo().AddLibHook("libVkLayer_GLES_RenderDoc.so"); + + loader_dlopen = (pfn__loader_dlopen)dlsym(RTLD_NEXT, "__loader_dlopen"); + + if(loader_dlopen) + { + PosixHookFunction("dlopen", (void *)&hooked_dlopen); + } + else + { + RDCWARN("Couldn't find __loader_dlopen, falling back to slow path for dlopen hooking"); + PosixHookFunction("dlsym", (void *)&hooked_dlsym); + } + + PosixHookFunction("android_dlopen_ext", (void *)&hooked_android_dlopen_ext); + + dl_iterate_phdr(dl_iterate_callback, NULL); +} + +void PosixHookReapply() +{ + dl_iterate_phdr(dl_iterate_callback, NULL); +} + +void *intercept_dlopen(const char *filename, int flag) +{ + if(GetHookInfo().IsLibHook(filename)) + return dlopen("libVkLayer_GLES_RenderDoc.so", flag); + + return NULL; +} + +void process_dlopen(const char *filename, int flag) +{ + if(!GetHookInfo().AlreadyHooked(filename)) + { + dl_iterate_phdr(dl_iterate_callback, NULL); + } + else + { + HOOK_DEBUG_PRINT("Ignoring"); + } +} + +extern "C" __attribute__((visibility("default"))) void *hooked_dlopen(const char *filename, int flag) +{ + // get caller address immediately. + const void *caller_addr = __builtin_return_address(0); + + HOOK_DEBUG_PRINT("hooked_dlopen for %s | %d", filename, flag); + void *ret = intercept_dlopen(filename, flag); + + // if we intercepted, return immediately + if(ret) + return ret; + + ret = loader_dlopen(filename, flag, caller_addr); + HOOK_DEBUG_PRINT("Got %p", ret); + + if(filename && ret) + process_dlopen(filename, flag); + + return ret; +} + +extern "C" __attribute__((visibility("default"))) void *hooked_android_dlopen_ext( + const char *__filename, int __flags, const android_dlextinfo *__info) +{ + HOOK_DEBUG_PRINT("hooked_android_dlopen_ext for %s | %d", __filename, __flags); + + void *ret = intercept_dlopen(__filename, __flags); + + // if we intercepted, return immediately + if(ret) + return ret; + + ret = android_dlopen_ext(__filename, __flags, __info); + HOOK_DEBUG_PRINT("Got %p", ret); + + if(__filename && ret) + process_dlopen(__filename, __flags); + + return ret; +} + +PosixScopedSuppressHooking::PosixScopedSuppressHooking() +{ + if(suppressTLS == 0) + return; + + uintptr_t old = (uintptr_t)Threading::GetTLSValue(suppressTLS); + Threading::SetTLSValue(suppressTLS, (void *)(old + 1)); +} + +PosixScopedSuppressHooking::~PosixScopedSuppressHooking() +{ + if(suppressTLS == 0) + return; + + uintptr_t old = (uintptr_t)Threading::GetTLSValue(suppressTLS); + Threading::SetTLSValue(suppressTLS, (void *)(old - 1)); +} + +bool hooks_suppressed() +{ + return (uintptr_t)Threading::GetTLSValue(suppressTLS) > 0; +} + +extern "C" __attribute__((visibility("default"))) void *hooked_dlsym(void *handle, const char *symbol) +{ + if(handle == NULL || symbol == NULL || hooks_suppressed()) + return dlsym(handle, symbol); + + void *repl = GetHookInfo().GetFunctionHook(symbol); + + if(repl == NULL) + return dlsym(handle, symbol); + + if(!GetHookInfo().AlreadyHooked(handle)) + { + dl_iterate_phdr(dl_iterate_callback, NULL); + } + + HOOK_DEBUG_PRINT("Got dlsym for %s which we want in %p...", symbol, handle); + + if(GetHookInfo().IsLibHook(handle)) + { + HOOK_DEBUG_PRINT("identified dlsym(%s) we want to interpose! returning %p", symbol, repl); + return repl; + } + + void *ret = dlsym(handle, symbol); + Dl_info info = {}; + dladdr(ret, &info); + HOOK_DEBUG_PRINT("real ret is %p in %s", ret, info.dli_fname); + return ret; +} \ No newline at end of file diff --git a/renderdoc/os/posix/apple/apple_hook.cpp b/renderdoc/os/posix/apple/apple_hook.cpp index 7f7476b8a..0391a269e 100644 --- a/renderdoc/os/posix/apple/apple_hook.cpp +++ b/renderdoc/os/posix/apple/apple_hook.cpp @@ -39,3 +39,24 @@ bool PosixHookDetect(const char *identifier) void PosixHookLibrary(const char *name, dlopenCallback cb) { } + +// android only hooking functions, not used on apple +PosixScopedSuppressHooking::PosixScopedSuppressHooking() +{ +} + +PosixScopedSuppressHooking::~PosixScopedSuppressHooking() +{ +} + +void PosixHookApply() +{ +} + +void PosixHookReapply() +{ +} + +void PosixHookFunction(char const *, void *) +{ +} diff --git a/renderdoc/os/posix/linux/linux_hook.cpp b/renderdoc/os/posix/linux/linux_hook.cpp index df7151393..34979a403 100644 --- a/renderdoc/os/posix/linux/linux_hook.cpp +++ b/renderdoc/os/posix/linux/linux_hook.cpp @@ -54,6 +54,9 @@ static std::map libraryHooks; void PosixHookLibrary(const char *name, dlopenCallback cb) { + if(cb == NULL) + return; + SCOPED_LOCK(libLock); libraryHooks[name] = cb; } @@ -116,3 +119,24 @@ void plthook_lib(void *handle) plthook_replace(plthook, "dlopen", (void *)dlopen, NULL); plthook_close(plthook); } + +// android only hooking functions, not used on linux +PosixScopedSuppressHooking::PosixScopedSuppressHooking() +{ +} + +PosixScopedSuppressHooking::~PosixScopedSuppressHooking() +{ +} + +void PosixHookApply() +{ +} + +void PosixHookReapply() +{ +} + +void PosixHookFunction(char const *, void *) +{ +} diff --git a/renderdoc/os/posix/posix_hook.h b/renderdoc/os/posix/posix_hook.h index aaf204810..bebbb0a1d 100644 --- a/renderdoc/os/posix/posix_hook.h +++ b/renderdoc/os/posix/posix_hook.h @@ -32,4 +32,18 @@ void PosixHookInit(); // to the callback and librenderdoc.so will be returned to user code void PosixHookLibrary(const char *name, dlopenCallback cb); +void PosixHookFunction(const char *name, void *hook); + +void PosixHookApply(); + +// this is needed on android, when we are PLT hooking to ensure hooks are applied as soon as +// possible. +void PosixHookReapply(); + +struct PosixScopedSuppressHooking +{ + PosixScopedSuppressHooking(); + ~PosixScopedSuppressHooking(); +}; + bool PosixHookDetect(const char *identifier); \ No newline at end of file diff --git a/renderdoc/renderdoc.vcxproj b/renderdoc/renderdoc.vcxproj index bc6aed428..fb9dbe04e 100644 --- a/renderdoc/renderdoc.vcxproj +++ b/renderdoc/renderdoc.vcxproj @@ -134,6 +134,7 @@ + @@ -310,6 +311,9 @@ + + + diff --git a/renderdoc/renderdoc.vcxproj.filters b/renderdoc/renderdoc.vcxproj.filters index 7be2b16bd..b904265a7 100644 --- a/renderdoc/renderdoc.vcxproj.filters +++ b/renderdoc/renderdoc.vcxproj.filters @@ -381,6 +381,9 @@ Android + + Android + @@ -665,6 +668,15 @@ Android + + Android + + + Android + + + Android +