Skip to content

ZJIT: Fix SP alignment on JIT-to-JIT calls for x86_64 #13900

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Jul 16, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions test/ruby/test_zjit.rb
Original file line number Diff line number Diff line change
Expand Up @@ -999,6 +999,26 @@ def test_require_rubygems_with_auto_compact
}, call_threshold: 2
end

def test_profile_under_nested_jit_call
assert_compiles '[nil, nil, 3]', %q{
def profile
1 + 2
end

def jit_call(flag)
if flag
profile
end
end

def entry(flag)
jit_call(flag)
end

[entry(false), entry(false), entry(true)]
}, call_threshold: 2
end

def test_bop_redefinition
assert_runs '[3, :+, 100]', %q{
def test
Expand Down
68 changes: 41 additions & 27 deletions zjit/src/backend/lir.rs
Original file line number Diff line number Diff line change
Expand Up @@ -270,6 +270,14 @@ impl From<VALUE> for Opnd {
}
}

/// Set of things we need to restore for side exits.
#[derive(Clone, Debug)]
pub struct SideExitContext {
pub pc: *const VALUE,
pub stack: Vec<Opnd>,
pub locals: Vec<Opnd>,
}

/// Branch target (something that we can jump to)
/// for branch instructions
#[derive(Clone, Debug)]
Expand All @@ -281,12 +289,14 @@ pub enum Target
Label(Label),
/// Side exit to the interpreter
SideExit {
pc: *const VALUE,
stack: Vec<Opnd>,
locals: Vec<Opnd>,
c_stack_bytes: usize,
/// Context to restore on regular side exits. None for side exits right
/// after JIT-to-JIT calls because we restore them before the JIT call.
context: Option<SideExitContext>,
/// We use this to enrich asm comments.
reason: SideExitReason,
// Some if the side exit should write this label. We use it for patch points.
/// The number of bytes we need to adjust the C stack pointer by.
c_stack_bytes: usize,
/// Some if the side exit should write this label. We use it for patch points.
label: Option<Label>,
},
}
Expand Down Expand Up @@ -767,7 +777,7 @@ impl<'a> Iterator for InsnOpndIterator<'a> {
Insn::Label(target) |
Insn::LeaJumpTarget { target, .. } |
Insn::PatchPoint(target) => {
if let Target::SideExit { stack, locals, .. } = target {
if let Target::SideExit { context: Some(SideExitContext { stack, locals, .. }), .. } = target {
let stack_idx = self.idx;
if stack_idx < stack.len() {
let opnd = &stack[stack_idx];
Expand All @@ -792,7 +802,7 @@ impl<'a> Iterator for InsnOpndIterator<'a> {
return Some(opnd);
}

if let Target::SideExit { stack, locals, .. } = target {
if let Target::SideExit { context: Some(SideExitContext { stack, locals, .. }), .. } = target {
let stack_idx = self.idx - 1;
if stack_idx < stack.len() {
let opnd = &stack[stack_idx];
Expand Down Expand Up @@ -923,7 +933,7 @@ impl<'a> InsnOpndMutIterator<'a> {
Insn::Label(target) |
Insn::LeaJumpTarget { target, .. } |
Insn::PatchPoint(target) => {
if let Target::SideExit { stack, locals, .. } = target {
if let Target::SideExit { context: Some(SideExitContext { stack, locals, .. }), .. } = target {
let stack_idx = self.idx;
if stack_idx < stack.len() {
let opnd = &mut stack[stack_idx];
Expand All @@ -948,7 +958,7 @@ impl<'a> InsnOpndMutIterator<'a> {
return Some(opnd);
}

if let Target::SideExit { stack, locals, .. } = target {
if let Target::SideExit { context: Some(SideExitContext { stack, locals, .. }), .. } = target {
let stack_idx = self.idx - 1;
if stack_idx < stack.len() {
let opnd = &mut stack[stack_idx];
Expand Down Expand Up @@ -1803,7 +1813,7 @@ impl Assembler
for (idx, target) in targets {
// Compile a side exit. Note that this is past the split pass and alloc_regs(),
// so you can't use a VReg or an instruction that needs to be split.
if let Target::SideExit { pc, stack, locals, c_stack_bytes, reason, label } = target {
if let Target::SideExit { context, reason, c_stack_bytes, label } = target {
asm_comment!(self, "Exit: {reason}");
let side_exit_label = if let Some(label) = label {
Target::Label(label)
Expand All @@ -1823,26 +1833,30 @@ impl Assembler
}
}

asm_comment!(self, "write stack slots: {stack:?}");
for (idx, &opnd) in stack.iter().enumerate() {
let opnd = split_store_source(self, opnd);
self.store(Opnd::mem(64, SP, idx as i32 * SIZEOF_VALUE_I32), opnd);
}
// Restore the PC and the stack for regular side exits. We don't do this for
// side exits right after JIT-to-JIT calls, which restore them before the call.
if let Some(SideExitContext { pc, stack, locals }) = context {
asm_comment!(self, "write stack slots: {stack:?}");
for (idx, &opnd) in stack.iter().enumerate() {
let opnd = split_store_source(self, opnd);
self.store(Opnd::mem(64, SP, idx as i32 * SIZEOF_VALUE_I32), opnd);
}

asm_comment!(self, "write locals: {locals:?}");
for (idx, &opnd) in locals.iter().enumerate() {
let opnd = split_store_source(self, opnd);
self.store(Opnd::mem(64, SP, (-local_size_and_idx_to_ep_offset(locals.len(), idx) - 1) * SIZEOF_VALUE_I32), opnd);
}
asm_comment!(self, "write locals: {locals:?}");
for (idx, &opnd) in locals.iter().enumerate() {
let opnd = split_store_source(self, opnd);
self.store(Opnd::mem(64, SP, (-local_size_and_idx_to_ep_offset(locals.len(), idx) - 1) * SIZEOF_VALUE_I32), opnd);
}

asm_comment!(self, "save cfp->pc");
self.load_into(Opnd::Reg(Assembler::SCRATCH_REG), Opnd::const_ptr(pc));
self.store(Opnd::mem(64, CFP, RUBY_OFFSET_CFP_PC), Opnd::Reg(Assembler::SCRATCH_REG));
asm_comment!(self, "save cfp->pc");
self.load_into(Opnd::Reg(Assembler::SCRATCH_REG), Opnd::const_ptr(pc));
self.store(Opnd::mem(64, CFP, RUBY_OFFSET_CFP_PC), Opnd::Reg(Assembler::SCRATCH_REG));

asm_comment!(self, "save cfp->sp");
self.lea_into(Opnd::Reg(Assembler::SCRATCH_REG), Opnd::mem(64, SP, stack.len() as i32 * SIZEOF_VALUE_I32));
let cfp_sp = Opnd::mem(64, CFP, RUBY_OFFSET_CFP_SP);
self.store(cfp_sp, Opnd::Reg(Assembler::SCRATCH_REG));
asm_comment!(self, "save cfp->sp");
self.lea_into(Opnd::Reg(Assembler::SCRATCH_REG), Opnd::mem(64, SP, stack.len() as i32 * SIZEOF_VALUE_I32));
let cfp_sp = Opnd::mem(64, CFP, RUBY_OFFSET_CFP_SP);
self.store(cfp_sp, Opnd::Reg(Assembler::SCRATCH_REG));
}

if c_stack_bytes > 0 {
asm_comment!(self, "restore C stack pointer");
Expand Down
38 changes: 20 additions & 18 deletions zjit/src/codegen.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ use crate::invariants::track_bop_assumption;
use crate::gc::get_or_create_iseq_payload;
use crate::state::ZJITState;
use crate::{asm::CodeBlock, cruby::*, options::debug, virtualmem::CodePtr};
use crate::backend::lir::{self, asm_comment, asm_ccall, Assembler, Opnd, Target, CFP, C_ARG_OPNDS, C_RET_OPND, EC, NATIVE_STACK_PTR, SP};
use crate::backend::lir::{self, asm_comment, asm_ccall, Assembler, Opnd, SideExitContext, Target, CFP, C_ARG_OPNDS, C_RET_OPND, EC, NATIVE_STACK_PTR, SP};
use crate::hir::{iseq_to_hir, Block, BlockId, BranchEdge, CallInfo, Invariant, RangeType, SideExitReason, SideExitReason::*, SpecialObjectType, SELF_PARAM_IDX};
use crate::hir::{Const, FrameState, Function, Insn, InsnId};
use crate::hir_type::{types::Fixnum, Type};
Expand Down Expand Up @@ -156,10 +156,6 @@ fn gen_entry(cb: &mut CodeBlock, iseq: IseqPtr, function: &Function, function_pt

// Restore registers for CFP, EC, and SP after use
asm_comment!(asm, "exit to the interpreter");
// On x86_64, maintain 16-byte stack alignment
if cfg!(target_arch = "x86_64") {
asm.cpop_into(SP);
}
asm.cpop_into(SP);
asm.cpop_into(EC);
asm.cpop_into(CFP);
Expand Down Expand Up @@ -511,10 +507,6 @@ fn gen_entry_prologue(asm: &mut Assembler, iseq: IseqPtr) {
asm.cpush(CFP);
asm.cpush(EC);
asm.cpush(SP);
// On x86_64, maintain 16-byte stack alignment
if cfg!(target_arch = "x86_64") {
asm.cpush(SP);
}
Comment on lines -514 to -517
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice, this looked suspect to me, so I'm glad we're fixing it.


// EC and CFP are passed as arguments
asm.mov(EC, C_ARG_OPNDS[0]);
Expand Down Expand Up @@ -782,7 +774,8 @@ fn gen_send_without_block_direct(
// TODO: Let side exit code pop all JIT frames to optimize away this cmp + je.
asm_comment!(asm, "side-exit if callee side-exits");
asm.cmp(ret, Qundef.into());
asm.je(ZJITState::get_exit_trampoline().into());
// Restore the C stack pointer on exit
asm.je(Target::SideExit { context: None, reason: CalleeSideExit, c_stack_bytes: jit.c_stack_bytes, label: None });

asm_comment!(asm, "restore SP register for the caller");
let new_sp = asm.sub(SP, sp_offset.into());
Expand Down Expand Up @@ -1120,11 +1113,13 @@ fn build_side_exit(jit: &mut JITState, state: &FrameState, reason: SideExitReaso
}

let target = Target::SideExit {
pc: state.pc,
stack,
locals,
c_stack_bytes: jit.c_stack_bytes,
context: Some(SideExitContext {
pc: state.pc,
stack,
locals,
}),
reason,
c_stack_bytes: jit.c_stack_bytes,
label,
};
Some(target)
Expand Down Expand Up @@ -1157,12 +1152,19 @@ fn max_num_params(function: &Function) -> usize {
/// the function needs to allocate on the stack for the stack frame.
fn aligned_stack_bytes(num_slots: usize) -> usize {
// Both x86_64 and arm64 require the stack to be aligned to 16 bytes.
// Since SIZEOF_VALUE is 8 bytes, we need to round up the size to the nearest even number.
let num_slots = if num_slots % 2 == 0 {
num_slots
} else {
let num_slots = if cfg!(target_arch = "x86_64") && num_slots % 2 == 0 {
// On x86_64, since the call instruction bumps the stack pointer by 8 bytes on entry,
// we need to round up `num_slots` to an odd number.
num_slots + 1
} else if cfg!(target_arch = "aarch64") && num_slots % 2 == 1 {
// On arm64, the stack pointer is always aligned to 16 bytes, so we need to round up
// `num_slots`` to an even number.
num_slots + 1
} else {
num_slots
};

const { assert!(SIZEOF_VALUE == 8, "aligned_stack_bytes() assumes SIZEOF_VALUE == 8"); }
num_slots * SIZEOF_VALUE
}

Expand Down
1 change: 1 addition & 0 deletions zjit/src/hir.rs
Original file line number Diff line number Diff line change
Expand Up @@ -414,6 +414,7 @@ pub enum SideExitReason {
GuardType(Type),
GuardBitEquals(VALUE),
PatchPoint(Invariant),
CalleeSideExit,
}

impl std::fmt::Display for SideExitReason {
Expand Down
Loading
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy