Working towards having YJIT and ZJIT in the same build, we need to deduplicate some glue code that would otherwise cause name collision. Add jit.c for this and build it for YJIT and ZJIT builds. Update bindgen to look at jit.c; some shuffling of functions in the output, but the set of functions shouldn't have changed.
336 lines
10 KiB
C
336 lines
10 KiB
C
#include "internal.h"
|
|
#include "internal/sanitizers.h"
|
|
#include "internal/string.h"
|
|
#include "internal/hash.h"
|
|
#include "internal/variable.h"
|
|
#include "internal/compile.h"
|
|
#include "internal/class.h"
|
|
#include "internal/fixnum.h"
|
|
#include "internal/numeric.h"
|
|
#include "internal/gc.h"
|
|
#include "internal/vm.h"
|
|
#include "vm_core.h"
|
|
#include "vm_callinfo.h"
|
|
#include "builtin.h"
|
|
#include "insns.inc"
|
|
#include "insns_info.inc"
|
|
#include "vm_sync.h"
|
|
#include "vm_insnhelper.h"
|
|
#include "probes.h"
|
|
#include "probes_helper.h"
|
|
#include "iseq.h"
|
|
#include "ruby/debug.h"
|
|
#include "internal/cont.h"
|
|
#include "zjit.h"
|
|
|
|
// For mmapp(), sysconf()
|
|
#ifndef _WIN32
|
|
#include <unistd.h>
|
|
#include <sys/mman.h>
|
|
#endif
|
|
|
|
#include <errno.h>
|
|
|
|
uint32_t
|
|
rb_zjit_get_page_size(void)
|
|
{
|
|
#if defined(_SC_PAGESIZE)
|
|
long page_size = sysconf(_SC_PAGESIZE);
|
|
if (page_size <= 0) rb_bug("zjit: failed to get page size");
|
|
|
|
// 1 GiB limit. x86 CPUs with PDPE1GB can do this and anything larger is unexpected.
|
|
// Though our design sort of assume we have fine grained control over memory protection
|
|
// which require small page sizes.
|
|
if (page_size > 0x40000000l) rb_bug("zjit page size too large");
|
|
|
|
return (uint32_t)page_size;
|
|
#else
|
|
#error "ZJIT supports POSIX only for now"
|
|
#endif
|
|
}
|
|
|
|
#if defined(MAP_FIXED_NOREPLACE) && defined(_SC_PAGESIZE)
|
|
// Align the current write position to a multiple of bytes
|
|
static uint8_t *
|
|
align_ptr(uint8_t *ptr, uint32_t multiple)
|
|
{
|
|
// Compute the pointer modulo the given alignment boundary
|
|
uint32_t rem = ((uint32_t)(uintptr_t)ptr) % multiple;
|
|
|
|
// If the pointer is already aligned, stop
|
|
if (rem == 0)
|
|
return ptr;
|
|
|
|
// Pad the pointer by the necessary amount to align it
|
|
uint32_t pad = multiple - rem;
|
|
|
|
return ptr + pad;
|
|
}
|
|
#endif
|
|
|
|
// Address space reservation. Memory pages are mapped on an as needed basis.
|
|
// See the Rust mm module for details.
|
|
uint8_t *
|
|
rb_zjit_reserve_addr_space(uint32_t mem_size)
|
|
{
|
|
#ifndef _WIN32
|
|
uint8_t *mem_block;
|
|
|
|
// On Linux
|
|
#if defined(MAP_FIXED_NOREPLACE) && defined(_SC_PAGESIZE)
|
|
uint32_t const page_size = (uint32_t)sysconf(_SC_PAGESIZE);
|
|
uint8_t *const cfunc_sample_addr = (void *)(uintptr_t)&rb_zjit_reserve_addr_space;
|
|
uint8_t *const probe_region_end = cfunc_sample_addr + INT32_MAX;
|
|
// Align the requested address to page size
|
|
uint8_t *req_addr = align_ptr(cfunc_sample_addr, page_size);
|
|
|
|
// Probe for addresses close to this function using MAP_FIXED_NOREPLACE
|
|
// to improve odds of being in range for 32-bit relative call instructions.
|
|
do {
|
|
mem_block = mmap(
|
|
req_addr,
|
|
mem_size,
|
|
PROT_NONE,
|
|
MAP_PRIVATE | MAP_ANONYMOUS | MAP_FIXED_NOREPLACE,
|
|
-1,
|
|
0
|
|
);
|
|
|
|
// If we succeeded, stop
|
|
if (mem_block != MAP_FAILED) {
|
|
ruby_annotate_mmap(mem_block, mem_size, "Ruby:rb_zjit_reserve_addr_space");
|
|
break;
|
|
}
|
|
|
|
// -4MiB. Downwards to probe away from the heap. (On x86/A64 Linux
|
|
// main_code_addr < heap_addr, and in case we are in a shared
|
|
// library mapped higher than the heap, downwards is still better
|
|
// since it's towards the end of the heap rather than the stack.)
|
|
req_addr -= 4 * 1024 * 1024;
|
|
} while (req_addr < probe_region_end);
|
|
|
|
// On MacOS and other platforms
|
|
#else
|
|
// Try to map a chunk of memory as executable
|
|
mem_block = mmap(
|
|
(void *)rb_zjit_reserve_addr_space,
|
|
mem_size,
|
|
PROT_NONE,
|
|
MAP_PRIVATE | MAP_ANONYMOUS,
|
|
-1,
|
|
0
|
|
);
|
|
#endif
|
|
|
|
// Fallback
|
|
if (mem_block == MAP_FAILED) {
|
|
// Try again without the address hint (e.g., valgrind)
|
|
mem_block = mmap(
|
|
NULL,
|
|
mem_size,
|
|
PROT_NONE,
|
|
MAP_PRIVATE | MAP_ANONYMOUS,
|
|
-1,
|
|
0
|
|
);
|
|
|
|
if (mem_block != MAP_FAILED) {
|
|
ruby_annotate_mmap(mem_block, mem_size, "Ruby:rb_zjit_reserve_addr_space:fallback");
|
|
}
|
|
}
|
|
|
|
// Check that the memory mapping was successful
|
|
if (mem_block == MAP_FAILED) {
|
|
perror("ruby: zjit: mmap:");
|
|
if(errno == ENOMEM) {
|
|
// No crash report if it's only insufficient memory
|
|
exit(EXIT_FAILURE);
|
|
}
|
|
rb_bug("mmap failed");
|
|
}
|
|
|
|
return mem_block;
|
|
#else
|
|
// Windows not supported for now
|
|
return NULL;
|
|
#endif
|
|
}
|
|
|
|
void rb_zjit_profile_disable(const rb_iseq_t *iseq);
|
|
|
|
void
|
|
rb_zjit_compile_iseq(const rb_iseq_t *iseq, rb_execution_context_t *ec, bool jit_exception)
|
|
{
|
|
RB_VM_LOCK_ENTER();
|
|
rb_vm_barrier();
|
|
|
|
// Convert ZJIT instructions back to bare instructions
|
|
rb_zjit_profile_disable(iseq);
|
|
|
|
// Compile a block version starting at the current instruction
|
|
uint8_t *rb_zjit_iseq_gen_entry_point(const rb_iseq_t *iseq, rb_execution_context_t *ec); // defined in Rust
|
|
uintptr_t code_ptr = (uintptr_t)rb_zjit_iseq_gen_entry_point(iseq, ec);
|
|
|
|
// TODO: support jit_exception
|
|
iseq->body->jit_entry = (rb_jit_func_t)code_ptr;
|
|
|
|
RB_VM_LOCK_LEAVE();
|
|
}
|
|
|
|
extern VALUE *rb_vm_base_ptr(struct rb_control_frame_struct *cfp);
|
|
|
|
bool
|
|
rb_zjit_multi_ractor_p(void)
|
|
{
|
|
return rb_multi_ractor_p();
|
|
}
|
|
|
|
bool
|
|
rb_zjit_constcache_shareable(const struct iseq_inline_constant_cache_entry *ice)
|
|
{
|
|
return (ice->flags & IMEMO_CONST_CACHE_SHAREABLE) != 0;
|
|
}
|
|
|
|
// Release the VM lock. The lock level must point to the same integer used to
|
|
// acquire the lock.
|
|
void
|
|
rb_zjit_vm_unlock(unsigned int *recursive_lock_level, const char *file, int line)
|
|
{
|
|
rb_vm_lock_leave(recursive_lock_level, file, line);
|
|
}
|
|
|
|
bool
|
|
rb_zjit_mark_writable(void *mem_block, uint32_t mem_size)
|
|
{
|
|
return mprotect(mem_block, mem_size, PROT_READ | PROT_WRITE) == 0;
|
|
}
|
|
|
|
void
|
|
rb_zjit_mark_executable(void *mem_block, uint32_t mem_size)
|
|
{
|
|
// Do not call mprotect when mem_size is zero. Some platforms may return
|
|
// an error for it. https://github.com/Shopify/ruby/issues/450
|
|
if (mem_size == 0) {
|
|
return;
|
|
}
|
|
if (mprotect(mem_block, mem_size, PROT_READ | PROT_EXEC)) {
|
|
rb_bug("Couldn't make JIT page (%p, %lu bytes) executable, errno: %s",
|
|
mem_block, (unsigned long)mem_size, strerror(errno));
|
|
}
|
|
}
|
|
|
|
// Free the specified memory block.
|
|
bool
|
|
rb_zjit_mark_unused(void *mem_block, uint32_t mem_size)
|
|
{
|
|
// On Linux, you need to use madvise MADV_DONTNEED to free memory.
|
|
// We might not need to call this on macOS, but it's not really documented.
|
|
// We generally prefer to do the same thing on both to ease testing too.
|
|
madvise(mem_block, mem_size, MADV_DONTNEED);
|
|
|
|
// On macOS, mprotect PROT_NONE seems to reduce RSS.
|
|
// We also call this on Linux to avoid executing unused pages.
|
|
return mprotect(mem_block, mem_size, PROT_NONE) == 0;
|
|
}
|
|
|
|
// Invalidate icache for arm64.
|
|
// `start` is inclusive and `end` is exclusive.
|
|
void
|
|
rb_zjit_icache_invalidate(void *start, void *end)
|
|
{
|
|
// Clear/invalidate the instruction cache. Compiles to nothing on x86_64
|
|
// but required on ARM before running freshly written code.
|
|
// On Darwin it's the same as calling sys_icache_invalidate().
|
|
#ifdef __GNUC__
|
|
__builtin___clear_cache(start, end);
|
|
#elif defined(__aarch64__)
|
|
#error No instruction cache clear available with this compiler on Aarch64!
|
|
#endif
|
|
}
|
|
|
|
// Acquire the VM lock and then signal all other Ruby threads (ractors) to
|
|
// contend for the VM lock, putting them to sleep. ZJIT uses this to evict
|
|
// threads running inside generated code so among other things, it can
|
|
// safely change memory protection of regions housing generated code.
|
|
void
|
|
rb_zjit_vm_lock_then_barrier(unsigned int *recursive_lock_level, const char *file, int line)
|
|
{
|
|
rb_vm_lock_enter(recursive_lock_level, file, line);
|
|
rb_vm_barrier();
|
|
}
|
|
|
|
// Convert a given ISEQ's instructions to zjit_* instructions
|
|
void
|
|
rb_zjit_profile_enable(const rb_iseq_t *iseq)
|
|
{
|
|
// This table encodes an opcode into the instruction's address
|
|
const void *const *insn_table = rb_vm_get_insns_address_table();
|
|
|
|
unsigned int insn_idx = 0;
|
|
while (insn_idx < iseq->body->iseq_size) {
|
|
int insn = rb_vm_insn_addr2opcode((void *)iseq->body->iseq_encoded[insn_idx]);
|
|
int zjit_insn = vm_bare_insn_to_zjit_insn(insn);
|
|
if (insn != zjit_insn) {
|
|
iseq->body->iseq_encoded[insn_idx] = (VALUE)insn_table[zjit_insn];
|
|
}
|
|
insn_idx += insn_len(insn);
|
|
}
|
|
}
|
|
|
|
// Convert a given ISEQ's ZJIT instructions to bare instructions
|
|
void
|
|
rb_zjit_profile_disable(const rb_iseq_t *iseq)
|
|
{
|
|
// This table encodes an opcode into the instruction's address
|
|
const void *const *insn_table = rb_vm_get_insns_address_table();
|
|
|
|
unsigned int insn_idx = 0;
|
|
while (insn_idx < iseq->body->iseq_size) {
|
|
int insn = rb_vm_insn_addr2opcode((void *)iseq->body->iseq_encoded[insn_idx]);
|
|
int bare_insn = vm_zjit_insn_to_bare_insn(insn);
|
|
if (insn != bare_insn) {
|
|
iseq->body->iseq_encoded[insn_idx] = (VALUE)insn_table[bare_insn];
|
|
}
|
|
insn_idx += insn_len(insn);
|
|
}
|
|
}
|
|
|
|
// Get profiling information for ISEQ
|
|
void *
|
|
rb_iseq_get_zjit_payload(const rb_iseq_t *iseq)
|
|
{
|
|
RUBY_ASSERT_ALWAYS(IMEMO_TYPE_P(iseq, imemo_iseq));
|
|
if (iseq->body) {
|
|
return iseq->body->zjit_payload;
|
|
}
|
|
else {
|
|
// Body is NULL when constructing the iseq.
|
|
return NULL;
|
|
}
|
|
}
|
|
|
|
// Set profiling information for ISEQ
|
|
void
|
|
rb_iseq_set_zjit_payload(const rb_iseq_t *iseq, void *payload)
|
|
{
|
|
RUBY_ASSERT_ALWAYS(IMEMO_TYPE_P(iseq, imemo_iseq));
|
|
RUBY_ASSERT_ALWAYS(iseq->body);
|
|
RUBY_ASSERT_ALWAYS(NULL == iseq->body->zjit_payload);
|
|
iseq->body->zjit_payload = payload;
|
|
}
|
|
|
|
// Primitives used by zjit.rb
|
|
VALUE rb_zjit_assert_compiles(rb_execution_context_t *ec, VALUE self);
|
|
|
|
void
|
|
rb_zjit_print_exception(void)
|
|
{
|
|
VALUE exception = rb_errinfo();
|
|
rb_set_errinfo(Qnil);
|
|
assert(RTEST(exception));
|
|
rb_warn("Ruby error: %"PRIsVALUE"", rb_funcall(exception, rb_intern("full_message"), 0));
|
|
}
|
|
|
|
// Preprocessed zjit.rb generated during build
|
|
#include "zjit.rbinc"
|