You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
448 lines
14 KiB
448 lines
14 KiB
// Copyright (C) 2017 The Android Open Source Project
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
//
|
|
|
|
#include <android-base/logging.h>
|
|
#include <atomic>
|
|
#include <iostream>
|
|
#include <iomanip>
|
|
#include <jni.h>
|
|
#include <jvmti.h>
|
|
#include <memory>
|
|
#include <string>
|
|
#include <vector>
|
|
|
|
namespace breakpoint_logger {
|
|
|
|
struct SingleBreakpointTarget {
|
|
std::string class_name;
|
|
std::string method_name;
|
|
std::string method_sig;
|
|
jlocation location;
|
|
};
|
|
|
|
struct BreakpointTargets {
|
|
std::vector<SingleBreakpointTarget> bps;
|
|
};
|
|
|
|
static void VMInitCB(jvmtiEnv* jvmti, JNIEnv* env, jthread thr ATTRIBUTE_UNUSED) {
|
|
BreakpointTargets* all_targets = nullptr;
|
|
jvmtiError err = jvmti->GetEnvironmentLocalStorage(reinterpret_cast<void**>(&all_targets));
|
|
if (err != JVMTI_ERROR_NONE || all_targets == nullptr) {
|
|
env->FatalError("unable to get breakpoint targets");
|
|
}
|
|
for (const SingleBreakpointTarget& target : all_targets->bps) {
|
|
jclass k = env->FindClass(target.class_name.c_str());
|
|
if (env->ExceptionCheck()) {
|
|
env->ExceptionDescribe();
|
|
env->FatalError("Could not find class!");
|
|
return;
|
|
}
|
|
jmethodID m = env->GetMethodID(k, target.method_name.c_str(), target.method_sig.c_str());
|
|
if (env->ExceptionCheck()) {
|
|
env->ExceptionClear();
|
|
m = env->GetStaticMethodID(k, target.method_name.c_str(), target.method_sig.c_str());
|
|
if (env->ExceptionCheck()) {
|
|
env->ExceptionDescribe();
|
|
env->FatalError("Could not find method!");
|
|
return;
|
|
}
|
|
}
|
|
err = jvmti->SetBreakpoint(m, target.location);
|
|
if (err != JVMTI_ERROR_NONE) {
|
|
env->FatalError("unable to set breakpoint");
|
|
return;
|
|
}
|
|
env->DeleteLocalRef(k);
|
|
}
|
|
}
|
|
|
|
class ScopedThreadInfo {
|
|
public:
|
|
ScopedThreadInfo(jvmtiEnv* jvmti_env, JNIEnv* env, jthread thread)
|
|
: jvmti_env_(jvmti_env), env_(env), free_name_(false) {
|
|
memset(&info_, 0, sizeof(info_));
|
|
if (thread == nullptr) {
|
|
info_.name = const_cast<char*>("<NULLPTR>");
|
|
} else if (jvmti_env->GetThreadInfo(thread, &info_) != JVMTI_ERROR_NONE) {
|
|
info_.name = const_cast<char*>("<UNKNOWN THREAD>");
|
|
} else {
|
|
free_name_ = true;
|
|
}
|
|
}
|
|
|
|
~ScopedThreadInfo() {
|
|
if (free_name_) {
|
|
jvmti_env_->Deallocate(reinterpret_cast<unsigned char*>(info_.name));
|
|
}
|
|
env_->DeleteLocalRef(info_.thread_group);
|
|
env_->DeleteLocalRef(info_.context_class_loader);
|
|
}
|
|
|
|
const char* GetName() const {
|
|
return info_.name;
|
|
}
|
|
|
|
private:
|
|
jvmtiEnv* jvmti_env_;
|
|
JNIEnv* env_;
|
|
bool free_name_;
|
|
jvmtiThreadInfo info_;
|
|
};
|
|
|
|
class ScopedClassInfo {
|
|
public:
|
|
ScopedClassInfo(jvmtiEnv* jvmti_env, jclass c)
|
|
: jvmti_env_(jvmti_env),
|
|
class_(c),
|
|
name_(nullptr),
|
|
generic_(nullptr),
|
|
file_(nullptr),
|
|
debug_ext_(nullptr) {}
|
|
|
|
~ScopedClassInfo() {
|
|
if (class_ != nullptr) {
|
|
jvmti_env_->Deallocate(reinterpret_cast<unsigned char*>(name_));
|
|
jvmti_env_->Deallocate(reinterpret_cast<unsigned char*>(generic_));
|
|
jvmti_env_->Deallocate(reinterpret_cast<unsigned char*>(file_));
|
|
jvmti_env_->Deallocate(reinterpret_cast<unsigned char*>(debug_ext_));
|
|
}
|
|
}
|
|
|
|
bool Init() {
|
|
if (class_ == nullptr) {
|
|
name_ = const_cast<char*>("<NONE>");
|
|
generic_ = const_cast<char*>("<NONE>");
|
|
return true;
|
|
} else {
|
|
jvmtiError ret1 = jvmti_env_->GetSourceFileName(class_, &file_);
|
|
jvmtiError ret2 = jvmti_env_->GetSourceDebugExtension(class_, &debug_ext_);
|
|
return jvmti_env_->GetClassSignature(class_, &name_, &generic_) == JVMTI_ERROR_NONE &&
|
|
ret1 != JVMTI_ERROR_MUST_POSSESS_CAPABILITY &&
|
|
ret1 != JVMTI_ERROR_INVALID_CLASS &&
|
|
ret2 != JVMTI_ERROR_MUST_POSSESS_CAPABILITY &&
|
|
ret2 != JVMTI_ERROR_INVALID_CLASS;
|
|
}
|
|
}
|
|
|
|
jclass GetClass() const {
|
|
return class_;
|
|
}
|
|
const char* GetName() const {
|
|
return name_;
|
|
}
|
|
// Generic type parameters, whatever is in the <> for a class
|
|
const char* GetGeneric() const {
|
|
return generic_;
|
|
}
|
|
const char* GetSourceDebugExtension() const {
|
|
if (debug_ext_ == nullptr) {
|
|
return "<UNKNOWN_SOURCE_DEBUG_EXTENSION>";
|
|
} else {
|
|
return debug_ext_;
|
|
}
|
|
}
|
|
const char* GetSourceFileName() const {
|
|
if (file_ == nullptr) {
|
|
return "<UNKNOWN_FILE>";
|
|
} else {
|
|
return file_;
|
|
}
|
|
}
|
|
|
|
private:
|
|
jvmtiEnv* jvmti_env_;
|
|
jclass class_;
|
|
char* name_;
|
|
char* generic_;
|
|
char* file_;
|
|
char* debug_ext_;
|
|
};
|
|
|
|
class ScopedMethodInfo {
|
|
public:
|
|
ScopedMethodInfo(jvmtiEnv* jvmti_env, JNIEnv* env, jmethodID method)
|
|
: jvmti_env_(jvmti_env),
|
|
env_(env),
|
|
method_(method),
|
|
declaring_class_(nullptr),
|
|
class_info_(nullptr),
|
|
name_(nullptr),
|
|
signature_(nullptr),
|
|
generic_(nullptr),
|
|
first_line_(-1) {}
|
|
|
|
~ScopedMethodInfo() {
|
|
env_->DeleteLocalRef(declaring_class_);
|
|
jvmti_env_->Deallocate(reinterpret_cast<unsigned char*>(name_));
|
|
jvmti_env_->Deallocate(reinterpret_cast<unsigned char*>(signature_));
|
|
jvmti_env_->Deallocate(reinterpret_cast<unsigned char*>(generic_));
|
|
}
|
|
|
|
bool Init() {
|
|
if (jvmti_env_->GetMethodDeclaringClass(method_, &declaring_class_) != JVMTI_ERROR_NONE) {
|
|
return false;
|
|
}
|
|
class_info_.reset(new ScopedClassInfo(jvmti_env_, declaring_class_));
|
|
jint nlines;
|
|
jvmtiLineNumberEntry* lines;
|
|
jvmtiError err = jvmti_env_->GetLineNumberTable(method_, &nlines, &lines);
|
|
if (err == JVMTI_ERROR_NONE) {
|
|
if (nlines > 0) {
|
|
first_line_ = lines[0].line_number;
|
|
}
|
|
jvmti_env_->Deallocate(reinterpret_cast<unsigned char*>(lines));
|
|
} else if (err != JVMTI_ERROR_ABSENT_INFORMATION &&
|
|
err != JVMTI_ERROR_NATIVE_METHOD) {
|
|
return false;
|
|
}
|
|
return class_info_->Init() &&
|
|
(jvmti_env_->GetMethodName(method_, &name_, &signature_, &generic_) == JVMTI_ERROR_NONE);
|
|
}
|
|
|
|
const ScopedClassInfo& GetDeclaringClassInfo() const {
|
|
return *class_info_;
|
|
}
|
|
|
|
jclass GetDeclaringClass() const {
|
|
return declaring_class_;
|
|
}
|
|
|
|
const char* GetName() const {
|
|
return name_;
|
|
}
|
|
|
|
const char* GetSignature() const {
|
|
return signature_;
|
|
}
|
|
|
|
const char* GetGeneric() const {
|
|
return generic_;
|
|
}
|
|
|
|
jint GetFirstLine() const {
|
|
return first_line_;
|
|
}
|
|
|
|
private:
|
|
jvmtiEnv* jvmti_env_;
|
|
JNIEnv* env_;
|
|
jmethodID method_;
|
|
jclass declaring_class_;
|
|
std::unique_ptr<ScopedClassInfo> class_info_;
|
|
char* name_;
|
|
char* signature_;
|
|
char* generic_;
|
|
jint first_line_;
|
|
|
|
friend std::ostream& operator<<(std::ostream& os, ScopedMethodInfo const& method);
|
|
};
|
|
|
|
std::ostream& operator<<(std::ostream& os, const ScopedMethodInfo* method) {
|
|
return os << *method;
|
|
}
|
|
|
|
std::ostream& operator<<(std::ostream& os, ScopedMethodInfo const& method) {
|
|
return os << method.GetDeclaringClassInfo().GetName() << "->" << method.GetName()
|
|
<< method.GetSignature() << " (source: "
|
|
<< method.GetDeclaringClassInfo().GetSourceFileName() << ":" << method.GetFirstLine()
|
|
<< ")";
|
|
}
|
|
|
|
static void BreakpointCB(jvmtiEnv* jvmti_env,
|
|
JNIEnv* env,
|
|
jthread thread,
|
|
jmethodID method,
|
|
jlocation location) {
|
|
ScopedThreadInfo info(jvmti_env, env, thread);
|
|
ScopedMethodInfo method_info(jvmti_env, env, method);
|
|
if (!method_info.Init()) {
|
|
LOG(ERROR) << "Unable to get method info!";
|
|
return;
|
|
}
|
|
LOG(WARNING) << "Breakpoint at location: 0x" << std::setw(8) << std::setfill('0') << std::hex
|
|
<< location << " in method " << method_info << " thread: " << info.GetName();
|
|
}
|
|
|
|
static std::string SubstrOf(const std::string& s, size_t start, size_t end) {
|
|
if (end == std::string::npos) {
|
|
end = s.size();
|
|
}
|
|
if (end == start) {
|
|
return "";
|
|
}
|
|
CHECK_GT(end, start) << "cannot get substr of " << s;
|
|
return s.substr(start, end - start);
|
|
}
|
|
|
|
static bool ParseSingleBreakpoint(const std::string& bp, /*out*/SingleBreakpointTarget* target) {
|
|
std::string option = bp;
|
|
if (option.empty() || option[0] != 'L' || option.find(';') == std::string::npos) {
|
|
LOG(ERROR) << option << " doesn't look like it has a class name";
|
|
return false;
|
|
}
|
|
target->class_name = SubstrOf(option, 1, option.find(';'));
|
|
|
|
option = SubstrOf(option, option.find(';') + 1, std::string::npos);
|
|
if (option.size() < 2 || option[0] != '-' || option[1] != '>') {
|
|
LOG(ERROR) << bp << " doesn't seem to indicate a method, expected ->";
|
|
return false;
|
|
}
|
|
option = SubstrOf(option, 2, std::string::npos);
|
|
size_t sig_start = option.find('(');
|
|
size_t loc_start = option.find('@');
|
|
if (option.empty() || sig_start == std::string::npos) {
|
|
LOG(ERROR) << bp << " doesn't seem to have a method sig!";
|
|
return false;
|
|
} else if (loc_start == std::string::npos ||
|
|
loc_start < sig_start ||
|
|
loc_start + 1 >= option.size()) {
|
|
LOG(ERROR) << bp << " doesn't seem to have a valid location!";
|
|
return false;
|
|
}
|
|
target->method_name = SubstrOf(option, 0, sig_start);
|
|
target->method_sig = SubstrOf(option, sig_start, loc_start);
|
|
target->location = std::stol(SubstrOf(option, loc_start + 1, std::string::npos));
|
|
return true;
|
|
}
|
|
|
|
static std::string RemoveLastOption(const std::string& op) {
|
|
if (op.find(',') == std::string::npos) {
|
|
return "";
|
|
} else {
|
|
return SubstrOf(op, op.find(',') + 1, std::string::npos);
|
|
}
|
|
}
|
|
|
|
// Fills targets with the breakpoints to add.
|
|
// Lname/of/Klass;->methodName(Lsig/of/Method)Lreturn/Type;@location,<...>
|
|
static bool ParseArgs(const std::string& start_options,
|
|
/*out*/BreakpointTargets* targets) {
|
|
for (std::string options = start_options;
|
|
!options.empty();
|
|
options = RemoveLastOption(options)) {
|
|
SingleBreakpointTarget target;
|
|
std::string next = SubstrOf(options, 0, options.find(','));
|
|
if (!ParseSingleBreakpoint(next, /*out*/ &target)) {
|
|
LOG(ERROR) << "Unable to parse breakpoint from " << next;
|
|
return false;
|
|
}
|
|
targets->bps.push_back(target);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
enum class StartType {
|
|
OnAttach, OnLoad,
|
|
};
|
|
|
|
static jint AgentStart(StartType start,
|
|
JavaVM* vm,
|
|
char* options,
|
|
void* reserved ATTRIBUTE_UNUSED) {
|
|
jvmtiEnv* jvmti = nullptr;
|
|
jvmtiError error = JVMTI_ERROR_NONE;
|
|
{
|
|
jint res = 0;
|
|
res = vm->GetEnv(reinterpret_cast<void**>(&jvmti), JVMTI_VERSION_1_1);
|
|
|
|
if (res != JNI_OK || jvmti == nullptr) {
|
|
LOG(ERROR) << "Unable to access JVMTI, error code " << res;
|
|
return JNI_ERR;
|
|
}
|
|
}
|
|
|
|
void* bp_target_mem = nullptr;
|
|
error = jvmti->Allocate(sizeof(BreakpointTargets),
|
|
reinterpret_cast<unsigned char**>(&bp_target_mem));
|
|
if (error != JVMTI_ERROR_NONE) {
|
|
LOG(ERROR) << "Unable to alloc memory for breakpoint target data";
|
|
return JNI_ERR;
|
|
}
|
|
|
|
BreakpointTargets* data = new(bp_target_mem) BreakpointTargets;
|
|
error = jvmti->SetEnvironmentLocalStorage(data);
|
|
if (error != JVMTI_ERROR_NONE) {
|
|
LOG(ERROR) << "Unable to set local storage";
|
|
return JNI_ERR;
|
|
}
|
|
|
|
if (!ParseArgs(options, /*out*/data)) {
|
|
LOG(ERROR) << "failed to parse breakpoint list!";
|
|
return JNI_ERR;
|
|
}
|
|
|
|
jvmtiCapabilities caps{};
|
|
caps.can_generate_breakpoint_events = JNI_TRUE;
|
|
caps.can_get_line_numbers = JNI_TRUE;
|
|
caps.can_get_source_file_name = JNI_TRUE;
|
|
caps.can_get_source_debug_extension = JNI_TRUE;
|
|
error = jvmti->AddCapabilities(&caps);
|
|
if (error != JVMTI_ERROR_NONE) {
|
|
LOG(ERROR) << "Unable to set caps";
|
|
return JNI_ERR;
|
|
}
|
|
|
|
jvmtiEventCallbacks callbacks{};
|
|
callbacks.Breakpoint = &BreakpointCB;
|
|
callbacks.VMInit = &VMInitCB;
|
|
|
|
error = jvmti->SetEventCallbacks(&callbacks, static_cast<jint>(sizeof(callbacks)));
|
|
|
|
if (error != JVMTI_ERROR_NONE) {
|
|
LOG(ERROR) << "Unable to set event callbacks.";
|
|
return JNI_ERR;
|
|
}
|
|
|
|
error = jvmti->SetEventNotificationMode(JVMTI_ENABLE,
|
|
JVMTI_EVENT_BREAKPOINT,
|
|
nullptr /* all threads */);
|
|
if (error != JVMTI_ERROR_NONE) {
|
|
LOG(ERROR) << "Unable to enable breakpoint event";
|
|
return JNI_ERR;
|
|
}
|
|
if (start == StartType::OnAttach) {
|
|
JNIEnv* env = nullptr;
|
|
jint res = 0;
|
|
res = vm->GetEnv(reinterpret_cast<void**>(&env), JNI_VERSION_1_2);
|
|
if (res != JNI_OK || env == nullptr) {
|
|
LOG(ERROR) << "Unable to get jnienv";
|
|
return JNI_ERR;
|
|
}
|
|
VMInitCB(jvmti, env, nullptr);
|
|
} else {
|
|
error = jvmti->SetEventNotificationMode(JVMTI_ENABLE,
|
|
JVMTI_EVENT_VM_INIT,
|
|
nullptr /* all threads */);
|
|
if (error != JVMTI_ERROR_NONE) {
|
|
LOG(ERROR) << "Unable to set event vminit";
|
|
return JNI_ERR;
|
|
}
|
|
}
|
|
return JNI_OK;
|
|
}
|
|
|
|
// Late attachment (e.g. 'am attach-agent').
|
|
extern "C" JNIEXPORT jint JNICALL Agent_OnAttach(JavaVM *vm, char* options, void* reserved) {
|
|
return AgentStart(StartType::OnAttach, vm, options, reserved);
|
|
}
|
|
|
|
// Early attachment
|
|
extern "C" JNIEXPORT jint JNICALL Agent_OnLoad(JavaVM* jvm, char* options, void* reserved) {
|
|
return AgentStart(StartType::OnLoad, jvm, options, reserved);
|
|
}
|
|
|
|
} // namespace breakpoint_logger
|
|
|