sa.cpp revision 1472
/*
* 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 <stdio.h>
#include <stdarg.h>
#include <stdlib.h>
#include <vector>
#include "sa.hpp"
#include "jni.h"
#include "jvmdi.h"
#ifndef WIN32
#include <inttypes.h>
#else
typedef int int32_t;
#endif
#ifdef WIN32
#include <windows.h>
#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<jthread> 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, "<error>", SA_CMD_BUF_SIZE);
strncpy(saCmdBkptResMethodSig, "<error>", 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, "<init>", "(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;
}
};