Categories: OpenJDK

Native Stack Printing on Windows AArch64

Another test failure I investigated last month was in the runtime/jni/nativeStack/TestNativeStack.java test. This is the setup I used to run the test in my MSYS environment.

export JDKARCH=aarch64
export DEBUGLEVEL=slowdebug
export JDKSRCPATH=/c/java/forks/openjdk/jdk
export JDKBUILDPATH="${JDKSRCPATH}/build/windows-${JDKARCH}-server-${DEBUGLEVEL}"
export JDKTOTEST="${JDKBUILDPATH}/images/jdk"
export JTREGNATIVEPATH1="${JDKBUILDPATH}/support/test/hotspot/jtreg/native/lib"
export JTREGNATIVEPATH2="${JDKBUILDPATH}/support/test/jdk/jtreg/native/lib"
export JTREGNATIVEPATH3="${JDKBUILDPATH}/support/test/lib/native/lib"
export GTESTPATH="${JDKBUILDPATH}/images/test/hotspot/gtest/server"
export JTREGBINPATH=/c/java/binaries/jtreg/jtreg-8+2

export TESTTORUN=test/hotspot/jtreg/runtime/jni/nativeStack/TestNativeStack.java

./run-jtreg-test.sh $JDKSRCPATH $JDKTOTEST $JTREGBINPATH/lib/jtreg.jar $TESTTORUN -nativepath:$JTREGNATIVEPATH1

The test fails with the output below. The key line is Native frames: <unavailable>.

STDOUT:
Command line: [C:\java\forks\openjdk\jdk\build\windows-aarch64-server-slowdebug\images\jdk\bin\java.exe -cp C:\java\forks\openjdk\jdk\JTwork\classes\runtime\jni\nativeStack\TestNativeStack.d;C:\java\forks\openjdk\jdk\test\hotspot\jtreg\runtime\jni\nativeStack;C:\java\forks\openjdk\jdk\JTwork\classes\test\lib;C:\java\binaries\jtreg\jtreg-7.5.2\lib\javatest.jar;C:\java\binaries\jtreg\jtreg-7.5.2\lib\jtreg.jar;C:\java\binaries\jtreg\jtreg-7.5.2\lib\junit-platform-console-standalone-1.11.0.jar;C:\java\binaries\jtreg\jtreg-7.5.2\lib\testng-7.3.0.jar;C:\java\binaries\jtreg\jtreg-7.5.2\lib\guice-5.1.0.jar;C:\java\binaries\jtreg\jtreg-7.5.2\lib\jcommander-1.82.jar -Xmx512m -Xcheck:jni -Djava.library.path=C:\java\forks\openjdk\jdk\build\windows-aarch64-server-slowdebug\support\test\hotspot\jtreg\native\lib TestNativeStack$Main ]
[2025-09-04T15:54:18.312130500Z] Gathering output for process 22380
[2025-09-04T15:54:18.753851700Z] Waiting for completion for process 22380
[2025-09-04T15:54:18.756861300Z] Waiting for completion finished for process 22380
Output and diagnostic info for process 22380 was saved into 'pid-22380-output.log'
STDERR:
 stdout: [Triggering a JNI warning
WARNING in native method: JNI call made without checking exceptions when required to from CallStaticObjectMethod
Native frames: <unavailable>
];
 stderr: [WARNING: A restricted method in java.lang.System has been called
WARNING: java.lang.System::loadLibrary has been called by TestNativeStack in an unnamed module (file:/C:/java/forks/openjdk/jdk/JTwork/classes/runtime/jni/nativeStack/TestNativeStack.d/)
WARNING: Use --enable-native-access=ALL-UNNAMED to avoid a warning for callers in this module
WARNING: Restricted methods will be blocked in a future release unless native access is enabled

]
 exitValue = -2147483645

java.lang.RuntimeException: Expected to get exit value of [0], exit value is: [-2147483645]
        at jdk.test.lib.process.OutputAnalyzer.shouldHaveExitValue(OutputAnalyzer.java:522)
        at TestNativeStack.main(TestNativeStack.java:57)
        at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104)
        at java.base/java.lang.reflect.Method.invoke(Method.java:565)
        at com.sun.javatest.regtest.agent.MainActionHelper$AgentVMRunnable.run(MainActionHelper.java:335)
        at java.base/java.lang.Thread.run(Thread.java:1474)

I added a DebugBreak call to the line outputing that message to see how we got there:

KernelBase.dll!...DebugBreak() Line 2582	C++
jvm.dll!NativeStackPrinter::print_stack_from_frame(outputStream * st, frame fr, char * buf, int buf_size, bool print_source_info, int max_frames) Line 80	C++
jvm.dll!NativeStackPrinter::print_stack_from_frame(outputStream * st, char * buf, int buf_size, bool print_source_info, int max_frames) Line 104	C++
jvm.dll!NativeStackPrinter::print_stack(outputStream * st, char * buf, int buf_size, unsigned char * & lastpc, bool print_source_info, int max_frames) Line 40	C++
jvm.dll!JavaThread::print_jni_stack() Line 1775	C++
jvm.dll!check_pending_exception(JavaThread * thr) Line 192	C++
jvm.dll!functionEnter(JavaThread * thr) Line 218	C++
jvm.dll!checked_jni_CallStaticObjectMethod(JNIEnv_ * env, _jclass * clazz, _jmethodID * methodID, ...) Line 1321	C++
nativeStack.dll!generateWarning(const JNINativeInterface_ * * env) Line 66	C
nativeStack.dll!thread_start(void * unused) Line 92	C
ucrtbase.dll!00007ffb0970b028()	Unknown
kernel32.dll!00007ffb0bbd8740()	Unknown
ntdll.dll!RtlUserThreadStart(long(*)(void *) StartAddress, void * Argument) Line 1184	C

The failure path is as follows. HAVE_PLATFORM_PRINT_NATIVE_STACK is not defined on Windows AArch64. Consequently, Windows AArch64 uses the implementation of os::platform_print_native_stack that simply returns false. This results in NativeStackPrinter::print_stack having to call NativeStackPrinter::print_stack_from_frame instead. However, the context is null. As a result, the frame used for printing the stack is obtained from os::current_frame(), which returns an empty frame. The frame’s pc() method returns nullptr and the “Native frames: <unavailable>” message is printed.

The fix for this issue is to define the os::platform_print_native_stack method for Windows AArch64 and share the implementation of the Windows x64 os::win32::platform_print_native_stack method with Windows AArch64. I opened [JDK-8369322] Implement native stack printing for Windows-AArch64 – Java Bug System and the associated PR 8369322: Implement native stack printing for Windows-AArch64 by swesonga · Pull Request #27680 · openjdk/jdk fixing this failure. With this change, the native frames are now printed as shown below:

Triggering a JNI warning
WARNING: A restricted method in java.lang.System has been called
WARNING: java.lang.System::loadLibrary has been called by TestNativeStack in an unnamed module (file:/C:/java/forks/openjdk/jdk/JTwork/classes/runtime/jni/nativeStack/TestNativeStack.d/)
WARNING: Use --enable-native-access=ALL-UNNAMED to avoid a warning for callers in this module
WARNING: Restricted methods will be blocked in a future release unless native access is enabled

Native thread is running and attaching as daemon ...
About to trigger JNI Warning
WARNING in native method: JNI call made without checking exceptions when required to from CallStaticObjectMethod
Native frames: (J=compiled Java code, j=interpreted, Vv=VM code, C=native code)
V  [jvm.dll+0x10e8aa8]  os::win32::platform_print_native_stack+0x58  (os_windows_aarch64.cpp:143)
V  [jvm.dll+0x10598ac]  os::platform_print_native_stack+0x34  (os_windows_aarch64.inline.hpp:38)
V  [jvm.dll+0x1059588]  NativeStackPrinter::print_stack+0x48  (nativeStackPrinter.cpp:35)
V  [jvm.dll+0xba74a0]  JavaThread::print_jni_stack+0x120  (javaThread.cpp:1775)
V  [jvm.dll+0xcb0e84]  check_pending_exception+0x84  (jniCheck.cpp:192)
V  [jvm.dll+0xcb0f24]  functionEnter+0x4c  (jniCheck.cpp:218)
V  [jvm.dll+0xcbbb70]  checked_jni_CallStaticObjectMethod+0xf0  (jniCheck.cpp:1321)
C  [nativeStack.dll+0x1264]  generateWarning+0x13c  (libnativeStack.c:66)
C  [nativeStack.dll+0x1364]  thread_start+0xa4  (libnativeStack.c:92)
C  [ucrtbase.dll+0x2b028]  (no source info available)
C  [KERNEL32.DLL+0x8740]  (no source info available)
C  [ntdll.dll+0xd47a4]  (no source info available)

Native thread terminating

C:\java\forks\openjdk\jdk\build\windows-aarch64-server-slowdebug\images\jdk\bin\java.exe (process 30308) exited with code 0 (0x0).
To automatically close the console when debugging stops, enable Tools->Options->Debugging->Automatically close the console when debugging stops.
Press any key to close this window . . .

Categories: Networks

Troubleshooting VPN Connectivity

Earlier this year I had trouble connecting to the VPN on one of my devices (Windows 11 Pro 24H2). I also got this error from Teams: We Couldn’t authenticate you. This could be due to a device or network problem, or Teams could be experiencing a technical issue – Microsoft Q&A. It was an interesting learning experience, getting to see parts of the OS/networking stack in action that I don’t usually even think about. The troubleshooting process started with the usual tasks: install all Windows updates, including the optional updates. That option (from my desktop) is shown below:

After all updates had been installed and the computer rebooted, I needed to sync the device from the Access Work or School settings. Sync enrolled device for Windows | Microsoft Learn explains how to do so. See also How to manually sync a Windows 11 device with Intune – Microsoft Q&A. The sync was successful and the device was up to date.

The Azure VPN Client had this error message: Failure in acquiring Microsoft Entra Token: Provider Error <number>: The server or proxy was not found. Someone else ran into this issue in Azure P2S VPN Disconnecting – Microsoft Q&A. This was unusual in my case because the Access work or school pane said that I was connected.

The next step was to run dsregcmd /status and review the device state and details. See Troubleshoot devices by using the dsregcmd command – Microsoft Entra ID | Microsoft Learn. The device had AzureAdJoined : YES but its DeviceAuthStatus was FAILED. Error: 0x...

Next, we examined this registry key: HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\Dnscache\Parameters\DnsPolicyConfig. Here is a screenshot from my desktop (not the device in question).

It turned out that there was a subkey with outdated configuration information! Deleting that subkey resolved all the connectivity issues. This was interesting to me because I assumed that general internet connectivity meant that there shouldn’t be any DNS issues whatsoever. I clearly didn’t have a good picture of how DNS is set up in these configurations and how these setups can go wrong.


Categories: OpenJDK

Why did Windows gtest catch a std::exception-derived exception

Last month, I investigated OpenJDK gtest failures on Windows. The error message was that the gtests Caught std::exception-derived exception escaping the death test statement. I tracked the commit responsible for the failures to 8343756: CAN_SHOW_REGISTERS_ON_ASSERT for Windows · openjdk/jdk@0054bbe.

gtest Death Test Structure

One of the failing tests is jdk/test/hotspot/gtest/utilities/test_vmerror.cpp.

TEST_VM_ASSERT_MSG(vmErrorTest, assert1, "assert.str == nullptr. failed: expected null") {
  vmassert(str == nullptr, "expected null");
}

The TEST_VM_ASSERT_MSG macro is defined as follows

