mirror of
https://github.com/open-goal/jak-project
synced 2026-08-31 17:43:06 -04:00
de25f39439
Got OpenGOAL building and running natively on Apple Silicon. This is the rest of the port after the smaller arm64 emitter PRs. I was told pushing one big PR was okay. This covers goalc, the runtime, linker, kernel and the GOAL asm. The new paths have native tests. I also found four more emitter bugs while running it. They're in scalar sqrt, 128-bit stores, scalar max and indexed stores above 4 GB. Spent a while cleaning it up so it's easier to read. 4am gotta sleep lol Closes #3841 --------- Co-authored-by: Tyler Wilding <xtvaser@gmail.com>
361 lines
10 KiB
C++
361 lines
10 KiB
C++
#include <array>
|
|
#include <cstddef>
|
|
#include <vector>
|
|
|
|
#include "common/common_types.h"
|
|
|
|
#include "gtest/gtest.h"
|
|
|
|
#if defined(__aarch64__)
|
|
|
|
#include "game/kernel/common/codegen.h"
|
|
|
|
#ifdef __APPLE__
|
|
#include <mach/mach.h>
|
|
#include <mach/mach_vm.h>
|
|
#include <sys/mman.h>
|
|
#endif
|
|
|
|
extern "C" {
|
|
#ifdef __APPLE__
|
|
u64 _call_goal_asm_arm64(u64 a0,
|
|
u64 a1,
|
|
u64 a2,
|
|
void* fptr,
|
|
void* st_ptr,
|
|
void* offset,
|
|
void* exec_offset) asm("_call_goal_asm_arm64");
|
|
u64 _call_goal_on_stack_asm_arm64(u64 rsp,
|
|
u64 u0,
|
|
u64 u1,
|
|
void* fptr,
|
|
void* st_ptr,
|
|
void* offset,
|
|
void* exec_offset) asm("_call_goal_on_stack_asm_arm64");
|
|
u64 _call_goal8_asm_arm64(void* func,
|
|
u64* arg_array,
|
|
u64 zero,
|
|
u64 pp,
|
|
u64 st,
|
|
void* offset,
|
|
void* exec_offset) asm("_call_goal8_asm_arm64");
|
|
#else
|
|
u64 _call_goal_asm_arm64(u64 a0,
|
|
u64 a1,
|
|
u64 a2,
|
|
void* fptr,
|
|
void* st_ptr,
|
|
void* offset,
|
|
void* exec_offset);
|
|
u64 _call_goal_on_stack_asm_arm64(u64 rsp,
|
|
u64 u0,
|
|
u64 u1,
|
|
void* fptr,
|
|
void* st_ptr,
|
|
void* offset,
|
|
void* exec_offset);
|
|
u64 _call_goal8_asm_arm64(void* func,
|
|
u64* arg_array,
|
|
u64 zero,
|
|
u64 pp,
|
|
u64 st,
|
|
void* offset,
|
|
void* exec_offset);
|
|
#endif
|
|
}
|
|
|
|
struct alignas(16) Arm64SavedRegisters {
|
|
u64 result = 0;
|
|
u64 x20 = 0;
|
|
u64 x21 = 0;
|
|
u64 x22 = 0;
|
|
u64 x27 = 0;
|
|
u64 sp = 0;
|
|
std::array<std::array<u64, 2>, 8> vectors = {};
|
|
};
|
|
|
|
static_assert(offsetof(Arm64SavedRegisters, vectors) == 48);
|
|
static_assert(sizeof(Arm64SavedRegisters) == 176);
|
|
|
|
extern "C" u64 arm64_test_clobber_goal_registers();
|
|
extern "C" u64 arm64_test_preservation_probe(Arm64SavedRegisters* out,
|
|
u64 callee,
|
|
u64 data,
|
|
u64 mode);
|
|
|
|
#ifdef __APPLE__
|
|
#define ARM64_TEST_SYMBOL(name) "_" #name
|
|
#else
|
|
#define ARM64_TEST_SYMBOL(name) #name
|
|
#endif
|
|
|
|
asm(".text\n"
|
|
".p2align 2\n"
|
|
".globl " ARM64_TEST_SYMBOL(arm64_test_clobber_goal_registers) "\n"
|
|
ARM64_TEST_SYMBOL(arm64_test_clobber_goal_registers) ":\n"
|
|
"movi v8.16b, #0x88\n"
|
|
"movi v9.16b, #0x89\n"
|
|
"movi v10.16b, #0x8a\n"
|
|
"movi v11.16b, #0x8b\n"
|
|
"movi v12.16b, #0x8c\n"
|
|
"movi v13.16b, #0x8d\n"
|
|
"movi v14.16b, #0x8e\n"
|
|
"movi v15.16b, #0x8f\n"
|
|
"mov x20, #0xaaaa\n"
|
|
"mov x21, #0xbbbb\n"
|
|
"mov x22, #0xcccc\n"
|
|
"mov x27, #0xdddd\n"
|
|
"mov x0, #42\n"
|
|
"ret\n"
|
|
".p2align 2\n"
|
|
".globl " ARM64_TEST_SYMBOL(arm64_test_preservation_probe) "\n"
|
|
ARM64_TEST_SYMBOL(arm64_test_preservation_probe) ":\n"
|
|
"stp x29, x30, [sp, #-16]!\n"
|
|
"mov x29, sp\n"
|
|
"stp x20, x21, [sp, #-16]!\n"
|
|
"stp x22, x27, [sp, #-16]!\n"
|
|
"sub sp, sp, #128\n"
|
|
"stp q8, q9, [sp]\n"
|
|
"stp q10, q11, [sp, #32]\n"
|
|
"stp q12, q13, [sp, #64]\n"
|
|
"stp q14, q15, [sp, #96]\n"
|
|
"sub sp, sp, #32\n"
|
|
"stp x0, x1, [sp]\n"
|
|
"stp x2, x3, [sp, #16]\n"
|
|
"mov x20, #0x2020\n"
|
|
"mov x21, #0x2121\n"
|
|
"mov x22, #0x2222\n"
|
|
"mov x27, #0x2727\n"
|
|
"movi v8.16b, #0x18\n"
|
|
"movi v9.16b, #0x19\n"
|
|
"movi v10.16b, #0x1a\n"
|
|
"movi v11.16b, #0x1b\n"
|
|
"movi v12.16b, #0x1c\n"
|
|
"movi v13.16b, #0x1d\n"
|
|
"movi v14.16b, #0x1e\n"
|
|
"movi v15.16b, #0x1f\n"
|
|
"ldr x11, [sp, #24]\n"
|
|
"cmp x11, #1\n"
|
|
"b.eq 1f\n"
|
|
"cmp x11, #2\n"
|
|
"b.eq 2f\n"
|
|
"mov x0, #7\n"
|
|
"mov x1, xzr\n"
|
|
"mov x2, xzr\n"
|
|
"ldr x3, [sp, #8]\n"
|
|
"mov x4, #0x1111\n"
|
|
"mov x5, #0x3333\n"
|
|
"mov x6, #0x4444\n"
|
|
"bl _call_goal_asm_arm64\n"
|
|
"b 3f\n"
|
|
"1:\n"
|
|
"ldr x0, [sp, #16]\n"
|
|
"mov x1, xzr\n"
|
|
"mov x2, xzr\n"
|
|
"ldr x3, [sp, #8]\n"
|
|
"mov x4, #0x1111\n"
|
|
"mov x5, #0x3333\n"
|
|
"mov x6, #0x4444\n"
|
|
"bl _call_goal_on_stack_asm_arm64\n"
|
|
"b 3f\n"
|
|
"2:\n"
|
|
"ldr x0, [sp, #8]\n"
|
|
"ldr x1, [sp, #16]\n"
|
|
"mov x2, xzr\n"
|
|
"mov x3, #0x1111\n"
|
|
"mov x4, #0x2222\n"
|
|
"mov x5, #0x3333\n"
|
|
"mov x6, #0x4444\n"
|
|
"bl _call_goal8_asm_arm64\n"
|
|
"3:\n"
|
|
"ldr x9, [sp]\n"
|
|
"str x0, [x9]\n"
|
|
"str x20, [x9, #8]\n"
|
|
"str x21, [x9, #16]\n"
|
|
"str x22, [x9, #24]\n"
|
|
"str x27, [x9, #32]\n"
|
|
"mov x10, sp\n"
|
|
"str x10, [x9, #40]\n"
|
|
"stp q8, q9, [x9, #48]\n"
|
|
"stp q10, q11, [x9, #80]\n"
|
|
"stp q12, q13, [x9, #112]\n"
|
|
"stp q14, q15, [x9, #144]\n"
|
|
"ldp q8, q9, [sp, #32]\n"
|
|
"ldp q10, q11, [sp, #64]\n"
|
|
"ldp q12, q13, [sp, #96]\n"
|
|
"ldp q14, q15, [sp, #128]\n"
|
|
"add sp, sp, #160\n"
|
|
"ldp x22, x27, [sp], #16\n"
|
|
"ldp x20, x21, [sp], #16\n"
|
|
"ldp x29, x30, [sp], #16\n"
|
|
"ret\n");
|
|
|
|
#undef ARM64_TEST_SYMBOL
|
|
|
|
namespace {
|
|
u64 g_callee_sp = 0;
|
|
u64 g_callee_arg0 = 0;
|
|
u64 g_callee_offset = 0;
|
|
u64 g_callee_exec_base = 0;
|
|
|
|
// AAPCS64 callee used by the trampoline tests
|
|
extern "C" u64 arm64_trampoline_test_callee(u64 a0);
|
|
extern "C" u64 arm64_trampoline_test_callee(u64 a0) {
|
|
u64 sp, offset, exec_base;
|
|
// writable and executable GOAL bases
|
|
asm volatile("mov %0, x22\n\tmov %1, x27\n\tmov %2, sp"
|
|
: "=r"(offset), "=r"(exec_base), "=r"(sp));
|
|
g_callee_sp = sp;
|
|
g_callee_offset = offset;
|
|
g_callee_exec_base = exec_base;
|
|
g_callee_arg0 = a0;
|
|
return 42;
|
|
}
|
|
} // namespace
|
|
|
|
TEST(ARM64Trampolines, call_goal) {
|
|
g_callee_sp = 0;
|
|
g_callee_arg0 = 0;
|
|
u64 st = 0x1111, off = 0x2222;
|
|
u64 exec = 0x3333;
|
|
u64 result = _call_goal_asm_arm64(7, 0, 0, (void*)arm64_trampoline_test_callee, (void*)st,
|
|
(void*)off, (void*)exec);
|
|
EXPECT_EQ(result, 42u);
|
|
EXPECT_EQ(g_callee_arg0, 7u);
|
|
EXPECT_EQ(g_callee_offset, off);
|
|
// x27 receives the executable base
|
|
EXPECT_EQ(g_callee_exec_base, exec);
|
|
}
|
|
|
|
TEST(ARM64Trampolines, call_goal_on_stack) {
|
|
const size_t kStackSize = 64 * 1024;
|
|
std::vector<u8> goal_stack(kStackSize, 0);
|
|
u64 stack_top = (u64)(goal_stack.data() + kStackSize) & ~u64(15);
|
|
|
|
g_callee_sp = 0;
|
|
u64 my_sp;
|
|
asm volatile("mov %0, sp" : "=r"(my_sp));
|
|
|
|
u64 result = _call_goal_on_stack_asm_arm64(stack_top, 0, 0, (void*)arm64_trampoline_test_callee,
|
|
(void*)0x1111, (void*)0x2222, (void*)0x3333);
|
|
EXPECT_EQ(result, 42u);
|
|
EXPECT_EQ(g_callee_offset, 0x2222u);
|
|
EXPECT_EQ(g_callee_exec_base, 0x3333u);
|
|
// callee runs on the supplied stack
|
|
EXPECT_GT(g_callee_sp, (u64)goal_stack.data());
|
|
EXPECT_LE(g_callee_sp, stack_top);
|
|
|
|
// caller stack is restored
|
|
u64 sp_after;
|
|
asm volatile("mov %0, sp" : "=r"(sp_after));
|
|
EXPECT_EQ(sp_after, my_sp);
|
|
}
|
|
|
|
TEST(ARM64Trampolines, preserves_native_saved_registers) {
|
|
const size_t kStackSize = 64 * 1024;
|
|
std::vector<u8> goal_stack(kStackSize, 0);
|
|
u64 stack_top = (u64)(goal_stack.data() + kStackSize) & ~u64(15);
|
|
std::array<u64, 8> args = {1, 2, 3, 4, 5, 6, 7, 8};
|
|
|
|
for (u64 mode = 0; mode < 3; mode++) {
|
|
SCOPED_TRACE(mode);
|
|
u64 data = 0;
|
|
if (mode == 1) {
|
|
data = stack_top;
|
|
} else if (mode == 2) {
|
|
data = (u64)args.data();
|
|
}
|
|
|
|
Arm64SavedRegisters saved;
|
|
arm64_test_preservation_probe(&saved, (u64)arm64_test_clobber_goal_registers, data, mode);
|
|
|
|
EXPECT_EQ(saved.result, 42u);
|
|
EXPECT_EQ(saved.x20, 0x2020u);
|
|
EXPECT_EQ(saved.x21, 0x2121u);
|
|
EXPECT_EQ(saved.x22, 0x2222u);
|
|
EXPECT_EQ(saved.x27, 0x2727u);
|
|
EXPECT_EQ(saved.sp & 15, 0u);
|
|
for (u64 i = 0; i < saved.vectors.size(); i++) {
|
|
u64 expected = (0x18 + i) * 0x0101010101010101ull;
|
|
EXPECT_EQ(saved.vectors[i][0], expected) << "v" << i + 8 << " low";
|
|
EXPECT_EQ(saved.vectors[i][1], expected) << "v" << i + 8 << " high";
|
|
}
|
|
}
|
|
}
|
|
|
|
TEST(ARM64CStub, pp_argument) {
|
|
u8 buf[64] = {};
|
|
int len = emit_arm64_c_stub(buf, 0x1122334455667788ull, 0x99aabbccddeeff00ull, true);
|
|
|
|
ASSERT_EQ(len, 10 * 4);
|
|
|
|
u32 words[10];
|
|
memcpy(words, buf, sizeof(words));
|
|
EXPECT_EQ(words[4], 0xaa1403e3u) << "mov x3, x20";
|
|
EXPECT_NE(words[4], 0xaa1403e2u) << "x2 is the third AAPCS64 argument register";
|
|
EXPECT_EQ(words[9], 0xd61f0120u) << "br x9";
|
|
|
|
// arg3_is_pp false leaves x2 alone
|
|
u8 buf2[64] = {};
|
|
int len2 = emit_arm64_c_stub(buf2, 0x1122334455667788ull, 0x99aabbccddeeff00ull, false);
|
|
EXPECT_EQ(len2, 9 * 4);
|
|
}
|
|
|
|
#ifdef __APPLE__
|
|
|
|
TEST(ARM64ICache, icache_invalidation) {
|
|
const size_t kSize = 64 * 1024;
|
|
u8* rw = (u8*)mmap(nullptr, kSize, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANON, -1, 0);
|
|
ASSERT_NE(rw, MAP_FAILED);
|
|
|
|
mach_vm_address_t exec_addr = 0;
|
|
vm_prot_t cur_prot = 0, max_prot = 0;
|
|
ASSERT_EQ(
|
|
mach_vm_remap(mach_task_self(), &exec_addr, kSize, 0, VM_FLAGS_ANYWHERE, mach_task_self(),
|
|
(mach_vm_address_t)rw, false, &cur_prot, &max_prot, VM_INHERIT_NONE),
|
|
KERN_SUCCESS);
|
|
ASSERT_EQ(
|
|
mach_vm_protect(mach_task_self(), exec_addr, kSize, false, VM_PROT_READ | VM_PROT_EXECUTE),
|
|
KERN_SUCCESS);
|
|
|
|
auto write_ret_const = [](u32* p, u32 imm) {
|
|
p[0] = 0x52800000u | (imm << 5); // movz w0, #imm
|
|
p[1] = 0xd65f03c0u; // ret
|
|
};
|
|
|
|
int stale_without = 0, stale_with = 0, trials = 0;
|
|
// one generated function per page
|
|
for (size_t off = 0; off + 4096 < kSize; off += 4096) {
|
|
auto* words = (u32*)(rw + off);
|
|
auto fn = (int (*)())(void*)((u8*)exec_addr + off);
|
|
|
|
write_ret_const(words, 11);
|
|
flush_icache((void*)fn, 8);
|
|
for (int i = 0; i < 100; i++) {
|
|
ASSERT_EQ(fn(), 11); // prime the instruction cache
|
|
}
|
|
|
|
write_ret_const(words, 22);
|
|
// executable view sees the write before the flush
|
|
ASSERT_EQ(*(volatile u32*)((u8*)exec_addr + off), words[0]);
|
|
if (fn() != 22) {
|
|
stale_without++;
|
|
}
|
|
|
|
flush_icache((void*)fn, 8);
|
|
if (fn() != 22) {
|
|
stale_with++;
|
|
}
|
|
trials++;
|
|
}
|
|
|
|
EXPECT_GT(trials, 0);
|
|
EXPECT_EQ(stale_with, 0) << "rewritten code stayed stale after the cache flush";
|
|
EXPECT_GT(stale_without, 0) << "expected stale code without a cache flush";
|
|
munmap(rw, kSize);
|
|
}
|
|
|
|
#endif // __APPLE__
|
|
|
|
#endif
|