Files
renderdoc/renderdoc/android/jdwp.cpp
T
2020-01-06 16:20:45 +00:00

453 lines
14 KiB
C++

/******************************************************************************
* The MIT License (MIT)
*
* Copyright (c) 2019-2020 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 <functional>
#include "core/core.h"
#include "strings/string_utils.h"
#include "android.h"
#include "android_utils.h"
namespace JDWP
{
void InjectVulkanLayerSearchPath(Connection &conn, threadID thread, int32_t slotIdx,
const rdcstr &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)",
(uint64_t)stringClass, (uint64_t)stringConcat);
return;
}
// get the callstack frames
rdcarray<StackFrame> 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(const rdcstr &deviceID, Network::Socket *sock)
{
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
Android::ABI abi = Android::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 = Android::GetABI(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");
}
if(abi == Android::ABI::unknown)
{
RDCERR("Unrecognised running ABI, falling back to armeabi-v7a");
abi = Android::ABI::armeabi_v7a;
}
rdcstr libPath = Android::GetPathForPackage(deviceID, Android::GetRenderDocPackageForABI(abi));
switch(abi)
{
case Android::ABI::unknown:
case Android::ABI::armeabi_v7a: libPath += "lib/arm"; break;
case Android::ABI::arm64_v8a: libPath += "lib/arm64"; break;
case Android::ABI::x86_64: libPath += "lib/x86_64"; break;
case Android::ABI::x86: libPath += "lib/x86"; break;
}
RDCLOG("Injecting RenderDoc from library in %s", libPath.c_str());
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/+/master/core/java/android/app/ApplicationLoaders.java
// for the public getClassLoader.
// look for both signatures in this order, as it goes from most recent to least recent. In some
// cases (e.g. with List<ClassLoader> sharedLibraries) the older function is still around as an
// overload that forwards on - so may not be called. This would cause us to wait for a function
// to be hit that was never hit.
const char *getClassLoaderSignatures[] = {
// ClassLoader getClassLoader(String zip, int targetSdkVersion, boolean isBundled,
// String librarySearchPath, String libraryPermittedPath,
// ClassLoader parent, String cacheKey,
// String classLoaderName, List<ClassLoader> sharedLibraries);
"(Ljava/lang/String;IZLjava/lang/String;Ljava/lang/String;Ljava/lang/ClassLoader;"
"Ljava/lang/String;Ljava/lang/String;Ljava/util/List;)Ljava/lang/ClassLoader;",
// ClassLoader getClassLoader(String zip, int targetSdkVersion, boolean isBundled,
// String librarySearchPath, String libraryPermittedPath,
// ClassLoader parent, String classLoaderName);
"(Ljava/lang/String;IZLjava/lang/String;Ljava/lang/String;Ljava/lang/ClassLoader;"
"Ljava/lang/String;)Ljava/lang/ClassLoader;",
// ClassLoader getClassLoader(String zip, int targetSdkVersion, boolean isBundled,
// String librarySearchPath, String libraryPermittedPath,
// ClassLoader parent);
"(Ljava/lang/String;IZLjava/lang/String;Ljava/lang/String;Ljava/lang/ClassLoader;)"
"Ljava/lang/ClassLoader;",
};
methodID vulkanLoaderMethod = 0;
for(const char *sig : getClassLoaderSignatures)
{
vulkanLoaderMethod = conn.GetMethod(vulkanLoaderClass, "getClassLoader", sig);
if(vulkanLoaderMethod)
{
RDCLOG("Got android.app.ApplicationLoaders.getClassLoader signature %s", sig);
break;
}
}
if(vulkanLoaderMethod)
{
rdcarray<VariableSlot> slots = conn.GetLocalVariables(vulkanLoaderClass, vulkanLoaderMethod);
int32_t slotIdx = -1, thisSlotIdx = -1;
bool slot4Exists = false;
for(const VariableSlot &s : slots)
{
if(s.name == "librarySearchPath")
{
slotIdx = s.slot;
break;
}
else if(s.name == "this")
{
thisSlotIdx = s.slot;
}
if(s.slot == 4)
slot4Exists = true;
}
// on some newer devices slots are not 0-based, try an offset from this if there is no slot 4
if(slotIdx == -1 && thisSlotIdx != -1 && !slot4Exists)
slotIdx = thisSlotIdx + 4;
// 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 &ev) {
return ev.MethodEntry.location.meth == 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, "<init>", "()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 &ev) { return ev.MethodEntry.location.meth == 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
rdcarray<StackFrame> 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.
//
// Note that because we're filtering on both classID and methodID, we need to return back the
// exact class in the inheritance hierarchy matching the methodID, otherwise we could filter on
// the derived class but a parent method, and have no hits.
//
// This can happen if the most derived class doesn't have an onCreate, and we have to search to a
// superclass
referenceTypeID onCreateClass = thisClass.RefType;
methodID onCreateMethod = conn.GetMethod(thisClass.RefType, "onCreate", "()V", &onCreateClass);
if(onCreateMethod == 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, onCreateClass}},
[onCreateMethod](const Event &ev) { return ev.MethodEntry.location.meth == onCreateMethod; });
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",
(uint64_t)getRuntime, (uint64_t)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 + "/" RENDERDOC_ANDROID_LIBRARY)});
if(ret.tag != Tag::Void)
{
RDCERR("Failed to call load(%s/%s)!", libPath.c_str(), RENDERDOC_ANDROID_LIBRARY);
return false;
}
return true;
}
}; // namespace JDWP
namespace Android
{
bool InjectWithJDWP(const rdcstr &deviceID, uint16_t jdwpport)
{
Network::Socket *sock = Network::CreateClientSocket("localhost", jdwpport, 500);
if(sock)
{
bool ret = JDWP::InjectLibraries(deviceID, sock);
delete sock;
return ret;
}
else
{
RDCERR("Couldn't make JDWP connection");
}
return false;
}
}; // namespace Android