#define TEST_VM_ASSERT_MSG(category, name, msg)                     \
  static void test_  ## category ## _ ## name ## _();               \
                                                                    \
  static void child_ ## category ## _ ## name ## _() {              \
    ::testing::GTEST_FLAG(throw_on_failure) = true;                 \
    test_ ## category ## _ ## name ## _();                          \
    gtest_exit_from_child_vm(0);                                    \
  }                                                                 \
                                                                    \
  TEST(category, CONCAT(name, _vm_assert)) {                        \
    ASSERT_EXIT(child_ ## category ## _ ## name ## _(),             \
                ::testing::ExitedWithCode(1),                       \
                "assert failed: " msg);                             \
  }                                                                 \
                                                                    \
  void test_ ## category ## _ ## name ## _()

gtest_exit_from_child_vm(0) cleanly exits the JVM after calling test_vmErrorTest_assert1(). The ASSERT_EXIT macro expects the JVM to crash. The overall design of the death tests is documented in googletest/googletest/include/gtest/gtest-death-test.h at v1.14.0 · google/googletest. The key takeaway is that a child process is started, it executes the death test, and its exit code and stderr are compared with the expected code and message (the latter via regex matching).

// Asserts that a given `statement` causes the program to exit, with an
// integer exit status that satisfies `predicate`, and emitting error output
// that matches `matcher`.
#define ASSERT_EXIT(statement, predicate, matcher) \
  GTEST_DEATH_TEST_(statement, predicate, matcher, GTEST_FATAL_FAILURE_)

I was trying to ensure my understanding of the exit code being an exact match is correct. The line EXPECT_EXIT(_exit(1), testing::ExitedWithCode(1), ""); from googletest/googletest/test/googletest-death-test-test.cc at v1.14.0 · google/googletest supports this hypothesis. The EXPECT_EXIT macro comment (below) left me wondering how ASSERT_EXIT does not continue on to successive tests. The difference between these two macros is in the final parameter, which is GTEST_NONFATAL_FAILURE_ for the EXPECT_EXIT macro.

// Like `ASSERT_EXIT`, but continues on to successive tests in the
// test suite, if any:
#define EXPECT_EXIT(statement, predicate, matcher) \
  GTEST_DEATH_TEST_(statement, predicate, matcher, GTEST_NONFATAL_FAILURE_)

The GTEST_DEATH_TEST_ macro creates a DeathTest instance and executes the death test statement. The WindowsDeathTest::AssumeRole() method, which is key in this, is described as follows: it

creates a child process with the same executable as the current process to run the death test. The child process is given the –gtest_filter and –gtest_internal_run_death_test flags such that it knows to run the current death test only.

The GTEST_EXECUTE_DEATH_TEST_STATEMENT_ macro was the source of the error message!

#define GTEST_EXECUTE_DEATH_TEST_STATEMENT_(statement, death_test)           \
  try {                                                                      \
    GTEST_SUPPRESS_UNREACHABLE_CODE_WARNING_BELOW_(statement);               \
  } catch (const ::std::exception& gtest_exception) {                        \
    fprintf(                                                                 \
        stderr,                                                              \
        "\n%s: Caught std::exception-derived exception escaping the "        \
        "death test statement. Exception message: %s\n",                     \
        ::testing::internal::FormatFileLocation(__FILE__, __LINE__).c_str(), \
        gtest_exception.what());                                             \
    fflush(stderr);                                                          \
    death_test->Abort(::testing::internal::DeathTest::TEST_THREW_EXCEPTION); \
  } catch (...) {                                                            \
    death_test->Abort(::testing::internal::DeathTest::TEST_THREW_EXCEPTION); \
  }

Root Causing the std::exception

The question now became, why were we catching this std::exception? I asked copilot: how does a windows access violation turn into a std::exception? Part of its answer mentioned the /EHsc compiler flag so I decided to examine the flags used to compile the JVM binaries. I searched for the regex out:[^\s]+jvm.dll in the build logs and found this jvm.dll linker command. Note that 2 separate jvm.dll files get built, one for the product and another for the gtests. The /IMPLIB (Name Import Library) | Microsoft Learn flag was present, but didn’t look relevant.

I then searched for cl.exe .+jvm.lib to get the compiler command line but this gave the compiler commands for gtest-all.cc and gmock-all.cc. The -EHsc flag (/EH (Exception handling model) | Microsoft Learn) was present for these 2 files though! Next, I searched for “gtestLauncher” and found the compiler command generating gtestLauncher.obj. Notice it didn’t have -EHsc!

I also realized that I should have searched for jvm.obj! Here is the single occurence of the jvm\.obj regex. It doesn’t have -EHsc either! Hmm, strange: jdk/make/hotspot/lib/CompileGtest.gmk says gtests should have it! I then searched the make folder for the regex CFLAGS_[^\w] and the primary suspect (of the 3 results) is the SetupCompilerFlags target in jdk/make/common/native/Flags.gmk. That target was last modified in 8325877: Split up NativeCompilation.gmk · openjdk/jdk@0d51b76.

Next, I examined build\windows-x86_64-server-slowdebug\configure-support\config.log and found these lines:

OPENJDK_TARGET_OS='windows'
...
OPENJDK_TARGET_OS_TYPE='windows'
...
TOOLCHAIN_TYPE='microsoft'

I thought that this snippet from jdk/make/common/native/Flags.gmk should have picked them up!

define SetupCompilerFlags
  # Pickup extra OPENJDK_TARGET_OS_TYPE, OPENJDK_TARGET_OS, TOOLCHAIN_TYPE and
  # OPENJDK_TARGET_OS plus OPENJDK_TARGET_CPU pair dependent variables for CFLAGS.
  $1_EXTRA_CFLAGS := $$($1_CFLAGS_$(OPENJDK_TARGET_OS_TYPE)) $$($1_CFLAGS_$(OPENJDK_TARGET_OS)) \
      $$($1_CFLAGS_$(TOOLCHAIN_TYPE)) \
      $$($1_CFLAGS_$(OPENJDK_TARGET_OS)_$(OPENJDK_TARGET_CPU))

What was the leading $1_ prefix though? I wasn’t sure but I tried this change next:

