From f092edb60a18c25cd201fcac3991df7e3b488b4e Mon Sep 17 00:00:00 2001 From: David Gow Date: Sun, 12 Oct 2025 21:44:33 +0800 Subject: [PATCH] x86: Correctly pass large integers and structs in registers with -Zregparm The -Zregparm option for 32-bit x86 modifies the calling convention to pass a number of arguments in registers instead of on the stack. (This is primarily used by the Linux kernel.) Currently, rustc will only pass integers of size <= 32 bits in registers, but gcc (and clang) will pass larger integers (e.g., 64-bit integers) across two registers if possible. This is not the case with fastcall/vectorcall. Equally, struct arguments will be split into registers and passed where possible. Supporting this in rustc required moving the 'inreg' determination into compute_abi_info, as this is the stage where we determine if structs are passed directly or indirectly. This involves updating the x86_win32 ABI implementation as well, as it previously shared the inreg handling, but has a different compute_abi_info implementation. Also add some assembly-llvm and codegen-llvm tests. Note that the LLVM IR generated doesn't exactly match clang, as rustc treats struct arguments as one argument in three registers, whereas clang seems to decompose it into three registers. It seems to work regardless... --- compiler/rustc_target/src/callconv/x86.rs | 122 +++++++----------- .../rustc_target/src/callconv/x86_win32.rs | 59 ++++++++- tests/assembly-llvm/regparm-module-flag.rs | 49 +++++++ tests/codegen-llvm/regparm-inreg.rs | 27 ++++ 4 files changed, 180 insertions(+), 77 deletions(-) diff --git a/compiler/rustc_target/src/callconv/x86.rs b/compiler/rustc_target/src/callconv/x86.rs index a80088e41cd31..ba21941d4a464 100644 --- a/compiler/rustc_target/src/callconv/x86.rs +++ b/compiler/rustc_target/src/callconv/x86.rs @@ -2,7 +2,7 @@ use rustc_abi::{ AddressSpace, Align, BackendRepr, HasDataLayout, Primitive, Reg, RegKind, TyAndLayout, }; -use crate::callconv::{ArgAttribute, FnAbi, PassMode, TyAbiInterface}; +use crate::callconv::{ArgAttribute, FnAbi, PassMode, TyAbiInterface, Uniform}; use crate::spec::{HasTargetSpec, RustcAbi}; #[derive(PartialEq)] @@ -58,6 +58,13 @@ where } } + // Some calling conversions pass arguments in registers. + // This is set with -Zregparm, or the fastcall/vectorcall (always 2 registers) + let mut free_regs = opts.regparm.unwrap_or(0).into(); + if opts.flavor == Flavor::FastcallOrVectorcall { + free_regs = 2; + } + for arg in fn_abi.args.iter_mut() { if arg.is_ignore() || !arg.layout.is_sized() { continue; @@ -72,7 +79,21 @@ where let align_4 = Align::from_bytes(4).unwrap(); let align_16 = Align::from_bytes(16).unwrap(); + let size_in_regs = arg.layout.size.bits().div_ceil(32); + let mut pass_in_reg = free_regs >= size_in_regs; + + // In fastcall/vectorcall only 32-bit integers can be passed in registers. + if opts.flavor == Flavor::FastcallOrVectorcall && size_in_regs > 1 { + pass_in_reg = false; + // Once we've had an argument which doesn't fit, don't try to fit any more. + free_regs = 0; + } + if arg.layout.is_aggregate() { + if opts.flavor == Flavor::FastcallOrVectorcall && size_in_regs > 1 { + pass_in_reg = false; + } + // We need to compute the alignment of the `byval` argument. The rules can be found in // `X86_32ABIInfo::getTypeStackAlignInBytes` in Clang's `TargetInfo.cpp`. Summarized // here, they are: @@ -119,83 +140,40 @@ where align_4 }; - arg.pass_by_stack_offset(Some(byval_align)); + if pass_in_reg { + arg.cast_to(Uniform::new(Reg::i32(), arg.layout.size)); + } else { + arg.pass_by_stack_offset(Some(byval_align)) + } } else { - arg.extend_integer_width_to(32); - } - } - - fill_inregs(cx, fn_abi, opts, false); -} - -pub(crate) fn fill_inregs<'a, Ty, C>( - cx: &C, - fn_abi: &mut FnAbi<'a, Ty>, - opts: X86Options, - rust_abi: bool, -) where - Ty: TyAbiInterface<'a, C> + Copy, -{ - if opts.flavor != Flavor::FastcallOrVectorcall && opts.regparm.is_none_or(|x| x == 0) { - return; - } - // Mark arguments as InReg like clang does it, - // so our fastcall/vectorcall is compatible with C/C++ fastcall/vectorcall. - - // Clang reference: lib/CodeGen/TargetInfo.cpp - // See X86_32ABIInfo::shouldPrimitiveUseInReg(), X86_32ABIInfo::updateFreeRegs() - - // IsSoftFloatABI is only set to true on ARM platforms, - // which in turn can't be x86? - - // 2 for fastcall/vectorcall, regparm limited by 3 otherwise - let mut free_regs = opts.regparm.unwrap_or(2).into(); + let unit = arg.layout.homogeneous_aggregate(cx).unwrap().unit().unwrap(); - // For types generating PassMode::Cast, InRegs will not be set. - // Maybe, this is a FIXME - let has_casts = fn_abi.args.iter().any(|arg| matches!(arg.mode, PassMode::Cast { .. })); - if has_casts && rust_abi { - return; - } - - for arg in fn_abi.args.iter_mut() { - let attrs = match arg.mode { - PassMode::Ignore | PassMode::Indirect { attrs: _, meta_attrs: None, on_stack: _ } => { - continue; + // Fastcall/regparm won't pass floats in registers + // (though regparm _will_ pass f]oat-containing structs in registers) + if unit.kind != RegKind::Integer { + pass_in_reg = false; } - PassMode::Direct(ref mut attrs) => attrs, - PassMode::Pair(..) - | PassMode::Indirect { attrs: _, meta_attrs: Some(_), on_stack: _ } - | PassMode::Cast { .. } => { - unreachable!("x86 shouldn't be passing arguments by {:?}", arg.mode) - } - }; - // At this point we know this must be a primitive of sorts. - let unit = arg.layout.homogeneous_aggregate(cx).unwrap().unit().unwrap(); - assert_eq!(unit.size, arg.layout.size); - if matches!(unit.kind, RegKind::Float | RegKind::Vector { .. }) { - continue; - } - - let size_in_regs = arg.layout.size.bits().div_ceil(32); - - if size_in_regs == 0 { - continue; - } - - if size_in_regs > free_regs { - break; - } - - free_regs -= size_in_regs; - - if arg.layout.size.bits() <= 32 && unit.kind == RegKind::Integer { - attrs.set(ArgAttribute::InReg); + arg.extend_integer_width_to(32); } - if free_regs == 0 { - break; + // Set the InReg annotation if we're passing by register + if pass_in_reg { + match arg.mode { + PassMode::Ignore + | PassMode::Indirect { attrs: _, meta_attrs: None, on_stack: _ } => {} + PassMode::Cast { pad_i32: _, ref mut cast } => { + cast.attrs.set(ArgAttribute::InReg); + } + PassMode::Direct(ref mut attrs) => { + attrs.set(ArgAttribute::InReg); + } + PassMode::Pair(..) + | PassMode::Indirect { attrs: _, meta_attrs: Some(_), on_stack: _ } => { + unreachable!("x86 shouldn't be passing arguments by {:?}", arg.mode) + } + }; + free_regs -= size_in_regs; } } } diff --git a/compiler/rustc_target/src/callconv/x86_win32.rs b/compiler/rustc_target/src/callconv/x86_win32.rs index 824e7cc098a46..0b13d02fa32f0 100644 --- a/compiler/rustc_target/src/callconv/x86_win32.rs +++ b/compiler/rustc_target/src/callconv/x86_win32.rs @@ -1,6 +1,7 @@ -use rustc_abi::{Align, HasDataLayout, Reg, TyAbiInterface}; +use rustc_abi::{Align, HasDataLayout, Reg, RegKind}; -use crate::callconv::FnAbi; +use crate::callconv::x86::Flavor; +use crate::callconv::{ArgAttribute, FnAbi, PassMode, TyAbiInterface, Uniform}; use crate::spec::HasTargetSpec; pub(crate) fn compute_abi_info<'a, Ty, C>( @@ -41,6 +42,13 @@ pub(crate) fn compute_abi_info<'a, Ty, C>( } } + // Some calling conversions pass arguments in registers. + // This is set with -Zregparm, or the fastcall/vectorcall (always 2 registers) + let mut free_regs = opts.regparm.unwrap_or(0).into(); + if opts.flavor == Flavor::FastcallOrVectorcall { + free_regs = 2; + } + for arg in fn_abi.args.iter_mut() { if arg.is_ignore() || !arg.layout.is_sized() { continue; @@ -51,6 +59,16 @@ pub(crate) fn compute_abi_info<'a, Ty, C>( continue; } + let size_in_regs = arg.layout.size.bits().div_ceil(32); + let mut pass_in_reg = free_regs >= size_in_regs; + + // In fastcall/vectorcall only 32-bit integers can be passed in registers. + if opts.flavor == Flavor::FastcallOrVectorcall && size_in_regs > 1 { + pass_in_reg = false; + // Once we've had an argument which doesn't fit, don't try to fit any more. + free_regs = 0; + } + // FIXME: MSVC 2015+ will pass the first 3 vector arguments in [XYZ]MM0-2 // See https://reviews.llvm.org/D72114 for Clang behavior @@ -73,14 +91,45 @@ pub(crate) fn compute_abi_info<'a, Ty, C>( ); arg.make_indirect(); } else if arg.layout.is_aggregate() { + if opts.flavor == Flavor::FastcallOrVectorcall && size_in_regs > 1 { + pass_in_reg = false; + } // Alignment of the `byval` argument. // The rules can be found in `X86_32ABIInfo::getTypeStackAlignInBytes` in Clang's `TargetInfo.cpp`. let byval_align = align_4; - arg.pass_by_stack_offset(Some(byval_align)); + if pass_in_reg { + arg.cast_to(Uniform::new(Reg::i32(), arg.layout.size)); + } else { + arg.pass_by_stack_offset(Some(byval_align)) + } } else { + let unit = arg.layout.homogeneous_aggregate(cx).unwrap().unit().unwrap(); + + // Fastcall/regparm won't pass floats in registers + // (though regparm _will_ pass f]oat-containing structs in registers) + if unit.kind != RegKind::Integer { + pass_in_reg = false; + } + arg.extend_integer_width_to(32); } + // Set the InReg annotation if we're passing by register + if pass_in_reg { + match arg.mode { + PassMode::Ignore + | PassMode::Indirect { attrs: _, meta_attrs: None, on_stack: _ } => {} + PassMode::Cast { pad_i32: _, ref mut cast } => { + cast.attrs.set(ArgAttribute::InReg); + } + PassMode::Direct(ref mut attrs) => { + attrs.set(ArgAttribute::InReg); + } + PassMode::Pair(..) + | PassMode::Indirect { attrs: _, meta_attrs: Some(_), on_stack: _ } => { + unreachable!("x86 shouldn't be passing arguments by {:?}", arg.mode) + } + }; + free_regs -= size_in_regs; + } } - - super::x86::fill_inregs(cx, fn_abi, opts, false); } diff --git a/tests/assembly-llvm/regparm-module-flag.rs b/tests/assembly-llvm/regparm-module-flag.rs index 4a08bfdf85e5f..bf7ccd61fcb66 100644 --- a/tests/assembly-llvm/regparm-module-flag.rs +++ b/tests/assembly-llvm/regparm-module-flag.rs @@ -16,9 +16,58 @@ extern crate minicore; use minicore::*; +#[repr(C)] +struct ThreeRegStruct { + a: i64, + b: i32, +} + unsafe extern "C" { fn memset(p: *mut c_void, val: i32, len: usize) -> *mut c_void; fn non_builtin_memset(p: *mut c_void, val: i32, len: usize) -> *mut c_void; + fn test_i64_arg(s: i64) -> i64; + fn test_struct_arg(s: ThreeRegStruct); +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn test_i64() -> i64 { + // REGPARM1-LABEL: test_i64 + // REGPARM1: pushl + // REGPARM1: pushl + // REGPARM1: calll test_i64_arg + + // REGPARM2-LABEL: test_i64 + // REGPARM2: movl $42, %eax + // REGPARM2: xorl %edx, %edx + // REGPARM2: jmp test_i64_arg + + // REGPARM3-LABEL: test_i64 + // REGPARM3: movl $42, %eax + // REGPARM3: xorl %edx, %edx + // REGPARM3: jmp test_i64_arg + unsafe { test_i64_arg(42) } +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn test_struct() { + // REGPARM1-LABEL: test_struct + // REGPARM1: movl $0, {{.*}}(%esp) + // REGPARM1: movl $42, {{.*}}(%esp) + // REGPARM1: movl $1, {{.*}}(%esp) + // REGPARM1: calll test_struct_arg + + // REGPARM2-LABEL: test_struct + // REGPARM2: movl $0, {{.*}}(%esp) + // REGPARM2: movl $42, {{.*}}(%esp) + // REGPARM2: movl $1, {{.*}}(%esp) + // REGPARM2: calll test_struct_arg + + // REGPARM3-LABEL: test_struct + // REGPARM3: movl $42, %eax + // REGPARM3: xorl %edx, %edx + // REGPARM3: movl $1, %ecx + // REGPARM3: jmp test_struct_arg + unsafe { test_struct_arg(ThreeRegStruct { a: 42, b: 1 }) } } #[unsafe(no_mangle)] diff --git a/tests/codegen-llvm/regparm-inreg.rs b/tests/codegen-llvm/regparm-inreg.rs index 77d4c206071e7..1ad2c4ca93bed 100644 --- a/tests/codegen-llvm/regparm-inreg.rs +++ b/tests/codegen-llvm/regparm-inreg.rs @@ -121,4 +121,31 @@ pub mod tests { // regparm3-SAME: i32 inreg noundef %_4) #[no_mangle] pub extern "C" fn f12(_: i32, _: __m256, _: i32, _: i32) {} + + // regparm0: @f13(i64 noundef %_1) + // regparm1: @f13(i64 noundef %_1) + // regparm2: @f13(i64 inreg noundef %_1) + // regparm3: @f13(i64 inreg noundef %_1) + #[no_mangle] + pub extern "C" fn f13(_: i64) {} + + // regparm0: @f14(ptr noalias nofree noundef byval([8 x i8]) align 4 captures(address) dereferenceable(8) %_1) + // regparm1: @f14(ptr noalias nofree noundef byval([8 x i8]) align 4 captures(address) dereferenceable(8) %_1) + // regparm2: @f14([2 x i32] inreg %0) + // regparm3: @f14([2 x i32] inreg %0) + #[no_mangle] + pub extern "C" fn f14(_: S2) {} + + #[repr(C)] + struct S3 { + x1: i32, + x2: i32, + x3: i32, + } + // regparm0: @f15(ptr noalias nofree noundef byval([12 x i8]) align 4 captures(address) dereferenceable(12) %_1) + // regparm1: @f15(ptr noalias nofree noundef byval([12 x i8]) align 4 captures(address) dereferenceable(12) %_1) + // regparm2: @f15(ptr noalias nofree noundef byval([12 x i8]) align 4 captures(address) dereferenceable(12) %_1) + // regparm3: @f15([3 x i32] inreg %0) + #[no_mangle] + pub extern "C" fn f15(_: S3) {} }