Skip to content

Commit 1f05172

Browse files
committed
ZJIT: Fix side-exit panicking when there's too many locals
Previously, ARM64 panicked due to compiled_side_exits() when the memory displacement got large enough to exceed the 9 bits limit. Usually, we split these kind of memory operands, but compiled_side_exits() runs after split. Using scratch registers, implement `Insn::Store` on ARM such that it can handle large displacements without split(). Do this for x86 as well, and remove arch specific code from compiled_side_exits(). We can now run `TestFixnum` and `TestKeywordArguments`. Since `Insn::Store` doesn't need splitting now, users enjoy lower register pressure. Downside is, using `Assembler::SCRATCH_REG` as a base register is now sometimes an error, depending on whether `Insn::Store` also needs to use the register. It seems a fair trade off since `SCRATCH_REG` is not often used, and we don't put it as a base register anywhere at the moment.
1 parent bc789ca commit 1f05172

5 files changed

Lines changed: 260 additions & 112 deletions

File tree

test/.excludes-zjit/TestFixnum.rb

Lines changed: 0 additions & 2 deletions
This file was deleted.

test/.excludes-zjit/TestKeywordArguments.rb

Lines changed: 0 additions & 1 deletion
This file was deleted.

zjit/src/backend/arm64/mod.rs

Lines changed: 171 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -195,7 +195,9 @@ impl Assembler
195195
// This register is caller-saved (so we don't have to save it before using it)
196196
pub const SCRATCH_REG: Reg = X16_REG;
197197
const SCRATCH0: A64Opnd = A64Opnd::Reg(Assembler::SCRATCH_REG);
198+
const SCRATCH0_REG: Reg = Assembler::SCRATCH_REG;
198199
const SCRATCH1: A64Opnd = A64Opnd::Reg(X17_REG);
200+
const SCRATCH1_REG: Reg = X17_REG;
199201

200202
/// Get the list of registers from which we will allocate on this platform
201203
pub fn get_alloc_regs() -> Vec<Reg> {
@@ -648,31 +650,6 @@ impl Assembler
648650
*opnd = split_load_operand(asm, *opnd);
649651
asm.push_insn(insn);
650652
},
651-
Insn::Store { dest, src } => {
652-
// The value being stored must be in a register, so if it's
653-
// not already one we'll load it first.
654-
let opnd1 = match src {
655-
// If the first operand is zero, then we can just use
656-
// the zero register.
657-
Opnd::UImm(0) | Opnd::Imm(0) => Opnd::Reg(XZR_REG),
658-
// Otherwise we'll check if we need to load it first.
659-
_ => split_load_operand(asm, *src)
660-
};
661-
662-
match dest {
663-
Opnd::Reg(_) => {
664-
// Store does not support a register as a dest operand.
665-
asm.mov(*dest, opnd1);
666-
}
667-
_ => {
668-
// The displacement for the STUR instruction can't be more
669-
// than 9 bits long. If it's longer, we need to load the
670-
// memory address into a register first.
671-
let opnd0 = split_memory_address(asm, *dest);
672-
asm.store(opnd0, opnd1);
673-
}
674-
}
675-
},
676653
Insn::Mul { left, right, .. } => {
677654
*left = split_load_operand(asm, *left);
678655
*right = split_load_operand(asm, *right);
@@ -839,6 +816,25 @@ impl Assembler
839816
}
840817
}
841818