diff --git a/make/hotspot/lib/CompileGtest.gmk b/make/hotspot/lib/CompileGtest.gmk
index d2cdc7685c9..3908e94b624 100644
--- a/make/hotspot/lib/CompileGtest.gmk
+++ b/make/hotspot/lib/CompileGtest.gmk
@@ -63,7 +63,7 @@ $(eval $(call SetupJdkLibrary, BUILD_GTEST_LIBGTEST, \
         unused-result zero-as-null-pointer-constant, \
     DISABLED_WARNINGS_clang := format-nonliteral undef unused-result, \
     DEFAULT_CFLAGS := false, \
-    CFLAGS := $(JVM_CFLAGS) \
+    CFLAGS := $(JVM_CFLAGS) -EHsc \
         -I$(GTEST_FRAMEWORK_SRC)/googletest \
         -I$(GTEST_FRAMEWORK_SRC)/googletest/include \
         -I$(GTEST_FRAMEWORK_SRC)/googlemock \
@@ -94,7 +94,7 @@ $(eval $(call SetupJdkLibrary, BUILD_GTEST_LIBJVM, \
     EXCLUDE_PATTERNS := $(JVM_EXCLUDE_PATTERNS), \
     EXTRA_OBJECT_FILES := $(BUILD_LIBJVM_ALL_OBJS), \
     DEFAULT_CFLAGS := false, \
-    CFLAGS := $(JVM_CFLAGS) \
+    CFLAGS := $(JVM_CFLAGS) -EHsc \
         -I$(GTEST_FRAMEWORK_SRC)/googletest/include \
         -I$(GTEST_FRAMEWORK_SRC)/googlemock/include \
         $(addprefix -I, $(GTEST_TEST_SRC)), \

The build failed with this error:

checking for apk... [not found]
checking for pandoc... [not found]
/cygdrive/d/java/forks/openjdk/jdk/build/.configure-support/generated-configure.sh: line 64028: syntax error: unexpected end of file
configure exiting with result code 2

That file appeared to be truncated??? VSCode was doing something related to building the Java projects in the repo. It is possible that something in VSCode could have interrupted this but I just removed the build folder then reexamined the change.

I decided to find out who uses SetupCompilerFlags and found jdk/make/common/NativeCompilation.gmk. It is in turn called by SetupJdkNativeCompilation (which I had been trying to change)! The actual compilation is kicked off by jdk/make/common/NativeCompilation.gmk and done by jdk/make/common/native/CompileFile.gmk. The latter calls SetupCompileFileFlags, which is defined in jdk/make/common/native/Flags.gmk. I noticed that it includes the extra CFLAGS and CXXFLAGS in jdk/make/common/native/Flags.gmk. The most important observation though was that jdk/make/common/native/CompileFile.gmk uses the CXXFLAGS for .cpp files! I tried this change but it didn’t pass the flags to the compiler either.

diff --git a/make/hotspot/lib/CompileGtest.gmk b/make/hotspot/lib/CompileGtest.gmk
index d2cdc7685c9..8241ad04cb9 100644
--- a/make/hotspot/lib/CompileGtest.gmk
+++ b/make/hotspot/lib/CompileGtest.gmk
@@ -69,6 +69,7 @@ $(eval $(call SetupJdkLibrary, BUILD_GTEST_LIBGTEST, \
         -I$(GTEST_FRAMEWORK_SRC)/googlemock \
         -I$(GTEST_FRAMEWORK_SRC)/googlemock/include, \
     CFLAGS_windows := -EHsc, \
+    CXXFLAGS_windows := -EHsc, \
     CFLAGS_macosx := -DGTEST_OS_MAC=1, \
     OPTIMIZATION := $(JVM_OPTIMIZATION), \
     COPY_DEBUG_SYMBOLS := $(GTEST_COPY_DEBUG_SYMBOLS), \
@@ -99,6 +100,7 @@ $(eval $(call SetupJdkLibrary, BUILD_GTEST_LIBJVM, \
         -I$(GTEST_FRAMEWORK_SRC)/googlemock/include \
         $(addprefix -I, $(GTEST_TEST_SRC)), \
     CFLAGS_windows := -EHsc, \
+    CXXFLAGS_windows := -EHsc, \
     CFLAGS_macosx := -DGTEST_OS_MAC=1, \
     DISABLED_WARNINGS_gcc := $(DISABLED_WARNINGS_gcc) \
         undef stringop-overflow, \

Was the code I was changing even used? I tried this change to answer that:

diff --git a/make/hotspot/lib/CompileGtest.gmk b/make/hotspot/lib/CompileGtest.gmk
index d2cdc7685c9..4554b3c89f5 100644
--- a/make/hotspot/lib/CompileGtest.gmk
+++ b/make/hotspot/lib/CompileGtest.gmk
@@ -66,9 +66,11 @@ $(eval $(call SetupJdkLibrary, BUILD_GTEST_LIBGTEST, \
     CFLAGS := $(JVM_CFLAGS) \
         -I$(GTEST_FRAMEWORK_SRC)/googletest \
         -I$(GTEST_FRAMEWORK_SRC)/googletest/include \
+        -I$(GTEST_FRAMEWORK_SRC)/googletest/include/saintstest \
         -I$(GTEST_FRAMEWORK_SRC)/googlemock \
         -I$(GTEST_FRAMEWORK_SRC)/googlemock/include, \
     CFLAGS_windows := -EHsc, \
+    CXXFLAGS_windows := -EHsc, \
     CFLAGS_macosx := -DGTEST_OS_MAC=1, \
     OPTIMIZATION := $(JVM_OPTIMIZATION), \
     COPY_DEBUG_SYMBOLS := $(GTEST_COPY_DEBUG_SYMBOLS), \
@@ -96,9 +98,11 @@ $(eval $(call SetupJdkLibrary, BUILD_GTEST_LIBJVM, \
     DEFAULT_CFLAGS := false, \
     CFLAGS := $(JVM_CFLAGS) \
         -I$(GTEST_FRAMEWORK_SRC)/googletest/include \
+        -I$(GTEST_FRAMEWORK_SRC)/googletest/include/saintstest \
         -I$(GTEST_FRAMEWORK_SRC)/googlemock/include \
         $(addprefix -I, $(GTEST_TEST_SRC)), \
     CFLAGS_windows := -EHsc, \
+    CXXFLAGS_windows := -EHsc, \
     CFLAGS_macosx := -DGTEST_OS_MAC=1, \
     DISABLED_WARNINGS_gcc := $(DISABLED_WARNINGS_gcc) \
         undef stringop-overflow, \

My include path didn’t appear in the include paths for gtestLauncher.obj! I searched the repo for googlemock and the only place that path could be coming from was CompileGtest.gmk. However, I then noticed that the gtest launcher has its own configuration section. Sheesh. Here is the diff that I used to definitively see how these includes work:

diff --git a/make/hotspot/lib/CompileGtest.gmk b/make/hotspot/lib/CompileGtest.gmk
index d2cdc7685c9..72161d6d5d2 100644
--- a/make/hotspot/lib/CompileGtest.gmk
+++ b/make/hotspot/lib/CompileGtest.gmk
@@ -66,9 +66,11 @@ $(eval $(call SetupJdkLibrary, BUILD_GTEST_LIBGTEST, \
     CFLAGS := $(JVM_CFLAGS) \
         -I$(GTEST_FRAMEWORK_SRC)/googletest \
         -I$(GTEST_FRAMEWORK_SRC)/googletest/include \
+        -I$(GTEST_FRAMEWORK_SRC)/googletest/include/saintstest1 \
         -I$(GTEST_FRAMEWORK_SRC)/googlemock \
         -I$(GTEST_FRAMEWORK_SRC)/googlemock/include, \
     CFLAGS_windows := -EHsc, \
+    CXXFLAGS_windows := -EHsc, \
     CFLAGS_macosx := -DGTEST_OS_MAC=1, \
     OPTIMIZATION := $(JVM_OPTIMIZATION), \
     COPY_DEBUG_SYMBOLS := $(GTEST_COPY_DEBUG_SYMBOLS), \
@@ -96,9 +98,11 @@ $(eval $(call SetupJdkLibrary, BUILD_GTEST_LIBJVM, \
     DEFAULT_CFLAGS := false, \
     CFLAGS := $(JVM_CFLAGS) \
         -I$(GTEST_FRAMEWORK_SRC)/googletest/include \
+        -I$(GTEST_FRAMEWORK_SRC)/googletest/include/saintstest2 \
         -I$(GTEST_FRAMEWORK_SRC)/googlemock/include \
         $(addprefix -I, $(GTEST_TEST_SRC)), \
     CFLAGS_windows := -EHsc, \
+    CXXFLAGS_windows := -EHsc, \
     CFLAGS_macosx := -DGTEST_OS_MAC=1, \
     DISABLED_WARNINGS_gcc := $(DISABLED_WARNINGS_gcc) \
         undef stringop-overflow, \
@@ -150,8 +154,10 @@ $(eval $(call SetupJdkExecutable, BUILD_GTEST_LAUNCHER, \
     CFLAGS := $(JVM_CFLAGS) \
         -I$(GTEST_FRAMEWORK_SRC)/googletest \
         -I$(GTEST_FRAMEWORK_SRC)/googletest/include \
+        -I$(GTEST_FRAMEWORK_SRC)/googletest/include/saintstest3 \
         -I$(GTEST_FRAMEWORK_SRC)/googlemock \
         -I$(GTEST_FRAMEWORK_SRC)/googlemock/include, \
+    CXXFLAGS_windows := -EHsc, \
     LD_SET_ORIGIN := false, \
     LDFLAGS_unix := $(call SET_SHARED_LIBRARY_ORIGIN), \
     JDK_LIBS := gtest:libjvm, \

gtestLauncher.exe was now being compiled with -EHsc but the gtests still failed. Since jvm.dll is compiled without -EHsc, I added it to see if the test behavior would change. I started by searching for libjvm in the codebase. This is the additional change I made:

diff --git a/make/hotspot/lib/CompileJvm.gmk b/make/hotspot/lib/CompileJvm.gmk
index 6b5edc85b23..f7ae373ff17 100644
--- a/make/hotspot/lib/CompileJvm.gmk
+++ b/make/hotspot/lib/CompileJvm.gmk
@@ -179,6 +179,7 @@ $(eval $(call SetupJdkLibrary, BUILD_LIBJVM, \
     EXCLUDE_PATTERNS := $(JVM_EXCLUDE_PATTERNS), \
     DEFAULT_CFLAGS := false, \
     CFLAGS := $(JVM_CFLAGS), \
+    CXXFLAGS_windows := -EHsc, \
     abstract_vm_version.cpp_CXXFLAGS := $(CFLAGS_VM_VERSION), \
     arguments.cpp_CXXFLAGS := $(CFLAGS_VM_VERSION), \
     DISABLED_WARNINGS_gcc := $(DISABLED_WARNINGS_gcc), \

At this point, I looked at the exception handler and it looked like what was happening was that returning EXCEPTION_CONTINUE_EXECUTION let the SEH handler in the gtests continue instead of the code down the report_vm_error path! I decided to create my own handler but needed to look up the syntax. I used Structured Exception Handling (C/C++) | Microsoft Learn.

diff --git a/src/hotspot/share/utilities/debug.hpp b/src/hotspot/share/utilities/debug.hpp
index 12724153659..e40c16c1c59 100644
--- a/src/hotspot/share/utilities/debug.hpp
+++ b/src/hotspot/share/utilities/debug.hpp
@@ -39,7 +39,21 @@ class oopDesc;
 #define CAN_SHOW_REGISTERS_ON_ASSERT
 extern char* g_assert_poison;
 extern const char* g_assert_poison_read_only;
+#if (defined(_WINDOWS))
+// We use structured exception handling when writing to the poison variable.
+// This allows us to continue execution and perform error reporting instead of
+// bailing out to other SEH handlers such as those in the googletest code.
+#include <excpt.h>
+#define TOUCH_ASSERT_POISON                                                  \
+do {                                                                         \
+  __try {                                                                    \
+    (*g_assert_poison) = 'X';                                                \
+  } __except (EXCEPTION_CONTINUE_EXECUTION) {                                \
+  }                                                                          \
+} while (0)
+#else
 #define TOUCH_ASSERT_POISON (*g_assert_poison) = 'X';
+#endif // _WINDOWS
 void initialize_assert_poison();
 void disarm_assert_poison();
 bool handle_assert_poison_fault(const void* ucVoid);

This change failed to build with the following errors, the most notable of which is Compiler Error C2712: cannot use __try in functions that require object unwinding.

...\jdk\src\hotspot\share\utilities/growableArray.hpp(81): error C2712: Cannot use __try in functions that require object unwinding
...\jdk\src\hotspot\share\classfile/vmClassID.hpp(41): error C3615: constexpr function 'EnumeratorRangeImpl::end_value' cannot result in a constant expression
...\jdk\src\hotspot\share\utilities/enumIterator.hpp(97): note: failure was caused by a statement or an expression that is not valid in a constexpr context
...\jdk\src\hotspot\share\utilities/unsigned5.hpp(190): error C3615: constexpr function 'UNSIGNED5::max_encoded_in_length' cannot result in a constant expression
...\jdk\src\hotspot\share\utilities/unsigned5.hpp(191): note: failure was caused by a statement or an expression that is not valid in a constexpr context
...\jdk\src\hotspot\cpu\x86\register_x86.hpp(61): error C3615: constexpr function 'Register::RegisterImpl::encoding' cannot result in a constant expression
...\jdk\src\hotspot\cpu\x86\register_x86.hpp(61): note: failure was caused by a statement or an expression that is not valid in a constexpr context
...\jdk\src\hotspot\cpu\x86\register_x86.hpp(233): error C3615: constexpr function 'XMMRegister::XMMRegisterImpl::encoding' cannot result in a constant expression
...\jdk\src\hotspot\cpu\x86\register_x86.hpp(233): note: failure was caused by a statement or an expression that is not valid in a constexpr context
...\jdk\src\hotspot\share\runtime/park.hpp(131): error C2712: Cannot use __try in functions that require object unwinding
...\jdk\src\hotspot\share\runtime/mutexLocker.hpp(235): error C2712: Cannot use __try in functions that require object unwinding
...\jdk\src\hotspot\share\runtime/mutexLocker.hpp(240): error C2712: Cannot use __try in functions that require object unwinding
...\jdk\src\hotspot\share\runtime/mutexLocker.hpp(250): error C2712: Cannot use __try in functions that require object unwinding
...\jdk\src\hotspot\share\runtime/mutexLocker.hpp(255): error C2712: Cannot use __try in functions that require object unwinding

At this point, I realized that I needed to disable SEH at the gtest level. I turned off GTEST_HAS_SEH with this change and finally got the gtests to pass!

diff --git a/make/hotspot/lib/CompileGtest.gmk b/make/hotspot/lib/CompileGtest.gmk
index d2cdc7685c9..d9e73fc3847 100644
--- a/make/hotspot/lib/CompileGtest.gmk
+++ b/make/hotspot/lib/CompileGtest.gmk
@@ -68,7 +68,7 @@ $(eval $(call SetupJdkLibrary, BUILD_GTEST_LIBGTEST, \
         -I$(GTEST_FRAMEWORK_SRC)/googletest/include \
         -I$(GTEST_FRAMEWORK_SRC)/googlemock \
         -I$(GTEST_FRAMEWORK_SRC)/googlemock/include, \
-    CFLAGS_windows := -EHsc, \
+    CFLAGS_windows := -EHsc -DGTEST_HAS_SEH=0, \
     CFLAGS_macosx := -DGTEST_OS_MAC=1, \
     OPTIMIZATION := $(JVM_OPTIMIZATION), \
     COPY_DEBUG_SYMBOLS := $(GTEST_COPY_DEBUG_SYMBOLS), \
@@ -98,7 +98,7 @@ $(eval $(call SetupJdkLibrary, BUILD_GTEST_LIBJVM, \
         -I$(GTEST_FRAMEWORK_SRC)/googletest/include \
         -I$(GTEST_FRAMEWORK_SRC)/googlemock/include \
         $(addprefix -I, $(GTEST_TEST_SRC)), \
-    CFLAGS_windows := -EHsc, \
+    CFLAGS_windows := -EHsc -DGTEST_HAS_SEH=0, \
     CFLAGS_macosx := -DGTEST_OS_MAC=1, \
     DISABLED_WARNINGS_gcc := $(DISABLED_WARNINGS_gcc) \
         undef stringop-overflow, \
@@ -152,6 +152,7 @@ $(eval $(call SetupJdkExecutable, BUILD_GTEST_LAUNCHER, \
         -I$(GTEST_FRAMEWORK_SRC)/googletest/include \
         -I$(GTEST_FRAMEWORK_SRC)/googlemock \
         -I$(GTEST_FRAMEWORK_SRC)/googlemock/include, \
+    CFLAGS_windows := -EHsc -DGTEST_HAS_SEH=0, \
     LD_SET_ORIGIN := false, \
     LDFLAGS_unix := $(call SET_SHARED_LIBRARY_ORIGIN), \
     JDK_LIBS := gtest:libjvm, \
diff --git a/make/hotspot/lib/CompileJvm.gmk b/make/hotspot/lib/CompileJvm.gmk
index 6b5edc85b23..42f3969c775 100644
--- a/make/hotspot/lib/CompileJvm.gmk
+++ b/make/hotspot/lib/CompileJvm.gmk
@@ -179,6 +179,7 @@ $(eval $(call SetupJdkLibrary, BUILD_LIBJVM, \
     EXCLUDE_PATTERNS := $(JVM_EXCLUDE_PATTERNS), \
     DEFAULT_CFLAGS := false, \
     CFLAGS := $(JVM_CFLAGS), \
+    CFLAGS_windows := -DGTEST_HAS_SEH=0, \
     abstract_vm_version.cpp_CXXFLAGS := $(CFLAGS_VM_VERSION), \
     arguments.cpp_CXXFLAGS := $(CFLAGS_VM_VERSION), \
     DISABLED_WARNINGS_gcc := $(DISABLED_WARNINGS_gcc), \

What was not sure of was whether the JVM reporting code was running (vs the JVM just exiting) and whether there was a narrower way to pass the GTEST_HAS_SEH define – I noticed it in thousands of lines in the compilation log, which might also explain why I was getting error C2712: Cannot use __try in functions that require object unwinding in many more places than I expected when I added the -EHsc flag when compiling jvm.obj. Therefore, it was logical to try to find the minimal diff that would fix the gtests. Here’s one I tried:

diff --git a/make/hotspot/lib/CompileGtest.gmk b/make/hotspot/lib/CompileGtest.gmk
index d2cdc7685c9..95794ff0bbe 100644
--- a/make/hotspot/lib/CompileGtest.gmk
+++ b/make/hotspot/lib/CompileGtest.gmk
@@ -152,6 +152,7 @@ $(eval $(call SetupJdkExecutable, BUILD_GTEST_LAUNCHER, \
         -I$(GTEST_FRAMEWORK_SRC)/googletest/include \
         -I$(GTEST_FRAMEWORK_SRC)/googlemock \
         -I$(GTEST_FRAMEWORK_SRC)/googlemock/include, \
+    CFLAGS_windows := -DGTEST_HAS_SEH=0, \
     LD_SET_ORIGIN := false, \
     LDFLAGS_unix := $(call SET_SHARED_LIBRARY_ORIGIN), \
     JDK_LIBS := gtest:libjvm, \
diff --git a/make/hotspot/lib/CompileJvm.gmk b/make/hotspot/lib/CompileJvm.gmk
index 6b5edc85b23..42f3969c775 100644
--- a/make/hotspot/lib/CompileJvm.gmk
+++ b/make/hotspot/lib/CompileJvm.gmk
@@ -179,6 +179,7 @@ $(eval $(call SetupJdkLibrary, BUILD_LIBJVM, \
     EXCLUDE_PATTERNS := $(JVM_EXCLUDE_PATTERNS), \
     DEFAULT_CFLAGS := false, \
     CFLAGS := $(JVM_CFLAGS), \
+    CFLAGS_windows := -DGTEST_HAS_SEH=0, \
     abstract_vm_version.cpp_CXXFLAGS := $(CFLAGS_VM_VERSION), \
     arguments.cpp_CXXFLAGS := $(CFLAGS_VM_VERSION), \
     DISABLED_WARNINGS_gcc := $(DISABLED_WARNINGS_gcc), \

The gtests built from the diff below still failed:

diff --git a/make/hotspot/lib/CompileGtest.gmk b/make/hotspot/lib/CompileGtest.gmk
index d2cdc7685c9..403613c406c 100644
--- a/make/hotspot/lib/CompileGtest.gmk
+++ b/make/hotspot/lib/CompileGtest.gmk
@@ -98,7 +98,7 @@ $(eval $(call SetupJdkLibrary, BUILD_GTEST_LIBJVM, \
         -I$(GTEST_FRAMEWORK_SRC)/googletest/include \
         -I$(GTEST_FRAMEWORK_SRC)/googlemock/include \
         $(addprefix -I, $(GTEST_TEST_SRC)), \
-    CFLAGS_windows := -EHsc, \
+    CFLAGS_windows := -EHsc -DGTEST_HAS_SEH=0, \
     CFLAGS_macosx := -DGTEST_OS_MAC=1, \
     DISABLED_WARNINGS_gcc := $(DISABLED_WARNINGS_gcc) \
         undef stringop-overflow, \
diff --git a/make/hotspot/lib/CompileJvm.gmk b/make/hotspot/lib/CompileJvm.gmk
index 6b5edc85b23..42f3969c775 100644
--- a/make/hotspot/lib/CompileJvm.gmk
+++ b/make/hotspot/lib/CompileJvm.gmk
@@ -179,6 +179,7 @@ $(eval $(call SetupJdkLibrary, BUILD_LIBJVM, \
     EXCLUDE_PATTERNS := $(JVM_EXCLUDE_PATTERNS), \
     DEFAULT_CFLAGS := false, \
     CFLAGS := $(JVM_CFLAGS), \
+    CFLAGS_windows := -DGTEST_HAS_SEH=0, \
     abstract_vm_version.cpp_CXXFLAGS := $(CFLAGS_VM_VERSION), \
     arguments.cpp_CXXFLAGS := $(CFLAGS_VM_VERSION), \
     DISABLED_WARNINGS_gcc := $(DISABLED_WARNINGS_gcc), \

This manual approach of finding the minimal change needed was tedious so I decided to add my own defines to see which portions of the gmk files are used and for which compile/link commands:

diff --git a/make/hotspot/lib/CompileGtest.gmk b/make/hotspot/lib/CompileGtest.gmk
index d2cdc7685c9..acf0eae159a 100644
--- a/make/hotspot/lib/CompileGtest.gmk
+++ b/make/hotspot/lib/CompileGtest.gmk
@@ -68,7 +68,7 @@ $(eval $(call SetupJdkLibrary, BUILD_GTEST_LIBGTEST, \
         -I$(GTEST_FRAMEWORK_SRC)/googletest/include \
         -I$(GTEST_FRAMEWORK_SRC)/googlemock \
         -I$(GTEST_FRAMEWORK_SRC)/googlemock/include, \
-    CFLAGS_windows := -EHsc, \
+    CFLAGS_windows := -EHsc -DGTEST_HAS_SEH=0 -DMYTEST_LOCATION1, \
     CFLAGS_macosx := -DGTEST_OS_MAC=1, \
     OPTIMIZATION := $(JVM_OPTIMIZATION), \
     COPY_DEBUG_SYMBOLS := $(GTEST_COPY_DEBUG_SYMBOLS), \
@@ -98,7 +98,7 @@ $(eval $(call SetupJdkLibrary, BUILD_GTEST_LIBJVM, \
         -I$(GTEST_FRAMEWORK_SRC)/googletest/include \
         -I$(GTEST_FRAMEWORK_SRC)/googlemock/include \
         $(addprefix -I, $(GTEST_TEST_SRC)), \
-    CFLAGS_windows := -EHsc, \
+    CFLAGS_windows := -EHsc -DGTEST_HAS_SEH=0 -DMYTEST_LOCATION2, \
     CFLAGS_macosx := -DGTEST_OS_MAC=1, \
     DISABLED_WARNINGS_gcc := $(DISABLED_WARNINGS_gcc) \
         undef stringop-overflow, \
@@ -152,6 +152,7 @@ $(eval $(call SetupJdkExecutable, BUILD_GTEST_LAUNCHER, \
         -I$(GTEST_FRAMEWORK_SRC)/googletest/include \
         -I$(GTEST_FRAMEWORK_SRC)/googlemock \
         -I$(GTEST_FRAMEWORK_SRC)/googlemock/include, \
+    CFLAGS_windows := -EHsc -DGTEST_HAS_SEH=0 -DMYTEST_LOCATION3, \
     LD_SET_ORIGIN := false, \
     LDFLAGS_unix := $(call SET_SHARED_LIBRARY_ORIGIN), \
     JDK_LIBS := gtest:libjvm, \
diff --git a/make/hotspot/lib/CompileJvm.gmk b/make/hotspot/lib/CompileJvm.gmk
index 6b5edc85b23..5994ffc6be1 100644
--- a/make/hotspot/lib/CompileJvm.gmk
+++ b/make/hotspot/lib/CompileJvm.gmk
@@ -179,6 +179,7 @@ $(eval $(call SetupJdkLibrary, BUILD_LIBJVM, \
     EXCLUDE_PATTERNS := $(JVM_EXCLUDE_PATTERNS), \
     DEFAULT_CFLAGS := false, \
     CFLAGS := $(JVM_CFLAGS), \
+    CFLAGS_windows := -DGTEST_HAS_SEH=0 -DMYTEST_LOCATION0, \
     abstract_vm_version.cpp_CXXFLAGS := $(CFLAGS_VM_VERSION), \
     arguments.cpp_CXXFLAGS := $(CFLAGS_VM_VERSION), \
     DISABLED_WARNINGS_gcc := $(DISABLED_WARNINGS_gcc), \

Location 1 only showed up for the 2 files below (matching the INCLUDE_FILES in make/hotspot/lib/CompileGtest.gmk), which made it clear that -DGTEST_HAS_SEH=0 was needed in this section.

  1. /cygdrive/c/repos/googletest/googlemock/src/gmock-all.cc
  2. /cygdrive/c/repos/googletest/googletest/src/gtest-all.cc

For location 2, there were 214 lines matching the regex DMYTEST_LOCATION2.+.cpp and 213 lines matching the regex DMYTEST_LOCATION2.+/test/hotspot/gtest/.+.cpp. The location 2 define was therefore correctly scoped to the gtests only. These 213 lines compiled files like test_blocktree.cpp and test_vmerror.cpp. The line that was different between the 2 regexes was the one compiling build/windows-x86_64-server-slowdebug/hotspot/variant-server/libjvm/gtest/objs/BUILD_GTEST_LIBJVM_pch.cpp. Location 3 was only used for compiling test/hotspot/gtest/gtestLauncher.cpp. The challenging case was location 0, which seemed to appear for way more files than it should. Was it really necessary? No it wasn’t! That made life much easier for me.

Inspecting gTest Code Coverage

In the course of this investigation, I considered using time travel debugging to see which code paths were executed. An alternative was to see whether the exception filter code was covered at the end of the gtest execution! The path to the code coverage tool is C:\Program Files\Microsoft Visual Studio\2022\Enterprise\Common7\IDE\Extensions\Microsoft\CodeCoverage.Console\Microsoft.CodeCoverage.Console.exe – it should be in the path by default in the Developer Command Prompt. I kicked it off with these commands:

cd build/windows-x86_64-server-slowdebug/images/test/hotspot/gtest/server
mkdir ../server-orig
cp * ../server-orig

Microsoft.CodeCoverage.Console.exe instrument gtestLauncher.exe
Microsoft.CodeCoverage.Console.exe instrument jvm.dll

Microsoft.CodeCoverage.Console.exe collect "gtestLauncher.exe -jdk:D:\java\forks\openjdk\jdk\build\windows-x86_64-server-slowdebug\images\jdk"

start output.coverage

Search for “exceptionfilter” in the Code Coverage Results pane to view the code coverage for the exception filter.

Verifying Execution Path

The first time I paused execution of the gtests in the debugger, stopped in jdk/src/hotspot/share/utilities/bitMap.cpp. I set a breakpoint there. I liked this location because I could move execution into the failure path (in the assembly view). This was how I saw the gtest structured exception handler kicking in without the JVM’s failure reporting code executing. With the tests now passing, I found the write to the poison location just going through without interruption. Did this mean the test was broken? Or did it mean that the exception filter ran and successfully said to continue execution? I think it has to be the latter but I’ll need time travel debugging to verify this. In the meantime, I sought to at least ensure there were multiple test processes involved.

Verifying Multiple Processes are Started

I started Process Monitor and added a filter for path containing “slowdebug”. Notice tons of PIDs for gtestlauncher.exe in the process monitor screenshot below (as expected).

I could successfully execute the error handling path by manually moving the program counter (RIP) after skipping into the failure path of BitMap::verify_range. Why didn’t the PID change in the debugger? Oh wait, was I was still stepping thereby causing recursion? This callstack did not support that hypothesis. Looks like it was just error reporting continuing to execute.

>	jvm.dll!BitMap::verify_range(unsigned __int64 beg, unsigned __int64 end) Line 212	C++
 	jvm.dll!BitMap::clear_range(unsigned __int64 beg, unsigned __int64 end) Line 280	C++
 	jvm.dll!JVMFlag::printFlags(outputStream * out, bool withComments, bool printRanges, bool skipDefaults) Line 706	C++
 	jvm.dll!VMError::report(outputStream * st, bool _verbose) Line 1260	C++
 	jvm.dll!VMError::report_and_die(int id, const char * message, const char * detail_fmt, char * detail_args, Thread * thread, unsigned char * pc, const void * siginfo, const void * context, const char * filename, int lineno, unsigned __int64 size) Line 1847	C++
 	jvm.dll!report_vm_error(const char * file, int line, const char * error_msg, const char * detail_fmt, ...) Line 195	C++
 	jvm.dll!CompressedKlassPointers::check_init<int>(int var) Line 154	C++
 	jvm.dll!CompressedKlassPointers::shift() Line 218	C++
 	jvm.dll!CompressedKlassPointers::print_mode(outputStream * st) Line 301	C++
 	jvm.dll!VMError::report(outputStream * st, bool _verbose) Line 1196	C++
 	jvm.dll!VMError::report_and_die(int id, const char * message, const char * detail_fmt, char * detail_args, Thread * thread, unsigned char * pc, const void * siginfo, const void * context, const char * filename, int lineno, unsigned __int64 size) Line 1847	C++
 	jvm.dll!report_vm_error(const char * file, int line, const char * error_msg, const char * detail_fmt, ...) Line 195	C++
 	jvm.dll!BitMap::verify_limit(unsigned __int64 bit) Line 206	C++
 	jvm.dll!BitMap::to_words_align_down(unsigned __int64 bit) Line 94	C++
 	jvm.dll!BitMap::word_addr(unsigned __int64 bit) Line 144	C++
 	jvm.dll!BitMap::set_bit(unsigned __int64 bit) Line 37	C++
 	jvm.dll!JfrEventVerifier::set_field_bit(unsigned __int64 field_idx) Line 41	C++
 	jvm.dll!JfrEvent<EventTenuringDistribution>::set_field_bit(unsigned __int64 field_idx) Line 267	C++
 	jvm.dll!EventObjectAllocationOutsideTLAB::set_objectClass(const Klass * new_value) Line 7304	C++
 	jvm.dll!trace_flag_changed<bool,EventBooleanFlagChanged>(JVMFlag * flag, const bool old_value, const bool new_value, const JVMFlagOrigin origin) Line 39	C++
 	jvm.dll!TypedFlagAccessImpl<bool,EventBooleanFlagChanged>::check_constraint_and_set(JVMFlag * flag, void * value_addr, JVMFlagOrigin origin, bool verbose) Line 78	C++
 	jvm.dll!FlagAccessImpl_bool::set_impl(JVMFlag * flag, void * value_addr, JVMFlagOrigin origin) Line 98	C++
 	jvm.dll!FlagAccessImpl::set(JVMFlag * flag, void * value, JVMFlagOrigin origin) Line 49	C++
 	jvm.dll!JVMFlagAccess::set_impl(JVMFlag * flag, void * value, JVMFlagOrigin origin) Line 307	C++
 	jvm.dll!JVMFlagAccess::set_or_assert(JVMFlagsEnum flag_enum, int type_enum, void * value, JVMFlagOrigin origin) Line 353	C++
 	jvm.dll!JVMFlagAccess::set<bool,0>(JVMFlagsEnum flag_enum, bool value, JVMFlagOrigin origin) Line 101	C++
 	jvm.dll!Flag_UseLargePagesIndividualAllocation_set(bool value, JVMFlagOrigin origin) Line 69	C++
 	jvm.dll!os::init() Line 4436	C++
 	jvm.dll!Threads::create_vm(JavaVMInitArgs * args, bool * canTryAgain) Line 463	C++
 	jvm.dll!JNI_CreateJavaVM_inner(JavaVM_ * * vm, void * * penv, void * args) Line 3589	C++
 	jvm.dll!JNI_CreateJavaVM(JavaVM_ * * vm, void * * penv, void * args) Line 3680	C++
 	jvm.dll!init_jvm(int argc, char * * argv, bool disable_error_handling, JavaVM_ * * jvm_ptr) Line 94	C++
 	jvm.dll!JVMInitializerListener::OnTestStart(const testing::TestInfo & test_info) Line 124	C++
 	jvm.dll!testing::internal::TestEventRepeater::OnTestStart(const testing::TestInfo & parameter) Line 3858	C++
 	jvm.dll!testing::TestInfo::Run() Line 2821	C++
 	jvm.dll!testing::TestSuite::Run() Line 3015	C++
 	jvm.dll!testing::internal::UnitTestImpl::RunAllTests() Line 5920	C++
 	jvm.dll!testing::internal::HandleSehExceptionsInMethodIfSupported<testing::internal::UnitTestImpl,bool>(testing::internal::UnitTestImpl * object, bool(testing::internal::UnitTestImpl::*)() method, const char * location) Line 2614	C++
 	jvm.dll!testing::internal::HandleExceptionsInMethodIfSupported<testing::internal::UnitTestImpl,bool>(testing::internal::UnitTestImpl * object, bool(testing::internal::UnitTestImpl::*)() method, const char * location) Line 2648	C++
 	jvm.dll!testing::UnitTest::Run() Line 5484	C++
 	jvm.dll!RUN_ALL_TESTS() Line 2317	C++
 	jvm.dll!runUnitTestsInner(int argc, char * * argv) Line 290	C++
 	jvm.dll!runUnitTests(int argc, char * * argv) Line 371	C++
 	gtestLauncher.exe!main(int argc, char * * argv) Line 40	C++
 	[Inline Frame] gtestLauncher.exe!invoke_main() Line 78	C++
 	gtestLauncher.exe!__scrt_common_main_seh() Line 288	C++
 	kernel32.dll!00007ffdcbdce8d7()	Unknown
 	ntdll.dll!00007ffdcc97c34c()	Unknown

One advantage of the stack above is that it showed how the os::init code gets executed (which I was curious about when wondering whether the exception filter was being set up). Disabling the breakpoint just before skipping into the failure path and resuming execution now led to the JVM dying:

[==========] Running 1197 tests from 205 test suites.
[----------] Global test environment set-up.
[----------] 3 tests from AltHashingTest
[ RUN      ] AltHashingTest.halfsiphash_test_ByteArray
[       OK ] AltHashingTest.halfsiphash_test_ByteArray (0 ms)
[ RUN      ] AltHashingTest.halfsiphash_test_CharArray
[       OK ] AltHashingTest.halfsiphash_test_CharArray (0 ms)
[ RUN      ] AltHashingTest.halfsiphash_test_FromReference
[       OK ] AltHashingTest.halfsiphash_test_FromReference (0 ms)
[----------] 3 tests from AltHashingTest (2 ms total)

[----------] 1 test from ThreadsListHandle
[ RUN      ] ThreadsListHandle.sanity_vm
#
# A fatal error has been detected by the Java Runtime Environment:
#
#  Internal Error (d:\java\forks\openjdk\jdk\src\hotspot\share\utilities\bitMap.cpp:208), pid=39132, tid=109872
#  assert(bit <= _size) failed: BitMap limit out of bounds: 0 > 64
#
# JRE version:  ((uninitialized)) (slowdebug build )
# Java VM: OpenJDK 64-Bit Server VM (slowdebug 26-internal-adhoc.saint.jdk, mixed mode, sharing, tiered, compressed class ptrs, unknown gc, windows-amd64)
# Core dump will be written. Default location: D:\java\forks\openjdk\jdk\build\windows-x86_64-server-slowdebug\images\test\hotspot\gtest\server\hs_err_pid39132.mdmp
#
# An error report file with more information is saved as:
# D:\java\forks\openjdk\jdk\build\windows-x86_64-server-slowdebug\images\test\hotspot\gtest\server\hs_err_pid39132.log
#
#

D:\java\forks\openjdk\jdk\build\windows-x86_64-server-slowdebug\images\test\hotspot\gtest\server\gtestLauncher.exe (process 39132) exited with code 1 (0x1).
To automatically close the console when debugging stops, enable Tools->Options->Debugging->Automatically close the console when debugging stops.
Press any key to close this window . . .

Here’s the stack for when the breakpoint is hit:

jvm.dll!BitMap::verify_limit(unsigned __int64 bit) Line 206
jvm.dll!BitMap::to_words_align_down(unsigned __int64 bit) Line 94
jvm.dll!BitMap::word_addr(unsigned __int64 bit) Line 144
jvm.dll!BitMap::set_bit(unsigned __int64 bit) Line 37
jvm.dll!JfrEventVerifier::set_field_bit(unsigned __int64 field_idx) Line 41
jvm.dll!JfrEvent<EventTenuringDistribution>::set_field_bit(unsigned __int64 field_idx) Line 267
jvm.dll!EventObjectAllocationOutsideTLAB::set_objectClass(const Klass * new_value) Line 7304
jvm.dll!trace_flag_changed<bool,EventBooleanFlagChanged>(JVMFlag * flag, const bool old_value, const bool new_value, const JVMFlagOrigin origin) Line 39
jvm.dll!TypedFlagAccessImpl<bool,EventBooleanFlagChanged>::check_constraint_and_set(JVMFlag * flag, void * value_addr, JVMFlagOrigin origin, bool verbose) Line 78
jvm.dll!FlagAccessImpl_bool::set_impl(JVMFlag * flag, void * value_addr, JVMFlagOrigin origin) Line 98
jvm.dll!FlagAccessImpl::set(JVMFlag * flag, void * value, JVMFlagOrigin origin) Line 49
jvm.dll!JVMFlagAccess::set_impl(JVMFlag * flag, void * value, JVMFlagOrigin origin) Line 307
jvm.dll!JVMFlagAccess::set_or_assert(JVMFlagsEnum flag_enum, int type_enum, void * value, JVMFlagOrigin origin) Line 353
jvm.dll!JVMFlagAccess::set<bool,0>(JVMFlagsEnum flag_enum, bool value, JVMFlagOrigin origin) Line 101
jvm.dll!Flag_UseLargePagesIndividualAllocation_set(bool value, JVMFlagOrigin origin) Line 69
jvm.dll!os::init() Line 4436
jvm.dll!Threads::create_vm(JavaVMInitArgs * args, bool * canTryAgain) Line 463
jvm.dll!JNI_CreateJavaVM_inner(JavaVM_ * * vm, void * * penv, void * args) Line 3589
jvm.dll!JNI_CreateJavaVM(JavaVM_ * * vm, void * * penv, void * args) Line 3680
jvm.dll!init_jvm(int argc, char * * argv, bool disable_error_handling, JavaVM_ * * jvm_ptr) Line 94
jvm.dll!JVMInitializerListener::OnTestStart(const testing::TestInfo & test_info) Line 124
jvm.dll!testing::internal::TestEventRepeater::OnTestStart(const testing::TestInfo & parameter) Line 3858
jvm.dll!testing::TestInfo::Run() Line 2821
jvm.dll!testing::TestSuite::Run() Line 3015
jvm.dll!testing::internal::UnitTestImpl::RunAllTests() Line 5920
jvm.dll!testing::internal::HandleSehExceptionsInMethodIfSupported<testing::internal::UnitTestImpl,bool>(testing::internal::UnitTestImpl * object, bool(testing::internal::UnitTestImpl::*)() method, const char * location) Line 2614
jvm.dll!testing::internal::HandleExceptionsInMethodIfSupported<testing::internal::UnitTestImpl,bool>(testing::internal::UnitTestImpl * object, bool(testing::internal::UnitTestImpl::*)() method, const char * location) Line 2648
jvm.dll!testing::UnitTest::Run() Line 5484
jvm.dll!RUN_ALL_TESTS() Line 2317
jvm.dll!runUnitTestsInner(int argc, char * * argv) Line 290
jvm.dll!runUnitTests(int argc, char * * argv) Line 371
gtestLauncher.exe!main(int argc, char * * argv) Line 40
[Inline Frame] gtestLauncher.exe!invoke_main() Line 78

Pull Request Feedback

With all this information at hand, I opened 8364664: gtest death tests failing on Windows by swesonga · Pull Request #26661 · openjdk/jdk. One reviewer asked about removing the -EHsc flag from the gtests altogether. I tried it with the change below:

diff --git a/make/hotspot/lib/CompileGtest.gmk b/make/hotspot/lib/CompileGtest.gmk
index d2cdc7685c9..2c6b5f23516 100644
--- a/make/hotspot/lib/CompileGtest.gmk
+++ b/make/hotspot/lib/CompileGtest.gmk
@@ -68,7 +68,6 @@ $(eval $(call SetupJdkLibrary, BUILD_GTEST_LIBGTEST, \
         -I$(GTEST_FRAMEWORK_SRC)/googletest/include \
         -I$(GTEST_FRAMEWORK_SRC)/googlemock \
         -I$(GTEST_FRAMEWORK_SRC)/googlemock/include, \
-    CFLAGS_windows := -EHsc, \
     CFLAGS_macosx := -DGTEST_OS_MAC=1, \
     OPTIMIZATION := $(JVM_OPTIMIZATION), \
     COPY_DEBUG_SYMBOLS := $(GTEST_COPY_DEBUG_SYMBOLS), \
@@ -98,7 +97,6 @@ $(eval $(call SetupJdkLibrary, BUILD_GTEST_LIBJVM, \
         -I$(GTEST_FRAMEWORK_SRC)/googletest/include \
         -I$(GTEST_FRAMEWORK_SRC)/googlemock/include \
         $(addprefix -I, $(GTEST_TEST_SRC)), \
-    CFLAGS_windows := -EHsc, \
     CFLAGS_macosx := -DGTEST_OS_MAC=1, \
     DISABLED_WARNINGS_gcc := $(DISABLED_WARNINGS_gcc) \
         undef stringop-overflow, \

The build failed with this error:

c:\progra~1\mib055~1\2022\enterprise\vc\tools\msvc\14.44.35207\include\__msvc_ostream.hpp(781): error C2220: the following warning is treated as an error
c:\progra~1\mib055~1\2022\enterprise\vc\tools\msvc\14.44.35207\include\__msvc_ostream.hpp(781): warning C4530: C++ exception handler used, but unwind semantics are not enabled. Specify /EHsc
c:\progra~1\mib055~1\2022\enterprise\vc\tools\msvc\14.44.35207\include\__msvc_ostream.hpp(781): note: the template instantiation context (the oldest one first) is
c:\repos\googletest\googletest\include\gtest/gtest-message.h(118): note: see reference to function template instantiation 'std::basic_ostream<char,std::char_traits<char>> &std::operator <<<std::char_traits<char>>(std::basic_ostream<char,std::char_traits<char>> &,const char *)' being compiled

This is expected since the googletest code is using C++ exception handling. The more significant revelation for me is that other groups are running gtests on Windows with --gtest_catch_exceptions=0 which disables the inbuilt exception handler. This is done using the GTestWrapper. This comment is helpful because it has links to the earlier issues around this space and explicitly clarifies that C++ exceptions are not used in libjvm code. While this PR did not result in a patch, it was an educational investigation for me!


Categories: OpenJDK

ShowRegistersOnAssertTest Failure on Windows AArch64

I recently investigated why the ShowRegistersOnAssertTest.java test was failing on Windows AArch64. This test was originally added in [JDK-8191101] Show register content in hs-err file on assert – Java Bug System to “retrieve the current context when an assert happens and make that part of the hs-err file.” I searched the codebase for the strings Registers:" and "RAX=" (used in the test) and verified that the hserr output the test is examining is generated by the os::print_context method (see the x86_64 Windows os::print_context, Linux os::print_context, and BSD os::print_context implementations).

The aarch64 Windows os::print_context, Linux os::print_context, and BSD os::print_context implementations use different register names. BSD’s os::print_context writes x0= to the error log whereas aarch64 Windows os::print_context writes X0 =. The BSD implementation should fail this test as well but the log in Update the copyright year · swesonga/jdk@3470f00 did not show this test running! It also didn’t appear in jdk/test/hotspot/jtreg/ProblemList.txt. Actually, a search for the string ShowRegistersOnAssertTest yielded results in that test file only. That led me to review it more closely and find that it didn’t run on macos because the test requires Linux or Windows. However, the fix for the test failure on Windows was obviously using the correct pattern for Windows:

diff --git a/test/hotspot/jtreg/runtime/ErrorHandling/ShowRegistersOnAssertTest.java b/test/hotspot/jtreg/runtime/ErrorHandling/ShowRegistersOnAssertTest.java
index 3b038ebd8a0..b0138625450 100644
--- a/test/hotspot/jtreg/runtime/ErrorHandling/ShowRegistersOnAssertTest.java
+++ b/test/hotspot/jtreg/runtime/ErrorHandling/ShowRegistersOnAssertTest.java
@@ -76,7 +76,11 @@ private static void do_test(boolean do_assert, // true - assert, false - guarant
             } else if (Platform.isX86()) {
                 pattern = new Pattern[] { Pattern.compile("Registers:"), Pattern.compile("EAX=.*")};
             } else if (Platform.isAArch64()) {
-                pattern = new Pattern[] { Pattern.compile("Registers:"), Pattern.compile("R0=.*")};
+                if (Platform.isLinux()) {
+                    pattern = new Pattern[] { Pattern.compile("Registers:"), Pattern.compile("R0=.*")};
+                } else if (Platform.isWindows()) {
+                    pattern = new Pattern[] { Pattern.compile("Registers:"), Pattern.compile("X0 =.*")};
+                }
             } else if (Platform.isS390x()) {
                 pattern = new Pattern[] { Pattern.compile("General Purpose Registers:"),
                                           Pattern.compile("^-{26}$"),

To verify that the data in the error log matched what the test expected, I used this change:

diff --git a/test/hotspot/jtreg/runtime/ErrorHandling/ShowRegistersOnAssertTest.java b/test/hotspot/jtreg/runtime/ErrorHandling/ShowRegistersOnAssertTest.java
index 3b038ebd8a0..76917f06a02 100644
--- a/test/hotspot/jtreg/runtime/ErrorHandling/ShowRegistersOnAssertTest.java
+++ b/test/hotspot/jtreg/runtime/ErrorHandling/ShowRegistersOnAssertTest.java
@@ -87,7 +87,9 @@ private static void do_test(boolean do_assert, // true - assert, false - guarant
                 pattern = new Pattern[] { Pattern.compile("Registers:") };
             }
             // Pattern match the hs_err_pid file.
-            HsErrFileUtils.checkHsErrFileContent(hs_err_file, pattern, false);
+            boolean is_verbose = false;
+            boolean printHserrOnError = true;
+            HsErrFileUtils.checkHsErrFileContent(hs_err_file, pattern, null, true, is_verbose, printHserrOnError);
         }
     }

I filed [JDK-8366483] ShowRegistersOnAssertTest uses wrong register pattern string for Windows on AArch64 – Java Bug System and fixed the test bug in 8366483: ShowRegistersOnAssertTest uses wrong register pattern string for Windows on AArch64 by swesonga · Pull Request #27022 · openjdk/jdk.


Categories: Hardware, Printers

Printing Issues, Oh Brother

I bought new toner for my Brother printer. It’s been a while since I replaced it, but I knew I could count on someone on YouTube to walk me through the process again. The folks that upload these videos deserve an award for their service – I would have wasted time fidgeting around.

Brother HL-L2390DW Installing Ink Toner.

My next issue was figuring out why I couldn’t use double sided printing in the Microsoft Edge browser. does edge have double sided printing – Search pointed me to this helpful post: New Microsoft Edge can’t find Print on both sides & Flip on long edge function while print PDF – Microsoft Q&A. Turns out I needed to enable this feature of the printer. Find the printer in the Printers & scanners panel then open “Printer properties

The Installable Options are in the Device Settings tab. Change the Duplex Unit setting to Installed.

Brother HL-L2390DW Printer Properties

The Print on both sides setting will now be displayed in Microsoft Edge.


Script for Parsing jtreg GitHub Actions Results

I was reviewing jtreg test failures in some GitHub actions last week. Since I was only interested in the failures, I decided to write a script to extract the failure details from the log files I had downloaded from GitHub. This looked like another task for the VS Code agent so I wrote the algorithm for extracting the failure details into individual text files, which would be easier to review. Below is the prompt I used.

write a python script that processes all the text files in a user-specified folder. For each text file:
- split it into sections using "--------------------------------------------------" as a separator
- generate a filename for the section from the first line after the separator. To do so, use the text after "TEST: " and replace /, #, and . with underscores and append the ".txt" extension.
- if the section contains the string "test jdk:" then write the content of the entire section to a file with the generated name.

I thought I would need to do at least some debugging of the script but I did not! The script worked flawlessly. I didn’t even need to execute it myself because the flow of using the agent included running the script on my raw folder and then generating a README file! See the script and the README at Add scripts generated by Claude Sonnet 4 VS Code agent · swesonga/scratchpad@f5e8057. The agent was using the Claude Sonnet 4 model.


Simple Batch File Editing with VSCode Copilot Agent

I need to verify that all symbols are available for every executable binary in any given release. I have been modifiying scratchpad/scripts/java/symbols/verify-symbols-initial.bat at 4a9adb7dfd0e67c129320c8a3c6ee38260695b13 · swesonga/scratchpad but realized that I waste a lot of time copy/pasting repo names and tags. I thought I would experiment with Copilot in VSCode to see if it could help me clean up this script. The prompts I used (also at scratchpad/scripts/java/symbols/verify-symbols-prompts.txt at 4a9adb7dfd0e67c129320c8a3c6ee38260695b13 · swesonga/scratchpad) are in the next listing:

delete all lines from ":: jdk21u x64"
replace 2025-07 with a variable called psu
replace aarch64 and x64 with a variable called arch
replace jdk21u with a variable called jdkrepo
replace jdk-21.0.6+7 with a variable called jdktag
replace D:\java\binaries\jdk\%arch%\%psu%\windows-%jdkrepo% with a variable called psudir
output a message showing the psudir after setting it
make the variables user-specified variables
make the variables command line arguments instead

I pasted the screenshots of the result of each prompt into the slideshow below. Notice that I was using Agent mode with Anthropic’s Claude Sonnet 4 model. I was not exactly saving much time in this scenario using the VSCode agent, but I was certainly understanding its capabilities.

I created a duplicate directory with the initial .bat file then used a single prompt with all the lines above. I was pleased that I got the same result!

I was a bit confused the 2nd time I worked through these prompts because the behavior of the agent was different! I didn’t notice that I had somehow switched from Agent mode to Ask mode but I have now learned the difference between these two modes.


Learning about Large Language Models – Part 2

This post lists some key questions and concepts from Chapter 4 of Sebastian Raschka’s Build a Large Language Model from Scratch book. It is a continuation of the Learning about Large Language Models post.

  1. About how many parameters are in a small GPT-2 model? See Language Models are Unsupervised Multitask Learners at https://mng.bz/yoBq. p94
  2. What does the term “parameters” refer to in the context of deep learning/LLMs? p93
  3. What are logits? p99
  4. Describe the challenges of vanishing or exploding gradients in training DNNs with many layers. What do these problems lead to?
  5. Why is layer normalization used?
  6. What is the main idea behind layer normalization (what does it do to the outputs)?
  7. What is a ReLU? p100
  8. What is the torch.var keepDim parameter used for? p101
  9. What about the correction parameter (formerly unbiased)? How does it relate to Bessel’s_correction? p103
  10. Explain the difference between layer normalization and batch normalization. p104
  11. When is layer normalization advantageous? p105
  12. What is a GELU? How can it lead to better optimization properties during training (compared to a ReLU)?
  13. What is one advantage of small non-zero outputs on negative inputs to a GELU? p106
  14. Explain the role of a FeedForward module in enhancing the model’s ability to learn from and generalize data. Compare with Feedforward neural network. p108
  15. Why were shortcut connections originally proposed for deep networks in computer vision? p109
  16. Which PyTorch method computes loss gradients? p112
  17. Explain Pre-LayerNorm and Post-LayerNorm. p115
  18. What does preservation of shape throughout the transformer block architecture enable? p116
  19. Explain weight-tying as used in the original GPT-2 architecture and its advantages. p121

This is the author’s video corresponding to chapter 4 of the book.

Build an LLM from Scratch 4: Implementing a GPT model from Scratch To Generate Text

Math Concepts

The author mentioned that OpenAI used the biased variance option when training their GPT-2 model. The reasons why Bessel’s correction is usually used in statistics is explained well in this video:

Why We Divide by N-1 in the Sample Variance (The Bessel’s Correction)

I didn’t think the reason we need activation functions was expounded upon. This final video provides a great explanation.

Why Do We Need Activation Functions in Neural Networks?

Categories: OpenJDK, Testing

Running jtreg shell tests on Windows

As part of the jdk11u release process, I needed to run some shell tests on my Windows desktop to determine whether they failed due to a product issue or an environment issue. I defaulted to using my Git Bash environment instead of Cygwin. This post shares some errors I ran into as a result of the different shell environment. The key takeaway was to run such tests in Cygwin whenever I encountered path errors.

rmic Tests

The rmic Tools Reference page describes it as follows:

You use the rmic compiler to generate stub and skeleton class files using the Java Remote Method Protocol (JRMP).

The first test failure I investigated was in jdk11u/test/jdk/sun/rmi/rmic/defaultStubVersion/run.sh at jdk-11.0.28+6 · openjdk/jdk11u. I used my run-jtreg-test.sh script to execute it:

./run-jtreg-test.sh /d/java/ms/openjdk-jdk11u /d/java/binaries/jdk/x64/2025-07/windows-jdk11u/jdk-11.0.28+6 /c/java/binaries/jtreg/jtreg-7.4+1/lib/jtreg.jar test/jdk/sun/rmi/rmic/defaultStubVersion/run.sh -nativepath:/d/java/binaries/jdk/x64/2025-07/windows-jdk11u/jdk-11.0.28+6-test-image/hotspot/jtreg/native

This is the actual command that was executed:

/d/java/binaries/jdk/x64/2025-07/windows-jdk11u/jdk-11.0.28+6/bin/java -Xmx512m -jar /c/java/binaries/jtreg/jtreg-7.4+1/lib/jtreg.jar -agentvm -ignore:quiet -automatic -xml -vmoption:-Xmx512m -timeoutFactor:4 -concurrency:1 -testjdk:/d/java/binaries/jdk/x64/2025-07/windows-jdk11u/jdk-11.0.28+6 -verbose:fail,error,summary -nativepath:/d/java/binaries/jdk/x64/2025-07/windows-jdk11u/jdk-11.0.28+6-test-image/hotspot/jtreg/native test/jdk/sun/rmi/rmic/defaultStubVersion/run.sh

The script failed on my local machine with the error below:

STDOUT:
STDERR:
+ defdir=./default_output
+ refdir=./reference_output
+ rm -rf ./default_output ./reference_output
+ mkdir ./default_output ./reference_output
+ /mnt/d/java/binaries/jdk/x64/2025-07/windows-jdk11u/jdk-11.0.28+6/bin/rmic -classpath D:/java/ms/openjdk-jdk11u/JTwork/classes/sun/rmi/rmic/defaultStubVersion/run.d -keep -nowrite -d ./default_output G1Impl
/mnt/d/java/ms/openjdk-jdk11u/test/jdk/sun/rmi/rmic/defaultStubVersion/run.sh: 49: /mnt/d/java/binaries/jdk/x64/2025-07/windows-jdk11u/jdk-11.0.28+6/bin/rmic: not found

I confirmed that rmic.exe exists in the bin directory of the jdk.

$ ls -1 /d/java/binaries/jdk/x64/2025-07/windows-jdk11u/jdk-11.0.28+6/bin/rmic*
/d/java/binaries/jdk/x64/2025-07/windows-jdk11u/jdk-11.0.28+6/bin/rmic.exe*

Changing the test to directly refer to rmic.exe instead of rmic (as shown in the diff below) resulted in the test passing on my machine. I concluded that this specific issue must therefore be a test bug (i.e. the test should avoid this issue on Windows). See Shell Tests in jtreg for possible ways to fix this. This is one reason why shell scripts are being discouraged for OpenJDK testing.

diff --git a/test/jdk/sun/rmi/rmic/defaultStubVersion/run.sh b/test/jdk/sun/rmi/rmic/defaultStubVersion/run.sh
index 02f71d0c81..0b80015cf4 100644
--- a/test/jdk/sun/rmi/rmic/defaultStubVersion/run.sh
+++ b/test/jdk/sun/rmi/rmic/defaultStubVersion/run.sh
@@ -46,8 +46,8 @@ refdir=./reference_output
 rm -rf $defdir $refdir
 mkdir $defdir $refdir

-${TESTJAVA}/bin/rmic -classpath ${TESTCLASSES:-.} -keep -nowrite -d $defdir G1Impl
-${TESTJAVA}/bin/rmic -classpath ${TESTCLASSES:-.} -keep -nowrite -d $refdir -v1.2 G1Impl
+${TESTJAVA}/bin/rmic.exe -classpath ${TESTCLASSES:-.} -keep -nowrite -d $defdir G1Impl
+${TESTJAVA}/bin/rmic.exe -classpath ${TESTCLASSES:-.} -keep -nowrite -d $refdir -v1.2 G1Impl

 diff -r $defdir $refdir

Notice that the diff(1) – Linux manual page command has an exit code of 0 if the files are identical. This test failed in the test environment with this warning from jdk11u/src/jdk.rmic/share/classes/sun/rmi/rmic/resources/rmic.properties at jdk-11.0.28+6 · openjdk/jdk11u

STDOUT:
Warning: generation and use of skeletons and static stubs for JRMP	
is deprecated. Skeletons are unnecessary, and static stubs have	
been superseded by dynamically generated stubs. Users are	
encouraged to migrate away from using rmic to generate skeletons and static	
stubs. See the documentation for java.rmi.server.UnicastRemoteObject.

This warning is output by the sun.rmi.rmic.Main.compile method. The arguments to rmic.exe are parsed by Main.parseArgs. The -v1.2 argument specifies the non-default generator to use. My local machine must be using a different default generator (or so I thought).

Outstanding Questions at this Point

  1. Why couldn’t rmic be found on my machine?
  2. Why did this test fail in the test environment?

jrunscript Test

The next test failure I dug into was jdk11u/test/jdk/sun/tools/jrunscript/jrunscript-eTest.sh at jdk-11.0.28+6 · openjdk/jdk11u. I switched to MSYS2 for this one.

./run-jtreg-test.sh /d/java/ms/openjdk-jdk11u /d/java/binaries/jdk/x64/2025-07/windows-jdk11u/jdk-11.0.28+6 /c/java/binaries/jtreg/jtreg-7.4+1/lib/jtreg.jar test/jdk/sun/tools/jrunscript/jrunscript-eTest.sh -nativepath:/d/java/binaries/jdk/x64/2025-07/windows-jdk11u/jdk-11.0.28+6-test-image/hotspot/jtreg/native

This failed with another path error.

STDOUT:
STDERR:
/mnt/d/java/ms/openjdk-jdk11u/test/jdk/sun/tools/jrunscript/jrunscript-eTest.sh: 33: .: cannot open D:/java/ms/openjdk-jdk11u/test/jdk/sun/tools/jrunscript/common.sh: No such file

My first thought was “where is the TESTSRC set“? It actually appeared in the jtreg output, confirming that it was correctly set:

TESTFILE=D:/java/ms/openjdk-jdk11u/test/jdk/sun/tools/jrunscript/jrunscript-eTest.sh \
TESTSRC=D:/java/ms/openjdk-jdk11u/test/jdk/sun/tools/jrunscript \
TESTSRCPATH=D:/java/ms/openjdk-jdk11u/test/jdk/sun/tools/jrunscript \

So what does the failing line do? I asked Copilot: what does this command do in a bash script? . ${TESTSRC-.}/common.sh

  1. . (dot command)
    This is shorthand for source, which executes the contents of a file in the current shell environment rather than in a subshell. This means any variables, functions, or changes made by the script will persist in the current shell.
  2. ${TESTSRC-.}
    This is a parameter expansion. It means:
    • Use the value of the TESTSRC environment variable if it is set.
    • If TESTSRC is not set, use . (the current directory) as the default.
  3. /common.sh
    This appends /common.sh to the path determined above.

This error was a Windows path issue again. Notepad++ could open D:/java/ms/openjdk-jdk11u/test/jdk/sun/tools/jrunscript/common.sh so the path itself was fine. I used this workaround:

diff --git a/test/jdk/sun/tools/jrunscript/jrunscript-eTest.sh b/test/jdk/sun/tools/jrunscript/jrunscript-eTest.sh
index 6a3d4e76ff..94001967b0 100644
--- a/test/jdk/sun/tools/jrunscript/jrunscript-eTest.sh
+++ b/test/jdk/sun/tools/jrunscript/jrunscript-eTest.sh
@@ -30,10 +30,11 @@
 # @run shell jrunscript-eTest.sh
 # @summary Test that output of 'jrunscript -e' matches the dash-e.out file

-. ${TESTSRC-.}/common.sh
+comn=`/mnt/c/software/msys64/usr/bin/cygpath.exe ${TESTSRC-.}/common.sh`
+. "/mnt$comn"

 setup
-${JAVA} ${TESTVMOPTS} ${TESTJAVAOPTS} -cp ${TESTCLASSES} CheckEngine
+${JAVA}.exe ${TESTVMOPTS} ${TESTJAVAOPTS} -cp ${TESTCLASSES} CheckEngine
 if [ $? -eq 2 ]; then
     echo "No js engine found and engine not required; test vacuously passes."
     exit 0

This got me to the actual test error:

STDOUT:
Output of jrunscript -e differ from expected output. Failed.
STDERR:
Warning: Nashorn engine is planned to be removed from a future JDK release
diff: D:/java/ms/openjdk-jdk11u/test/jdk/sun/tools/jrunscript/dash-e.out: No such file or directory

I couldn’t see which command generated the output though, so I added set -ex to the top of the script (like run.sh in the previous test). This was the resulting output:

STDOUT:
STDERR:
+ /mnt/c/software/msys64/usr/bin/cygpath.exe D:/java/ms/openjdk-jdk11u/test/jdk/sun/tools/jrunscript/common.sh
+ comn=/d/java/ms/openjdk-jdk11u/test/jdk/sun/tools/jrunscript/common.sh
+ . /mnt/d/java/ms/openjdk-jdk11u/test/jdk/sun/tools/jrunscript/common.sh
+ setup
+ [ /mnt/d/java/binaries/jdk/x64/2025-07/windows-jdk11u/jdk-11.0.28+6 =  ]
+ [ D:/java/ms/openjdk-jdk11u/JTwork/classes/sun/tools/jrunscript/jrunscript-eTest.d =  ]
+ [ D:/java/ms/openjdk-jdk11u/test/jdk/sun/tools/jrunscript =  ]
+ uname -s
+ OS=Linux
+ PS=:
+ FS=/
+ golden_diff=diff
+ JRUNSCRIPT=/mnt/d/java/binaries/jdk/x64/2025-07/windows-jdk11u/jdk-11.0.28+6/bin/jrunscript
+ JAVAC=/mnt/d/java/binaries/jdk/x64/2025-07/windows-jdk11u/jdk-11.0.28+6/bin/javac
+ JAVA=/mnt/d/java/binaries/jdk/x64/2025-07/windows-jdk11u/jdk-11.0.28+6/bin/java
+ /mnt/d/java/binaries/jdk/x64/2025-07/windows-jdk11u/jdk-11.0.28+6/bin/java.exe -Xmx512m -cp D:/java/ms/openjdk-jdk11u/JTwork/classes/sun/tools/jrunscript/jrunscript-eTest.d CheckEngine
Warning: Nashorn engine is planned to be removed from a future JDK release
+ [ 0 -eq 2 ]
+ rm -f jrunscript-eTest.out
+ /mnt/d/java/binaries/jdk/x64/2025-07/windows-jdk11u/jdk-11.0.28+6/bin/jrunscript -J-Dnashorn.args.prepend=--no-deprecation-warning -J-Djava.awt.headless=true -l nashorn -e println('hello')

Aha! Notice the root cause of the filename issues: OS=Linux! This also confirmed that I was using the same diff command. I didn’t get the error message at jdk11u/test/jdk/sun/tools/jrunscript/jrunscript-eTest.sh at jdk-11.0.28+5 · openjdk/jdk11u because because of set -ex (the e means exit immediately if any command fails, which does not match the behavior this test requires). After removing the e, I got this output:

STDOUT:
Output of jrunscript -e differ from expected output. Failed.
STDERR:
+ /mnt/c/software/msys64/usr/bin/cygpath.exe D:/java/ms/openjdk-jdk11u/test/jdk/sun/tools/jrunscript/common.sh
+ comn=/d/java/ms/openjdk-jdk11u/test/jdk/sun/tools/jrunscript/common.sh
+ . /mnt/d/java/ms/openjdk-jdk11u/test/jdk/sun/tools/jrunscript/common.sh
+ setup
+ [ /mnt/d/java/binaries/jdk/x64/2025-07/windows-jdk11u/jdk-11.0.28+6 =  ]
+ [ D:/java/ms/openjdk-jdk11u/JTwork/classes/sun/tools/jrunscript/jrunscript-eTest.d =  ]
+ [ D:/java/ms/openjdk-jdk11u/test/jdk/sun/tools/jrunscript =  ]
+ uname -s
+ OS=Linux
+ PS=:
+ FS=/
+ golden_diff=diff
+ JRUNSCRIPT=/mnt/d/java/binaries/jdk/x64/2025-07/windows-jdk11u/jdk-11.0.28+6/bin/jrunscript
+ JAVAC=/mnt/d/java/binaries/jdk/x64/2025-07/windows-jdk11u/jdk-11.0.28+6/bin/javac
+ JAVA=/mnt/d/java/binaries/jdk/x64/2025-07/windows-jdk11u/jdk-11.0.28+6/bin/java
+ /mnt/d/java/binaries/jdk/x64/2025-07/windows-jdk11u/jdk-11.0.28+6/bin/java.exe -Xmx512m -cp D:/java/ms/openjdk-jdk11u/JTwork/classes/sun/tools/jrunscript/jrunscript-eTest.d CheckEngine
Warning: Nashorn engine is planned to be removed from a future JDK release
+ [ 0 -eq 2 ]
+ rm -f jrunscript-eTest.out
+ /mnt/d/java/binaries/jdk/x64/2025-07/windows-jdk11u/jdk-11.0.28+6/bin/jrunscript -J-Dnashorn.args.prepend=--no-deprecation-warning -J-Djava.awt.headless=true -l nashorn -e println('hello')
+ diff jrunscript-eTest.out D:/java/ms/openjdk-jdk11u/test/jdk/sun/tools/jrunscript/dash-e.out
diff: D:/java/ms/openjdk-jdk11u/test/jdk/sun/tools/jrunscript/dash-e.out: No such file or directory
+ [ 2 != 0 ]
+ echo Output of jrunscript -e differ from expected output. Failed.
+ rm -f jrunscript-eTest.out
+ exit 1

The ls command confirmed that the file exists:

$ ls -1 `cygpath D:/java/ms/openjdk-jdk11u/test/jdk/sun/tools/jrunscript/dash-e.out`
/d/java/ms/openjdk-jdk11u/test/jdk/sun/tools/jrunscript/dash-e.out

I patched the script as shown in the next diff:

diff --git a/test/jdk/sun/tools/jrunscript/jrunscript-eTest.sh b/test/jdk/sun/tools/jrunscript/jrunscript-eTest.sh
index 6a3d4e76ff..4c7130857e 100644
--- a/test/jdk/sun/tools/jrunscript/jrunscript-eTest.sh
+++ b/test/jdk/sun/tools/jrunscript/jrunscript-eTest.sh
@@ -30,10 +30,13 @@
 # @run shell jrunscript-eTest.sh
 # @summary Test that output of 'jrunscript -e' matches the dash-e.out file

-. ${TESTSRC-.}/common.sh
+set -x
+
+comn=`/mnt/c/software/msys64/usr/bin/cygpath.exe ${TESTSRC-.}/common.sh`
+. "/mnt$comn"

 setup
-${JAVA} ${TESTVMOPTS} ${TESTJAVAOPTS} -cp ${TESTCLASSES} CheckEngine
+${JAVA}.exe ${TESTVMOPTS} ${TESTJAVAOPTS} -cp ${TESTCLASSES} CheckEngine
 if [ $? -eq 2 ]; then
     echo "No js engine found and engine not required; test vacuously passes."
     exit 0
@@ -44,7 +47,9 @@ fi
 rm -f jrunscript-eTest.out 2>/dev/null
 ${JRUNSCRIPT} -J-Dnashorn.args.prepend=--no-deprecation-warning -J-Djava.awt.headless=true -l nashorn -e "println('hello')" > jrunscript-eTest.out 2>&1

-$golden_diff jrunscript-eTest.out ${TESTSRC}/dash-e.out
+diffarg=`/mnt/c/software/msys64/usr/bin/cygpath.exe ${TESTSRC}/dash-e.out`
+
+$golden_diff jrunscript-eTest.out "/mnt$diffarg"
 if [ $? != 0 ]
 then
   echo "Output of jrunscript -e differ from expected output. Failed."

Avoiding Path Issues

This madness (in the diff above) that made me realize that I needed to fix the path issues and that perhaps Cygwin was the better environment for these tests. Sure enough, the test passed the first time I executed it in Cygwin:

./run-jtreg-test.sh /cygdrive/d/java/ms/dups/openjdk-jdk11u D:/java/binaries/jdk/x64/2025-07/windows-jdk11u/jdk-11.0.28+6 C:/java/binaries/jtreg/jtreg-7.4+1/lib/jtreg.jar test/jdk/sun/tools/jrunscript/jrunscript-eTest.sh -nativepath:D:/java/binaries/jdk/x64/2025-07/windows-jdk11u/jdk-11.0.28+6-test-image/hotspot/jtreg/native
Executing: D:/java/binaries/jdk/x64/2025-07/windows-jdk11u/jdk-11.0.28+6/bin/java -Xmx512m -jar C:/java/binaries/jtreg/jtreg-7.4+1/lib/jtreg.jar -agentvm -ignore:quiet -automatic -xml -vmoption:-Xmx512m -timeoutFactor:4 -concurrency:1 -testjdk:D:/java/binaries/jdk/x64/2025-07/windows-jdk11u/jdk-11.0.28+6 -verbose:fail,error,summary -nativepath:D:/java/binaries/jdk/x64/2025-07/windows-jdk11u/jdk-11.0.28+6-test-image/hotspot/jtreg/native test/jdk/sun/tools/jrunscript/jrunscript-eTest.sh
XML output  to D:\java\ms\dups\openjdk-jdk11u\JTwork
Passed: sun/tools/jrunscript/jrunscript-eTest.sh
Test results: passed: 1

Moral of the story: run OpenJDK shell tests on Windows in the Cygwin environment! At this point, the only outstanding question is about the difference between the local machine and the test environment the build was executed in. I thought that the fact that I didn’t get the rmic.jrmp.stubs.deprecated warning meant that a different default generator was used on my machine. However, I realized that the stdout messages were not being displayed! That warning was present in the output but I needed to open JTwork\sun\rmi\rmic\defaultStubVersion\run.jtr to see it!

----------System.out:(11/743)----------
Warning: generation and use of skeletons and static stubs for JRMP	
is deprecated. Skeletons are unnecessary, and static stubs have	
been superseded by dynamically generated stubs. Users are	
encouraged to migrate away from using rmic to generate skeletons and static	
stubs. See the documentation for java.rmi.server.UnicastRemoteObject.
Warning: generation and use of skeletons and static stubs for JRMP	
is deprecated. Skeletons are unnecessary, and static stubs have	
been superseded by dynamically generated stubs. Users are	
encouraged to migrate away from using rmic to generate skeletons and static	
stubs. See the documentation for java.rmi.server.UnicastRemoteObject.
TEST PASSED: default output identical to -v1.2 output
----------System.err:(8/706)----------
+ defdir=./default_output
+ refdir=./reference_output
+ rm -rf ./default_output ./reference_output
+ mkdir ./default_output ./reference_output
+ D:/java/binaries/jdk/x64/2025-07/windows-jdk11u/jdk-11.0.28+6/bin/rmic -classpath D:/java/ms/openjdk-jdk11u/JTwork/classes/sun/rmi/rmic/defaultStubVersion/run.d -keep -nowrite -d ./default_output G1Impl
+ D:/java/binaries/jdk/x64/2025-07/windows-jdk11u/jdk-11.0.28+6/bin/rmic -classpath D:/java/ms/openjdk-jdk11u/JTwork/classes/sun/rmi/rmic/defaultStubVersion/run.d -keep -nowrite -d ./reference_output -v1.2 G1Impl
+ diff -r ./default_output ./reference_output
+ echo 'TEST PASSED: default output identical to -v1.2 output'

This was sufficient for me to confirm that the build behaves as expected in this scenario.


Exporting to 3mf in Blender

Once I had finished Setting up my First 3D Printer, I wanted to design and print custom models. The Anycubic Slicer Next software can only open 3mf files. Being new to this space means that I had never even heard of this format before. Here are some useful resources for learning more about it:

In SOLIDWORKS, the Save As dialog is all you need to export to 3mf.

SOLIDWORKS “Save As” Dialog

The AI overview for the does blender support 3mf – Google Search indicated that it does via a plugin. The top result was the Ghostkeeper/Blender3mfFormat: Blender add-on to import/export 3MF files. The current release is Release 1.0.2 – Bug Fixes · Ghostkeeper/Blender3mfFormat. These are the steps from the plugin GitHub readme with a slideshow of screenshots of the Blender app:

  1. Download the latest release from the releases page. This is a .zip archive.
  2. In Blender, go to Edit -> Preferences and open the Add-ons tab on the left.
  3. Click on the Install… button at the top. Navigate to the .zip you downloaded.
  4. Under the “Community” category, find the add-on called “Import-Export: 3MF format”. Make sure that it’s enabled. (Note: If searching with the search box, exclude the “Import-Export” text since this is the category, not part of the add-on name. Just search for “3MF” instead.)

As promised on the plugins page, the export option is now available:

Blender 3mf Export Command

Determining Model Dimensions

Before exporting my blender model, I wanted to understand its real world dimensions. object dimensions blender – Google Search led me to interface – Where can I find the dimensions of an object? – Blender Stack Exchange. Turns out it’s as easy as pressing “N”.

Blender’s Transform Panel

Note that there is a “Scale” option on the export dialog. I just ensured my dimensions were real world dimensions. Here is the simple blender model I made in the Anycubic Slicer Next software. I was able to successfully print it.

Anycubic Slicer Next with a 3mf exported from Blender