diff --git a/ddprof-lib/src/main/cpp/hotspot/vmStructs.cpp b/ddprof-lib/src/main/cpp/hotspot/vmStructs.cpp index 0a04e46343..03024ca53b 100644 --- a/ddprof-lib/src/main/cpp/hotspot/vmStructs.cpp +++ b/ddprof-lib/src/main/cpp/hotspot/vmStructs.cpp @@ -179,7 +179,7 @@ void VMStructs::init_offsets_and_addresses() { continue; \ } -#define END_TYPE() continue; } +#define END_TYPE() } DECLARE_TYPE_FIELD_DO(MATCH_TYPE_NAMES, READ_FIELD_VALUE, READ_FIELD_VALUE_WITH_VERSION, END_TYPE) DECLARE_V27_TYPE_FIELD_DO(MATCH_TYPE_NAMES, READ_FIELD_VALUE, READ_FIELD_VALUE_WITH_VERSION, END_TYPE) #undef MATCH_TYPE_NAMES diff --git a/ddprof-lib/src/main/cpp/hotspot/vmStructs.h b/ddprof-lib/src/main/cpp/hotspot/vmStructs.h index c61ccbc3b3..ba07b6d2e1 100644 --- a/ddprof-lib/src/main/cpp/hotspot/vmStructs.h +++ b/ddprof-lib/src/main/cpp/hotspot/vmStructs.h @@ -782,7 +782,9 @@ DECLARE(VMContinuationEntry) // https://github.com/openjdk/jdk/blob/master/src/hotspot/share/runtime/continuationEntry.cpp uintptr_t entryFP() const { assert(type_size() > 0); // must not be called before ContinuationEntry is resolved - return (uintptr_t)this + type_size(); + constexpr uintptr_t alignment = 2 * sizeof(uintptr_t); + const uintptr_t size = (type_size() + alignment - 1) & ~(alignment - 1); + return (uintptr_t)this + size; } // Returns the enclosing ContinuationEntry when continuations are nested diff --git a/ddprof-lib/src/test/cpp/continuationMetadata_ut.cpp b/ddprof-lib/src/test/cpp/continuationMetadata_ut.cpp new file mode 100644 index 0000000000..f1504ae6f2 --- /dev/null +++ b/ddprof-lib/src/test/cpp/continuationMetadata_ut.cpp @@ -0,0 +1,198 @@ +/* + * Copyright 2026, Datadog, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +#include + +#include "../../main/cpp/hotspot/vmStructs.h" + +#include +#include +#include +#include +#include + +// Supplies exported table addresses through the real CodeCache/readSymbol path. +// Derivation exposes protected metadata initialization without changing production code. +class ContinuationMetadataTest : public ::testing::Test, protected VMStructs { +protected: + struct Field { + const char* type; + const char* name; + int32_t is_static; + uint64_t offset; + void* address; + }; + + struct Type { + const char* name; + uint64_t size; + }; + + struct JavaThreadLayout { + uintptr_t state; + void* continuation; + }; + + struct EntryLayout { + void* parent; + }; + + CodeCache symbols{"continuation metadata fixture"}; + Field fields[4]{}; + Type types[2]{}; + void* barrier = nullptr; + void* return_pc = nullptr; + + void SetUp() override { + saved = {_libjvm, _thread_state_offset, _cont_entry_offset, + _cont_entry_parent_offset, _cont_return_barrier_addr, + _cont_entry_return_pc_addr, TYPE_SIZE_NAME(VMContinuationEntry)}; + _libjvm = &symbols; + _thread_state_offset = -1; + _cont_entry_offset = -1; + _cont_entry_parent_offset = -1; + _cont_return_barrier_addr = nullptr; + _cont_entry_return_pc_addr = nullptr; + TYPE_SIZE_NAME(VMContinuationEntry) = 0; + + addExport("gHotSpotVMStructs", structs_address); + addExport("gHotSpotVMStructEntryArrayStride", structs_stride); + addExport("gHotSpotVMStructEntryTypeNameOffset", field_type_offset); + addExport("gHotSpotVMStructEntryFieldNameOffset", field_name_offset); + addExport("gHotSpotVMStructEntryIsStaticOffset", field_static_offset); + addExport("gHotSpotVMStructEntryOffsetOffset", field_offset_offset); + addExport("gHotSpotVMStructEntryAddressOffset", field_address_offset); + addExport("gHotSpotVMTypes", types_address); + addExport("gHotSpotVMTypeEntryArrayStride", types_stride); + addExport("gHotSpotVMTypeEntryTypeNameOffset", type_name_offset); + addExport("gHotSpotVMTypeEntrySizeOffset", type_size_offset); + } + + void TearDown() override { + _libjvm = saved.libjvm; + _thread_state_offset = saved.thread_state; + _cont_entry_offset = saved.cont_entry; + _cont_entry_parent_offset = saved.parent; + _cont_return_barrier_addr = saved.barrier; + _cont_entry_return_pc_addr = saved.return_pc; + TYPE_SIZE_NAME(VMContinuationEntry) = saved.entry_size; + } + + // Calls the production dispatcher on caller-supplied rows plus its null terminator. + void parseFields(std::initializer_list rows) { + ASSERT_LT(rows.size(), sizeof(fields) / sizeof(fields[0])); + std::copy(rows.begin(), rows.end(), fields); + fields[rows.size()] = {}; + structs_address = reinterpret_cast(fields); + init_offsets_and_addresses(); + } + + void parseEntrySize(uint64_t size) { + types[0] = {"ContinuationEntry", size}; + types_address = reinterpret_cast(types); + init_type_sizes(); + } + +private: + struct Saved { + CodeCache* libjvm; + int thread_state; + int cont_entry; + int parent; + void* barrier; + void* return_pc; + uint64_t entry_size; + } saved{}; + + uintptr_t structs_address = 0; + uintptr_t structs_stride = sizeof(Field); + uintptr_t field_type_offset = offsetof(Field, type); + uintptr_t field_name_offset = offsetof(Field, name); + uintptr_t field_static_offset = offsetof(Field, is_static); + uintptr_t field_offset_offset = offsetof(Field, offset); + uintptr_t field_address_offset = offsetof(Field, address); + uintptr_t types_address = 0; + uintptr_t types_stride = sizeof(Type); + uintptr_t type_name_offset = offsetof(Type, name); + uintptr_t type_size_offset = offsetof(Type, size); + + void addExport(const char* name, uintptr_t& value) { + symbols.add(&value, sizeof(value), name); + } +}; + +TEST_F(ContinuationMetadataTest, ReadsExistingJavaThreadField) { + parseFields({{"JavaThread", "_thread_state", 0, offsetof(JavaThreadLayout, state), nullptr}}); + + EXPECT_EQ(offsetof(JavaThreadLayout, state), _thread_state_offset); +} + +TEST_F(ContinuationMetadataTest, ReadsContinuationEntryFields) { + parseFields({{"ContinuationEntry", "_parent", 0, offsetof(EntryLayout, parent), nullptr}, + {"ContinuationEntry", "_return_pc", 1, 0, &return_pc}}); + + EXPECT_EQ(offsetof(EntryLayout, parent), _cont_entry_parent_offset); + EXPECT_EQ(&return_pc, _cont_entry_return_pc_addr); +} + +TEST_F(ContinuationMetadataTest, ReadsJavaThreadContinuationEntry) { + parseEntrySize(64); + parseFields({{"JavaThread", "_cont_entry", 0, offsetof(JavaThreadLayout, continuation), nullptr}}); + + ASSERT_EQ(64u, VMContinuationEntry::type_size()); + EXPECT_EQ(offsetof(JavaThreadLayout, continuation), _cont_entry_offset) + << "B01: the original JavaThread group must allow later continuation fields to be read"; +} + +TEST_F(ContinuationMetadataTest, ReadsThreadAliasContinuationEntry) { + parseFields({{"Thread", "_cont_entry", 0, offsetof(JavaThreadLayout, continuation), nullptr}}); + + EXPECT_EQ(offsetof(JavaThreadLayout, continuation), _cont_entry_offset) + << "B01: the exported Thread alias must reach the continuation field group"; +} + +TEST_F(ContinuationMetadataTest, ReadsStubRoutinesContinuationReturnBarrier) { + parseFields({{"StubRoutines", "_cont_returnBarrier", 1, 0, &barrier}}); + + EXPECT_EQ(&barrier, _cont_return_barrier_addr) + << "B01: the original StubRoutines group must allow its later continuation field"; +} + +TEST_F(ContinuationMetadataTest, MissingContinuationMetadataRemainsUnavailable) { + parseFields({{"JavaThread", "_thread_state", 0, offsetof(JavaThreadLayout, state), nullptr}}); + + EXPECT_EQ(-1, _cont_entry_offset); + EXPECT_EQ(nullptr, _cont_return_barrier_addr); + EXPECT_EQ(0u, VMContinuationEntry::type_size()); +} + +TEST_F(ContinuationMetadataTest, EntryFpUsesAlignedSizeWithoutPadding) { + alignas(16) std::array stack{}; + parseEntrySize(64); + VMContinuationEntry* entry = VMContinuationEntry::cast_raw(stack.data()); + + EXPECT_EQ(reinterpret_cast(&stack[8]), entry->entryFP()); +} + +TEST_F(ContinuationMetadataTest, EntryFpSkipsPaddingIn56ByteLayout) { + // OpenJDK ContinuationEntry::size() rounds sizeof to 2*wordSize; entry_fp() + // adds that size on x86 and aarch64. The installed JDK 27 exports sizeof=56. + // https://github.com/openjdk/jdk/blob/jdk-25-ga/src/hotspot/share/runtime/continuationEntry.hpp + // https://github.com/openjdk/jdk/blob/jdk-25-ga/src/hotspot/cpu/aarch64/continuationEntry_aarch64.inline.hpp + alignas(16) std::array stack{}; + uintptr_t saved_fp = reinterpret_cast(&stack[11]); + uintptr_t saved_pc = reinterpret_cast(&barrier); + stack[7] = 0xabad1dea; // padding between the exported object size and the saved FP + stack[8] = saved_fp; + stack[9] = saved_pc; + parseEntrySize(56); + VMContinuationEntry* entry = VMContinuationEntry::cast_raw(stack.data()); + uintptr_t* actual_fp = reinterpret_cast(entry->entryFP()); + + EXPECT_EQ(reinterpret_cast(&stack[8]), entry->entryFP()) + << "B02: exported sizeof=56 needs an eight-byte pad before the entry frame"; + EXPECT_EQ(saved_fp, actual_fp[0]) << "entryFP must point at the saved frame pointer"; + EXPECT_EQ(saved_pc, actual_fp[1]) << "the following slot must contain the return address"; +}