Alex Light | 0e151e7 | 2017-10-25 10:50:35 -0700 | [diff] [blame] | 1 | // Copyright (C) 2017 The Android Open Source Project |
| 2 | // |
| 3 | // Licensed under the Apache License, Version 2.0 (the "License"); |
| 4 | // you may not use this file except in compliance with the License. |
| 5 | // You may obtain a copy of the License at |
| 6 | // |
| 7 | // http://www.apache.org/licenses/LICENSE-2.0 |
| 8 | // |
| 9 | // Unless required by applicable law or agreed to in writing, software |
| 10 | // distributed under the License is distributed on an "AS IS" BASIS, |
| 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 12 | // See the License for the specific language governing permissions and |
| 13 | // limitations under the License. |
| 14 | // |
| 15 | |
| 16 | #include <android-base/logging.h> |
| 17 | #include <atomic> |
| 18 | #include <iostream> |
| 19 | #include <iomanip> |
| 20 | #include <jni.h> |
| 21 | #include <jvmti.h> |
| 22 | #include <memory> |
| 23 | #include <string> |
| 24 | #include <vector> |
| 25 | |
| 26 | namespace breakpoint_logger { |
| 27 | |
| 28 | struct SingleBreakpointTarget { |
| 29 | std::string class_name; |
| 30 | std::string method_name; |
| 31 | std::string method_sig; |
| 32 | jlocation location; |
| 33 | }; |
| 34 | |
| 35 | struct BreakpointTargets { |
| 36 | std::vector<SingleBreakpointTarget> bps; |
| 37 | }; |
| 38 | |
| 39 | static void VMInitCB(jvmtiEnv* jvmti, JNIEnv* env, jthread thr ATTRIBUTE_UNUSED) { |
| 40 | BreakpointTargets* all_targets = nullptr; |
| 41 | jvmtiError err = jvmti->GetEnvironmentLocalStorage(reinterpret_cast<void**>(&all_targets)); |
| 42 | if (err != JVMTI_ERROR_NONE || all_targets == nullptr) { |
| 43 | env->FatalError("unable to get breakpoint targets"); |
| 44 | } |
| 45 | for (const SingleBreakpointTarget& target : all_targets->bps) { |
| 46 | jclass k = env->FindClass(target.class_name.c_str()); |
| 47 | if (env->ExceptionCheck()) { |
| 48 | env->ExceptionDescribe(); |
| 49 | env->FatalError("Could not find class!"); |
| 50 | return; |
| 51 | } |
| 52 | jmethodID m = env->GetMethodID(k, target.method_name.c_str(), target.method_sig.c_str()); |
| 53 | if (env->ExceptionCheck()) { |
| 54 | env->ExceptionClear(); |
| 55 | m = env->GetStaticMethodID(k, target.method_name.c_str(), target.method_sig.c_str()); |
| 56 | if (env->ExceptionCheck()) { |
| 57 | env->ExceptionDescribe(); |
| 58 | env->FatalError("Could not find method!"); |
| 59 | return; |
| 60 | } |
| 61 | } |
| 62 | err = jvmti->SetBreakpoint(m, target.location); |
| 63 | if (err != JVMTI_ERROR_NONE) { |
| 64 | env->FatalError("unable to set breakpoint"); |
| 65 | return; |
| 66 | } |
| 67 | env->DeleteLocalRef(k); |
| 68 | } |
| 69 | } |
| 70 | |
| 71 | class ScopedThreadInfo { |
| 72 | public: |
| 73 | ScopedThreadInfo(jvmtiEnv* jvmti_env, JNIEnv* env, jthread thread) |
| 74 | : jvmti_env_(jvmti_env), env_(env), free_name_(false) { |
| 75 | memset(&info_, 0, sizeof(info_)); |
| 76 | if (thread == nullptr) { |
| 77 | info_.name = const_cast<char*>("<NULLPTR>"); |
| 78 | } else if (jvmti_env->GetThreadInfo(thread, &info_) != JVMTI_ERROR_NONE) { |
| 79 | info_.name = const_cast<char*>("<UNKNOWN THREAD>"); |
| 80 | } else { |
| 81 | free_name_ = true; |
| 82 | } |
| 83 | } |
| 84 | |
| 85 | ~ScopedThreadInfo() { |
| 86 | if (free_name_) { |
| 87 | jvmti_env_->Deallocate(reinterpret_cast<unsigned char*>(info_.name)); |
| 88 | } |
| 89 | env_->DeleteLocalRef(info_.thread_group); |
| 90 | env_->DeleteLocalRef(info_.context_class_loader); |
| 91 | } |
| 92 | |
| 93 | const char* GetName() const { |
| 94 | return info_.name; |
| 95 | } |
| 96 | |
| 97 | private: |
| 98 | jvmtiEnv* jvmti_env_; |
| 99 | JNIEnv* env_; |
| 100 | bool free_name_; |
| 101 | jvmtiThreadInfo info_; |
| 102 | }; |
| 103 | |
| 104 | class ScopedClassInfo { |
| 105 | public: |
| 106 | ScopedClassInfo(jvmtiEnv* jvmti_env, jclass c) |
| 107 | : jvmti_env_(jvmti_env), |
| 108 | class_(c), |
| 109 | name_(nullptr), |
| 110 | generic_(nullptr), |
| 111 | file_(nullptr), |
| 112 | debug_ext_(nullptr) {} |
| 113 | |
| 114 | ~ScopedClassInfo() { |
| 115 | if (class_ != nullptr) { |
| 116 | jvmti_env_->Deallocate(reinterpret_cast<unsigned char*>(name_)); |
| 117 | jvmti_env_->Deallocate(reinterpret_cast<unsigned char*>(generic_)); |
| 118 | jvmti_env_->Deallocate(reinterpret_cast<unsigned char*>(file_)); |
| 119 | jvmti_env_->Deallocate(reinterpret_cast<unsigned char*>(debug_ext_)); |
| 120 | } |
| 121 | } |
| 122 | |
| 123 | bool Init() { |
| 124 | if (class_ == nullptr) { |
| 125 | name_ = const_cast<char*>("<NONE>"); |
| 126 | generic_ = const_cast<char*>("<NONE>"); |
| 127 | return true; |
| 128 | } else { |
| 129 | jvmtiError ret1 = jvmti_env_->GetSourceFileName(class_, &file_); |
| 130 | jvmtiError ret2 = jvmti_env_->GetSourceDebugExtension(class_, &debug_ext_); |
| 131 | return jvmti_env_->GetClassSignature(class_, &name_, &generic_) == JVMTI_ERROR_NONE && |
| 132 | ret1 != JVMTI_ERROR_MUST_POSSESS_CAPABILITY && |
| 133 | ret1 != JVMTI_ERROR_INVALID_CLASS && |
| 134 | ret2 != JVMTI_ERROR_MUST_POSSESS_CAPABILITY && |
| 135 | ret2 != JVMTI_ERROR_INVALID_CLASS; |
| 136 | } |
| 137 | } |
| 138 | |
| 139 | jclass GetClass() const { |
| 140 | return class_; |
| 141 | } |
| 142 | const char* GetName() const { |
| 143 | return name_; |
| 144 | } |
| 145 | // Generic type parameters, whatever is in the <> for a class |
| 146 | const char* GetGeneric() const { |
| 147 | return generic_; |
| 148 | } |
| 149 | const char* GetSourceDebugExtension() const { |
| 150 | if (debug_ext_ == nullptr) { |
| 151 | return "<UNKNOWN_SOURCE_DEBUG_EXTENSION>"; |
| 152 | } else { |
| 153 | return debug_ext_; |
| 154 | } |
| 155 | } |
| 156 | const char* GetSourceFileName() const { |
| 157 | if (file_ == nullptr) { |
| 158 | return "<UNKNOWN_FILE>"; |
| 159 | } else { |
| 160 | return file_; |
| 161 | } |
| 162 | } |
| 163 | |
| 164 | private: |
| 165 | jvmtiEnv* jvmti_env_; |
| 166 | jclass class_; |
| 167 | char* name_; |
| 168 | char* generic_; |
| 169 | char* file_; |
| 170 | char* debug_ext_; |
| 171 | }; |
| 172 | |
| 173 | class ScopedMethodInfo { |
| 174 | public: |
| 175 | ScopedMethodInfo(jvmtiEnv* jvmti_env, JNIEnv* env, jmethodID method) |
| 176 | : jvmti_env_(jvmti_env), |
| 177 | env_(env), |
| 178 | method_(method), |
| 179 | declaring_class_(nullptr), |
| 180 | class_info_(nullptr), |
| 181 | name_(nullptr), |
| 182 | signature_(nullptr), |
| 183 | generic_(nullptr), |
| 184 | first_line_(-1) {} |
| 185 | |
| 186 | ~ScopedMethodInfo() { |
| 187 | env_->DeleteLocalRef(declaring_class_); |
| 188 | jvmti_env_->Deallocate(reinterpret_cast<unsigned char*>(name_)); |
| 189 | jvmti_env_->Deallocate(reinterpret_cast<unsigned char*>(signature_)); |
| 190 | jvmti_env_->Deallocate(reinterpret_cast<unsigned char*>(generic_)); |
| 191 | } |
| 192 | |
| 193 | bool Init() { |
| 194 | if (jvmti_env_->GetMethodDeclaringClass(method_, &declaring_class_) != JVMTI_ERROR_NONE) { |
| 195 | return false; |
| 196 | } |
| 197 | class_info_.reset(new ScopedClassInfo(jvmti_env_, declaring_class_)); |
| 198 | jint nlines; |
| 199 | jvmtiLineNumberEntry* lines; |
| 200 | jvmtiError err = jvmti_env_->GetLineNumberTable(method_, &nlines, &lines); |
| 201 | if (err == JVMTI_ERROR_NONE) { |
| 202 | if (nlines > 0) { |
| 203 | first_line_ = lines[0].line_number; |
| 204 | } |
| 205 | jvmti_env_->Deallocate(reinterpret_cast<unsigned char*>(lines)); |
| 206 | } else if (err != JVMTI_ERROR_ABSENT_INFORMATION && |
| 207 | err != JVMTI_ERROR_NATIVE_METHOD) { |
| 208 | return false; |
| 209 | } |
| 210 | return class_info_->Init() && |
| 211 | (jvmti_env_->GetMethodName(method_, &name_, &signature_, &generic_) == JVMTI_ERROR_NONE); |
| 212 | } |
| 213 | |
| 214 | const ScopedClassInfo& GetDeclaringClassInfo() const { |
| 215 | return *class_info_; |
| 216 | } |
| 217 | |
| 218 | jclass GetDeclaringClass() const { |
| 219 | return declaring_class_; |
| 220 | } |
| 221 | |
| 222 | const char* GetName() const { |
| 223 | return name_; |
| 224 | } |
| 225 | |
| 226 | const char* GetSignature() const { |
| 227 | return signature_; |
| 228 | } |
| 229 | |
| 230 | const char* GetGeneric() const { |
| 231 | return generic_; |
| 232 | } |
| 233 | |
| 234 | jint GetFirstLine() const { |
| 235 | return first_line_; |
| 236 | } |
| 237 | |
| 238 | private: |
| 239 | jvmtiEnv* jvmti_env_; |
| 240 | JNIEnv* env_; |
| 241 | jmethodID method_; |
| 242 | jclass declaring_class_; |
| 243 | std::unique_ptr<ScopedClassInfo> class_info_; |
| 244 | char* name_; |
| 245 | char* signature_; |
| 246 | char* generic_; |
| 247 | jint first_line_; |
| 248 | |
| 249 | friend std::ostream& operator<<(std::ostream& os, ScopedMethodInfo const& method); |
| 250 | }; |
| 251 | |
| 252 | std::ostream& operator<<(std::ostream& os, const ScopedMethodInfo* method) { |
| 253 | return os << *method; |
| 254 | } |
| 255 | |
| 256 | std::ostream& operator<<(std::ostream& os, ScopedMethodInfo const& method) { |
| 257 | return os << method.GetDeclaringClassInfo().GetName() << "->" << method.GetName() |
| 258 | << method.GetSignature() << " (source: " |
| 259 | << method.GetDeclaringClassInfo().GetSourceFileName() << ":" << method.GetFirstLine() |
| 260 | << ")"; |
| 261 | } |
| 262 | |
| 263 | static void BreakpointCB(jvmtiEnv* jvmti_env, |
| 264 | JNIEnv* env, |
| 265 | jthread thread, |
| 266 | jmethodID method, |
| 267 | jlocation location) { |
| 268 | ScopedThreadInfo info(jvmti_env, env, thread); |
| 269 | ScopedMethodInfo method_info(jvmti_env, env, method); |
| 270 | if (!method_info.Init()) { |
| 271 | LOG(ERROR) << "Unable to get method info!"; |
| 272 | return; |
| 273 | } |
| 274 | LOG(WARNING) << "Breakpoint at location: 0x" << std::setw(8) << std::setfill('0') << std::hex |
| 275 | << location << " in method " << method_info << " thread: " << info.GetName(); |
| 276 | } |
| 277 | |
| 278 | static std::string SubstrOf(const std::string& s, size_t start, size_t end) { |
| 279 | if (end == std::string::npos) { |
| 280 | end = s.size(); |
| 281 | } |
| 282 | if (end == start) { |
| 283 | return ""; |
| 284 | } |
| 285 | CHECK_GT(end, start) << "cannot get substr of " << s; |
| 286 | return s.substr(start, end - start); |
| 287 | } |
| 288 | |
| 289 | static bool ParseSingleBreakpoint(const std::string& bp, /*out*/SingleBreakpointTarget* target) { |
| 290 | std::string option = bp; |
| 291 | if (option.empty() || option[0] != 'L' || option.find(';') == std::string::npos) { |
| 292 | LOG(ERROR) << option << " doesn't look like it has a class name"; |
| 293 | return false; |
| 294 | } |
| 295 | target->class_name = SubstrOf(option, 1, option.find(';')); |
| 296 | |
| 297 | option = SubstrOf(option, option.find(';') + 1, std::string::npos); |
| 298 | if (option.size() < 2 || option[0] != '-' || option[1] != '>') { |
| 299 | LOG(ERROR) << bp << " doesn't seem to indicate a method, expected ->"; |
| 300 | return false; |
| 301 | } |
| 302 | option = SubstrOf(option, 2, std::string::npos); |
| 303 | size_t sig_start = option.find('('); |
| 304 | size_t loc_start = option.find('@'); |
| 305 | if (option.empty() || sig_start == std::string::npos) { |
| 306 | LOG(ERROR) << bp << " doesn't seem to have a method sig!"; |
| 307 | return false; |
| 308 | } else if (loc_start == std::string::npos || |
| 309 | loc_start < sig_start || |
| 310 | loc_start + 1 >= option.size()) { |
| 311 | LOG(ERROR) << bp << " doesn't seem to have a valid location!"; |
| 312 | return false; |
| 313 | } |
| 314 | target->method_name = SubstrOf(option, 0, sig_start); |
| 315 | target->method_sig = SubstrOf(option, sig_start, loc_start); |
| 316 | target->location = std::stol(SubstrOf(option, loc_start + 1, std::string::npos)); |
| 317 | return true; |
| 318 | } |
| 319 | |
| 320 | static std::string RemoveLastOption(const std::string& op) { |
| 321 | if (op.find(',') == std::string::npos) { |
| 322 | return ""; |
| 323 | } else { |
| 324 | return SubstrOf(op, op.find(',') + 1, std::string::npos); |
| 325 | } |
| 326 | } |
| 327 | |
| 328 | // Fills targets with the breakpoints to add. |
| 329 | // Lname/of/Klass;->methodName(Lsig/of/Method)Lreturn/Type;@location,<...> |
| 330 | static bool ParseArgs(const std::string& start_options, |
| 331 | /*out*/BreakpointTargets* targets) { |
| 332 | for (std::string options = start_options; |
| 333 | !options.empty(); |
| 334 | options = RemoveLastOption(options)) { |
| 335 | SingleBreakpointTarget target; |
| 336 | std::string next = SubstrOf(options, 0, options.find(',')); |
| 337 | if (!ParseSingleBreakpoint(next, /*out*/ &target)) { |
| 338 | LOG(ERROR) << "Unable to parse breakpoint from " << next; |
| 339 | return false; |
| 340 | } |
| 341 | targets->bps.push_back(target); |
| 342 | } |
| 343 | return true; |
| 344 | } |
| 345 | |
| 346 | enum class StartType { |
| 347 | OnAttach, OnLoad, |
| 348 | }; |
| 349 | |
| 350 | static jint AgentStart(StartType start, |
| 351 | JavaVM* vm, |
| 352 | char* options, |
| 353 | void* reserved ATTRIBUTE_UNUSED) { |
| 354 | jvmtiEnv* jvmti = nullptr; |
| 355 | jvmtiError error = JVMTI_ERROR_NONE; |
| 356 | { |
| 357 | jint res = 0; |
| 358 | res = vm->GetEnv(reinterpret_cast<void**>(&jvmti), JVMTI_VERSION_1_1); |
| 359 | |
| 360 | if (res != JNI_OK || jvmti == nullptr) { |
| 361 | LOG(ERROR) << "Unable to access JVMTI, error code " << res; |
| 362 | return JNI_ERR; |
| 363 | } |
| 364 | } |
| 365 | |
| 366 | void* bp_target_mem = nullptr; |
| 367 | error = jvmti->Allocate(sizeof(BreakpointTargets), |
| 368 | reinterpret_cast<unsigned char**>(&bp_target_mem)); |
| 369 | if (error != JVMTI_ERROR_NONE) { |
| 370 | LOG(ERROR) << "Unable to alloc memory for breakpoint target data"; |
| 371 | return JNI_ERR; |
| 372 | } |
| 373 | |
| 374 | BreakpointTargets* data = new(bp_target_mem) BreakpointTargets; |
| 375 | error = jvmti->SetEnvironmentLocalStorage(data); |
| 376 | if (error != JVMTI_ERROR_NONE) { |
| 377 | LOG(ERROR) << "Unable to set local storage"; |
| 378 | return JNI_ERR; |
| 379 | } |
| 380 | |
| 381 | if (!ParseArgs(options, /*out*/data)) { |
| 382 | LOG(ERROR) << "failed to parse breakpoint list!"; |
| 383 | return JNI_ERR; |
| 384 | } |
| 385 | |
Igor Murashkin | 5573c37 | 2017-11-16 13:34:30 -0800 | [diff] [blame] | 386 | jvmtiCapabilities caps{}; |
Alex Light | 0e151e7 | 2017-10-25 10:50:35 -0700 | [diff] [blame] | 387 | caps.can_generate_breakpoint_events = JNI_TRUE; |
| 388 | caps.can_get_line_numbers = JNI_TRUE; |
| 389 | caps.can_get_source_file_name = JNI_TRUE; |
| 390 | caps.can_get_source_debug_extension = JNI_TRUE; |
| 391 | error = jvmti->AddCapabilities(&caps); |
| 392 | if (error != JVMTI_ERROR_NONE) { |
| 393 | LOG(ERROR) << "Unable to set caps"; |
| 394 | return JNI_ERR; |
| 395 | } |
| 396 | |
Igor Murashkin | 5573c37 | 2017-11-16 13:34:30 -0800 | [diff] [blame] | 397 | jvmtiEventCallbacks callbacks{}; |
Alex Light | 0e151e7 | 2017-10-25 10:50:35 -0700 | [diff] [blame] | 398 | callbacks.Breakpoint = &BreakpointCB; |
| 399 | callbacks.VMInit = &VMInitCB; |
| 400 | |
| 401 | error = jvmti->SetEventCallbacks(&callbacks, static_cast<jint>(sizeof(callbacks))); |
| 402 | |
| 403 | if (error != JVMTI_ERROR_NONE) { |
| 404 | LOG(ERROR) << "Unable to set event callbacks."; |
| 405 | return JNI_ERR; |
| 406 | } |
| 407 | |
| 408 | error = jvmti->SetEventNotificationMode(JVMTI_ENABLE, |
| 409 | JVMTI_EVENT_BREAKPOINT, |
| 410 | nullptr /* all threads */); |
| 411 | if (error != JVMTI_ERROR_NONE) { |
| 412 | LOG(ERROR) << "Unable to enable breakpoint event"; |
| 413 | return JNI_ERR; |
| 414 | } |
| 415 | if (start == StartType::OnAttach) { |
| 416 | JNIEnv* env = nullptr; |
| 417 | jint res = 0; |
| 418 | res = vm->GetEnv(reinterpret_cast<void**>(&env), JNI_VERSION_1_2); |
| 419 | if (res != JNI_OK || env == nullptr) { |
| 420 | LOG(ERROR) << "Unable to get jnienv"; |
| 421 | return JNI_ERR; |
| 422 | } |
| 423 | VMInitCB(jvmti, env, nullptr); |
| 424 | } else { |
| 425 | error = jvmti->SetEventNotificationMode(JVMTI_ENABLE, |
| 426 | JVMTI_EVENT_VM_INIT, |
| 427 | nullptr /* all threads */); |
| 428 | if (error != JVMTI_ERROR_NONE) { |
| 429 | LOG(ERROR) << "Unable to set event vminit"; |
| 430 | return JNI_ERR; |
| 431 | } |
| 432 | } |
| 433 | return JNI_OK; |
| 434 | } |
| 435 | |
| 436 | // Late attachment (e.g. 'am attach-agent'). |
| 437 | extern "C" JNIEXPORT jint JNICALL Agent_OnAttach(JavaVM *vm, char* options, void* reserved) { |
| 438 | return AgentStart(StartType::OnAttach, vm, options, reserved); |
| 439 | } |
| 440 | |
| 441 | // Early attachment |
| 442 | extern "C" JNIEXPORT jint JNICALL Agent_OnLoad(JavaVM* jvm, char* options, void* reserved) { |
| 443 | return AgentStart(StartType::OnLoad, jvm, options, reserved); |
| 444 | } |
| 445 | |
| 446 | } // namespace breakpoint_logger |
| 447 | |