blob: 5b49d6031a693fe33825edd9a104917be41093aa [file] [log] [blame]
/*
* Copyright (C) 2019 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 "tools/base/debug/agent/native/async-stack/inject_hooks.h"
#include <cstdlib>
#include "slicer/instrumentation.h"
#include "tools/base/debug/agent/native/log.h"
namespace debug {
namespace {
// These class names and method names must be kept in sync with IntelliJ.
const char* kCaptureStorageDesc =
"Lcom/intellij/rt/debugger/agent/CaptureStorage;";
const char* kCaptureHook = "capture";
const char* kInsertEnterHook = "insertEnter";
const char* kInsertExitHook = "insertExit";
} // namespace
bool InjectionPoint::Apply(std::shared_ptr<ir::DexFile> dex_ir) {
// Apply instrumentation to all methods with the correct name.
bool found = false;
for (auto& method : dex_ir->encoded_methods) {
const char* clazz = method->decl->parent->descriptor->c_str();
const char* name = method->decl->name->c_str();
if (class_desc().compare(clazz) != 0 ||
method_name_.compare(name) != 0 ||
method->code == nullptr) {
continue;
}
found = true;
auto sig = method->decl->prototype->Signature();
lir::CodeIr ir(method.get(), dex_ir);
AsyncStackTransform transform(&ir, kind_, *capture_key_);
if (transform.Apply()) {
ir.Assemble();
Log::V("Async stack instrumentation applied to %s %s%s", clazz, name,
sig.c_str());
} else {
Log::E("Failed to apply async stack instrumentation to %s %s%s (%s)",
clazz, name, sig.c_str(), transform.error().c_str());
return false;
}
}
if (!found) {
Log::E("Async stack: could not find method %s in class %s",
method_name_.c_str(), class_desc().c_str());
// We still return true in this case, because [dex_ir]
// is still in a valid state.
}
return true;
}
AsyncStackTransform::AsyncStackTransform(lir::CodeIr* ir, InjectionKind kind,
const CaptureKey& capture_key)
: ir_(ir),
kind_(kind),
capture_key_(capture_key),
orig_method_start_(*ir_->instructions.begin()) {
auto code = ir_->ir_method->code;
if (code != nullptr) {
orig_ins_start_ = code->registers - code->ins_count;
}
}
bool AsyncStackTransform::Apply() {
if (!CheckValid()) {
return false;
}
if (!AllocateScratchRegs()) {
return false;
}
InjectEntryHook();
if (kind_ == kInsert) {
InjectExitHook();
}
return true;
}
bool AsyncStackTransform::CheckValid() {
if (ir_->ir_method->code == nullptr || ir_->instructions.empty()) {
error_ = "Expected nonempty method body";
return false;
}
if (ir_->ir_method->access_flags & dex::kAccConstructor) {
// TODO: "An instance initializer must call another instance
// initializer (same class or superclass) before any instance
// members can be accessed."
error_ = "Constructor injection points not yet supported";
return false;
}
if (!capture_key_.CheckValid(this)) {
return false;
}
return true;
}
bool ReceiverKey::CheckValid(AsyncStackTransform* t) const {
if (t->ir_->ir_method->access_flags & dex::kAccStatic) {
t->error_ = "Used `this` as capture key in static method";
return false;
}
return true;
}
bool ParamKey::CheckValid(AsyncStackTransform* t) const {
auto param_type_list = t->ir_->ir_method->decl->prototype->param_types;
if (param_type_list == nullptr) {
t->error_ = "Used parameter key in method with no parameters";
return false;
}
auto& param_types = param_type_list->types;
if (param_ >= param_types.size()) {
t->error_ = "Parameter index is out of bounds";
return false;
}
if (param_types[param_]->GetCategory() != ir::Type::Category::Reference) {
t->error_ = "Capture key must be an object";
return false;
}
return true;
}
bool AsyncStackTransform::AllocateScratchRegs() {
// Note: we disable register renumbering in order to simplify our bookkeeping.
// In particular, if renumbering were allowed then it would be harder to
// know where to find method arguments.
slicer::AllocateScratchRegs regalloc(kNumScratchRegs_, /*renumber*/ false);
if (!regalloc.Apply(ir_)) {
error_ = "Failed to allocate scratch registers";
return false;
}
auto& regs = regalloc.ScratchRegs();
for (auto reg : regs) {
if (reg >= kMaxRegsSupported_) {
error_ = "Methods with this many registers not yet supported";
return false;
}
}
auto it = regs.begin();
scratch_ = *it++;
exn_stash_ = *it++;
return true;
}
void AsyncStackTransform::InjectEntryHook() {
auto pos = orig_method_start_;
auto hook_name = kind_ == kCapture ? kCaptureHook : kInsertEnterHook;
auto entry_hook = BuildHookReference(hook_name);
auto arg = capture_key_.ComputeReg(this);
auto invoke = BuildHookInvoke(entry_hook, arg);
ir_->instructions.InsertBefore(pos, invoke);
}
void AsyncStackTransform::InjectExitHook() {
auto exit_hook = BuildHookReference(kInsertExitHook);
// Invokes the exit hook before [pos].
auto invoke_exit_hook = [&](lir::Instruction* pos) {
// TODO: The insertExit() hook uses the capture key only for logging
// purposes, so to simplify things we just pass null for now.
auto load_null = ir_->Alloc<lir::Bytecode>();
load_null->opcode = dex::OP_CONST_16;
load_null->operands.push_back(ir_->Alloc<lir::VReg>(scratch_));
load_null->operands.push_back(ir_->Alloc<lir::Const32>(0));
ir_->instructions.InsertBefore(pos, load_null);
auto invoke = BuildHookInvoke(exit_hook, scratch_);
ir_->instructions.InsertBefore(pos, invoke);
};
// Invoke the insert exit hook at all method return points.
for (auto insn : ir_->instructions) {
if (auto bytecode = dynamic_cast<lir::Bytecode*>(insn)) {
auto flags = dex::GetFlagsFromOpcode(bytecode->opcode);
if (flags & dex::kReturn) {
invoke_exit_hook(/*pos*/ insn);
}
}
}
// Create a finally-block that intercepts all exceptions.
auto finally = ir_->Alloc<lir::Label>(0);
RedirectAllExceptions(finally);
ir_->instructions.push_back(finally);
// Save the in-flight exception for later.
auto exn = ir_->Alloc<lir::VReg>(exn_stash_);
auto move_exn = ir_->Alloc<lir::Bytecode>();
move_exn->opcode = dex::OP_MOVE_EXCEPTION;
move_exn->operands.push_back(exn);
ir_->instructions.push_back(move_exn);
// Invoke exit hook and then rethrow.
invoke_exit_hook(/*pos*/ *ir_->instructions.end());
auto rethrow = ir_->Alloc<lir::Bytecode>();
rethrow->opcode = dex::OP_THROW;
rethrow->operands.push_back(exn);
ir_->instructions.push_back(rethrow);
}
void AsyncStackTransform::RedirectAllExceptions(lir::Label* finally) {
// Try-blocks must be non-overlapping, so we cannot simply wrap the
// entire method with a catch-all try-block. Instead, we install a
// catch-all handler in all existing try-blocks and create new
// try-blocks to cover the gaps between.
// Returns whether there are bytecode instructions in the range [begin, end).
// Used to ensure that we do not create empty try-blocks.
auto contains_bytecode = [](lir::Instruction* begin, lir::Instruction* end) {
for (auto it = begin; it != end; it = it->next) {
if (dynamic_cast<lir::Bytecode*>(it)) {
return true;
}
}
return false;
};
// Wraps bytecode between the [prev] and [next] try-blocks with a try-block.
// Handles the cases where [prev] or [next] are null.
auto cover_gap = [&](lir::TryBlockEnd* prev, lir::TryBlockBegin* next) {
auto gap_begin = prev != nullptr ? prev->next : orig_method_start_;
auto gap_end = next != nullptr ? next : *ir_->instructions.end();
if (!contains_bytecode(gap_begin, gap_end)) {
return; // Try-block ranges are required to be nonempty.
}
auto try_begin = ir_->Alloc<lir::TryBlockBegin>();
auto try_end = ir_->Alloc<lir::TryBlockEnd>();
try_end->try_begin = try_begin;
try_end->catch_all = finally;
ir_->instructions.InsertBefore(gap_begin, try_begin);
ir_->instructions.InsertBefore(gap_end, try_end);
};
// Install catch-all handlers and fill all gaps.
lir::TryBlockEnd* prev_end = nullptr;
for (auto insn : ir_->instructions) {
if (auto try_end = dynamic_cast<lir::TryBlockEnd*>(insn)) {
cover_gap(prev_end, try_end->try_begin);
if (try_end->catch_all == nullptr) {
try_end->catch_all = finally;
}
prev_end = try_end;
}
}
cover_gap(prev_end, nullptr);
}
dex::u4 ReceiverKey::ComputeReg(AsyncStackTransform* t) const {
// "Instance methods are passed a this reference as their first argument."
return t->orig_ins_start_;
}
dex::u4 ParamKey::ComputeReg(AsyncStackTransform* t) const {
// "The N arguments to a method land in the last N registers of the method's
// invocation frame, in order. Wide arguments consume two registers.
// Instance methods are passed a this reference as their first argument."
auto& param_types = t->ir_->ir_method->decl->prototype->param_types->types;
bool is_static = t->ir_->ir_method->access_flags & dex::kAccStatic;
dex::u4 reg_idx = is_static ? 0 : 1;
for (dex::u4 i = 0; i < param_; ++i) {
auto category = param_types[i]->GetCategory();
reg_idx += category == ir::Type::Category::WideScalar ? 2 : 1;
}
return t->orig_ins_start_ + reg_idx;
}
lir::Method* AsyncStackTransform::BuildHookReference(const char* name) {
// All three hook methods have the same signature: (Ljava/lang/Object;)V
ir::Builder builder(ir_->dex_ir);
auto ascii_name = builder.GetAsciiString(name);
auto proto = builder.GetProto(
builder.GetType("V"),
builder.GetTypeList({builder.GetType("Ljava/lang/Object;")}));
auto clazz = builder.GetType(kCaptureStorageDesc);
auto decl = builder.GetMethodDecl(ascii_name, proto, clazz);
return ir_->Alloc<lir::Method>(decl, decl->orig_index);
}
lir::Bytecode* AsyncStackTransform::BuildHookInvoke(lir::Method* hook,
dex::u4 arg) {
// We use invoke-static/range so that we don't have to worry about
// the register number being small enough.
auto invoke = ir_->Alloc<lir::Bytecode>();
invoke->opcode = dex::OP_INVOKE_STATIC_RANGE;
invoke->operands.push_back(ir_->Alloc<lir::VRegRange>(arg, 1));
invoke->operands.push_back(hook);
return invoke;
}
} // namespace debug