Files
sdk/runtime/vm/compiler/relocation_test.cc
Ryan Macnak d36adbacaf [vm] Remove the VM isolate.
The former contents of the VM isolate are now included into each isolate group. This makes each isolate group's heap independent, and in particular allows each heap to be allocated to a separate pointer cage (not done in this CL).

The duplicated stubs that allowed PC relative calls are removed, since the originals can now be the target of PC relative calls.

The bootstrapping needing to load an AppJIT or AppAOT snapshot is reduced to allocating the oddballs. The code is entirely dropped in the AOT runtime, but the JIT runtime still has it to allow for flags to affect the compilation of the stub code. Further refactoring might be able to remove this for the JIT runtime too, with only gen_snapshot knowing how to bootstrap.

Class serialization no longer distinguishes predefined classes.

The page containing null is marked as never-evacuate. null, false and true must not move because the compiler relies on their low bits having certain patterns for some optimizations. (Previously, the entire VM isolate heap never moved.)

Compaction is disabled for IA32. Due to register pressure, some stub calls must not use a scratch register and embed the address of Code.

The page containing the call-through-safepoint stub is frozen when running with --write-protect-code and the stub is created at runtime (instead of loaded from an AppJIT or AppAOT snapshot). This stub must remain executable even during a safepoint, as a foreign call might during return during a safepoint and only block after the stub directs it to the runtime.

The snapshot symbols are renamed to kDartSnapshotData and kDartSnapshotText. There is no need to distinguish the VM isolate's snapshot, and snaphots are per isolate group not per isolate. Aliases with the old names are added to ease migration.

Some global flags that were automatically set based on the VM isolate's snapshot are now isolate group flags and automatically set by the isolate group's snapshot.

TEST=ci
Change-Id: Iee82016057d609112e9b021d178fc3d4d18b5044
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/500621
Reviewed-by: Alexander Markov <alexmarkov@google.com>
Reviewed-by: Tess Strickland <sstrickl@google.com>
SLSA-Policy-Verified: SLSA Policy Verification Service <devtools-gerritcodereview-exitgate@google.com>
Commit-Queue: Ryan Macnak <rmacnak@google.com>
2026-05-18 11:35:03 -07:00

469 lines
18 KiB
C++

