ndk_program_tests: add arm64 setjmp test
Specifically verify that callee-saved registers are being properly restored.
Flag: EXEMPT NDK
Test: This test works both as static and as dynamic executable
Test: Test fails if we skip restoring some
callee-saved registers in the emulation
Bug: 322873334
Change-Id: Ie3e77d53d23efb44eac9272bbb13468b70c325d3
diff --git a/tests/ndk_program_tests/Android.bp b/tests/ndk_program_tests/Android.bp
index 84ab38c..3e9d009 100644
--- a/tests/ndk_program_tests/Android.bp
+++ b/tests/ndk_program_tests/Android.bp
@@ -133,6 +133,7 @@
"arm64/handle_not_executable_test.cc",
"arm64/runtime_code_patching_test.cc",
"arm64/sigill_test.cc",
+ "arm64/setjmp_test.cc",
],
},
},
diff --git a/tests/ndk_program_tests/arm64/setjmp_test.cc b/tests/ndk_program_tests/arm64/setjmp_test.cc
new file mode 100644
index 0000000..a9a69ea
--- /dev/null
+++ b/tests/ndk_program_tests/arm64/setjmp_test.cc
@@ -0,0 +1,162 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "gtest/gtest.h"
+
+#include <setjmp.h>
+
+#include <bit>
+#include <cstdint>
+#include <cstdio>
+
+namespace {
+
+constexpr uint64_t kClobberVal{0xdead'beef'dead'beef};
+
+jmp_buf g_jmp_buf;
+
+// Use naked assembly since otherwise compiler may save/restore callee saved before longjmp instead
+// of making sure they are clobbered.
+[[gnu::naked]] void ClobberAndLongjmp(void* jmp_buf_ptr, int ret_val, void* longjmp_ptr) {
+ // clang-format off
+ asm volatile(
+ "mov x19, %0\n\t" "mov x20, %0\n\t" "mov x21, %0\n\t"
+ "mov x22, %0\n\t" "mov x23, %0\n\t" "mov x24, %0\n\t"
+ "mov x25, %0\n\t" "mov x26, %0\n\t" "mov x27, %0\n\t" "mov x28, %0\n\t"
+ "fmov d8, %d1\n\t" "fmov d9, %d1\n\t" "fmov d10, %d1\n\t" "fmov d11, %d1\n\t"
+ "fmov d12, %d1\n\t" "fmov d13, %d1\n\t" "fmov d14, %d1\n\t" "fmov d15, %d1\n\t"
+ // jmp_buf_ptr and ret_val are already in x0 and x1.
+ "blr x2"
+ :
+ : "r"(kClobberVal), "w"(std::bit_cast<double>(kClobberVal))
+ // Make sure kClobberVal is not allocated to x0-x2 since they carry function arguments.
+ : "x0", "x1", "x2",
+ // Clobbers.
+ "x19", "x20", "x21", "x22", "x23", "x24", "x25", "x26", "x27", "x28",
+ "v8", "v9", "v10", "v11", "v12", "v13", "v14", "v15");
+ // clang-format on
+}
+
+TEST(SetJmp, CalleeSavedRegistersRestoredArm64) {
+ // x20-x28 (9 registers). x19 is used and tested in a special way.
+ // d8-d15 (8 registers).
+ constexpr size_t kNumSavedRegs = 9 + 8;
+ using RegisterState = uint64_t[kNumSavedRegs];
+
+ // 1. SETUP
+ // Has to be volatile, so that compiler doesn't optimize it out around the setjmp.
+ volatile RegisterState magic_values;
+ volatile RegisterState restored_values{};
+ for (size_t i = 0; i < kNumSavedRegs; i++) {
+ magic_values[i] = uint64_t{0x0101'0101'0101'0101} * i;
+ }
+
+ // Group everything in one structure, so that we can pass it into the assembly in one register.
+ volatile struct AsmParams {
+ volatile RegisterState* input;
+ volatile RegisterState* output;
+ jmp_buf* jmp_buf_ptr;
+ void* setjmp_ptr;
+ int setjmp_ret_val;
+ } asm_params{
+ .input = &magic_values,
+ .output = &restored_values,
+ .jmp_buf_ptr = &g_jmp_buf,
+ .setjmp_ptr = reinterpret_cast<void*>(&setjmp),
+ };
+
+ volatile bool longjmp_happeded = false;
+
+ register uintptr_t asm_params_ptr asm("x19") = std::bit_cast<uintptr_t>(&asm_params);
+ // 2. EXECUTION
+ asm volatile(
+ // asm_params_ptr comes in x19.
+ // Also x19 is callee-saved so it's preserved over setjmp/longjmp.
+ // Warning: we cannot allocate more stack space here to carry values over setjmp
+ // since on the first return stack will be deallocated and clobbered.
+ "ldr x9, [x19, %[input_offset]]\n\t"
+ // Load magic values into registers.
+ "ldr x20, [x9], #8\n\t"
+ "ldp x21, x22, [x9], #16\n\t"
+ "ldp x23, x24, [x9], #16\n\t"
+ "ldp x25, x26, [x9], #16\n\t"
+ "ldp x27, x28, [x9], #16\n\t"
+ "ldp d8, d9, [x9], #16\n\t"
+ "ldp d10, d11, [x9], #16\n\t"
+ "ldp d12, d13, [x9], #16\n\t"
+ "ldp d14, d15, [x9], #16\n\t"
+
+ "ldr x0, [x19, %[jmp_buf_ptr_offset]]\n\t"
+ "ldr x1, [x19, %[setjmp_ptr_offset]]\n\t"
+ "blr x1\n\t"
+ "str x0, [x19, %[setjmp_ret_val_offset]]\n\t"
+ "cbz x0, .L_done_%= \n\t"
+
+ // --- RESTORED PATH (only runs after longjmp) ---
+ "ldr x9, [x19, %[output_offset]]\n\t"
+ // Store registers to the output buffer.
+ "str x20, [x9], #8\n\t"
+ "stp x21, x22, [x9], #16\n\t"
+ "stp x23, x24, [x9], #16\n\t"
+ "stp x25, x26, [x9], #16\n\t"
+ "stp x27, x28, [x9], #16\n\t"
+ "stp d8, d9, [x9], #16\n\t"
+ "stp d10, d11, [x9], #16\n\t"
+ "stp d12, d13, [x9], #16\n\t"
+ "stp d14, d15, [x9], #16\n\t"
+
+ ".L_done_%=: \n\t"
+ :
+ : "r"(asm_params_ptr),
+ [input_offset] "i"(offsetof(AsmParams, input)),
+ [output_offset] "i"(offsetof(AsmParams, output)),
+ [setjmp_ptr_offset] "i"(offsetof(AsmParams, setjmp_ptr)),
+ [jmp_buf_ptr_offset] "i"(offsetof(AsmParams, jmp_buf_ptr)),
+ [setjmp_ret_val_offset] "i"(offsetof(AsmParams, setjmp_ret_val))
+ // We initialize callee-saved registers except x19 with magic values and setjmp call
+ // clobbers caller-saved. This means that we clobber everything except x19. x18 (scs)
+ // and x29 (fp) are reserved and cannot be used on the clobber list. sp is restored by
+ // setjmp.
+ // clang-format off
+ : "x0", "x1", "x2", "x3", "x4", "x5", "x6", "x7", "x8", "x9",
+ "x10", "x11", "x12", "x13", "x14", "x15", "x16", "x17", /*x18*/ /*x19*/
+ "x20", "x21", "x22", "x23", "x24", "x25", "x26", "x27", "x28", /*x29*/
+ "x30", "memory", "cc",
+ // Vector registers.
+ "v0", "v1", "v2", "v3", "v4", "v5", "v6", "v7", "v8", "v9",
+ "v10", "v11", "v12", "v13", "v14", "v15", "v16", "v17", "v18", "v19",
+ "v20", "v21", "v22", "v23", "v24", "v25", "v26", "v27", "v28", "v29",
+ "v30", "v31");
+ // clang-format on
+
+ printf("setjmp returned %d\n", asm_params.setjmp_ret_val);
+
+ if (asm_params.setjmp_ret_val == 0) {
+ longjmp_happeded = true;
+ ClobberAndLongjmp(&g_jmp_buf, 1, reinterpret_cast<void*>(&longjmp));
+ FAIL() << "longjmp() did not execute. This line should not be reached.";
+ }
+
+ // 3. VERIFICATION
+ ASSERT_TRUE(longjmp_happeded);
+
+ for (size_t i = 0; i < kNumSavedRegs; i++) {
+ EXPECT_EQ(magic_values[i], restored_values[i])
+ << "register isn't properly restored for index " << i;
+ }
+}
+
+} // namespace
diff --git a/tests/ndk_program_tests/setjmp_test.cc b/tests/ndk_program_tests/setjmp_test.cc
index f45e31d..60b263f 100644
--- a/tests/ndk_program_tests/setjmp_test.cc
+++ b/tests/ndk_program_tests/setjmp_test.cc
@@ -14,6 +14,8 @@
* limitations under the License.
*/
+#include "gtest/gtest.h"
+
#include <setjmp.h>
#include <signal.h>