819+
/// Load a VALUE to a register and remember it for GC marking and reference updating
820+
fn emit_load_gc_value(cb: &mut CodeBlock, gc_offsets: &mut Vec<CodePtr>, dest: A64Opnd, value: VALUE) {
821+
// We dont need to check if it's a special const
822+
// here because we only allow these operands to hit
823+
// this point if they're not a special const.
824+
assert!(!value.special_const_p());
825+
826+
// This assumes only load instructions can contain
827+
// references to GC'd Value operands. If the value
828+
// being loaded is a heap object, we'll report that
829+
// back out to the gc_offsets list.
830+
ldr_literal(cb, dest, 2.into());
831+
b(cb, InstructionOffset::from_bytes(4 + (SIZEOF_VALUE as i32)));
832+
cb.write_bytes(&value.as_u64().to_le_bytes());
833+
834+
let ptr_offset = cb.get_write_ptr().sub_bytes(SIZEOF_VALUE);
835+
gc_offsets.push(ptr_offset);
836+
}
837+
842838
/// Emit a push instruction for the given operand by adding to the stack
843839
/// pointer and then storing the given value.
844840
fn emit_push(cb: &mut CodeBlock, opnd: A64Opnd) {
@@ -1009,12 +1005,84 @@ impl Assembler
10091005
Insn::LShift { opnd, shift, out } => {
10101006
lsl(cb, out.into(), opnd.into(), shift.into());
10111007
},
1012-
Insn::Store { dest, src } => {
1008+
store_insn @ Insn::Store { dest, src } => {
1009+
// With minor exceptions, as long as `dest` is a Mem, all forms of `src` are
1010+
// accepted. As a rule of thumb, avoid using Assembler::SCRATCH as a memory
1011+
// base register to gurantee things will work.
1012+
let &Opnd::Mem(Mem { num_bits: dest_num_bits, base: MemBase::Reg(base_reg_no), disp }) = dest else {
1013+
panic!("Unexpected Insn::Store destination in arm64_emit: {dest:?}");
1014+
};
1015+
1016+
// This kind of tricky clobber can only happen for explicit use of SCRATCH_REG,
1017+
// so we panic to get the author to change their code.
1018+
#[track_caller]
1019+
fn assert_no_clobber(store_insn: &Insn, user_use: u8, backend_use: Reg) {
1020+
assert_ne!(
1021+
backend_use.reg_no,
1022+
user_use,
1023+
"Emitting {store_insn:?} would clobber {user_use:?}, in conflict with its semantics"
1024+
);
1025+
}
1026+
1027+
// Split src into SCRATCH0 if necessary
1028+
let src_reg: A64Reg = match src {
1029+
Opnd::Reg(reg) => *reg,
1030+
// Use zero register when possible
1031+
Opnd::UImm(0) | Opnd::Imm(0) => XZR_REG,
1032+
// Immediates
1033+
&Opnd::Imm(imm) => {
1034+
assert_no_clobber(store_insn, base_reg_no, Self::SCRATCH0_REG);
1035+
emit_load_value(cb, Self::SCRATCH0, imm as u64);
1036+
Self::SCRATCH0_REG
1037+
}
1038+
&Opnd::UImm(imm) => {
1039+
assert_no_clobber(store_insn, base_reg_no, Self::SCRATCH0_REG);
1040+
emit_load_value(cb, Self::SCRATCH0, imm);
1041+
Self::SCRATCH0_REG
1042+
}
1043+
&Opnd::Value(value) => {
1044+
assert_no_clobber(store_insn, base_reg_no, Self::SCRATCH0_REG);
1045+
emit_load_gc_value(cb, &mut gc_offsets, Self::SCRATCH0, value);
1046+
Self::SCRATCH0_REG
1047+
}
1048+
src_mem @ &Opnd::Mem(Mem { num_bits: src_num_bits, base: MemBase::Reg(src_base_reg_no), disp: src_disp }) => {
1049+
// For mem-to-mem store, load the source into SCRATCH0
1050+
assert_no_clobber(store_insn, base_reg_no, Self::SCRATCH0_REG);
1051+
let src_mem = if mem_disp_fits_bits(src_disp) {
1052+
src_mem.into()
1053+
} else {
1054+
// Split the load address into SCRATCH0 first if necessary
1055+
assert_no_clobber(store_insn, src_base_reg_no, Self::SCRATCH0_REG);
1056+
load_effective_address(cb, Self::SCRATCH0, src_base_reg_no, src_disp);
1057+
A64Opnd::new_mem(dest_num_bits, Self::SCRATCH0, 0)
1058+
};
1059+
match src_num_bits {
1060+
64 | 32 => ldur(cb, Self::SCRATCH0, src_mem),
1061+
16 => ldurh(cb, Self::SCRATCH0, src_mem),
1062+
8 => ldurb(cb, Self::SCRATCH0, src_mem),
1063+
num_bits => panic!("unexpected num_bits: {num_bits}")
1064+
};
1065+
Self::SCRATCH0_REG
1066+
}
1067+
src @ (Opnd::Mem(_) | Opnd::None | Opnd::VReg { .. }) => panic!("Unexpected source operand during arm64_emit: {src:?}")
1068+
};
1069+
let src = A64Opnd::Reg(src_reg);
1070+
1071+
// Split dest into SCRATCH1 if necessary.
1072+
let dest = if mem_disp_fits_bits(disp) {
1073+
dest.into()
1074+
} else {
1075+
assert_no_clobber(store_insn, src_reg.reg_no, Self::SCRATCH1_REG);
1076+
assert_no_clobber(store_insn, base_reg_no, Self::SCRATCH1_REG);
1077+
load_effective_address(cb, Self::SCRATCH1, base_reg_no, disp);
1078+
A64Opnd::new_mem(dest_num_bits, Self::SCRATCH1, 0)
1079+
};
1080+
10131081
// This order may be surprising but it is correct. The way
10141082
// the Arm64 assembler works, the register that is going to
10151083
// be stored is first and the address is second. However in
10161084
// our IR we have the address first and the register second.
1017-
match dest.rm_num_bits() {
1085+
match dest_num_bits {
10181086
64 | 32 => stur(cb, src.into(), dest.into()),
10191087
16 => sturh(cb, src.into(), dest.into()),
10201088
num_bits => panic!("unexpected dest num_bits: {} (src: {:#?}, dest: {:#?})", num_bits, src, dest),
@@ -1041,21 +1109,7 @@ impl Assembler
10411109
};
10421110
},
10431111
Opnd::Value(value) => {
1044-
// We dont need to check if it's a special const
1045-
// here because we only allow these operands to hit
1046-
// this point if they're not a special const.
1047-
assert!(!value.special_const_p());
1048-
1049-
// This assumes only load instructions can contain
1050-
// references to GC'd Value operands. If the value
1051-
// being loaded is a heap object, we'll report that
1052-
// back out to the gc_offsets list.
1053-
ldr_literal(cb, out.into(), 2.into());
1054-
b(cb, InstructionOffset::from_bytes(4 + (SIZEOF_VALUE as i32)));
1055-
cb.write_bytes(&value.as_u64().to_le_bytes());
1056-
1057-
let ptr_offset = cb.get_write_ptr().sub_bytes(SIZEOF_VALUE);
1058-
gc_offsets.push(ptr_offset);
1112+
emit_load_gc_value(cb, &mut gc_offsets, out.into(), value);
10591113
},
10601114
Opnd::None => {
10611115
unreachable!("Attempted to load from None operand");
@@ -1093,20 +1147,7 @@ impl Assembler
10931147
let &Opnd::Mem(Mem { num_bits: _, base: MemBase::Reg(base_reg_no), disp }) = opnd else {
10941148
panic!("Unexpected Insn::Lea operand in arm64_emit: {opnd:?}");
10951149
};
1096-
let out: A64Opnd = out.into();
1097-
let base_reg = A64Opnd::Reg(A64Reg { num_bits: 64, reg_no: base_reg_no });
1098-
assert_ne!(31, out.unwrap_reg().reg_no, "Insn::Lea sp, [sp, #imm] not always encodable. Use add/sub instead.");
1099-
1100-
if ShiftedImmediate::try_from(disp.unsigned_abs() as u64).is_ok() {
1101-
// Use ADD/SUB if the displacement fits
1102-
add(cb, out, base_reg, A64Opnd::new_imm(disp.into()));
1103-
} else {
1104-
// Use add_extended() to interpret reg_no=31 as sp
1105-
// since the base register is never the zero register.
1106-
// Careful! Only the first two operands can refer to sp.
1107-
emit_load_value(cb, out, disp as u64);
1108-
add_extended(cb, out, base_reg, out);
1109-
};
1150+
load_effective_address(cb, out.into(), base_reg_no, disp);
11101151
}
11111152
Insn::LeaJumpTarget { out, target, .. } => {
11121153
if let Target::Label(label_idx) = target {
@@ -1337,6 +1378,22 @@ impl Assembler
13371378
}
13381379
}
13391380

1381+
fn load_effective_address(cb: &mut CodeBlock, out: A64Opnd, base_reg_no: u8, disp: i32) {
1382+
let base_reg = A64Opnd::Reg(A64Reg { num_bits: 64, reg_no: base_reg_no });
1383+
assert_ne!(31, out.unwrap_reg().reg_no, "Lea sp, [sp, #imm] not always encodable. Use add/sub instead.");
1384+
1385+
if ShiftedImmediate::try_from(disp.unsigned_abs() as u64).is_ok() {
1386+
// Use ADD/SUB if the displacement fits
1387+
add(cb, out, base_reg, A64Opnd::new_imm(disp.into()));
1388+
} else {
1389+
// Use add_extended() to interpret reg_no=31 as sp
1390+
// since the base register is never the zero register.
1391+
// Careful! Only the first two operands can refer to sp.
1392+
emit_load_value(cb, out, disp as u64);
1393+
add_extended(cb, out, base_reg, out);
1394+
};
1395+
}
1396+
13401397
/// LIR Instructions that are lowered to an instruction that have 2 input registers and an output
13411398
/// register can look to merge with a succeeding `Insn::Mov`.
13421399
/// For example:
@@ -1625,6 +1682,64 @@ mod tests {
16251682
");
16261683
}
16271684

1685+
#[test]
1686+
fn test_store() {
1687+
let (mut asm, mut cb) = setup_asm();
1688+
1689+
// Large memory offsets in combinations of destination and source
1690+
let large_mem = Opnd::mem(64, NATIVE_STACK_PTR, -0x305);
1691+
let small_mem = Opnd::mem(64, C_RET_OPND, 0);
1692+
asm.store(small_mem, large_mem);
1693+
asm.store(large_mem, small_mem);
1694+
asm.store(large_mem, large_mem);
1695+
1696+
asm.compile_with_num_regs(&mut cb, 0);
1697+
assert_disasm!(cb, "f0170cd1100240f8100000f8100040f8f1170cd1300200f8f0170cd1100240f8f1170cd1300200f8", "
1698+
0x0: sub x16, sp, #0x305
1699+
0x4: ldur x16, [x16]
1700+
0x8: stur x16, [x0]
1701+
0xc: ldur x16, [x0]
1702+
0x10: sub x17, sp, #0x305
1703+
0x14: stur x16, [x17]
1704+
0x18: sub x16, sp, #0x305
1705+
0x1c: ldur x16, [x16]
1706+
0x20: sub x17, sp, #0x305
1707+
0x24: stur x16, [x17]
1708+
");
1709+
}
1710+
1711+
#[test]
1712+
fn test_store_value_without_split() {
1713+
let (mut asm, mut cb) = setup_asm();
1714+
1715+
let imitation_heap_value = VALUE(0x1000);
1716+
assert!(imitation_heap_value.heap_object_p());
1717+
asm.store(Opnd::mem(VALUE_BITS, SP, 0), imitation_heap_value.into());
1718+
1719+
// Side exit code are compiled without the split pass, so we directly call emit here to
1720+
// emulate that scenario.
1721+
let gc_offsets = asm.arm64_emit(&mut cb).unwrap();
1722+
assert_eq!(1, gc_offsets.len(), "VALUE source operand should be reported as gc offset");
1723+
1724+
assert_disasm!(cb, "50000058030000140010000000000000b00200f8", "
1725+
0x0: ldr x16, #8
1726+
0x4: b #0x10
1727+
0x8: .byte 0x00, 0x10, 0x00, 0x00
1728+
0xc: .byte 0x00, 0x00, 0x00, 0x00
1729+
0x10: stur x16, [x21]
1730+
");
1731+
}
1732+
1733+
#[test]
1734+
#[should_panic]
1735+
fn test_store_unserviceable() {
1736+
let (mut asm, mut cb) = setup_asm();
1737+
// This would put the source into SCRATCH_REG, messing up the destination
1738+
asm.store(Opnd::mem(64, Opnd::Reg(Assembler::SCRATCH_REG), 0), 0x83902.into());
1739+
1740+
asm.compile_with_num_regs(&mut cb, 0);
1741+
}
1742+
16281743
/*
16291744
#[test]
16301745
fn test_emit_lea_label() {

zjit/src/backend/lir.rs

Lines changed: 0 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1822,29 +1822,16 @@ impl Assembler
18221822
};
18231823
self.write_label(side_exit_label.clone());
18241824

1825-
// Load an operand that cannot be used as a source of Insn::Store
1826-
fn split_store_source(asm: &mut Assembler, opnd: Opnd) -> Opnd {
1827-
if matches!(opnd, Opnd::Mem(_) | Opnd::Value(_)) ||
1828-
(cfg!(target_arch = "aarch64") && matches!(opnd, Opnd::UImm(_))) {
1829-
asm.load_into(Opnd::Reg(Assembler::SCRATCH_REG), opnd);
1830-
Opnd::Reg(Assembler::SCRATCH_REG)
1831-
} else {
1832-
opnd
1833-
}
1834-
}
1835-
18361825
// Restore the PC and the stack for regular side exits. We don't do this for
18371826
// side exits right after JIT-to-JIT calls, which restore them before the call.
18381827
if let Some(SideExitContext { pc, stack, locals }) = context {
18391828
asm_comment!(self, "write stack slots: {stack:?}");
18401829
for (idx, &opnd) in stack.iter().enumerate() {
1841-
let opnd = split_store_source(self, opnd);
18421830
self.store(Opnd::mem(64, SP, idx as i32 * SIZEOF_VALUE_I32), opnd);
18431831
}
18441832

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

0 commit comments

Comments
 (0)