/* * Copyright (c) 2002, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it * under the terms of the GNU General Public License version 2 only, as * published by the Free Software Foundation. * * This code is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License * version 2 for more details (a copy is included in the LICENSE file that * accompanied this code). * * You should have received a copy of the GNU General Public License version * 2 along with this work; if not, write to the Free Software Foundation, * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. * * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA * or visit www.oracle.com if you need additional information or have any * questions. * */ #include #include #include #include #include "sa.hpp" #include "jni.h" #include "jvmdi.h" #ifndef WIN32 #include #else typedef int int32_t; #endif #ifdef WIN32 #include #define YIELD() Sleep(0) #define SLEEP() Sleep(10) #define vsnprintf _vsnprintf #else Error: please port YIELD() and SLEEP() macros to your platform #endif using namespace std; ////////////////////////////////////////////////////////////////////// // // // Exported "interface" for Java language-level interaction between // // the SA and the VM. Note that the SA knows about the layout of // // certain VM data structures and that knowledge is taken advantage // // of in this code, although this interfaces with the VM via JVMDI. // // // ////////////////////////////////////////////////////////////////////// extern "C" { ///////////////////////////////////// // // // Events sent by the VM to the SA // // // ///////////////////////////////////// // Set by the SA when it attaches. Indicates that events should be // posted via these exported variables, and that the VM should wait // for those events to be acknowledged by the SA (via its setting // saEventPending to 0). JNIEXPORT volatile int32_t saAttached = 0; // Set to nonzero value by the VM when an event has been posted; set // back to 0 by the SA when it has processed that event. JNIEXPORT volatile int32_t saEventPending = 0; // Kind of the event (from jvmdi.h) JNIEXPORT volatile int32_t saEventKind = 0; // // Exception events // JNIEXPORT jthread saExceptionThread; JNIEXPORT jclass saExceptionClass; JNIEXPORT jmethodID saExceptionMethod; JNIEXPORT int32_t saExceptionLocation; JNIEXPORT jobject saExceptionException; JNIEXPORT jclass saExceptionCatchClass; JNIEXPORT jmethodID saExceptionCatchMethod; JNIEXPORT int32_t saExceptionCatchLocation; // // Breakpoint events // JNIEXPORT jthread saBreakpointThread; JNIEXPORT jclass saBreakpointClass; JNIEXPORT jmethodID saBreakpointMethod; JNIEXPORT jlocation saBreakpointLocation; /////////////////////////////////////// // // // Commands sent by the SA to the VM // // // /////////////////////////////////////// extern JNIEXPORT const int32_t SA_CMD_SUSPEND_ALL = 0; extern JNIEXPORT const int32_t SA_CMD_RESUME_ALL = 1; extern JNIEXPORT const int32_t SA_CMD_TOGGLE_BREAKPOINT = 2; extern JNIEXPORT const int32_t SA_CMD_BUF_SIZE = 1024; // SA sets this to a nonzero value when it is requesting a command // to be processed; VM sets it back to 0 when the command has been // executed JNIEXPORT volatile int32_t saCmdPending = 0; // SA sets this to one of the manifest constants above to indicate // the kind of command to be executed JNIEXPORT volatile int32_t saCmdType = 0; // VM sets this to 0 if the last command succeeded or a nonzero // value if it failed JNIEXPORT volatile int32_t saCmdResult = 0; // If last command failed, this buffer will contain a descriptive // error message JNIEXPORT char saCmdResultErrMsg[SA_CMD_BUF_SIZE]; // // Toggling of breakpoint command arguments. // // Originally there were separate set/clear breakpoint commands // taking a class name, method name and signature, and the iteration // through the debug information was done in the SA. It turns out // that doing this work in the target VM is significantly faster, // and since interactivity when setting and clearing breakpoints is // important, the solution which resulted in more C/C++ code was used. // // Source file name JNIEXPORT char saCmdBkptSrcFileName[SA_CMD_BUF_SIZE]; // Package name ('/' as separator instead of '.') JNIEXPORT char saCmdBkptPkgName[SA_CMD_BUF_SIZE]; // Line number JNIEXPORT int32_t saCmdBkptLineNumber; // Output back to SA: indicator whether the last failure of a // breakpoint toggle command was really an error or just a lack of // debug information covering the requested line. 0 if not error. // Valid only if saCmdResult != 0. JNIEXPORT int32_t saCmdBkptResWasError; // Output back to SA: resulting line number at which the breakpoint // was set or cleared (valid only if saCmdResult == 0) JNIEXPORT int32_t saCmdBkptResLineNumber; // Output back to SA: resulting byte code index at which the // breakpoint was set or cleared (valid only if saCmdResult == 0) JNIEXPORT int32_t saCmdBkptResBCI; // Output back to SA: indicator whether the breakpoint operation // resulted in a set or cleared breakpoint; nonzero if set, zero if // cleared (valid only if saCmdResult == 0) JNIEXPORT int32_t saCmdBkptResWasSet; // Output back to SA: method name the breakpoint was set in (valid // only if saCmdResult == 0) JNIEXPORT char saCmdBkptResMethodName[SA_CMD_BUF_SIZE]; // Output back to SA: method signature (JNI style) the breakpoint // was set in (valid only if saCmdResult == 0) JNIEXPORT char saCmdBkptResMethodSig[SA_CMD_BUF_SIZE]; } // Internal state static JavaVM* jvm = NULL; static JVMDI_Interface_1* jvmdi = NULL; static jthread debugThreadObj = NULL; static bool suspended = false; static vector suspendedThreads; static JVMDI_RawMonitor eventLock = NULL; class MonitorLocker { private: JVMDI_RawMonitor lock; public: MonitorLocker(JVMDI_RawMonitor lock) { this->lock = lock; if (lock != NULL) { jvmdi->RawMonitorEnter(lock); } } ~MonitorLocker() { if (lock != NULL) { jvmdi->RawMonitorExit(lock); } } }; class JvmdiDeallocator { private: void* ptr; public: JvmdiDeallocator(void* ptr) { this->ptr = ptr; } ~JvmdiDeallocator() { jvmdi->Deallocate((jbyte*) ptr); } }; class JvmdiRefListDeallocator { private: JNIEnv* env; jobject* refList; jint refCount; public: JvmdiRefListDeallocator(JNIEnv* env, jobject* refList, jint refCount) { this->env = env; this->refList = refList; this->refCount = refCount; } ~JvmdiRefListDeallocator() { for (int i = 0; i < refCount; i++) { env->DeleteGlobalRef(refList[i]); } jvmdi->Deallocate((jbyte*) refList); } }; static void stop(char* msg) { fprintf(stderr, "%s", msg); fprintf(stderr, "\n"); exit(1); } // This fills in the command result error message, sets the command // result to -1, and clears the pending command flag static void reportErrorToSA(const char* str, ...) { va_list varargs; va_start(varargs, str); vsnprintf(saCmdResultErrMsg, sizeof(saCmdResultErrMsg), str, varargs); va_end(varargs); saCmdResult = -1; saCmdPending = 0; } static bool packageNameMatches(char* clazzName, char* pkg) { int pkgLen = strlen(pkg); int clazzNameLen = strlen(clazzName); if (pkgLen >= clazzNameLen + 1) { return false; } if (strncmp(clazzName, pkg, pkgLen)) { return false; } // Ensure that '/' is the next character if non-empty package name int l = pkgLen; if (l > 0) { if (clazzName[l] != '/') { return false; } l++; } // Ensure that there are no more trailing slashes while (l < clazzNameLen) { if (clazzName[l++] == '/') { return false; } } return true; } static void executeOneCommand(JNIEnv* env) { switch (saCmdType) { case SA_CMD_SUSPEND_ALL: { if (suspended) { reportErrorToSA("Target process already suspended"); return; } // We implement this by getting all of the threads and calling // SuspendThread on each one, except for the thread object // corresponding to this thread. Each thread for which the call // succeeded (i.e., did not return JVMDI_ERROR_INVALID_THREAD) // is added to a list which is remembered for later resumption. // Note that this currently has race conditions since a thread // might be started after we call GetAllThreads and since a // thread for which we got an error earlier might be resumed by // the VM while we are busy suspending other threads. We could // solve this by looping until there are no more threads we can // suspend, but a more robust and scalable solution is to add // this functionality to the JVMDI interface (i.e., // "suspendAll"). Probably need to provide an exclude list for // such a routine. jint threadCount; jthread* threads; if (jvmdi->GetAllThreads(&threadCount, &threads) != JVMDI_ERROR_NONE) { reportErrorToSA("Error while getting thread list"); return; } for (int i = 0; i < threadCount; i++) { jthread thr = threads[i]; if (!env->IsSameObject(thr, debugThreadObj)) { jvmdiError err = jvmdi->SuspendThread(thr); if (err == JVMDI_ERROR_NONE) { // Remember this thread and do not free it suspendedThreads.push_back(thr); continue; } else { fprintf(stderr, " SA: Error %d while suspending thread\n", err); // FIXME: stop, resume all threads, report error } } env->DeleteGlobalRef(thr); } // Free up threads jvmdi->Deallocate((jbyte*) threads); // Suspension is complete suspended = true; break; } case SA_CMD_RESUME_ALL: { if (!suspended) { reportErrorToSA("Target process already suspended"); return; } saCmdResult = 0; bool errorOccurred = false; jvmdiError firstError; for (int i = 0; i < suspendedThreads.size(); i++) { jthread thr = suspendedThreads[i]; jvmdiError err = jvmdi->ResumeThread(thr); env->DeleteGlobalRef(thr); if (err != JVMDI_ERROR_NONE) { if (!errorOccurred) { errorOccurred = true; firstError = err; } } } suspendedThreads.clear(); suspended = false; if (errorOccurred) { reportErrorToSA("Error %d while resuming threads", firstError); return; } break; } case SA_CMD_TOGGLE_BREAKPOINT: { saCmdBkptResWasError = 1; // Search line number info for all loaded classes jint classCount; jclass* classes; jvmdiError glcRes = jvmdi->GetLoadedClasses(&classCount, &classes); if (glcRes != JVMDI_ERROR_NONE) { reportErrorToSA("Error %d while getting loaded classes", glcRes); return; } JvmdiRefListDeallocator rld(env, (jobject*) classes, classCount); bool done = false; bool gotOne = false; jclass targetClass; jmethodID targetMethod; jlocation targetLocation; jint targetLineNumber; for (int i = 0; i < classCount && !done; i++) { fflush(stderr); jclass clazz = classes[i]; char* srcName; jvmdiError sfnRes = jvmdi->GetSourceFileName(clazz, &srcName); if (sfnRes == JVMDI_ERROR_NONE) { JvmdiDeallocator de1(srcName); if (!strcmp(srcName, saCmdBkptSrcFileName)) { // Got a match. Now see whether the package name of the class also matches char* clazzName; jvmdiError sigRes = jvmdi->GetClassSignature(clazz, &clazzName); if (sigRes != JVMDI_ERROR_NONE) { reportErrorToSA("Error %d while getting a class's signature", sigRes); return; } JvmdiDeallocator de2(clazzName); if (packageNameMatches(clazzName + 1, saCmdBkptPkgName)) { // Iterate through all methods jint methodCount; jmethodID* methods; if (jvmdi->GetClassMethods(clazz, &methodCount, &methods) != JVMDI_ERROR_NONE) { reportErrorToSA("Error while getting methods of class %s", clazzName); return; } JvmdiDeallocator de3(methods); for (int j = 0; j < methodCount && !done; j++) { jmethodID m = methods[j]; jint entryCount; JVMDI_line_number_entry* table; jvmdiError lnRes = jvmdi->GetLineNumberTable(clazz, m, &entryCount, &table); if (lnRes == JVMDI_ERROR_NONE) { JvmdiDeallocator de4(table); // Look for line number greater than or equal to requested line for (int k = 0; k < entryCount && !done; k++) { JVMDI_line_number_entry& entry = table[k]; if (entry.line_number >= saCmdBkptLineNumber && (!gotOne || entry.line_number < targetLineNumber)) { gotOne = true; targetClass = clazz; targetMethod = m; targetLocation = entry.start_location; targetLineNumber = entry.line_number; done = (targetLineNumber == saCmdBkptLineNumber); } } } else if (lnRes != JVMDI_ERROR_ABSENT_INFORMATION) { reportErrorToSA("Unexpected error %d while fetching line number table", lnRes); return; } } } } } else if (sfnRes != JVMDI_ERROR_ABSENT_INFORMATION) { reportErrorToSA("Unexpected error %d while fetching source file name", sfnRes); return; } } bool wasSet = true; if (gotOne) { // Really toggle this breakpoint jvmdiError bpRes; bpRes = jvmdi->SetBreakpoint(targetClass, targetMethod, targetLocation); if (bpRes == JVMDI_ERROR_DUPLICATE) { bpRes = jvmdi->ClearBreakpoint(targetClass, targetMethod, targetLocation); wasSet = false; } if (bpRes != JVMDI_ERROR_NONE) { reportErrorToSA("Unexpected error %d while setting or clearing breakpoint at bci %d, line %d", bpRes, targetLocation, targetLineNumber); return; } } else { saCmdBkptResWasError = 0; reportErrorToSA("No debug information found covering this line"); return; } // Provide result saCmdBkptResLineNumber = targetLineNumber; saCmdBkptResBCI = targetLocation; saCmdBkptResWasSet = (wasSet ? 1 : 0); { char* methodName; char* methodSig; if (jvmdi->GetMethodName(targetClass, targetMethod, &methodName, &methodSig) == JVMDI_ERROR_NONE) { JvmdiDeallocator mnd(methodName); JvmdiDeallocator msd(methodSig); strncpy(saCmdBkptResMethodName, methodName, SA_CMD_BUF_SIZE); strncpy(saCmdBkptResMethodSig, methodSig, SA_CMD_BUF_SIZE); } else { strncpy(saCmdBkptResMethodName, "", SA_CMD_BUF_SIZE); strncpy(saCmdBkptResMethodSig, "", SA_CMD_BUF_SIZE); } } break; } default: reportErrorToSA("Command %d not yet supported", saCmdType); return; } // Successful command execution saCmdResult = 0; saCmdPending = 0; } static void saCommandThread(void *arg) { JNIEnv* env = NULL; if (jvm->GetEnv((void **) &env, JNI_VERSION_1_2) != JNI_OK) { stop("Error while starting Serviceability Agent " "command thread: could not get JNI environment"); } while (1) { // Wait for command while (!saCmdPending) { SLEEP(); } executeOneCommand(env); } } static void saEventHook(JNIEnv *env, JVMDI_Event *event) { MonitorLocker ml(eventLock); saEventKind = event->kind; if (event->kind == JVMDI_EVENT_VM_INIT) { // Create event lock if (jvmdi->CreateRawMonitor("Serviceability Agent Event Lock", &eventLock) != JVMDI_ERROR_NONE) { stop("Unable to create Serviceability Agent's event lock"); } // Start thread which receives commands from the SA. jclass threadClass = env->FindClass("java/lang/Thread"); if (threadClass == NULL) stop("Unable to find class java/lang/Thread"); jstring threadName = env->NewStringUTF("Serviceability Agent Command Thread"); if (threadName == NULL) stop("Unable to allocate debug thread name"); jmethodID ctor = env->GetMethodID(threadClass, "", "(Ljava/lang/String;)V"); if (ctor == NULL) stop("Unable to find appropriate constructor for java/lang/Thread"); // Allocate thread object jthread thr = (jthread) env->NewObject(threadClass, ctor, threadName); if (thr == NULL) stop("Unable to allocate debug thread's java/lang/Thread instance"); // Remember which thread this is debugThreadObj = env->NewGlobalRef(thr); if (debugThreadObj == NULL) stop("Unable to allocate global ref for debug thread object"); // Start thread jvmdiError err; if ((err = jvmdi->RunDebugThread(thr, &saCommandThread, NULL, JVMDI_THREAD_NORM_PRIORITY)) != JVMDI_ERROR_NONE) { char buf[256]; sprintf(buf, "Error %d while starting debug thread", err); stop(buf); } // OK, initialization is done return; } if (!saAttached) { return; } switch (event->kind) { case JVMDI_EVENT_EXCEPTION: { fprintf(stderr, "SA: Exception thrown -- ignoring\n"); saExceptionThread = event->u.exception.thread; saExceptionClass = event->u.exception.clazz; saExceptionMethod = event->u.exception.method; saExceptionLocation = event->u.exception.location; saExceptionException = event->u.exception.exception; saExceptionCatchClass = event->u.exception.catch_clazz; saExceptionCatchClass = event->u.exception.catch_clazz; saExceptionCatchMethod = event->u.exception.catch_method; saExceptionCatchLocation = event->u.exception.catch_location; // saEventPending = 1; break; } case JVMDI_EVENT_BREAKPOINT: { saBreakpointThread = event->u.breakpoint.thread; saBreakpointClass = event->u.breakpoint.clazz; saBreakpointMethod = event->u.breakpoint.method; saBreakpointLocation = event->u.breakpoint.location; saEventPending = 1; break; } default: break; } while (saAttached && saEventPending) { SLEEP(); } } extern "C" { JNIEXPORT jint JNICALL JVM_OnLoad(JavaVM *vm, char *options, void *reserved) { jvm = vm; if (jvm->GetEnv((void**) &jvmdi, JVMDI_VERSION_1) != JNI_OK) { return -1; } if (jvmdi->SetEventHook(&saEventHook) != JVMDI_ERROR_NONE) { return -1; } return 0; } };