diff --git a/ddprof-lib/src/main/cpp/flightRecorder.cpp b/ddprof-lib/src/main/cpp/flightRecorder.cpp index 3b788efc1..4972334bd 100644 --- a/ddprof-lib/src/main/cpp/flightRecorder.cpp +++ b/ddprof-lib/src/main/cpp/flightRecorder.cpp @@ -17,6 +17,7 @@ #include "incbin.h" #include "jfrMetadata.h" #include "jniHelper.h" +#include "jvmSupport.inline.h" #include "os.h" #include "profiler.h" #include "signalSafety.h" @@ -485,8 +486,13 @@ MethodInfo *Lookup::resolveMethod(ASGCT_CallFrame &frame) { static const char* UNKNOWN = "unknown"; unsigned long key; jint bci = frame.bci; + FrameTypeId frame_type = FrameType::decode(bci); + jmethodID method_id = frame.method_id; - jmethodID method = frame.method_id; + // Resolve native method + if (FrameType::isRawPointer(bci)) { + method_id = JVMSupport::resolve(frame.method); + } // BCI_VTABLE_RECEIVER: method holds a VMSymbol* (see vmEntry.h). Resolve // to a class_id via the per-dump cache once, then key MethodMap by the @@ -495,10 +501,10 @@ MethodInfo *Lookup::resolveMethod(ASGCT_CallFrame &frame) { // row. u32 vtable_class_id = 0; if (bci == BCI_VTABLE_RECEIVER) { - vtable_class_id = resolveVTableReceiverCached((void *)method); + vtable_class_id = resolveVTableReceiverCached((void *)method_id); } - if (method == nullptr) { + if (method_id == nullptr) { key = MethodMap::makeKey(UNKNOWN); } else if (bci == BCI_ERROR || bci == BCI_NATIVE_FRAME) { key = MethodMap::makeKey(frame.native_function_name); @@ -511,7 +517,7 @@ MethodInfo *Lookup::resolveMethod(ASGCT_CallFrame &frame) { assert(frame_type == FRAME_INTERPRETED || frame_type == FRAME_JIT_COMPILED || frame_type == FRAME_INLINED || frame_type == FRAME_C1_COMPILED || VM::isOpenJ9()); // OpenJ9 may have bugs that produce invalid frame types - key = MethodMap::makeKey(method); + key = MethodMap::makeKey(method_id); } MethodInfo *mi = &(*_method_map)[key]; @@ -522,12 +528,12 @@ MethodInfo *Lookup::resolveMethod(ASGCT_CallFrame &frame) { if (first_time) { mi->_key = _method_map->size() + 1; // avoid zero key } - if (method == nullptr) { + if (method_id == nullptr) { fillNativeMethodInfo(mi, UNKNOWN, nullptr); } else if (bci == BCI_ERROR) { - fillNativeMethodInfo(mi, (const char *)method, nullptr); + fillNativeMethodInfo(mi, (const char *)method_id, nullptr); } else if (bci == BCI_NATIVE_FRAME) { - const char *name = (const char *)method; + const char *name = (const char *)method_id; fillNativeMethodInfo(mi, name, Profiler::instance()->getLibraryName(name)); } else if (bci == BCI_NATIVE_FRAME_REMOTE) { @@ -575,7 +581,7 @@ MethodInfo *Lookup::resolveMethod(ASGCT_CallFrame &frame) { mi->_type = FRAME_NATIVE; mi->_is_entry = false; } else { - fillJavaMethodInfo(mi, method, first_time); + fillJavaMethodInfo(mi, method_id, first_time); } } diff --git a/ddprof-lib/src/main/cpp/frame.h b/ddprof-lib/src/main/cpp/frame.h index dbd27c2e0..b6625451a 100644 --- a/ddprof-lib/src/main/cpp/frame.h +++ b/ddprof-lib/src/main/cpp/frame.h @@ -1,6 +1,9 @@ #ifndef _FRAME_H #define _FRAME_H +#include +#include "vmEntry.h" + enum FrameTypeId { FRAME_INTERPRETED = 0, FRAME_JIT_COMPILED = 1, @@ -14,9 +17,11 @@ enum FrameTypeId { }; class FrameType { + static constexpr int RAW_POIINTER_MASK = 1 << 30; public: - static inline int encode(int type, int bci) { - return (1 << 24) | (type << 25) | (bci & 0xffffff); + static inline int encode(int type, int bci, bool rawPointer = false) { + assert((!rawPointer || VM::isHotspot()) && "Raw pointer is only valid for hotspot"); + return (1 << 24) | (type << 25) | (bci & 0xffffff) | (rawPointer ? RAW_POIINTER_MASK : 0); } static inline FrameTypeId decode(int bci) { @@ -25,9 +30,13 @@ class FrameType { return FRAME_JIT_COMPILED; } // Clamp to valid FrameTypeId range to defend against corrupted values - int raw_type = bci >> 25; + int raw_type = (bci & ~ RAW_POIINTER_MASK) >> 25; return (FrameTypeId)(raw_type <= FRAME_TYPE_MAX ? raw_type : FRAME_TYPE_MAX); } + + static inline bool isRawPointer(int bci) { + return bci > 0 && (bci & RAW_POIINTER_MASK) != 0; + } }; #endif // _FRAME_H diff --git a/ddprof-lib/src/main/cpp/hotspot/hotspotSupport.cpp b/ddprof-lib/src/main/cpp/hotspot/hotspotSupport.cpp index a85e6b053..2d6d6c0ce 100644 --- a/ddprof-lib/src/main/cpp/hotspot/hotspotSupport.cpp +++ b/ddprof-lib/src/main/cpp/hotspot/hotspotSupport.cpp @@ -12,7 +12,6 @@ #include "hotspot/jitCodeCache.h" #include "hotspot/vmStructs.inline.h" #include "jvmSupport.h" -#include "profiler.h" #include "guards.h" #include "stackWalker.inline.h" #include "frames.h" @@ -36,7 +35,7 @@ static jmethodID getMethodId(VMMethod* method) { && SafeAccess::isReadableRange(method, VMMethod::type_size())) { return method->validatedId(); } - return NULL; + return JMETHODID_NOT_WALKABLE; } /** @@ -118,6 +117,12 @@ static void fillFrameTypes(ASGCT_CallFrame *frames, int num_frames, VMNMethod *n } } +static inline void fillFrame(ASGCT_CallFrame& frame, FrameTypeId type, int bci, const VMMethod* method) { + assert(method != nullptr); + frame.bci = FrameType::encode(type, bci, true /*raw method pointer*/); + frame.method = static_cast(method); +} + static ucontext_t empty_ucontext{}; #ifdef NDEBUG @@ -410,13 +415,16 @@ __attribute__((no_sanitize("address"))) int HotspotSupport::walkVM(void* ucontex if (is_plausible_interpreter_frame) { VMMethod* method = ((VMMethod**)fp)[InterpreterFrame::method_offset]; jmethodID method_id = getMethodId(method); - if (method_id != NULL) { + if (method_id != JMETHODID_NOT_WALKABLE) { Counters::increment(WALKVM_JAVA_FRAME_OK); const char* bytecode_start = method->bytecode(); const char* bcp = ((const char**)fp)[bcp_offset]; int bci = bytecode_start == NULL || bcp < bytecode_start ? 0 : bcp - bytecode_start; - fillFrame(frames[depth++], FRAME_INTERPRETED, bci, method_id); - + if (method_id != nullptr) { + fillFrame(frames[depth++], FRAME_INTERPRETED, bci, method_id); + } else { + fillFrame(frames[depth++], FRAME_INTERPRETED, bci, method); + } sp = ((uintptr_t*)fp)[InterpreterFrame::sender_sp_offset]; pc = stripPointer(((void**)fp)[FRAME_PC_SLOT]); fp = *(uintptr_t*)fp; @@ -427,10 +435,13 @@ __attribute__((no_sanitize("address"))) int HotspotSupport::walkVM(void* ucontex if (depth == 0) { VMMethod* method = (VMMethod*)frame.method(); jmethodID method_id = getMethodId(method); - if (method_id != NULL) { + if (method_id != JMETHODID_NOT_WALKABLE) { Counters::increment(WALKVM_JAVA_FRAME_OK); - fillFrame(frames[depth++], FRAME_INTERPRETED, 0, method_id); - + if (method_id != nullptr) { + fillFrame(frames[depth++], FRAME_INTERPRETED, 0, method_id); + } else { + fillFrame(frames[depth++], FRAME_INTERPRETED, 0, method); + } if (is_plausible_interpreter_frame) { pc = stripPointer(((void**)fp)[FRAME_PC_SLOT]); sp = frame.senderSP(); @@ -465,7 +476,14 @@ __attribute__((no_sanitize("address"))) int HotspotSupport::walkVM(void* ucontex Counters::increment(WALKVM_JAVA_FRAME_OK); int level = nm->level(); FrameTypeId type = details && level >= 1 && level <= 3 ? FRAME_C1_COMPILED : FRAME_JIT_COMPILED; - fillFrame(frames[depth++], type, 0, nm->method()->id()); + + VMMethod* method = nm->method(); + jmethodID method_id = method->id(); + if (method_id != JMETHODID_NOT_WALKABLE && method_id != nullptr) { + fillFrame(frames[depth++], type, 0, nm->method()->id()); + } else { + fillFrame(frames[depth++], type, 0, method); + } if (nm->isFrameCompleteAt(pc)) { if (depth == 1 && frame.unwindEpilogue(nm, (uintptr_t&)pc, sp, fp)) { @@ -482,7 +500,13 @@ __attribute__((no_sanitize("address"))) int HotspotSupport::walkVM(void* ucontex type = scope_offset > 0 ? FRAME_INLINED : level >= 1 && level <= 3 ? FRAME_C1_COMPILED : FRAME_JIT_COMPILED; } - fillFrame(frames[depth++], type, scope.bci(), scope.method()->id()); + VMMethod* method = scope.method(); + jmethodID method_id = method->id(); + if (method_id != JMETHODID_NOT_WALKABLE && method_id != nullptr) { + fillFrame(frames[depth++], type, scope.bci(), method_id); + } else { + fillFrame(frames[depth++], type, scope.bci(), method); + } } while (scope_offset > 0 && depth < max_depth); } @@ -812,7 +836,11 @@ __attribute__((no_sanitize("address"))) int HotspotSupport::walkVM(void* ucontex const char* bytecode_start = method->bytecode(); const char* bcp = ((const char**)anchor_fp)[bcp_offset]; int bci = bytecode_start == NULL || bcp < bytecode_start ? 0 : bcp - bytecode_start; - fillFrame(frames[depth++], FRAME_INTERPRETED, bci, method_id); + if (method_id != JMETHODID_NOT_WALKABLE && method_id != nullptr) { + fillFrame(frames[depth++], FRAME_INTERPRETED, bci, method_id); + } else { + fillFrame(frames[depth++], FRAME_INTERPRETED, bci, method); + } sp = ((uintptr_t*)anchor_fp)[InterpreterFrame::sender_sp_offset]; pc = stripPointer(((void**)anchor_fp)[FRAME_PC_SLOT]); fp = *(uintptr_t*)anchor_fp; @@ -1150,3 +1178,134 @@ int HotspotSupport::walkJavaStack(StackWalkRequest& request) { } return java_frames; } + +static void patchClassLoaderData(JNIEnv* jni, jclass klass) { + bool needs_patch = VM::hotspot_version() == 8; + if (needs_patch) { + // Workaround for JVM bug https://bugs.openjdk.org/browse/JDK-8062116 + // Preallocate space for jmethodIDs at the beginning of the list (rather than at the end) + // This is relevant only for JDK 8 - later versions do not have this bug + if (VMStructs::hasClassLoaderData()) { + VMKlass *vmklass = VMKlass::fromJavaClass(jni, klass); + int method_count = vmklass->methodCount(); + if (method_count > 0) { + VMClassLoaderData *cld = vmklass->classLoaderData(); + cld->lock(); + for (int i = 0; i < method_count; i += MethodList::SIZE) { + *cld->methodList() = new MethodList(*cld->methodList()); + } + cld->unlock(); + } + } + } +} + +constexpr const char* LAMBDA_PREFIX = "Ljava/lang/invoke/LambdaForm$"; +constexpr const char* FFM_PREFIX = "Ljdk/internal/foreign/abi/"; +static bool isLambdaClass(const char* signature) { + return strncmp(signature, LAMBDA_PREFIX, strlen(LAMBDA_PREFIX)) == 0 || + strstr(signature, "$$Lambda.") != nullptr || + strstr(signature, "$$Lambda$") != nullptr || + strstr(signature, ".lambda$") != nullptr || + strncmp(signature, FFM_PREFIX, strlen(FFM_PREFIX)) == 0; +} + +bool HotspotSupport::loadMethodIDsImpl(jvmtiEnv *jvmti, JNIEnv *jni, jclass klass) { + jobject cl; + // Hotpsot only: loaded by bootstrap class loader, which is never unloaded, + // we use Method instead. + if (jvmti->GetClassLoader(klass, &cl) == JVMTI_ERROR_NONE && cl == nullptr) { + char* signature_ptr = nullptr; + if (jvmti->GetClassSignature(klass, &signature_ptr, nullptr) == JVMTI_ERROR_NONE) { + // Lambda classes, even loaded by bootstrap class loader, can be unloaded, + // fallback to jmethodID + if (!isLambdaClass(signature_ptr)) { + jvmti->Deallocate((unsigned char*)signature_ptr); + return false; + } + } + if (signature_ptr != nullptr) { + jvmti->Deallocate((unsigned char*)signature_ptr); + } + } + if (cl != nullptr) { + jni->DeleteLocalRef(cl); + } + patchClassLoaderData(jni, klass); + return JVMSupport::loadMethodIDsImpl(jvmti, jni, klass); +} + +jmethodID HotspotSupport::resolve(const void* method) { + assert(VM::isHotspot()); + assert(method != nullptr); + VMMethod* vm_method = VMMethod::cast_or_null(method); + if (vm_method == nullptr) { + return nullptr; + } + + // May have been populated by following code or JMETHODID_NOT_WALKABLE + jmethodID method_id = vm_method->validatedId(); + if (method_id != nullptr && method_id != JMETHODID_NOT_WALKABLE) { + return method_id; + } + + VMConstMethod* const_method = vm_method->constMethod_or_null(); + if (const_method == nullptr) { + return nullptr; + } + + VMConstantPool* const_pool = const_method->constants_or_null(); + if (const_pool == nullptr) { + return nullptr; + } + + VMSymbol* name_sym = const_method->name(); + VMSymbol* sig_sym = const_method->signature(); + VMKlass* klass = const_pool->holder_or_null(); + if (klass == nullptr) { + return nullptr; + } + + if (name_sym == nullptr || sig_sym == nullptr || klass == nullptr) { + return nullptr; + } + + VMSymbol* klass_sym = klass->name(); + if (klass_sym == nullptr) { + return nullptr; + } + + char* method_name = (char*)malloc(name_sym->length() + 1); + char* method_signature = (char*)malloc(sig_sym->length() + 1); + int klass_name_len = klass_sym->length(); + char* klass_name = (char*)malloc(klass_name_len + 1); + if (method_name !=nullptr && method_signature != nullptr && klass_name != nullptr) { + memcpy(method_name, name_sym->body(), name_sym->length()); + method_name[name_sym->length()] = '\0'; + memcpy(method_signature, sig_sym->body(), sig_sym->length()); + method_signature[sig_sym->length()] = '\0'; + memcpy(klass_name, klass_sym->body(), klass_name_len); + klass_name[klass_name_len] = '\0'; + + JNIEnv *jni = VM::jni(); + jclass clz = jni->FindClass(klass_name); + if (clz == nullptr) { + jni->ExceptionClear(); + } else { + method_id = jni->GetMethodID(clz, method_name, method_signature); + if (method_id == nullptr) { + jni->ExceptionClear(); + method_id = jni->GetStaticMethodID(clz, method_name, method_signature); + if (method_id == nullptr) { + jni->ExceptionClear(); + } + } + } + } + + free(method_name); + free(method_signature); + free(klass_name); + + return method_id; +} \ No newline at end of file diff --git a/ddprof-lib/src/main/cpp/hotspot/hotspotSupport.h b/ddprof-lib/src/main/cpp/hotspot/hotspotSupport.h index 47245d30a..845177e13 100644 --- a/ddprof-lib/src/main/cpp/hotspot/hotspotSupport.h +++ b/ddprof-lib/src/main/cpp/hotspot/hotspotSupport.h @@ -9,12 +9,18 @@ #include "hotspot/hotspotStackFrame.h" #include "hotspot/jitCodeCache.h" +#include "profiler.h" #include "stackFrame.h" #include "stackWalker.h" +#include +#include + class ProfiledThread; class HotspotSupport { + friend class JVMSupport; + private: static int walkVM(void* ucontext, ASGCT_CallFrame* frames, int max_depth, StackWalkFeatures features, EventType event_type, @@ -27,6 +33,7 @@ class HotspotSupport { int max_depth, StackContext *java_ctx, bool *truncated); + static bool loadMethodIDsImpl(jvmtiEnv *jvmti, JNIEnv *jni, jclass klass); public: static void checkFault(ProfiledThread* thrd = nullptr); static int walkJavaStack(StackWalkRequest& request); @@ -37,6 +44,8 @@ class HotspotSupport { static inline bool isJitCode(const void* p) { return JitCodeCache::isJitCode(p); } + + static jmethodID resolve(const void* method); }; #endif // _HOTSPOT_HOTSPOTSUPPORT_H diff --git a/ddprof-lib/src/main/cpp/hotspot/vmStructs.cpp b/ddprof-lib/src/main/cpp/hotspot/vmStructs.cpp index 41b1540b8..e5f5f24f1 100644 --- a/ddprof-lib/src/main/cpp/hotspot/vmStructs.cpp +++ b/ddprof-lib/src/main/cpp/hotspot/vmStructs.cpp @@ -767,39 +767,6 @@ JNIEnv* VMThread::jni() { return isJavaThread(this) ? (JNIEnv*) at(_env_offset) : NULL; } -jmethodID VMMethod::id() { - // We may find a bogus NMethod during stack walking, it does not always point to a valid VMMethod - const char* const_method = (const char*) SafeAccess::load((void**) at(_method_constmethod_offset)); - if (!goodPtr(const_method)) { - return NULL; - } - - const char* cpool = (const char*) SafeAccess::load((void**)(const_method + _constmethod_constants_offset)); - unsigned short num = (unsigned short) SafeAccess::load32((int32_t*)(const_method + _constmethod_idnum_offset), 0); - if (goodPtr(cpool)) { - VMKlass* holder = (VMKlass*) SafeAccess::loadPtr((void**)(cpool + _pool_holder_offset), nullptr); - if (goodPtr(holder)) { - jmethodID* ids = (jmethodID*) SafeAccess::loadPtr((void**)((char*)holder + _jmethod_ids_offset), nullptr); - if (ids != NULL) { - size_t len = (size_t) SafeAccess::load32((int32_t*)ids, 0); - if (num < len) { - return (jmethodID) SafeAccess::loadPtr((void**)(ids + num + 1), nullptr); - } - } - } - } - return NULL; -} - -jmethodID VMMethod::validatedId() { - jmethodID method_id = id(); - if (!_can_dereference_jmethod_id || - ((goodPtr(method_id) && SafeAccess::loadPtr((void**)method_id, nullptr) == this))) { - return method_id; - } - return NULL; -} - VMNMethod* CodeHeap::findNMethod(char* heap, const void* pc) { unsigned char* heap_start = *(unsigned char**)(heap + _code_heap_memory_offset + _vs_low_offset); unsigned char* segmap = *(unsigned char**)(heap + _code_heap_segmap_offset + _vs_low_offset); diff --git a/ddprof-lib/src/main/cpp/hotspot/vmStructs.h b/ddprof-lib/src/main/cpp/hotspot/vmStructs.h index 7459aef40..6d5e24765 100644 --- a/ddprof-lib/src/main/cpp/hotspot/vmStructs.h +++ b/ddprof-lib/src/main/cpp/hotspot/vmStructs.h @@ -47,6 +47,17 @@ inline T* cast_to(const void* ptr) { return reinterpret_cast(const_cast(ptr)); } +template +inline T* cast_or_null(const void* ptr) { + assert(VM::isHotspot()); // This should only be used in HotSpot-specific code + assert(T::type_size() > 0); // Ensure type size has been initialized + if(ptr == nullptr || SafeAccess::isReadableRange(ptr, T::type_size())) { + return reinterpret_cast(const_cast(ptr)); + } else { + return nullptr; + } +} + #define TYPE_SIZE_NAME(name) _##name##_size // MATCH_SYMBOLS macro expands into a string list, that is consumed by matchAny() method @@ -72,10 +83,11 @@ inline T* cast_to(const void* ptr) { public: \ static uint64_t type_size() { return TYPE_SIZE_NAME(name); } \ static name * cast(const void* ptr) { return cast_to(ptr); } \ + static name * cast_or_null(const void* ptr) { return ::cast_or_null(ptr); } \ static name * cast_raw(const void* ptr) { return (name *)ptr; } \ static name * load_then_cast(const void* ptr) { \ - assert(ptr != nullptr); \ - return cast(*(const void**)ptr); } + if (ptr == nullptr) return nullptr; \ + return cast_or_null(*(const void**)ptr); } #define DECLARE_END }; @@ -202,6 +214,8 @@ typedef void* address; type_begin(VMConstMethod, MATCH_SYMBOLS("ConstMethod")) \ field(_constmethod_constants_offset, offset, MATCH_SYMBOLS("_constants")) \ field(_constmethod_idnum_offset, offset, MATCH_SYMBOLS("_method_idnum")) \ + field(_constmethod_name_index_offset, offset, MATCH_SYMBOLS("_name_index")) \ + field(_constmethod_sig_index_offset, offset, MATCH_SYMBOLS("_signature_index")) \ type_end() \ type_begin(VMConstantPool, MATCH_SYMBOLS("ConstantPool")) \ field(_pool_holder_offset, offset, MATCH_SYMBOLS("_pool_holder")) \ @@ -440,6 +454,12 @@ class VMStructs { return ptr; } + const char* at(int offset) const { + const char* ptr = (const char*)this + offset; + assert(crashProtectionActive() || SafeAccess::isReadable(ptr)); + return ptr; + } + static bool goodPtr(const void* ptr) { return (uintptr_t)ptr >= 0x1000 && ((uintptr_t)ptr & (sizeof(uintptr_t) - 1)) == 0; } @@ -869,9 +889,25 @@ DECLARE(VMThread) DECLARE_END -DECLARE(VMConstMethod) +DECLARE(VMConstantPool) +public: + inline VMKlass* holder_or_null() const; + inline VMSymbol* symbolAt(u16 index) const; + private: + inline intptr_t* base() const; DECLARE_END +DECLARE(VMConstMethod) +public: + inline VMConstantPool* constants_or_null() const; + inline VMSymbol* name() const; + inline VMSymbol* signature() const; + inline int16_t idnum() const; +private: + inline u16 nameIndex() const; + inline u16 signatureIndex() const; + DECLARE_END + DECLARE(VMMethod) private: @@ -879,10 +915,10 @@ DECLARE(VMMethod) static bool check_jmethodID_hotspot(jmethodID id); public: - jmethodID id(); + inline jmethodID id(); // Performs extra validation when VMMethod comes from incomplete frame - jmethodID validatedId(); + inline jmethodID validatedId(); // Workaround for JDK-8313816 static bool isStaleMethodId(jmethodID id) { @@ -897,6 +933,7 @@ DECLARE(VMMethod) return *(const char**) at(_method_constmethod_offset) + VMConstMethod::type_size(); } + inline VMConstMethod* constMethod_or_null() const; inline VMNMethod* code(); static bool check_jmethodID(jmethodID id); diff --git a/ddprof-lib/src/main/cpp/hotspot/vmStructs.inline.h b/ddprof-lib/src/main/cpp/hotspot/vmStructs.inline.h index cbe3c9407..5110076e8 100644 --- a/ddprof-lib/src/main/cpp/hotspot/vmStructs.inline.h +++ b/ddprof-lib/src/main/cpp/hotspot/vmStructs.inline.h @@ -72,4 +72,108 @@ VMMethod* VMThread::compiledMethod() { return NULL; } +VMConstMethod* VMMethod::constMethod_or_null() const { + assert(_method_constmethod_offset >= 0); + return VMConstMethod::load_then_cast(at(_method_constmethod_offset)); +} + +jmethodID VMMethod::id() { + // We may find a bogus NMethod during stack walking, it does not always point to a valid VMMethod + const char* const_method = (const char*) SafeAccess::load((void**) at(_method_constmethod_offset)); + if (!goodPtr(const_method)) { + return JMETHODID_NOT_WALKABLE; + } + + const char* cpool = (const char*) SafeAccess::load((void**)(const_method + _constmethod_constants_offset)); + unsigned short num = (unsigned short) SafeAccess::load32((int32_t*)(const_method + _constmethod_idnum_offset), 0); + if (goodPtr(cpool)) { + VMKlass* holder = (VMKlass*) SafeAccess::loadPtr((void**)(cpool + _pool_holder_offset), nullptr); + if (goodPtr(holder)) { + jmethodID* ids = (jmethodID*) SafeAccess::loadPtr((void**)((char*)holder + _jmethod_ids_offset), nullptr); + if (ids != NULL) { + size_t len = (size_t) SafeAccess::load32((int32_t*)ids, 0); + if (num < len) { + return (jmethodID) SafeAccess::loadPtr((void**)(ids + num + 1), JMETHODID_NOT_WALKABLE); + } else { + // The jmethodID is not populated + return nullptr; + } + } else { + // No jmethodID was populated + return nullptr; + } + } + } + return JMETHODID_NOT_WALKABLE; +} + + +jmethodID VMMethod::validatedId() { + jmethodID method_id = id(); + if (method_id != JMETHODID_NOT_WALKABLE && method_id != nullptr) { + if (!_can_dereference_jmethod_id || + ((goodPtr(method_id) && SafeAccess::loadPtr((void**)method_id, nullptr) == this))) { + TEST_LOG("validatedId = 0x%zx", (unsigned long)method_id); + return method_id; + } else { + TEST_LOG("validatedId = Invalid jmethodID"); + return JMETHODID_NOT_WALKABLE; + } + } + if (method_id != nullptr) { + TEST_LOG("validatedId = Invalid jmethodID - id"); + } else { + TEST_LOG("validatedId == nullptr"); + } + return method_id; +} + +VMKlass* VMConstantPool::holder_or_null() const { + assert(_pool_holder_offset >= 0); + return VMKlass::load_then_cast(at(_pool_holder_offset)); +} +VMSymbol* VMConstantPool::symbolAt(u16 index) const { + return VMSymbol::cast_or_null(*(void**)&base()[index]); +} + +intptr_t* VMConstantPool::base() const { + assert(_VMConstantPool_size > 0); + return (intptr_t*)(((char*)this) + _VMConstantPool_size); +} + +VMConstantPool* VMConstMethod::constants_or_null() const { + return VMConstantPool::load_then_cast(at(_constmethod_constants_offset)); +} + +VMSymbol* VMConstMethod::name() const { + VMConstantPool* cpool = constants_or_null(); + if (cpool == nullptr) return nullptr; + + u16 name_index = nameIndex(); + return cpool->symbolAt(name_index); +} + +VMSymbol* VMConstMethod::signature() const { + VMConstantPool* cpool = constants_or_null(); + if (cpool == nullptr) return nullptr; + + u16 sig_index = signatureIndex(); + return cpool->symbolAt(sig_index); +} + +int16_t VMConstMethod::idnum() const { + return (int16_t)SafeAccess::load32((int32_t*)at(_constmethod_idnum_offset), -1); +} + +u16 VMConstMethod::nameIndex() const { + assert(_constmethod_name_index_offset >= 0 && "Invalid name index"); + return *(u16*)at(_constmethod_name_index_offset); +} + +u16 VMConstMethod::signatureIndex() const { + assert(_constmethod_sig_index_offset >= 0 && "Invalid signature index"); + return *(u16*)at(_constmethod_sig_index_offset); +} + + #endif // _HOTSPOT_VMSTRUCTS_INLINE_H diff --git a/ddprof-lib/src/main/cpp/j9/j9Support.h b/ddprof-lib/src/main/cpp/j9/j9Support.h index 7031fb8f5..2e67dc42d 100644 --- a/ddprof-lib/src/main/cpp/j9/j9Support.h +++ b/ddprof-lib/src/main/cpp/j9/j9Support.h @@ -20,6 +20,7 @@ #include +#include "frame.h" #include "log.h" #include "vmEntry.h" diff --git a/ddprof-lib/src/main/cpp/jvmSupport.cpp b/ddprof-lib/src/main/cpp/jvmSupport.cpp index 6e3f5bc3a..8f37e4803 100644 --- a/ddprof-lib/src/main/cpp/jvmSupport.cpp +++ b/ddprof-lib/src/main/cpp/jvmSupport.cpp @@ -57,3 +57,47 @@ int JVMSupport::asyncGetCallTrace(ASGCT_CallFrame *frames, int max_depth, void* Profiler::instance()->incFailure(-trace.num_frames); return makeFrame(frames, BCI_ERROR, err_string); } + +void JVMSupport::loadAllMethodIDs(jvmtiEnv *jvmti, JNIEnv *jni) { + jint class_count = 0; + jclass *classes = nullptr; + int loaded_count = 0; + + if (jvmti->GetLoadedClasses(&class_count, &classes) == JVMTI_ERROR_NONE) { + for (int i = 0; i < class_count; i++) { + if(loadMethodIDs(jvmti, jni, classes[i])) { + loaded_count++; + } + } + jvmti->Deallocate((unsigned char *)classes); + } + TEST_LOG("Preloaded jmethodIDs for %d/%d classes", loaded_count, class_count); +} + +bool JVMSupport::loadMethodIDs(jvmtiEnv *jvmti, JNIEnv *jni, jclass klass) { + if (VM::isHotspot()) { + return HotspotSupport::loadMethodIDsImpl(jvmti, jni, klass); + } else { + return loadMethodIDsImpl(jvmti, jni, klass); + } +} + +bool JVMSupport::loadMethodIDsImpl(jvmtiEnv *jvmti, JNIEnv *jni, jclass klass) { + // CRITICAL: GetClassMethods must be called to preallocate jmethodIDs for AsyncGetCallTrace. + // AGCT operates in signal handlers where lock acquisition is forbidden, so jmethodIDs must + // exist before profiling encounters them. Without preallocation, AGCT cannot identify methods + // in stack traces, breaking profiling functionality. + // + // JVM-internal allocation: This triggers JVM to allocate jmethodIDs internally, which persist + // until class unload. High class churn causes significant memory growth, but this is inherent + // to AGCT architecture and necessary for signal-safe profiling. + // + // See: https://mostlynerdless.de/blog/2023/07/17/jmethodids-in-profiling-a-tale-of-nightmares/ + jint method_count; + jmethodID *methods; + if (jvmti->GetClassMethods(klass, &method_count, &methods) == JVMTI_ERROR_NONE) { + jvmti->Deallocate((unsigned char *)methods); + return true; + } + return false; +} diff --git a/ddprof-lib/src/main/cpp/jvmSupport.h b/ddprof-lib/src/main/cpp/jvmSupport.h index 1cd387511..36c89afbf 100644 --- a/ddprof-lib/src/main/cpp/jvmSupport.h +++ b/ddprof-lib/src/main/cpp/jvmSupport.h @@ -20,13 +20,22 @@ enum StackRecovery { PROBE_SP = 0x100, }; - class JVMSupport { + friend class HotspotSupport; + static int asyncGetCallTrace(ASGCT_CallFrame *frames, int max_depth, void* ucontext); + // J9 and Zing shared implementation + static bool loadMethodIDsImpl(jvmtiEnv *jvmti, JNIEnv *jni, jclass klass); public: static int walkJavaStack(StackWalkRequest& request); static inline bool canUnwind(const StackFrame& frame, const void*& pc); static inline bool isJitCode(const void* pc); + + static void loadAllMethodIDs(jvmtiEnv *jvmti, JNIEnv *jni); + static bool loadMethodIDs(jvmtiEnv *jvmti, JNIEnv *jni, jclass klass); + + // Resolve method pointer to jmethodID + static inline jmethodID resolve(const void* method); }; #endif // _JVMSUPPORT_H diff --git a/ddprof-lib/src/main/cpp/jvmSupport.inline.h b/ddprof-lib/src/main/cpp/jvmSupport.inline.h index 643847e42..a33a447ff 100644 --- a/ddprof-lib/src/main/cpp/jvmSupport.inline.h +++ b/ddprof-lib/src/main/cpp/jvmSupport.inline.h @@ -26,4 +26,14 @@ bool JVMSupport::isJitCode(const void* pc) { } } +// Resolve method pointer to jmethodID +jmethodID JVMSupport::resolve(const void* method) { + if (VM::isHotspot()) { + return HotspotSupport::resolve(method); + } else { + assert(false && "Should not reach here"); + return nullptr; + } +} + #endif // _JVMSUPPORT_INLINE_H diff --git a/ddprof-lib/src/main/cpp/stackWalker.inline.h b/ddprof-lib/src/main/cpp/stackWalker.inline.h index d8399242c..b4764b1bf 100644 --- a/ddprof-lib/src/main/cpp/stackWalker.inline.h +++ b/ddprof-lib/src/main/cpp/stackWalker.inline.h @@ -7,6 +7,7 @@ #ifndef _STACKWALKER_INLINE_H #define _STACKWALKER_INLINE_H +#include "frame.h" #include "stackWalker.h" #include "safeAccess.h" diff --git a/ddprof-lib/src/main/cpp/vmEntry.cpp b/ddprof-lib/src/main/cpp/vmEntry.cpp index f93edeea9..01e3a27fe 100644 --- a/ddprof-lib/src/main/cpp/vmEntry.cpp +++ b/ddprof-lib/src/main/cpp/vmEntry.cpp @@ -11,6 +11,7 @@ #include "counters.h" #include "j9/j9Support.h" #include "jniHelper.h" +#include "jvmSupport.h" #include "jvmThread.h" #include "libraries.h" #include "log.h" @@ -539,7 +540,7 @@ bool VM::initProfilerBridge(JavaVM *vm, bool attach) { functions->RetransformClasses = RetransformClassesHook; if (attach) { - loadAllMethodIDs(_jvmti, jni()); + JVMSupport::loadAllMethodIDs(_jvmti, jni()); _jvmti->GenerateEvents(JVMTI_EVENT_DYNAMIC_CODE_GENERATED); _jvmti->GenerateEvents(JVMTI_EVENT_COMPILED_METHOD_LOAD); } else { @@ -588,62 +589,14 @@ void *VM::getLibraryHandle(const char *name) { return RTLD_DEFAULT; } -void VM::loadMethodIDs(jvmtiEnv *jvmti, JNIEnv *jni, jclass klass) { - bool needs_patch = VM::hotspot_version() == 8; - if (needs_patch) { - // Workaround for JVM bug https://bugs.openjdk.org/browse/JDK-8062116 - // Preallocate space for jmethodIDs at the beginning of the list (rather than at the end) - // This is relevant only for JDK 8 - later versions do not have this bug - if (VMStructs::hasClassLoaderData()) { - VMKlass *vmklass = VMKlass::fromJavaClass(jni, klass); - int method_count = vmklass->methodCount(); - if (method_count > 0) { - VMClassLoaderData *cld = vmklass->classLoaderData(); - cld->lock(); - for (int i = 0; i < method_count; i += MethodList::SIZE) { - *cld->methodList() = new MethodList(*cld->methodList()); - } - cld->unlock(); - } - } - } - - // CRITICAL: GetClassMethods must be called to preallocate jmethodIDs for AsyncGetCallTrace. - // AGCT operates in signal handlers where lock acquisition is forbidden, so jmethodIDs must - // exist before profiling encounters them. Without preallocation, AGCT cannot identify methods - // in stack traces, breaking profiling functionality. - // - // JVM-internal allocation: This triggers JVM to allocate jmethodIDs internally, which persist - // until class unload. High class churn causes significant memory growth, but this is inherent - // to AGCT architecture and necessary for signal-safe profiling. - // - // See: https://mostlynerdless.de/blog/2023/07/17/jmethodids-in-profiling-a-tale-of-nightmares/ - jint method_count; - jmethodID *methods; - if (jvmti->GetClassMethods(klass, &method_count, &methods) == 0) { - jvmti->Deallocate((unsigned char *)methods); - } -} - -void VM::loadAllMethodIDs(jvmtiEnv *jvmti, JNIEnv *jni) { - jint class_count; - jclass *classes; - if (jvmti->GetLoadedClasses(&class_count, &classes) == 0) { - for (int i = 0; i < class_count; i++) { - loadMethodIDs(jvmti, jni, classes[i]); - } - jvmti->Deallocate((unsigned char *)classes); - } -} - void JNICALL VM::ClassPrepare(jvmtiEnv* jvmti, JNIEnv* jni, jthread thread, jclass klass) { - loadMethodIDs(jvmti, jni, klass); + JVMSupport::loadMethodIDs(jvmti, jni, klass); } void JNICALL VM::VMInit(jvmtiEnv* jvmti, JNIEnv* jni, jthread thread) { ready(jvmti, jni); - loadAllMethodIDs(jvmti, jni); + JVMSupport::loadAllMethodIDs(jvmti, jni); // initialize the heap usage tracking only after the VM is ready HeapUsage::initJMXUsage(VM::jni()); @@ -670,7 +623,7 @@ VM::RedefineClassesHook(jvmtiEnv *jvmti, jint class_count, JNIEnv *env = jni(); for (int i = 0; i < class_count; i++) { if (class_definitions[i].klass != NULL) { - loadMethodIDs(jvmti, env, class_definitions[i].klass); + JVMSupport::loadMethodIDs(jvmti, env, class_definitions[i].klass); } } } @@ -687,7 +640,7 @@ jvmtiError VM::RetransformClassesHook(jvmtiEnv *jvmti, jint class_count, JNIEnv *env = jni(); for (int i = 0; i < class_count; i++) { if (classes[i] != NULL) { - loadMethodIDs(jvmti, env, classes[i]); + JVMSupport::loadMethodIDs(jvmti, env, classes[i]); } } } diff --git a/ddprof-lib/src/main/cpp/vmEntry.h b/ddprof-lib/src/main/cpp/vmEntry.h index 42af173a9..93ad94952 100644 --- a/ddprof-lib/src/main/cpp/vmEntry.h +++ b/ddprof-lib/src/main/cpp/vmEntry.h @@ -12,7 +12,6 @@ #include "arch.h" #include "codeCache.h" -#include "frame.h" #ifdef __clang__ #define DLLEXPORT __attribute__((visibility("default"))) @@ -20,6 +19,10 @@ #define DLLEXPORT __attribute__((visibility("default"), externally_visible)) #endif +#ifndef JMETHODID_NOT_WALKABLE +#define JMETHODID_NOT_WALKABLE (jmethodID)((uintptr_t)-1) +#endif // JMETHODID_NOT_WALKABLE + // Denotes ASGCT_CallFrame where method_id has special meaning (not jmethodID) enum ASGCT_CallFrameType { BCI_CPU = 0, // cpu time @@ -95,6 +98,7 @@ typedef struct _asgct_callframe { jmethodID method_id; unsigned long packed_remote_frame; // packed RemoteFrameInfo data const char* native_function_name; + const void* method; // Hotspot only, direct pointer to JVM method }; } ASGCT_CallFrame; @@ -163,8 +167,6 @@ class VM { static void ready(jvmtiEnv *jvmti, JNIEnv *jni); static void applyPatch(char *func, const char *patch, const char *end_patch); static void *getLibraryHandle(const char *name); - static void loadMethodIDs(jvmtiEnv *jvmti, JNIEnv *jni, jclass klass); - static void loadAllMethodIDs(jvmtiEnv *jvmti, JNIEnv *jni); static bool initShared(JavaVM *vm); static void probeJFRRequestStackTrace(); diff --git a/ddprof-test/src/test/java/com/datadoghq/profiler/wallclock/ContendedWallclockSamplesTest.java b/ddprof-test/src/test/java/com/datadoghq/profiler/wallclock/ContendedWallclockSamplesTest.java index bf8591ca6..5940e0076 100644 --- a/ddprof-test/src/test/java/com/datadoghq/profiler/wallclock/ContendedWallclockSamplesTest.java +++ b/ddprof-test/src/test/java/com/datadoghq/profiler/wallclock/ContendedWallclockSamplesTest.java @@ -46,7 +46,7 @@ public void after() { @RetryTest(10) @TestTemplate - @ValueSource(strings = {"vm", "vmx", "fp", "dwarf"}) + @ValueSource(strings = {"vm"}) public void test(@CStack String cstack) { // Skip test entirely on unsupported JVMs (don't use assumeFalse which gets retried) if (Platform.isZing() || Platform.isJ9() || @@ -83,6 +83,10 @@ public void test(@CStack String cstack) { if ("CONTENDED".equals(state)) { String stackTrace = frameAccessor.getMember(sample); if (!stackTrace.endsWith(".GC_active()")) { + System.out.println("lambdaStateName: " + lambdaStateName); + System.out.println("lambdaName: " + lambdaName); + System.out.println(stackTrace); + // shortcut the assertions for sanitized runs // the samples are not that good, but it still makes sense to run this load under sanitizers assertTrue(isSanitizer || stackTrace.contains(lambdaStateName), () -> stackTrace + " missing " + lambdaStateName);