3cd6705f58
On x64, we allocate one big code region such that we can use near jumps and near calls. The jump table did not make use of that design yet. This CL changes that by emitting jump table slots as near jumps. This also speeds up patching jump table slots significantly, since far jumps populate the inline constant pool, which is unneeded overhead in this case. As a drive-by, this CL cleans up the API of near_call and near_jmp. The current semantics is broken, and only works because this is only used for WebAssembly calls which are patched anyway after code generation. Also, x64 now uses the same path in test-jump-table-assembler.cc as arm64 to ensure that all targets are within near-call-distance. R=mstarzinger@chromium.org Bug: v8:8916 Change-Id: Iffc34e248b72167307ffdab62dd2212c4ae86a32 Reviewed-on: https://chromium-review.googlesource.com/c/v8/v8/+/1561313 Reviewed-by: Michael Starzinger <mstarzinger@chromium.org> Commit-Queue: Clemens Hammacher <clemensh@chromium.org> Cr-Commit-Position: refs/heads/master@{#60777}
263 lines
9.5 KiB
C++
263 lines
9.5 KiB
C++
// Copyright 2018 the V8 project authors. All rights reserved.
|
|
// Use of this source code is governed by a BSD-style license that can be
|
|
// found in the LICENSE file.
|
|
|
|
#include <bitset>
|
|
|
|
#include "src/assembler-inl.h"
|
|
#include "src/macro-assembler-inl.h"
|
|
#include "src/simulator.h"
|
|
#include "src/utils.h"
|
|
#include "src/wasm/jump-table-assembler.h"
|
|
#include "test/cctest/cctest.h"
|
|
#include "test/common/assembler-tester.h"
|
|
|
|
namespace v8 {
|
|
namespace internal {
|
|
namespace wasm {
|
|
|
|
#if 0
|
|
#define TRACE(...) PrintF(__VA_ARGS__)
|
|
#else
|
|
#define TRACE(...)
|
|
#endif
|
|
|
|
#define __ masm.
|
|
|
|
namespace {
|
|
|
|
static volatile int global_stop_bit = 0;
|
|
|
|
constexpr int kJumpTableSlotCount = 128;
|
|
constexpr uint32_t kJumpTableSize =
|
|
JumpTableAssembler::SizeForNumberOfSlots(kJumpTableSlotCount);
|
|
|
|
#if V8_TARGET_ARCH_ARM64 || V8_TARGET_ARCH_X64
|
|
constexpr uint32_t kAvailableBufferSlots =
|
|
(kMaxWasmCodeMemory - kJumpTableSize) / AssemblerBase::kMinimalBufferSize;
|
|
constexpr uint32_t kBufferSlotStartOffset =
|
|
RoundUp<AssemblerBase::kMinimalBufferSize>(kJumpTableSize);
|
|
#else
|
|
constexpr uint32_t kAvailableBufferSlots = 0;
|
|
#endif
|
|
|
|
Address GenerateJumpTableThunk(
|
|
Address jump_target, byte* thunk_slot_buffer,
|
|
std::bitset<kAvailableBufferSlots>* used_slots,
|
|
std::vector<std::unique_ptr<TestingAssemblerBuffer>>* thunk_buffers) {
|
|
#if V8_TARGET_ARCH_ARM64 || V8_TARGET_ARCH_X64
|
|
// To guarantee that the branch range lies within the near-call range,
|
|
// generate the thunk in the same (kMaxWasmCodeMemory-sized) buffer as the
|
|
// jump_target itself.
|
|
//
|
|
// Allocate a slot that we haven't already used. This is necessary because
|
|
// each test iteration expects to generate two unique addresses and we leave
|
|
// each slot executable (and not writable).
|
|
base::RandomNumberGenerator* rng =
|
|
CcTest::i_isolate()->random_number_generator();
|
|
// Ensure a chance of completion without too much thrashing.
|
|
DCHECK(used_slots->count() < (used_slots->size() / 2));
|
|
int buffer_index;
|
|
do {
|
|
buffer_index = rng->NextInt(kAvailableBufferSlots);
|
|
} while (used_slots->test(buffer_index));
|
|
used_slots->set(buffer_index);
|
|
byte* buffer =
|
|
thunk_slot_buffer + buffer_index * AssemblerBase::kMinimalBufferSize;
|
|
|
|
#else
|
|
USE(thunk_slot_buffer);
|
|
USE(used_slots);
|
|
thunk_buffers->emplace_back(AllocateAssemblerBuffer(
|
|
AssemblerBase::kMinimalBufferSize, GetRandomMmapAddr()));
|
|
byte* buffer = thunk_buffers->back()->start();
|
|
#endif
|
|
|
|
MacroAssembler masm(
|
|
nullptr, AssemblerOptions{}, CodeObjectRequired::kNo,
|
|
ExternalAssemblerBuffer(buffer, AssemblerBase::kMinimalBufferSize));
|
|
|
|
Label exit;
|
|
Register scratch = kReturnRegister0;
|
|
Address stop_bit_address = reinterpret_cast<Address>(&global_stop_bit);
|
|
#if V8_TARGET_ARCH_X64
|
|
__ Move(scratch, stop_bit_address, RelocInfo::NONE);
|
|
__ testl(MemOperand(scratch, 0), Immediate(1));
|
|
__ j(not_zero, &exit);
|
|
__ Jump(jump_target, RelocInfo::NONE);
|
|
#elif V8_TARGET_ARCH_IA32
|
|
__ Move(scratch, Immediate(stop_bit_address, RelocInfo::NONE));
|
|
__ test(MemOperand(scratch, 0), Immediate(1));
|
|
__ j(not_zero, &exit);
|
|
__ jmp(jump_target, RelocInfo::NONE);
|
|
#elif V8_TARGET_ARCH_ARM
|
|
__ mov(scratch, Operand(stop_bit_address, RelocInfo::NONE));
|
|
__ ldr(scratch, MemOperand(scratch, 0));
|
|
__ tst(scratch, Operand(1));
|
|
__ b(ne, &exit);
|
|
__ Jump(jump_target, RelocInfo::NONE);
|
|
#elif V8_TARGET_ARCH_ARM64
|
|
__ Mov(scratch, Operand(stop_bit_address, RelocInfo::NONE));
|
|
__ Ldr(scratch, MemOperand(scratch, 0));
|
|
__ Tbnz(scratch, 0, &exit);
|
|
__ Mov(scratch, Immediate(jump_target, RelocInfo::NONE));
|
|
__ Br(scratch);
|
|
#elif V8_TARGET_ARCH_PPC64
|
|
__ mov(scratch, Operand(stop_bit_address, RelocInfo::NONE));
|
|
__ LoadP(scratch, MemOperand(scratch));
|
|
__ cmpi(scratch, Operand::Zero());
|
|
__ bne(&exit);
|
|
__ mov(scratch, Operand(jump_target, RelocInfo::NONE));
|
|
__ Jump(scratch);
|
|
#elif V8_TARGET_ARCH_S390X
|
|
__ mov(scratch, Operand(stop_bit_address, RelocInfo::NONE));
|
|
__ LoadP(scratch, MemOperand(scratch));
|
|
__ CmpP(scratch, Operand(0));
|
|
__ bne(&exit);
|
|
__ mov(scratch, Operand(jump_target, RelocInfo::NONE));
|
|
__ Jump(scratch);
|
|
#elif V8_TARGET_ARCH_MIPS64
|
|
__ li(scratch, Operand(stop_bit_address, RelocInfo::NONE));
|
|
__ Lw(scratch, MemOperand(scratch, 0));
|
|
__ Branch(&exit, ne, scratch, Operand(zero_reg));
|
|
__ Jump(jump_target, RelocInfo::NONE);
|
|
#elif V8_TARGET_ARCH_MIPS
|
|
__ li(scratch, Operand(stop_bit_address, RelocInfo::NONE));
|
|
__ lw(scratch, MemOperand(scratch, 0));
|
|
__ Branch(&exit, ne, scratch, Operand(zero_reg));
|
|
__ Jump(jump_target, RelocInfo::NONE);
|
|
#else
|
|
#error Unsupported architecture
|
|
#endif
|
|
__ bind(&exit);
|
|
__ Ret();
|
|
|
|
CodeDesc desc;
|
|
masm.GetCode(nullptr, &desc);
|
|
return reinterpret_cast<Address>(buffer);
|
|
}
|
|
|
|
class JumpTableRunner : public v8::base::Thread {
|
|
public:
|
|
JumpTableRunner(Address slot_address, int runner_id)
|
|
: Thread(Options("JumpTableRunner")),
|
|
slot_address_(slot_address),
|
|
runner_id_(runner_id) {}
|
|
|
|
void Run() override {
|
|
TRACE("Runner #%d is starting ...\n", runner_id_);
|
|
GeneratedCode<void>::FromAddress(CcTest::i_isolate(), slot_address_).Call();
|
|
TRACE("Runner #%d is stopping ...\n", runner_id_);
|
|
USE(runner_id_);
|
|
}
|
|
|
|
private:
|
|
Address slot_address_;
|
|
int runner_id_;
|
|
};
|
|
|
|
class JumpTablePatcher : public v8::base::Thread {
|
|
public:
|
|
JumpTablePatcher(Address slot_start, uint32_t slot_index, Address thunk1,
|
|
Address thunk2)
|
|
: Thread(Options("JumpTablePatcher")),
|
|
slot_start_(slot_start),
|
|
slot_index_(slot_index),
|
|
thunks_{thunk1, thunk2} {}
|
|
|
|
void Run() override {
|
|
TRACE("Patcher is starting ...\n");
|
|
constexpr int kNumberOfPatchIterations = 64;
|
|
for (int i = 0; i < kNumberOfPatchIterations; ++i) {
|
|
TRACE(" patch slot " V8PRIxPTR_FMT " to thunk #%d\n",
|
|
slot_start_ + JumpTableAssembler::SlotIndexToOffset(slot_index_),
|
|
i % 2);
|
|
JumpTableAssembler::PatchJumpTableSlot(
|
|
slot_start_, slot_index_, thunks_[i % 2], WasmCode::kFlushICache);
|
|
}
|
|
TRACE("Patcher is stopping ...\n");
|
|
}
|
|
|
|
private:
|
|
Address slot_start_;
|
|
uint32_t slot_index_;
|
|
Address thunks_[2];
|
|
};
|
|
|
|
} // namespace
|
|
|
|
// This test is intended to stress concurrent patching of jump-table slots. It
|
|
// uses the following setup:
|
|
// 1) Picks a particular slot of the jump-table. Slots are iterated over to
|
|
// ensure multiple entries (at different offset alignments) are tested.
|
|
// 2) Starts multiple runners that spin through the above slot. The runners
|
|
// use thunk code that will jump to the same jump-table slot repeatedly
|
|
// until the {global_stop_bit} indicates a test-end condition.
|
|
// 3) Start a patcher that repeatedly patches the jump-table slot back and
|
|
// forth between two thunk. If there is a race then chances are high that
|
|
// one of the runners is currently executing the jump-table slot.
|
|
TEST(JumpTablePatchingStress) {
|
|
constexpr int kNumberOfRunnerThreads = 5;
|
|
|
|
#if V8_TARGET_ARCH_ARM64 || V8_TARGET_ARCH_X64
|
|
// We need the branches (from GenerateJumpTableThunk) to be within near-call
|
|
// range of the jump table slots. The address hint to AllocateAssemblerBuffer
|
|
// is not reliable enough to guarantee that we can always achieve this with
|
|
// separate allocations, so for Arm64 we generate all code in a single
|
|
// kMaxMasmCodeMemory-sized chunk.
|
|
//
|
|
// TODO(wasm): Currently {kMaxWasmCodeMemory} limits code sufficiently, so
|
|
// that the jump table only supports {near_call} distances.
|
|
STATIC_ASSERT(kMaxWasmCodeMemory >= kJumpTableSize);
|
|
auto buffer = AllocateAssemblerBuffer(kMaxWasmCodeMemory);
|
|
byte* thunk_slot_buffer = buffer->start() + kBufferSlotStartOffset;
|
|
#else
|
|
auto buffer = AllocateAssemblerBuffer(kJumpTableSize);
|
|
byte* thunk_slot_buffer = nullptr;
|
|
#endif
|
|
|
|
std::bitset<kAvailableBufferSlots> used_thunk_slots;
|
|
buffer->MakeWritableAndExecutable();
|
|
|
|
// Iterate through jump-table slots to hammer at different alignments within
|
|
// the jump-table, thereby increasing stress for variable-length ISAs.
|
|
Address slot_start = reinterpret_cast<Address>(buffer->start());
|
|
for (int slot = 0; slot < kJumpTableSlotCount; ++slot) {
|
|
TRACE("Hammering on jump table slot #%d ...\n", slot);
|
|
uint32_t slot_offset = JumpTableAssembler::SlotIndexToOffset(slot);
|
|
std::vector<std::unique_ptr<TestingAssemblerBuffer>> thunk_buffers;
|
|
Address thunk1 =
|
|
GenerateJumpTableThunk(slot_start + slot_offset, thunk_slot_buffer,
|
|
&used_thunk_slots, &thunk_buffers);
|
|
Address thunk2 =
|
|
GenerateJumpTableThunk(slot_start + slot_offset, thunk_slot_buffer,
|
|
&used_thunk_slots, &thunk_buffers);
|
|
TRACE(" generated thunk1: " V8PRIxPTR_FMT "\n", thunk1);
|
|
TRACE(" generated thunk2: " V8PRIxPTR_FMT "\n", thunk2);
|
|
JumpTableAssembler::PatchJumpTableSlot(slot_start, slot, thunk1,
|
|
WasmCode::kFlushICache);
|
|
|
|
for (auto& buf : thunk_buffers) buf->MakeExecutable();
|
|
// Start multiple runner threads and a patcher thread that hammer on the
|
|
// same jump-table slot concurrently.
|
|
std::list<JumpTableRunner> runners;
|
|
for (int runner = 0; runner < kNumberOfRunnerThreads; ++runner) {
|
|
runners.emplace_back(slot_start + slot_offset, runner);
|
|
}
|
|
JumpTablePatcher patcher(slot_start, slot, thunk1, thunk2);
|
|
global_stop_bit = 0; // Signal runners to keep going.
|
|
for (auto& runner : runners) runner.Start();
|
|
patcher.Start();
|
|
patcher.Join();
|
|
global_stop_bit = -1; // Signal runners to stop.
|
|
for (auto& runner : runners) runner.Join();
|
|
}
|
|
}
|
|
|
|
#undef __
|
|
#undef TRACE
|
|
|
|
} // namespace wasm
|
|
} // namespace internal
|
|
} // namespace v8
|