// Copyright (c) 2021, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
#include "platform/assert.h"
#include "vm/allocation.h"
#include "vm/code_patcher.h"
#include "vm/compiler/assembler/assembler.h"
#include "vm/compiler/relocation.h"
#include "vm/instructions.h"
#include "vm/longjump.h"
#include "vm/unit_test.h"
#define __ assembler->
namespace dart {
#if defined(DART_PRECOMPILER) && !defined(TARGET_ARCH_IA32)
DECLARE_FLAG(int, lower_pc_relative_call_distance);
DECLARE_FLAG(int, upper_pc_relative_call_distance);
struct RelocatorTestHelper {
const intptr_t kTrampolineSize =
Utils::RoundUp(PcRelativeTrampolineJumpPattern::kLengthInBytes,
compiler::target::Instructions::kBarePayloadAlignment);
// The callers on arm/arm64 have to save LR before calling, so the call
// instruction will be 4 byte sinto the instruction stream.
#if defined(TARGET_ARCH_ARM64)
static constexpr intptr_t kOffsetOfCall = 4;
#elif defined(TARGET_ARCH_ARM)
static constexpr intptr_t kOffsetOfCall = 4;
#elif defined(TARGET_ARCH_RISCV32)
static constexpr intptr_t kOffsetOfCall = 4;
#elif defined(TARGET_ARCH_RISCV64)
static constexpr intptr_t kOffsetOfCall = 4;
#else
static constexpr intptr_t kOffsetOfCall = 0;
#endif
explicit RelocatorTestHelper(Thread* thread)
: thread(thread),
locker(thread, thread->isolate_group()->program_lock()),
safepoint_scope(thread) {
// So the relocator uses the correct instruction size layout.
FLAG_precompiled_mode = true;
FLAG_lower_pc_relative_call_distance = -128;
FLAG_upper_pc_relative_call_distance = 128;
}
~RelocatorTestHelper() { FLAG_precompiled_mode = false; }
void CreateInstructions(std::initializer_list<intptr_t> sizes) {
for (auto size : sizes) {
codes.Add(&Code::Handle(AllocationInstruction(size)));
}
}
CodePtr AllocationInstruction(uintptr_t size) {
const auto& instructions = Instructions::Handle(Instructions::New(
size, /*has_monomorphic_entry=*/false, /*should_be_aligned=*/false));
uword addr = instructions.PayloadStart();
for (uintptr_t i = 0; i < (size / 4); ++i) {
*reinterpret_cast<uint32_t*>(addr + 4 * i) =
static_cast<uint32_t>(kBreakInstructionFiller);
}
const auto& code = Code::Handle(Code::New(0));
code.SetActiveInstructions(instructions, 0);
code.set_instructions(instructions);
return code.ptr();
}
void EmitPcRelativeCallFunction(intptr_t idx, intptr_t to_idx) {
const Code& code = *codes[idx];
const Code& target = *codes[to_idx];
EmitCodeFor(code, [&](compiler::Assembler* assembler) {
#if defined(TARGET_ARCH_ARM64)
SPILLS_RETURN_ADDRESS_FROM_LR_TO_REGISTER(
__ stp(LR, R1,
compiler::Address(CSP, -2 * kWordSize,
compiler::Address::PairPreIndex)));
#elif defined(TARGET_ARCH_ARM)
SPILLS_RETURN_ADDRESS_FROM_LR_TO_REGISTER(__ PushList((1 << LR)));
#elif defined(TARGET_ARCH_RISCV32) || defined(TARGET_ARCH_RISCV64)
__ PushRegister(RA);
#endif
__ GenerateUnRelocatedPcRelativeCall();
AddPcRelativeCallTargetAt(__ CodeSize(), code, target);
#if defined(TARGET_ARCH_ARM64)
RESTORES_RETURN_ADDRESS_FROM_REGISTER_TO_LR(
__ ldp(LR, R1,
compiler::Address(CSP, 2 * kWordSize,
compiler::Address::PairPostIndex)));
#elif defined(TARGET_ARCH_ARM)
RESTORES_RETURN_ADDRESS_FROM_REGISTER_TO_LR(__ PopList((1 << LR)));
#elif defined(TARGET_ARCH_RISCV32) || defined(TARGET_ARCH_RISCV64)
__ PopRegister(RA);
#endif
__ Ret();
});
}
void EmitReturn42Function(intptr_t idx) {
const Code& code = *codes[idx];
EmitCodeFor(code, [&](compiler::Assembler* assembler) {
#if defined(TARGET_ARCH_X64)
__ LoadImmediate(RAX, 42);
#elif defined(TARGET_ARCH_ARM) || defined(TARGET_ARCH_ARM64)
__ LoadImmediate(R0, 42);
#elif defined(TARGET_ARCH_RISCV32) || defined(TARGET_ARCH_RISCV64)
__ LoadImmediate(A0, 42);
#endif
__ Ret();
});
}
void EmitCodeFor(const Code& code,
std::function<void(compiler::Assembler* assembler)> fun) {
const auto& inst = Instructions::Handle(code.instructions());
compiler::Assembler assembler(nullptr);
fun(&assembler);
const uword addr = inst.PayloadStart();
memmove(reinterpret_cast<void*>(addr),
reinterpret_cast<void*>(assembler.CodeAddress(0)),
assembler.CodeSize());
if (FLAG_disassemble) {
OS::PrintErr("Disassemble:\n");
code.Disassemble();
}
}
void AddPcRelativeCallTargetAt(intptr_t offset,
const Code& code,
const Code& target) {
const auto& kind_and_offset = Smi::Handle(
Smi::New(Code::KindField::encode(Code::kPcRelativeCall) |
Code::EntryPointField::encode(Code::kDefaultEntry) |
Code::OffsetField::encode(offset)));
AddCall(code, target, kind_and_offset);
}
void AddCall(const Code& code,
const Code& target,
const Smi& kind_and_offset) {
auto& call_targets = Array::Handle(code.static_calls_target_table());
if (call_targets.IsNull()) {
call_targets = Array::New(Code::kSCallTableEntryLength);
} else {
call_targets = Array::Grow(
call_targets, call_targets.Length() + Code::kSCallTableEntryLength);
}
StaticCallsTable table(call_targets);
auto entry = table[table.Length() - 1];
entry.Set<Code::kSCallTableKindAndOffset>(kind_and_offset);
entry.Set<Code::kSCallTableCodeOrTypeTarget>(target);
entry.Set<Code::kSCallTableFunctionTarget>(
Function::Handle(Function::null()));
code.set_static_calls_target_table(call_targets);
}
void BuildImageAndRunTest(
std::function<void(const GrowableArray<ImageWriterCommand>&, uword*)>
fun) {
auto& image = Instructions::Handle();
uword entrypoint = 0;
{
GrowableArray<CodePtr> raw_codes;
for (auto code : codes) {
raw_codes.Add(code->ptr());
}
GrowableArray<ImageWriterCommand> commands;
CodeRelocator::Relocate(thread, &raw_codes, &commands);
uword expected_offset = 0;
fun(commands, &expected_offset);
image = BuildImage(&commands);
entrypoint = image.EntryPoint() + expected_offset;
for (intptr_t i = 0; i < commands.length(); ++i) {
if (commands[i].op == ImageWriterCommand::InsertBytesOfTrampoline) {
delete[] commands[i].insert_trampoline_bytes.buffer;
commands[i].insert_trampoline_bytes.buffer = nullptr;
}
}
}
typedef intptr_t (*Fun)() DART_UNUSED;
#if defined(TARGET_ARCH_X64)
EXPECT_EQ(42, reinterpret_cast<Fun>(entrypoint)());
#elif defined(TARGET_ARCH_ARM) || defined(TARGET_ARCH_RISCV32)
EXPECT_EQ(42, EXECUTE_TEST_CODE_INT32(Fun, entrypoint));
#elif defined(TARGET_ARCH_ARM64) || defined(TARGET_ARCH_RISCV64)
EXPECT_EQ(42, EXECUTE_TEST_CODE_INT64(Fun, entrypoint));
#endif
}
InstructionsPtr BuildImage(GrowableArray<ImageWriterCommand>* commands) {
intptr_t size = 0;
for (intptr_t i = 0; i < commands->length(); ++i) {
switch ((*commands)[i].op) {
case ImageWriterCommand::InsertBytesOfTrampoline:
size += (*commands)[i].insert_trampoline_bytes.buffer_length;
break;
case ImageWriterCommand::InsertPadding:
size += (*commands)[i].insert_padding.padding_length;
break;
case ImageWriterCommand::InsertInstructionOfCode:
size += ImageWriter::SizeInSnapshot(Code::InstructionsOf(
(*commands)[i].insert_instruction_of_code.code));
break;
}
}
auto& instructions = Instructions::Handle(Instructions::New(
size, /*has_monomorphic_entry=*/false, /*should_be_aligned=*/false));
{
uword addr = instructions.PayloadStart();
for (intptr_t i = 0; i < commands->length(); ++i) {
switch ((*commands)[i].op) {
case ImageWriterCommand::InsertBytesOfTrampoline: {
const auto entry = (*commands)[i].insert_trampoline_bytes;
const auto current_size = entry.buffer_length;
ASSERT(addr + current_size <= instructions.PayloadStart() + size);
memmove(reinterpret_cast<void*>(addr), entry.buffer, current_size);
addr += current_size;
break;
}
case ImageWriterCommand::InsertPadding: {
const auto entry = (*commands)[i].insert_padding;
const auto current_size = entry.padding_length;
ASSERT(addr + current_size <= instructions.PayloadStart() + size);
memset(reinterpret_cast<void*>(addr), 0, current_size);
addr += current_size;
break;
}
case ImageWriterCommand::InsertInstructionOfCode: {
const auto entry = (*commands)[i].insert_instruction_of_code;
const auto current_size =
ImageWriter::SizeInSnapshot(Code::InstructionsOf(entry.code));
ASSERT(addr + current_size <= instructions.PayloadStart() + size);
memmove(reinterpret_cast<void*>(addr),
reinterpret_cast<void*>(Instructions::PayloadStart(
Code::InstructionsOf(entry.code))),
current_size);
addr += current_size;
break;
}
}
}
if (FLAG_write_protect_code) {
ASSERT(!VirtualMemory::ShouldDualMapExecutablePages());
const uword address = UntaggedObject::ToAddr(instructions.ptr());
const auto size = instructions.ptr()->untag()->HeapSize();
VirtualMemory::WriteProtectCode(reinterpret_cast<void*>(address), size);
}
CPU::FlushICache(instructions.PayloadStart(), instructions.Size());
}
return instructions.ptr();
}
Thread* thread;
SafepointWriteRwLocker locker;
GcSafepointOperationScope safepoint_scope;
GrowableArray<const Code*> codes;
};
ISOLATE_UNIT_TEST_CASE(CodeRelocator_DirectForwardCall) {
RelocatorTestHelper helper(thread);
const intptr_t fmax = FLAG_upper_pc_relative_call_distance;
// The gap is 8 bytes smaller than what could be directly forward-called,
// because the relocator's decision when to insert a trampoline is purely
// based on whether unresolved calls can reach such a trampoline if the next
// instruction is emitted (not taking into account that the next instruction
// might actually make some of those unresolved calls resolved).
helper.CreateInstructions({
20, // caller (call instruction @helper.kOffsetOfCall)
fmax - (20 - helper.kOffsetOfCall) - 8, // 8 bytes less than maximum gap
8 // forward call target
});
helper.EmitPcRelativeCallFunction(0, 2);
helper.EmitReturn42Function(2);
helper.BuildImageAndRunTest(
[&](const GrowableArray<ImageWriterCommand>& commands,
uword* entry_point) {
EXPECT_EQ(3, commands.length());
// This makes an in-range forward call.
EXPECT_EQ(ImageWriterCommand::InsertInstructionOfCode, commands[0].op);
EXPECT_EQ(ImageWriterCommand::InsertInstructionOfCode, commands[1].op);
// This is is the target of the forwards call.
EXPECT_EQ(ImageWriterCommand::InsertInstructionOfCode, commands[2].op);
*entry_point = commands[0].expected_offset;
});
}
ISOLATE_UNIT_TEST_CASE(CodeRelocator_OutOfRangeForwardCall) {
RelocatorTestHelper helper(thread);
const intptr_t fmax = FLAG_upper_pc_relative_call_distance;
helper.CreateInstructions({
20, // caller (call instruction @helper.kOffsetOfCall)
fmax - (20 - helper.kOffsetOfCall) + 4, // 4 bytes above maximum gap
8 // forwards call target
});
helper.EmitPcRelativeCallFunction(0, 2);
helper.EmitReturn42Function(2);
helper.BuildImageAndRunTest(
[&](const GrowableArray<ImageWriterCommand>& commands,
uword* entry_point) {
EXPECT_EQ(4, commands.length());
// This makes an out-of-range forward call.
EXPECT_EQ(ImageWriterCommand::InsertInstructionOfCode, commands[0].op);
// This is the last change the relocator thinks it can ensure the
// out-of-range call above can call a trampoline - so it injets it here
// and no later.
EXPECT_EQ(ImageWriterCommand::InsertBytesOfTrampoline, commands[1].op);
EXPECT_EQ(ImageWriterCommand::InsertInstructionOfCode, commands[2].op);
// This is the target of the forwards call.
EXPECT_EQ(ImageWriterCommand::InsertInstructionOfCode, commands[3].op);
*entry_point = commands[0].expected_offset;
});
}
ISOLATE_UNIT_TEST_CASE(CodeRelocator_DirectBackwardCall) {
RelocatorTestHelper helper(thread);
const intptr_t bmax = -FLAG_lower_pc_relative_call_distance;
helper.CreateInstructions({
8, // backwards call target
bmax - 8 - helper.kOffsetOfCall, // maximize out backwards call range
20 // caller (call instruction @helper.kOffsetOfCall)
});
helper.EmitReturn42Function(0);
helper.EmitPcRelativeCallFunction(2, 0);
helper.BuildImageAndRunTest(
[&](const GrowableArray<ImageWriterCommand>& commands,
uword* entry_point) {
EXPECT_EQ(3, commands.length());
// This is the backwards call target.
EXPECT_EQ(ImageWriterCommand::InsertInstructionOfCode, commands[0].op);
EXPECT_EQ(ImageWriterCommand::InsertInstructionOfCode, commands[1].op);
// This makes an in-range backwards call.
EXPECT_EQ(ImageWriterCommand::InsertInstructionOfCode, commands[2].op);
*entry_point = commands[2].expected_offset;
});
}
ISOLATE_UNIT_TEST_CASE(CodeRelocator_OutOfRangeBackwardCall) {
RelocatorTestHelper helper(thread);
const intptr_t bmax = -FLAG_lower_pc_relative_call_distance;
const intptr_t fmax = FLAG_upper_pc_relative_call_distance;
helper.CreateInstructions({
8, // backward call target
bmax - 8 - helper.kOffsetOfCall + 4, // 4 bytes exceeding backwards range
20, // caller (call instruction @helper.kOffsetOfCall)
fmax - (20 - helper.kOffsetOfCall) -
4, // 4 bytes less than forward range
4,
4, // out-of-range, so trampoline has to be inserted before this
});
helper.EmitReturn42Function(0);
helper.EmitPcRelativeCallFunction(2, 0);
helper.BuildImageAndRunTest(
[&](const GrowableArray<ImageWriterCommand>& commands,
uword* entry_point) {
EXPECT_EQ(7, commands.length());
// This is the backwards call target.
EXPECT_EQ(ImageWriterCommand::InsertInstructionOfCode, commands[0].op);
EXPECT_EQ(ImageWriterCommand::InsertInstructionOfCode, commands[1].op);
// This makes an out-of-range backwards call. The relocator will make
// the call go to a trampoline instead. It will delay insertion of the
// trampoline until it almost becomes out-of-range.
EXPECT_EQ(ImageWriterCommand::InsertInstructionOfCode, commands[2].op);
EXPECT_EQ(ImageWriterCommand::InsertInstructionOfCode, commands[3].op);
EXPECT_EQ(ImageWriterCommand::InsertInstructionOfCode, commands[4].op);
// This is the last change the relocator thinks it can ensure the
// out-of-range call above can call a trampoline - so it injets it here
// and no later.
EXPECT_EQ(ImageWriterCommand::InsertBytesOfTrampoline, commands[5].op);
EXPECT_EQ(ImageWriterCommand::InsertInstructionOfCode, commands[6].op);
*entry_point = commands[2].expected_offset;
});
}
ISOLATE_UNIT_TEST_CASE(CodeRelocator_OutOfRangeBackwardCall2) {
RelocatorTestHelper helper(thread);
const intptr_t bmax = -FLAG_lower_pc_relative_call_distance;
helper.CreateInstructions({
8, // backwards call target
bmax - 8 - helper.kOffsetOfCall + 4, // 4 bytes exceeding backwards range
20, // caller (call instruction @helper.kOffsetOfCall)
4,
});
helper.EmitReturn42Function(0);
helper.EmitPcRelativeCallFunction(2, 0);
helper.BuildImageAndRunTest(
[&](const GrowableArray<ImageWriterCommand>& commands,
uword* entry_point) {
EXPECT_EQ(5, commands.length());
// This is the backwards call target.
EXPECT_EQ(ImageWriterCommand::InsertInstructionOfCode, commands[0].op);
EXPECT_EQ(ImageWriterCommand::InsertInstructionOfCode, commands[1].op);
// This makes an out-of-range backwards call. The relocator will make
// the call go to a trampoline instead. It will delay insertion of the
// trampoline until it almost becomes out-of-range (or in this case no
// more instructions follow).
EXPECT_EQ(ImageWriterCommand::InsertInstructionOfCode, commands[2].op);
EXPECT_EQ(ImageWriterCommand::InsertInstructionOfCode, commands[3].op);
// There's no other instructions coming, so the relocator will resolve
// any pending out-of-range calls by inserting trampolines at the end.
EXPECT_EQ(ImageWriterCommand::InsertBytesOfTrampoline, commands[4].op);
*entry_point = commands[4].expected_offset;
});
}
UNIT_TEST_CASE(PCRelativeCallPatterns) {
{
uint8_t instruction[PcRelativeCallPattern::kLengthInBytes] = {};
PcRelativeCallPattern pattern(reinterpret_cast<uword>(&instruction));
pattern.set_distance(PcRelativeCallPattern::kLowerCallingRange);
EXPECT_EQ(PcRelativeCallPattern::kLowerCallingRange, pattern.distance());
pattern.set_distance(PcRelativeCallPattern::kUpperCallingRange);
EXPECT_EQ(PcRelativeCallPattern::kUpperCallingRange, pattern.distance());
}
{
uint8_t instruction[PcRelativeTailCallPattern::kLengthInBytes] = {};
PcRelativeTailCallPattern pattern(reinterpret_cast<uword>(&instruction));
pattern.set_distance(PcRelativeTailCallPattern::kLowerCallingRange);
EXPECT_EQ(PcRelativeTailCallPattern::kLowerCallingRange,
pattern.distance());
pattern.set_distance(PcRelativeTailCallPattern::kUpperCallingRange);
EXPECT_EQ(PcRelativeTailCallPattern::kUpperCallingRange,
pattern.distance());
}
}
#endif // defined(DART_PRECOMPILER) && !defined(TARGET_ARCH_IA32)
} // namespace dart