1dc4277e5b
TEST=ci Change-Id: Ic162deea4a39869a158726616e5c8dc0ff058817 Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/475781 Reviewed-by: Alexander Markov <alexmarkov@google.com> Commit-Queue: Ryan Macnak <rmacnak@google.com>
470 lines
18 KiB
C++
470 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,
|
|
/*is_vm_isolate=*/false);
|
|
|
|
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
|