| // 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 "instruction_decoder.h" |
| |
| #include <android-base/logging.h> |
| #include <atomic> |
| #include <jni.h> |
| #include <jvmti.h> |
| #include <map> |
| #include <memory> |
| #include <mutex> |
| |
| // We could probably return a JNI_ERR here but lets crash instead if something fails. |
| #define CHECK_JVMTI_ERROR(jvmti, errnum) \ |
| CHECK_EQ(JVMTI_ERROR_NONE, (errnum)) << GetJvmtiErrorString((jvmti), (errnum)) << (" ") |
| |
| namespace titrace { |
| |
| static const char* GetJvmtiErrorString(jvmtiEnv* jvmti, jvmtiError errnum) { |
| char* errnum_str = nullptr; |
| jvmti->GetErrorName(errnum, /*out*/ &errnum_str); |
| if (errnum_str == nullptr) { |
| return "Unknown"; |
| } |
| |
| return errnum_str; |
| } |
| |
| // Type-safe wrapper for JVMTI-allocated memory. |
| // Deallocates with jvmtiEnv::Deallocate. |
| template <typename T> |
| struct TiMemory { |
| explicit TiMemory(jvmtiEnv* env, T* mem, size_t size) : env_(env), mem_(mem), size_(size) { |
| } |
| |
| ~TiMemory() { |
| if (mem_ != nullptr) { |
| env_->Deallocate(static_cast<unsigned char*>(mem_)); |
| } |
| mem_ = nullptr; |
| } |
| |
| TiMemory(const TiMemory& other) = delete; |
| TiMemory(TiMemory&& other) noexcept { |
| env_ = other.env_; |
| mem_ = other.mem_; |
| size_ = other.size_; |
| |
| if (this != &other) { |
| other.env_ = nullptr; |
| other.mem_ = nullptr; |
| other.size_ = 0u; |
| } |
| } |
| |
| TiMemory& operator=(TiMemory&& other) noexcept { |
| if (mem_ != other.mem_) { |
| TiMemory::~TiMemory(); |
| } |
| new (this) TiMemory(std::move(other)); |
| return *this; |
| } |
| |
| T* GetMemory() { |
| return mem_; |
| } |
| |
| size_t Size() { |
| return size_ / sizeof(T); |
| } |
| |
| private: |
| jvmtiEnv* env_; |
| T* mem_; |
| size_t size_; |
| }; |
| |
| struct MethodBytecode { |
| explicit MethodBytecode(jvmtiEnv* env, unsigned char* memory, jint size) |
| : bytecode_(env, memory, static_cast<size_t>(size)) { |
| } |
| |
| TiMemory<uint8_t> bytecode_; |
| }; |
| |
| struct TraceStatistics { |
| static void Initialize(jvmtiEnv* jvmti) { |
| TraceStatistics& stats = GetSingleton(); |
| |
| bool is_ri = true; |
| { |
| jvmtiError error; |
| char* value_ptr; |
| error = jvmti->GetSystemProperty("java.vm.name", /*out*/ &value_ptr); |
| CHECK_JVMTI_ERROR(jvmti, error) << "Failed to get property 'java.vm.name'"; |
| CHECK(value_ptr != nullptr) << "Returned property was null for 'java.vm.name'"; |
| |
| if (strcmp("Dalvik", value_ptr) == 0) { |
| is_ri = false; |
| } |
| } |
| |
| InstructionFileFormat format = |
| is_ri ? InstructionFileFormat::kClass : InstructionFileFormat::kDex; |
| stats.instruction_decoder_.reset(InstructionDecoder::NewInstance(format)); |
| |
| CHECK_GE(arraysize(stats.instruction_counter_), |
| stats.instruction_decoder_->GetMaximumOpcode()); |
| } |
| |
| static TraceStatistics& GetSingleton() { |
| static TraceStatistics stats; |
| return stats; |
| } |
| |
| void Log() { |
| LOG(INFO) << "================================================"; |
| LOG(INFO) << " TI Trace // Summary "; |
| LOG(INFO) << "++++++++++++++++++++++++++++++++++++++++++++++++"; |
| LOG(INFO) << " * Single step counter: " << single_step_counter_; |
| LOG(INFO) << "+++++++++++ Instructions Count ++++++++++++"; |
| |
| size_t total = single_step_counter_; |
| for (size_t i = 0; i < arraysize(instruction_counter_); ++i) { |
| size_t inst_count = instruction_counter_[i]; |
| if (inst_count > 0) { |
| const char* opcode_name = instruction_decoder_->GetName(i); |
| LOG(INFO) << " * " << opcode_name << "(op:" << i << "), count: " << inst_count |
| << ", % of total: " << (100.0 * inst_count / total); |
| } |
| } |
| |
| LOG(INFO) << "------------------------------------------------"; |
| } |
| |
| void OnSingleStep(jvmtiEnv* jvmti_env, jmethodID method, jlocation location) { |
| // Counters do not need a happens-before. |
| // Use the weakest memory order simply to avoid tearing. |
| single_step_counter_.fetch_add(1u, std::memory_order_relaxed); |
| |
| MethodBytecode& bytecode = LookupBytecode(jvmti_env, method); |
| |
| // Decode jlocation value that depends on the bytecode format. |
| size_t actual_location = instruction_decoder_->LocationToOffset(static_cast<size_t>(location)); |
| |
| // Decode the exact instruction and increment its counter. |
| CHECK_LE(actual_location, bytecode.bytecode_.Size()); |
| RecordInstruction(bytecode.bytecode_.GetMemory() + actual_location); |
| } |
| |
| private: |
| void RecordInstruction(const uint8_t* instruction) { |
| uint8_t opcode = instruction[0]; |
| // Counters do not need a happens-before. |
| // Use the weakest memory order simply to avoid tearing. |
| instruction_counter_[opcode].fetch_add(1u, std::memory_order_relaxed); |
| } |
| |
| MethodBytecode& LookupBytecode(jvmtiEnv* jvmti_env, jmethodID method) { |
| jvmtiError error; |
| std::lock_guard<std::mutex> lock(bytecode_cache_mutex_); |
| |
| auto it = bytecode_cache_.find(method); |
| if (it == bytecode_cache_.end()) { |
| jint bytecode_count_ptr = 0; |
| unsigned char* bytecodes_ptr = nullptr; |
| |
| error = jvmti_env->GetBytecodes(method, &bytecode_count_ptr, &bytecodes_ptr); |
| CHECK_JVMTI_ERROR(jvmti_env, error) << "Failed to get bytecodes for method " << method; |
| CHECK(bytecodes_ptr != nullptr) << "Bytecode ptr was null for method " << method; |
| CHECK_GE(bytecode_count_ptr, 0) << "Bytecode size too small for method " << method; |
| |
| // std::pair<iterator, bool inserted> |
| auto&& pair = bytecode_cache_.insert( |
| std::make_pair(method, MethodBytecode(jvmti_env, bytecodes_ptr, bytecode_count_ptr))); |
| it = pair.first; |
| } |
| |
| // Returning the address is safe. if map is resized, the contents will not move. |
| return it->second; |
| } |
| |
| std::unique_ptr<InstructionDecoder> instruction_decoder_; |
| |
| std::atomic<size_t> single_step_counter_{0u}; |
| std::atomic<size_t> instruction_counter_[256]{}; |
| |
| // Cache the bytecode to avoid calling into JVMTI repeatedly. |
| // TODO: invalidate if the bytecode was updated? |
| std::map<jmethodID, MethodBytecode> bytecode_cache_; |
| // bytecode cache is thread-safe. |
| std::mutex bytecode_cache_mutex_; |
| }; |
| |
| struct EventCallbacks { |
| static void SingleStep(jvmtiEnv* jvmti_env, |
| [[maybe_unused]] JNIEnv* jni_env, |
| [[maybe_unused]] jthread thread, |
| jmethodID method, |
| jlocation location) { |
| TraceStatistics& stats = TraceStatistics::GetSingleton(); |
| stats.OnSingleStep(jvmti_env, method, location); |
| } |
| |
| // Use "kill -SIGQUIT" to generate a data dump request. |
| // Useful when running an android app since it doesn't go through |
| // a normal Agent_OnUnload. |
| static void DataDumpRequest([[maybe_unused]] jvmtiEnv* jvmti_env) { |
| TraceStatistics& stats = TraceStatistics::GetSingleton(); |
| stats.Log(); |
| } |
| }; |
| |
| } // namespace titrace |
| |
| // Late attachment (e.g. 'am attach-agent'). |
| JNIEXPORT jint JNICALL Agent_OnAttach(JavaVM *vm, char* options, void* reserved) { |
| return Agent_OnLoad(vm, options, reserved); |
| } |
| |
| // Early attachment (e.g. 'java -agent[lib|path]:filename.so'). |
| JNIEXPORT jint JNICALL Agent_OnLoad(JavaVM* jvm, |
| char* /* options */, |
| void* /* reserved */) { |
| using namespace titrace; // NOLINT [build/namespaces] [5] |
| |
| android::base::InitLogging(/* argv= */nullptr); |
| |
| jvmtiEnv* jvmti = nullptr; |
| { |
| jint res = 0; |
| // Magic number that the agent can use to attach to non-debuggable apps on userdebug. |
| constexpr jint kArtTiVersion = JVMTI_VERSION_1_2 | 0x40000000; |
| res = jvm->GetEnv(reinterpret_cast<void**>(&jvmti), JVMTI_VERSION_1_1); |
| |
| if (res != JNI_OK || jvmti == nullptr) { |
| res = jvm->GetEnv(reinterpret_cast<void**>(&jvmti), kArtTiVersion); |
| } |
| if (res != JNI_OK || jvmti == nullptr) { |
| LOG(FATAL) << "Unable to access JVMTI, error code " << res; |
| } |
| } |
| |
| LOG(INFO) << "Agent_OnLoad: Hello World"; |
| |
| { |
| // Initialize our instruction file-format decoder. |
| TraceStatistics::Initialize(jvmti); |
| } |
| |
| jvmtiError error{}; |
| |
| // Set capabilities. |
| { |
| jvmtiCapabilities caps = {}; |
| caps.can_generate_single_step_events = 1; |
| caps.can_get_bytecodes = 1; |
| |
| error = jvmti->AddCapabilities(&caps); |
| CHECK_JVMTI_ERROR(jvmti, error) |
| << "Unable to get necessary JVMTI capabilities"; |
| } |
| |
| // Set callbacks. |
| { |
| jvmtiEventCallbacks callbacks = {}; |
| callbacks.SingleStep = &EventCallbacks::SingleStep; |
| callbacks.DataDumpRequest = &EventCallbacks::DataDumpRequest; |
| |
| error = jvmti->SetEventCallbacks(&callbacks, |
| static_cast<jint>(sizeof(callbacks))); |
| CHECK_JVMTI_ERROR(jvmti, error) << "Unable to set event callbacks"; |
| } |
| |
| // Enable events notification. |
| { |
| error = jvmti->SetEventNotificationMode(JVMTI_ENABLE, |
| JVMTI_EVENT_SINGLE_STEP, |
| nullptr /* all threads */); |
| CHECK_JVMTI_ERROR(jvmti, error) |
| << "Failed to enable SINGLE_STEP notification"; |
| |
| error = jvmti->SetEventNotificationMode(JVMTI_ENABLE, |
| JVMTI_EVENT_DATA_DUMP_REQUEST, |
| nullptr /* all threads */); |
| CHECK_JVMTI_ERROR(jvmti, error) |
| << "Failed to enable DATA_DUMP_REQUEST notification"; |
| } |
| |
| return JNI_OK; |
| } |
| |
| // Note: This is not called for normal Android apps, |
| // use "kill -SIGQUIT" instead to generate a data dump request. |
| JNIEXPORT void JNICALL Agent_OnUnload([[maybe_unused]] JavaVM* vm) { |
| using namespace titrace; // NOLINT [build/namespaces] [5] |
| LOG(INFO) << "Agent_OnUnload: Goodbye"; |
| |
| TraceStatistics::GetSingleton().Log(); |
| } |