From c10e018e1c2dd3351af1f40f9b20ea23cfeace36 Mon Sep 17 00:00:00 2001 From: Kevin Newton Date: Mon, 13 Jun 2022 17:16:33 -0400 Subject: LDADDAL, STUR, BL (https://github.com/Shopify/ruby/pull/299) * LDADDAL instruction * STUR * BL instruction * Remove num_bits from imm and uimm * Tests for imm_fits_bits and uimm_fits_bits * Reorder arguments to LDADDAL --- yjit/src/asm/arm64/inst/atomic.rs | 86 ++++++++++++++++++++++++ yjit/src/asm/arm64/inst/call.rs | 67 +++++++++++++++++++ yjit/src/asm/arm64/inst/mod.rs | 137 +++++++++++++++++++++++++++++++++----- yjit/src/asm/arm64/inst/store.rs | 105 +++++++++++++++++++++++++++++ yjit/src/asm/arm64/opnd.rs | 42 ++---------- 5 files changed, 384 insertions(+), 53 deletions(-) create mode 100644 yjit/src/asm/arm64/inst/atomic.rs create mode 100644 yjit/src/asm/arm64/inst/call.rs create mode 100644 yjit/src/asm/arm64/inst/store.rs diff --git a/yjit/src/asm/arm64/inst/atomic.rs b/yjit/src/asm/arm64/inst/atomic.rs new file mode 100644 index 0000000000..5ce497209c --- /dev/null +++ b/yjit/src/asm/arm64/inst/atomic.rs @@ -0,0 +1,86 @@ +/// The size of the register operands to this instruction. +enum Size { + /// Using 32-bit registers. + Size32 = 0b10, + + /// Using 64-bit registers. + Size64 = 0b11 +} + +/// A convenience function so that we can convert the number of bits of an +/// register operand directly into a Size enum variant. +impl From for Size { + fn from(num_bits: u8) -> Self { + match num_bits { + 64 => Size::Size64, + 32 => Size::Size32, + _ => panic!("Invalid number of bits: {}", num_bits) + } + } +} + +/// The struct that represents an A64 atomic instruction that can be encoded. +/// +/// +-------------+-------------+-------------+-------------+-------------+-------------+-------------+-------------+ +/// | 31 30 29 28 | 27 26 25 24 | 23 22 21 20 | 19 18 17 16 | 15 14 13 12 | 11 10 09 08 | 07 06 05 04 | 03 02 01 00 | +/// | 1 1 1 0 0 0 1 1 1 0 0 0 0 0 0 | +/// | size rs.............. rn.............. rt.............. | +/// +-------------+-------------+-------------+-------------+-------------+-------------+-------------+-------------+ +/// +pub struct Atomic { + /// The register holding the value to be loaded. + rt: u8, + + /// The base register. + rn: u8, + + /// The register holding the data value to be operated on. + rs: u8, + + /// The size of the registers used in this instruction. + size: Size +} + +impl Atomic { + /// LDADDAL + /// https://developer.arm.com/documentation/ddi0596/2021-12/Base-Instructions/LDADD--LDADDA--LDADDAL--LDADDL--Atomic-add-on-word-or-doubleword-in-memory-?lang=en + pub fn ldaddal(rs: u8, rt: u8, rn: u8, num_bits: u8) -> Self { + Self { rt, rn, rs, size: num_bits.into() } + } +} + +/// https://developer.arm.com/documentation/ddi0602/2022-03/Index-by-Encoding/Loads-and-Stores?lang=en +const FAMILY: u32 = 0b0100; + +impl From for u32 { + /// Convert an instruction into a 32-bit value. + fn from(inst: Atomic) -> Self { + 0 + | ((inst.size as u32) << 30) + | (0b11 << 28) + | (FAMILY << 25) + | (0b111 << 21) + | ((inst.rs as u32) << 16) + | ((inst.rn as u32) << 5) + | (inst.rt as u32) + } +} + +impl From for [u8; 4] { + /// Convert an instruction into a 4 byte array. + fn from(inst: Atomic) -> [u8; 4] { + let result: u32 = inst.into(); + result.to_le_bytes() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_ldaddal() { + let result: u32 = Atomic::ldaddal(20, 21, 22, 64).into(); + assert_eq!(0xf8f402d5, result); + } +} diff --git a/yjit/src/asm/arm64/inst/call.rs b/yjit/src/asm/arm64/inst/call.rs new file mode 100644 index 0000000000..6f23acf9f5 --- /dev/null +++ b/yjit/src/asm/arm64/inst/call.rs @@ -0,0 +1,67 @@ +/// The struct that represents an A64 branch with link instruction that can be +/// encoded. +/// +/// +-------------+-------------+-------------+-------------+-------------+-------------+-------------+-------------+ +/// | 31 30 29 28 | 27 26 25 24 | 23 22 21 20 | 19 18 17 16 | 15 14 13 12 | 11 10 09 08 | 07 06 05 04 | 03 02 01 00 | +/// | 1 0 0 1 0 1 | +/// | imm26.................................................................................... | +/// +-------------+-------------+-------------+-------------+-------------+-------------+-------------+-------------+ +/// +pub struct Call { + /// The PC-relative offset to jump to (which will be multiplied by 4). + imm26: i32 +} + +impl Call { + /// BL + /// https://developer.arm.com/documentation/ddi0596/2021-12/Base-Instructions/BL--Branch-with-Link-?lang=en + pub fn bl(imm26: i32) -> Self { + Self { imm26 } + } +} + +/// https://developer.arm.com/documentation/ddi0602/2022-03/Index-by-Encoding/Branches--Exception-Generating-and-System-instructions?lang=en +const FAMILY: u32 = 0b101; + +impl From for u32 { + /// Convert an instruction into a 32-bit value. + fn from(inst: Call) -> Self { + let imm26 = (inst.imm26 as u32) & ((1 << 26) - 1); + + 0 + | (1 << 31) + | (FAMILY << 26) + | imm26 + } +} + +impl From for [u8; 4] { + /// Convert an instruction into a 4 byte array. + fn from(inst: Call) -> [u8; 4] { + let result: u32 = inst.into(); + result.to_le_bytes() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_bl() { + let result: u32 = Call::bl(0).into(); + assert_eq!(0x94000000, result); + } + + #[test] + fn test_bl_positive() { + let result: u32 = Call::bl(256).into(); + assert_eq!(0x94000100, result); + } + + #[test] + fn test_bl_negative() { + let result: u32 = Call::bl(-256).into(); + assert_eq!(0x97ffff00, result); + } +} diff --git a/yjit/src/asm/arm64/inst/mod.rs b/yjit/src/asm/arm64/inst/mod.rs index eec9d116b2..c96e9328ff 100644 --- a/yjit/src/asm/arm64/inst/mod.rs +++ b/yjit/src/asm/arm64/inst/mod.rs @@ -1,19 +1,42 @@ +mod atomic; mod branch; +mod call; mod data_imm; mod data_reg; mod load; mod mov; mod sf; +mod store; +use core::num; + +use atomic::Atomic; use branch::Branch; +use call::Call; use data_imm::DataImm; use data_reg::DataReg; use load::Load; use mov::Mov; +use store::Store; -use crate::asm::{CodeBlock, imm_num_bits}; +use crate::asm::CodeBlock; use super::opnd::*; +/// Checks that a signed value fits within the specified number of bits. +const fn imm_fits_bits(imm: i64, num_bits: u8) -> bool { + let minimum = if num_bits == 64 { i64::MIN } else { -2_i64.pow((num_bits as u32) - 1) }; + let maximum = if num_bits == 64 { i64::MAX } else { 2_i64.pow((num_bits as u32) - 1) - 1 }; + + imm >= minimum && imm <= maximum +} + +/// Checks that an unsigned value fits within the specified number of bits. +const fn uimm_fits_bits(uimm: u64, num_bits: u8) -> bool { + let maximum = if num_bits == 64 { u64::MAX } else { 2_u64.pow(num_bits as u32) - 1 }; + + uimm <= maximum +} + /// ADD - add rn and rm, put the result in rd, don't update flags pub fn add(cb: &mut CodeBlock, rd: A64Opnd, rn: A64Opnd, rm: A64Opnd) { let bytes: [u8; 4] = match (rd, rn, rm) { @@ -27,9 +50,9 @@ pub fn add(cb: &mut CodeBlock, rd: A64Opnd, rn: A64Opnd, rm: A64Opnd) { }, (A64Opnd::Reg(rd), A64Opnd::Reg(rn), A64Opnd::UImm(imm12)) => { assert!(rd.num_bits == rn.num_bits, "rd and rn must be of the same size."); - assert!(imm12.num_bits <= 12, "The immediate operand must be 12 bits or less."); + assert!(uimm_fits_bits(imm12, 12), "The immediate operand must be 12 bits or less."); - DataImm::add(rd.reg_no, rn.reg_no, imm12.value as u16, rd.num_bits).into() + DataImm::add(rd.reg_no, rn.reg_no, imm12 as u16, rd.num_bits).into() }, _ => panic!("Invalid operand combination to add instruction."), }; @@ -50,9 +73,9 @@ pub fn adds(cb: &mut CodeBlock, rd: A64Opnd, rn: A64Opnd, rm: A64Opnd) { }, (A64Opnd::Reg(rd), A64Opnd::Reg(rn), A64Opnd::UImm(imm12)) => { assert!(rd.num_bits == rn.num_bits, "rd and rn must be of the same size."); - assert!(imm12.num_bits <= 12, "The immediate operand must be 12 bits or less."); + assert!(uimm_fits_bits(imm12, 12), "The immediate operand must be 12 bits or less."); - DataImm::adds(rd.reg_no, rn.reg_no, imm12.value as u16, rd.num_bits).into() + DataImm::adds(rd.reg_no, rn.reg_no, imm12 as u16, rd.num_bits).into() }, _ => panic!("Invalid operand combination to adds instruction."), }; @@ -60,6 +83,20 @@ pub fn adds(cb: &mut CodeBlock, rd: A64Opnd, rn: A64Opnd, rm: A64Opnd) { cb.write_bytes(&bytes); } +/// BL - branch with link (offset is number of instructions to jump) +pub fn bl(cb: &mut CodeBlock, imm26: A64Opnd) { + let bytes: [u8; 4] = match imm26 { + A64Opnd::Imm(imm26) => { + assert!(imm_fits_bits(imm26, 26), "The immediate operand must be 26 bits or less."); + + Call::bl(imm26 as i32).into() + }, + _ => panic!("Invalid operand combination to bl instruction.") + }; + + cb.write_bytes(&bytes); +} + /// BR - branch to a register pub fn br(cb: &mut CodeBlock, rn: A64Opnd) { let bytes: [u8; 4] = match rn { @@ -70,14 +107,31 @@ pub fn br(cb: &mut CodeBlock, rn: A64Opnd) { cb.write_bytes(&bytes); } +/// LDADDAL - atomic add with acquire and release semantics +pub fn ldaddal(cb: &mut CodeBlock, rs: A64Opnd, rt: A64Opnd, rn: A64Opnd) { + let bytes: [u8; 4] = match (rs, rt, rn) { + (A64Opnd::Reg(rs), A64Opnd::Reg(rt), A64Opnd::Reg(rn)) => { + assert!( + rs.num_bits == rt.num_bits && rt.num_bits == rn.num_bits, + "All operands must be of the same size." + ); + + Atomic::ldaddal(rs.reg_no, rt.reg_no, rn.reg_no, rs.num_bits).into() + }, + _ => panic!("Invalid operand combination to ldaddal instruction."), + }; + + cb.write_bytes(&bytes); +} + /// LDUR - load a memory address into a register pub fn ldur(cb: &mut CodeBlock, rt: A64Opnd, rn: A64Opnd) { let bytes: [u8; 4] = match (rt, rn) { (A64Opnd::Reg(rt), A64Opnd::Mem(rn)) => { assert!(rt.num_bits == rn.num_bits, "Expected registers to be the same size"); - assert!(imm_num_bits(rn.disp.into()) <= 9, "Expected displacement to be 9 bits or less"); + assert!(imm_fits_bits(rn.disp.into(), 9), "Expected displacement to be 9 bits or less"); - Load::ldur(rt.reg_no, rn.base_reg_no, rn.disp.try_into().unwrap(), rt.num_bits).into() + Load::ldur(rt.reg_no, rn.base_reg_no, rn.disp as i16, rt.num_bits).into() }, _ => panic!("Invalid operands for LDUR") }; @@ -89,9 +143,9 @@ pub fn ldur(cb: &mut CodeBlock, rt: A64Opnd, rn: A64Opnd) { pub fn movk(cb: &mut CodeBlock, rd: A64Opnd, imm16: A64Opnd, shift: u8) { let bytes: [u8; 4] = match (rd, imm16) { (A64Opnd::Reg(rd), A64Opnd::UImm(imm16)) => { - assert!(imm16.num_bits <= 16, "The immediate operand must be 16 bits or less."); + assert!(uimm_fits_bits(imm16, 16), "The immediate operand must be 16 bits or less."); - Mov::movk(rd.reg_no, imm16.value as u16, shift, rd.num_bits).into() + Mov::movk(rd.reg_no, imm16 as u16, shift, rd.num_bits).into() }, _ => panic!("Invalid operand combination to movk instruction.") }; @@ -103,9 +157,9 @@ pub fn movk(cb: &mut CodeBlock, rd: A64Opnd, imm16: A64Opnd, shift: u8) { pub fn movz(cb: &mut CodeBlock, rd: A64Opnd, imm16: A64Opnd, shift: u8) { let bytes: [u8; 4] = match (rd, imm16) { (A64Opnd::Reg(rd), A64Opnd::UImm(imm16)) => { - assert!(imm16.num_bits <= 16, "The immediate operand must be 16 bits or less."); + assert!(uimm_fits_bits(imm16, 16), "The immediate operand must be 16 bits or less."); - Mov::movz(rd.reg_no, imm16.value as u16, shift, rd.num_bits).into() + Mov::movz(rd.reg_no, imm16 as u16, shift, rd.num_bits).into() }, _ => panic!("Invalid operand combination to movz instruction.") }; @@ -113,6 +167,21 @@ pub fn movz(cb: &mut CodeBlock, rd: A64Opnd, imm16: A64Opnd, shift: u8) { cb.write_bytes(&bytes); } +/// STUR - store a value in a register at a memory address +pub fn stur(cb: &mut CodeBlock, rt: A64Opnd, rn: A64Opnd) { + let bytes: [u8; 4] = match (rt, rn) { + (A64Opnd::Reg(rt), A64Opnd::Mem(rn)) => { + assert!(rt.num_bits == rn.num_bits, "Expected registers to be the same size"); + assert!(imm_fits_bits(rn.disp.into(), 9), "Expected displacement to be 9 bits or less"); + + Store::stur(rt.reg_no, rn.base_reg_no, rn.disp as i16, rt.num_bits).into() + }, + _ => panic!("Invalid operand combination to stur instruction.") + }; + + cb.write_bytes(&bytes); +} + /// SUB - subtract rm from rn, put the result in rd, don't update flags pub fn sub(cb: &mut CodeBlock, rd: A64Opnd, rn: A64Opnd, rm: A64Opnd) { let bytes: [u8; 4] = match (rd, rn, rm) { @@ -126,9 +195,9 @@ pub fn sub(cb: &mut CodeBlock, rd: A64Opnd, rn: A64Opnd, rm: A64Opnd) { }, (A64Opnd::Reg(rd), A64Opnd::Reg(rn), A64Opnd::UImm(imm12)) => { assert!(rd.num_bits == rn.num_bits, "rd and rn must be of the same size."); - assert!(imm12.num_bits <= 12, "The immediate operand must be 12 bits or less."); + assert!(uimm_fits_bits(imm12, 12), "The immediate operand must be 12 bits or less."); - DataImm::sub(rd.reg_no, rn.reg_no, imm12.value as u16, rd.num_bits).into() + DataImm::sub(rd.reg_no, rn.reg_no, imm12 as u16, rd.num_bits).into() }, _ => panic!("Invalid operand combination to sub instruction."), }; @@ -149,9 +218,9 @@ pub fn subs(cb: &mut CodeBlock, rd: A64Opnd, rn: A64Opnd, rm: A64Opnd) { }, (A64Opnd::Reg(rd), A64Opnd::Reg(rn), A64Opnd::UImm(imm12)) => { assert!(rd.num_bits == rn.num_bits, "rd and rn must be of the same size."); - assert!(imm12.num_bits <= 12, "The immediate operand must be 12 bits or less."); + assert!(uimm_fits_bits(imm12, 12), "The immediate operand must be 12 bits or less."); - DataImm::subs(rd.reg_no, rn.reg_no, imm12.value as u16, rd.num_bits).into() + DataImm::subs(rd.reg_no, rn.reg_no, imm12 as u16, rd.num_bits).into() }, _ => panic!("Invalid operand combination to subs instruction."), }; @@ -181,6 +250,29 @@ mod tests { assert_eq!(format!("{:x}", cb), bytes); } + #[test] + fn test_imm_fits_bits() { + assert!(imm_fits_bits(i8::MAX.into(), 8)); + assert!(imm_fits_bits(i8::MIN.into(), 8)); + + assert!(imm_fits_bits(i16::MAX.into(), 16)); + assert!(imm_fits_bits(i16::MIN.into(), 16)); + + assert!(imm_fits_bits(i32::MAX.into(), 32)); + assert!(imm_fits_bits(i32::MIN.into(), 32)); + + assert!(imm_fits_bits(i64::MAX.into(), 64)); + assert!(imm_fits_bits(i64::MIN.into(), 64)); + } + + #[test] + fn test_uimm_fits_bits() { + assert!(uimm_fits_bits(u8::MAX.into(), 8)); + assert!(uimm_fits_bits(u16::MAX.into(), 16)); + assert!(uimm_fits_bits(u32::MAX.into(), 32)); + assert!(uimm_fits_bits(u64::MAX.into(), 64)); + } + #[test] fn test_add_register() { check_bytes("2000028b", |cb| add(cb, X0, X1, X2)); @@ -201,11 +293,21 @@ mod tests { check_bytes("201c00b1", |cb| adds(cb, X0, X1, A64Opnd::new_uimm(7))); } + #[test] + fn test_bl() { + check_bytes("00040094", |cb| bl(cb, A64Opnd::new_imm(1024))); + } + #[test] fn test_br() { check_bytes("80021fd6", |cb| br(cb, X20)); } + #[test] + fn test_ldaddal() { + check_bytes("8b01eaf8", |cb| ldaddal(cb, X10, X11, X12)); + } + #[test] fn test_ldur() { check_bytes("20b047f8", |cb| ldur(cb, X0, A64Opnd::new_mem(X1, 123))); @@ -231,6 +333,11 @@ mod tests { check_bytes("80025fd6", |cb| ret(cb, X20)); } + #[test] + fn test_stur() { + check_bytes("6a0108f8", |cb| stur(cb, X10, A64Opnd::new_mem(X11, 128))); + } + #[test] fn test_sub_register() { check_bytes("200002cb", |cb| sub(cb, X0, X1, X2)); diff --git a/yjit/src/asm/arm64/inst/store.rs b/yjit/src/asm/arm64/inst/store.rs new file mode 100644 index 0000000000..42b9055ae8 --- /dev/null +++ b/yjit/src/asm/arm64/inst/store.rs @@ -0,0 +1,105 @@ +/// The size of the operands being operated on. +enum Size { + Size32 = 0b10, + Size64 = 0b11, +} + +/// A convenience function so that we can convert the number of bits of an +/// register operand directly into an Sf enum variant. +impl From for Size { + fn from(num_bits: u8) -> Self { + match num_bits { + 64 => Size::Size64, + 32 => Size::Size32, + _ => panic!("Invalid number of bits: {}", num_bits) + } + } +} + +/// The struct that represents an A64 store instruction that can be encoded. +/// +/// STUR +/// +-------------+-------------+-------------+-------------+-------------+-------------+-------------+-------------+ +/// | 31 30 29 28 | 27 26 25 24 | 23 22 21 20 | 19 18 17 16 | 15 14 13 12 | 11 10 09 08 | 07 06 05 04 | 03 02 01 00 | +/// | 1 1 1 0 0 0 0 0 0 0 0 | +/// | size. imm9.......................... rn.............. rt.............. | +/// +-------------+-------------+-------------+-------------+-------------+-------------+-------------+-------------+ +/// +pub struct Store { + /// The number of the register to be transferred. + rt: u8, + + /// The register holding the memory location. + rn: u8, + + /// The optional signed immediate byte offset from the base register. + imm9: i16, + + /// The size of the operands being operated on. + size: Size +} + +impl Store { + /// STUR (store register, unscaled) + /// https://developer.arm.com/documentation/ddi0596/2021-12/Base-Instructions/STUR--Store-Register--unscaled--?lang=en + pub fn stur(rt: u8, rn: u8, imm9: i16, num_bits: u8) -> Self { + Self { + rt, + rn, + imm9, + size: num_bits.into() + } + } +} + +/// https://developer.arm.com/documentation/ddi0602/2022-03/Index-by-Encoding/Loads-and-Stores?lang=en +const FAMILY: u32 = 0b0100; + +impl From for u32 { + /// Convert an instruction into a 32-bit value. + fn from(inst: Store) -> Self { + let imm9 = (inst.imm9 as u32) & ((1 << 9) - 1); + + 0 + | ((inst.size as u32) << 30) + | (0b11 << 28) + | (FAMILY << 25) + | (imm9 << 12) + | ((inst.rn as u32) << 5) + | (inst.rt as u32) + } +} + +impl From for [u8; 4] { + /// Convert an instruction into a 4 byte array. + fn from(inst: Store) -> [u8; 4] { + let result: u32 = inst.into(); + result.to_le_bytes() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_stur() { + let inst = Store::stur(0, 1, 0, 64); + let result: u32 = inst.into(); + assert_eq!(0xf8000020, result); + } + + #[test] + fn test_stur_negative_offset() { + let inst = Store::stur(0, 1, -1, 64); + let result: u32 = inst.into(); + assert_eq!(0xf81ff020, result); + } + + #[test] + fn test_stur_positive_offset() { + let inst = Store::stur(0, 1, 255, 64); + let result: u32 = inst.into(); + assert_eq!(0xf80ff020, result); + } +} diff --git a/yjit/src/asm/arm64/opnd.rs b/yjit/src/asm/arm64/opnd.rs index ba8ecd166d..aa73d438fe 100644 --- a/yjit/src/asm/arm64/opnd.rs +++ b/yjit/src/asm/arm64/opnd.rs @@ -1,39 +1,5 @@ use crate::asm::{imm_num_bits, uimm_num_bits}; -/// This operand represents a signed immediate value. -#[derive(Clone, Copy, Debug, Eq, PartialEq)] -pub struct A64Imm -{ - // Size in bits - pub num_bits: u8, - - // The value of the immediate - pub value: i64 -} - -impl A64Imm { - pub fn new(value: i64) -> Self { - Self { num_bits: imm_num_bits(value), value } - } -} - -/// This operand represents an unsigned immediate value. -#[derive(Clone, Copy, Debug, Eq, PartialEq)] -pub struct A64UImm -{ - // Size in bits - pub num_bits: u8, - - // The value of the immediate - pub value: u64 -} - -impl A64UImm { - pub fn new(value: u64) -> Self { - Self { num_bits: uimm_num_bits(value), value } - } -} - /// This operand represents a register. #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub struct A64Reg @@ -80,10 +46,10 @@ pub enum A64Opnd None, // Immediate value - Imm(A64Imm), + Imm(i64), // Unsigned immediate - UImm(A64UImm), + UImm(u64), // Register Reg(A64Reg), @@ -95,12 +61,12 @@ pub enum A64Opnd impl A64Opnd { /// Create a new immediate value operand. pub fn new_imm(value: i64) -> Self { - A64Opnd::Imm(A64Imm::new(value)) + A64Opnd::Imm(value) } /// Create a new unsigned immediate value operand. pub fn new_uimm(value: u64) -> Self { - A64Opnd::UImm(A64UImm::new(value)) + A64Opnd::UImm(value) } /// Creates a new memory operand. -- cgit v1.2.3