Skip to content
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

rustc_target: Add alignment to indirectly-passed by-value types, correcting the alignment of byval on x86 in the process. #103830

Closed
wants to merge 1 commit into from
Closed
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
2 changes: 1 addition & 1 deletion compiler/rustc_target/src/abi/call/m68k.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ fn classify_ret<Ty>(ret: &mut ArgAbi<'_, Ty>) {

fn classify_arg<Ty>(arg: &mut ArgAbi<'_, Ty>) {
if arg.layout.is_aggregate() {
arg.make_indirect_byval();
arg.make_indirect_byval(None);
} else {
arg.extend_integer_width_to(32);
}
Expand Down
19 changes: 13 additions & 6 deletions compiler/rustc_target/src/abi/call/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -499,9 +499,7 @@ impl<'a, Ty> ArgAbi<'a, Ty> {
.set(ArgAttribute::NonNull)
.set(ArgAttribute::NoUndef);
attrs.pointee_size = layout.size;
// FIXME(eddyb) We should be doing this, but at least on
// i686-pc-windows-msvc, it results in wrong stack offsets.
// attrs.pointee_align = Some(layout.align.abi);
attrs.pointee_align = Some(layout.align.abi);

let extra_attrs = layout.is_unsized().then_some(ArgAttributes::new());

Expand All @@ -518,11 +516,19 @@ impl<'a, Ty> ArgAbi<'a, Ty> {
self.mode = Self::indirect_pass_mode(&self.layout);
}

pub fn make_indirect_byval(&mut self) {
pub fn make_indirect_byval(&mut self, byval_align: Option<Align>) {
self.make_indirect();
match self.mode {
PassMode::Indirect { attrs: _, extra_attrs: _, ref mut on_stack } => {
PassMode::Indirect { ref mut attrs, extra_attrs: _, ref mut on_stack } => {
*on_stack = true;

// Some platforms, like 32-bit x86, change the alignment of the type when passing
// `byval`. Account for that.
if let Some(byval_align) = byval_align {
// On all targets with byval align this is currently true, so let's assert it.
debug_assert!(byval_align >= Align::from_bytes(4).unwrap());
attrs.pointee_align = Some(byval_align);
oli-obk marked this conversation as resolved.
Show resolved Hide resolved
}
}
_ => unreachable!(),
}
Expand Down Expand Up @@ -659,7 +665,8 @@ impl<'a, Ty> FnAbi<'a, Ty> {
{
if abi == spec::abi::Abi::X86Interrupt {
if let Some(arg) = self.args.first_mut() {
arg.make_indirect_byval();
// FIXME(pcwalton): This probably should use the x86 `byval` ABI...
Copy link
Member

@bjorn3 bjorn3 Nov 2, 2022

Choose a reason for hiding this comment

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

For extern "x86-interrupt" fn(_: InterruptStackFrame) the argument is passed as pointer without byval attribute AFAIK.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Are you saying I should change make_indirect_byval() to make_indirect() here?

Copy link
Contributor

Choose a reason for hiding this comment

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

LLVM requires this argument to be marked byval. You'll get a verifier error otherwise.

arg.make_indirect_byval(None);
}
return Ok(());
}
Expand Down
2 changes: 1 addition & 1 deletion compiler/rustc_target/src/abi/call/wasm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ where
{
arg.extend_integer_width_to(32);
if arg.layout.is_aggregate() && !unwrap_trivial_aggregate(cx, arg) {
arg.make_indirect_byval();
arg.make_indirect_byval(None);
}
}

Expand Down
35 changes: 31 additions & 4 deletions compiler/rustc_target/src/abi/call/x86.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use crate::abi::call::{ArgAttribute, FnAbi, PassMode, Reg, RegKind};
use crate::abi::{HasDataLayout, TyAbiInterface};
use crate::abi::{Align, HasDataLayout, TyAbiInterface};
use crate::spec::HasTargetSpec;

#[derive(PartialEq)]
Expand Down Expand Up @@ -53,11 +53,38 @@ where
if arg.is_ignore() {
continue;
}
if arg.layout.is_aggregate() {
arg.make_indirect_byval();
} else {
if !arg.layout.is_aggregate() {
arg.extend_integer_width_to(32);
continue;
}

// 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:
//
// 1. If the natural alignment of the type is less than or equal to 4, the alignment is 4.
//
// 2. Otherwise, on Linux, the alignment of any vector type is the natural alignment.
// (This doesn't matter here because we ensure we have an aggregate with the check above.)
//
// 3. Otherwise, on Apple platforms, the alignment of anything that contains a vector type
// is 16.
//
// 4. If none of these conditions are true, the alignment is 4.
let t = cx.target_spec();
let align_4 = Align::from_bytes(4).unwrap();
let align_16 = Align::from_bytes(16).unwrap();
let byval_align = if arg.layout.align.abi < align_4 {
align_4
} else if t.is_like_osx && arg.layout.align.abi >= align_16 {
// FIXME(pcwalton): This is dubious--we should actually be looking inside the type to
// determine if it contains SIMD vector values--but I think it's fine?
align_16
} else {
align_4
};

arg.make_indirect_byval(Some(byval_align));
}

if flavor == Flavor::FastcallOrVectorcall {
Expand Down
2 changes: 1 addition & 1 deletion compiler/rustc_target/src/abi/call/x86_64.rs
Original file line number Diff line number Diff line change
Expand Up @@ -213,7 +213,7 @@ where
match cls_or_mem {
Err(Memory) => {
if is_arg {
arg.make_indirect_byval();
arg.make_indirect_byval(None);
} else {
// `sret` parameter thus one less integer register available
arg.make_indirect();
Expand Down
56 changes: 56 additions & 0 deletions src/test/codegen/align-byval.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
// ignore-x86
// ignore-aarch64
// ignore-aarch64_be
// ignore-arm
// ignore-armeb
// ignore-avr
// ignore-bpfel
// ignore-bpfeb
// ignore-hexagon
// ignore-mips
// ignore-mips64
// ignore-msp430
// ignore-powerpc64
// ignore-powerpc64le
// ignore-powerpc
// ignore-r600
// ignore-amdgcn
// ignore-sparc
// ignore-sparcv9
// ignore-sparcel
// ignore-s390x
// ignore-tce
// ignore-thumb
// ignore-thumbeb
// ignore-xcore
// ignore-nvptx
// ignore-nvptx64
// ignore-le32
// ignore-le64
// ignore-amdil
// ignore-amdil64
// ignore-hsail
// ignore-hsail64
// ignore-spir
// ignore-spir64
// ignore-kalimba
// ignore-shave
Copy link
Contributor

Choose a reason for hiding this comment

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

This looks really fragile. I'd suggest to implement this in the same way as https://github.com/rust-lang/rust/blob/master/tests/codegen/abi-repr-ext.rs, i.e. a no_core test explicitly specifying targets. That should also allow you to test a non-windows target for the issue below.

//
// Tests that `byval` alignment is properly specified (#80127).
// The only targets that use `byval` are m68k, wasm, x86-64, and x86. Note that
// x86 has special rules (see #103830), and it's therefore ignored here.

#[repr(C)]
#[repr(align(16))]
struct Foo {
a: [i32; 16],
}

extern "C" {
// CHECK: declare void @f({{.*}}byval(%Foo) align 16{{.*}})
Copy link
Member

Choose a reason for hiding this comment

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

The Windows x64 ABI mandates passing Foo via reference so the generated IR looks like this, which causes the test to fail:

; Function Attrs: uwtable
declare void @f(ptr noalias nocapture noundef align 16 dereferenceable(64)) unnamed_addr #1

Can we conditionally check for ptr on Windows and byval(%Foo) on !Window?

fn f(foo: Foo);
Copy link
Contributor

@BGR360 BGR360 Nov 2, 2022

Choose a reason for hiding this comment

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

You have to exceed a certain number of parameters for the original bug to surface. Enough to fill up the registers. I think the function signature in #80127 was the minimal repro; I remember trying to minimize it further and having not much luck.

I'm trying to use godbolt to figure it out but I'm having frustrating usability problems, and my personal device isn't x86_64 -_-

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I think testing for the align attribute directly is better, because the test case in #80127 might randomly start to succeed if future changes to regalloc cause things to accidentally align, no?

Copy link
Contributor

@BGR360 BGR360 Nov 2, 2022

Choose a reason for hiding this comment

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

If it's a regression test it should never start to succeed, it could only ever start to fail right? I guess I'm confused by what you're saying.

Also, my memory is quite fuzzy, but I thought that part of the underlying cause of the bug was that our logic for choosing how to pass args on x86_64 was wrong for stack arguments (which would require your test to have enough arguments to get to that point I think). But judging by you saying "I think testing for the align attribute directly is better", is it just that we were never putting align on any of the byval args?

Copy link
Contributor

Choose a reason for hiding this comment

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

Idk, I think I fundamentally don't trust a test that just checks the LLVM IR, because I don't understand it well enough for that to be convincing over a real integration test that proves that the C code can operate with the Rust code. But I recognize that that's probably quite hard to test in the rust repo. Perhaps someone else knows a way?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

If it's a regression test it should never start to succeed, it could only ever start to fail right? I guess I'm confused by what you're saying.

I'm saying it could get broken accidentally but we wouldn't notice it because it could accidentally start to pass. I can add an interop test though.

Copy link
Contributor

Choose a reason for hiding this comment

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

That would be awesome. Thank you for tackling this bug that has existed for so so long 🙏🙏

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Added the test.

}

pub fn main() {
unsafe { f(Foo { a: [1; 16] }) }
}
4 changes: 2 additions & 2 deletions src/test/codegen/function-arguments-noopt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ pub fn borrow_call(x: &i32, f: fn(&i32) -> &i32) -> &i32 {
f(x)
}

// CHECK: void @struct_({{%S\*|ptr}} sret(%S){{( %0)?}}, {{%S\*|ptr}} %x)
// CHECK: void @struct_({{%S\*|ptr}} sret(%S) align 4{{( %0)?}}, {{%S\*|ptr}} align 4 %x)
#[no_mangle]
pub fn struct_(x: S) -> S {
x
Expand All @@ -45,7 +45,7 @@ pub fn struct_(x: S) -> S {
// CHECK-LABEL: @struct_call
#[no_mangle]
pub fn struct_call(x: S, f: fn(S) -> S) -> S {
// CHECK: call void %f({{%S\*|ptr}} sret(%S){{( %0)?}}, {{%S\*|ptr}} %{{.+}})
// CHECK: call void %f({{%S\*|ptr}} sret(%S) align 4{{( %0)?}}, {{%S\*|ptr}} align 4 %{{.+}})
f(x)
}

Expand Down
4 changes: 2 additions & 2 deletions src/test/codegen/function-arguments.rs
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@ pub fn mutable_notunpin_borrow(_: &mut NotUnpin) {
pub fn notunpin_borrow(_: &NotUnpin) {
}

// CHECK: @indirect_struct({{%S\*|ptr}} noalias nocapture noundef readonly dereferenceable(32) %_1)
// CHECK: @indirect_struct({{%S\*|ptr}} noalias nocapture noundef readonly align 4 dereferenceable(32) %_1)
#[no_mangle]
pub fn indirect_struct(_: S) {
}
Expand All @@ -151,7 +151,7 @@ pub fn _box(x: Box<i32>) -> Box<i32> {
x
}

// CHECK: @struct_return({{%S\*|ptr}} noalias nocapture noundef sret(%S) dereferenceable(32){{( %0)?}})
// CHECK: @struct_return({{%S\*|ptr}} noalias nocapture noundef sret(%S) align 4 dereferenceable(32){{( %0)?}})
#[no_mangle]
pub fn struct_return() -> S {
S {
Expand Down
5 changes: 5 additions & 0 deletions src/test/run-make-fulldeps/extern-fn-explicit-align/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
include ../tools.mk

all: $(call NATIVE_STATICLIB,test)
$(RUSTC) test.rs
$(call RUN,test) || exit 1
35 changes: 35 additions & 0 deletions src/test/run-make-fulldeps/extern-fn-explicit-align/test.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
#include <assert.h>
#include <stdbool.h>
#include <stdint.h>
#include <string.h>

struct TwoU64s
{
uint64_t a;
uint64_t b;
} __attribute__((aligned(16)));

struct BoolAndU32
{
bool a;
uint32_t b;
};

int32_t many_args(
void *a,
void *b,
const char *c,
uint64_t d,
bool e,
struct BoolAndU32 f,
void *g,
struct TwoU64s h,
void *i,
void *j,
void *k,
void *l,
const char *m)
{
assert(strcmp(m, "Hello world") == 0);
return 0;
}
61 changes: 61 additions & 0 deletions src/test/run-make-fulldeps/extern-fn-explicit-align/test.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
// Issue #80127: Passing structs via FFI should work with explicit alignment.

use std::ffi::CString;
use std::ptr::null_mut;

#[derive(Clone, Copy, Debug, PartialEq)]
#[repr(C)]
#[repr(align(16))]
pub struct TwoU64s {
pub a: u64,
pub b: u64,
}

#[repr(C)]
#[derive(Debug, Copy, Clone)]
pub struct BoolAndU32 {
pub a: bool,
pub b: u32,
}

#[link(name = "test", kind = "static")]
extern "C" {
fn many_args(
a: *mut (),
b: *mut (),
c: *const i8,
d: u64,
e: bool,
f: BoolAndU32,
g: *mut (),
h: TwoU64s,
i: *mut (),
j: *mut (),
k: *mut (),
l: *mut (),
m: *const i8,
) -> i32;
}

fn main() {
let two_u64s = TwoU64s { a: 1, b: 2 };
let bool_and_u32 = BoolAndU32 { a: true, b: 3 };
let string = CString::new("Hello world").unwrap();
unsafe {
many_args(
null_mut(),
null_mut(),
null_mut(),
4,
true,
bool_and_u32,
null_mut(),
two_u64s,
null_mut(),
null_mut(),
null_mut(),
null_mut(),
string.as_ptr(),
);
}
}