Skip to content

Commit

Permalink
Add a custom lowering of vaarg for xtensa.
Browse files Browse the repository at this point in the history
LLVM does not include an implementation of the va_arg instruction for
Xtensa. From what I understand, this is a conscious decision and
instead language frontends are encouraged to implement it themselves.
The rationale seems to be that loading values correctly requires
language and ABI-specific knowledge that LLVM lacks.

This is true of most architectures, and rustc already provides
implementation for a number of them. This commit extends the support to
include Xtensa.

See https://lists.llvm.org/pipermail/llvm-dev/2017-August/116337.html
for some discussion on the topic.

Unfortunately there does not seem to be a reference document for the
semantics of the va_list and va_arg on Xtensa. The most reliable source
is the GCC implementation, which this commit tries to follow. Clang also
provides its own compatible implementation.

This was tested for all the types that rustc allows in variadics.
  • Loading branch information
plietar authored and MabezDev committed Dec 1, 2023
1 parent bd6bb31 commit 9805351
Showing 1 changed file with 95 additions and 2 deletions.
97 changes: 95 additions & 2 deletions compiler/rustc_codegen_llvm/src/va_arg.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,23 @@ use rustc_middle::ty::layout::{HasTyCtxt, LayoutOf};
use rustc_middle::ty::Ty;
use rustc_target::abi::{Align, Endian, HasDataLayout, Size};

fn round_up_to_alignment<'ll>(
bx: &mut Builder<'_, 'll, '_>,
mut value: &'ll Value,
align: Align,
) -> &'ll Value {
value = bx.add(value, bx.cx().const_i32(align.bytes() as i32 - 1));
return bx.and(value, bx.cx().const_i32(-(align.bytes() as i32)));
}

fn round_pointer_up_to_alignment<'ll>(
bx: &mut Builder<'_, 'll, '_>,
addr: &'ll Value,
align: Align,
ptr_ty: &'ll Type,
) -> &'ll Value {
let mut ptr_as_int = bx.ptrtoint(addr, bx.cx().type_isize());
ptr_as_int = bx.add(ptr_as_int, bx.cx().const_i32(align.bytes() as i32 - 1));
ptr_as_int = bx.and(ptr_as_int, bx.cx().const_i32(-(align.bytes() as i32)));
ptr_as_int = round_up_to_alignment(bx, ptr_as_int, align);
bx.inttoptr(ptr_as_int, ptr_ty)
}

Expand Down Expand Up @@ -249,6 +257,90 @@ fn emit_s390x_va_arg<'ll, 'tcx>(
bx.load(val_type, val_addr, layout.align.abi)
}

fn emit_xtensa_va_arg<'ll, 'tcx>(
bx: &mut Builder<'_, 'll, 'tcx>,
list: OperandRef<'tcx, &'ll Value>,
target_ty: Ty<'tcx>,
) -> &'ll Value {
// Implementation of va_arg for Xtensa. There doesn't seem to be an authoritative source for
// this, other than "what GCC does".
//
// The va_list type has three fields:
// struct __va_list_tag {
// int32_t *va_stk; // Arguments passed on the stack
// int32_t *va_reg; // Arguments passed in registers, saved to memory by the prologue.
// int32_t va_ndx; // Offset into the arguments, in bytes
// };
//
// Whether an argument is loaded from va_stk or va_reg depends on the value of va_ndx.
// The first 24 bytes (equivalent to 6 registers) come from va_reg, the rest from va_stk.
//
// Arguments are never split between registers and the stack. For example, if loading an 8 byte
// value and va_ndx = 20, we instead bump the offset and read everything from va_stk.
let va_list_addr = list.immediate();
let va_list_layout = list.deref(bx.cx).layout;
let va_list_ty = va_list_layout.llvm_type(bx);

let layout = bx.cx.layout_of(target_ty);
let from_stack = bx.append_sibling_block("va_arg.from_stack");
let from_regsave = bx.append_sibling_block("va_arg.from_regsave");
let end = bx.append_sibling_block("va_arg.end");

let offset_ptr =
bx.struct_gep(va_list_ty, va_list_addr, va_list_layout.llvm_field_index(bx.cx, 2));
let mut offset = bx.load(bx.type_i32(), offset_ptr, bx.tcx().data_layout.i32_align.abi);

let slot_size = layout.size.align_to(Align::from_bytes(4).unwrap()).bytes() as i32;
if layout.align.abi.bytes() > 4 {
offset = round_up_to_alignment(bx, offset, layout.align.abi);
}

// Update the offset in va_list, by adding the slot's size.
let offset_next = bx.add(offset, bx.const_i32(slot_size));
bx.store(offset_next, offset_ptr, bx.tcx().data_layout.pointer_align.abi);

// Figure out where to look for our value. We do that by checking the end of our slot (offset_next).
// If that is within the regsave area, then load from there. Otherwise load from the stack area.
let regsave_size = bx.const_i32(24);
let use_regsave = bx.icmp(IntPredicate::IntULE, offset_next, regsave_size);
bx.cond_br(use_regsave, from_regsave, from_stack);

bx.switch_to_block(from_regsave);
let regsave_area_ptr =
bx.struct_gep(va_list_ty, va_list_addr, va_list_layout.llvm_field_index(bx.cx, 1));
let regsave_area =
bx.load(bx.type_ptr(), regsave_area_ptr, bx.tcx().data_layout.pointer_align.abi);
let regsave_value_ptr = bx.inbounds_gep(bx.type_i8(), regsave_area, &[offset]);
bx.br(end);

bx.switch_to_block(from_stack);

// The first time we switch from regsave to stack we needs to adjust our offsets a bit.
// va_stk is set up such that the first stack argument is always at va_stk + 32.
// The corrected offset is written back into the va_list struct.
let needs_correction = bx.icmp(IntPredicate::IntULE, offset, regsave_size);
let offset_corrected = bx.select(needs_correction, bx.const_i32(32), offset);
let offset_next_corrected =
bx.select(needs_correction, bx.const_i32(32 + slot_size), offset_next);
bx.store(offset_next_corrected, offset_ptr, bx.tcx().data_layout.pointer_align.abi);

let stack_area_ptr =
bx.struct_gep(va_list_ty, va_list_addr, va_list_layout.llvm_field_index(bx.cx, 0));
let stack_area = bx.load(bx.type_ptr(), stack_area_ptr, bx.tcx().data_layout.pointer_align.abi);
let stack_value_ptr = bx.inbounds_gep(bx.type_i8(), stack_area, &[offset_corrected]);
bx.br(end);

bx.switch_to_block(end);

// On big-endian, for values smaller than the slot size we'd have to align the read to the end
// of the slot rather than the start. While the ISA and GCC support big-endian, all the Xtensa
// targets supported by rustc are litte-endian so don't worry about it.
assert!(bx.tcx().sess.target.endian == Endian::Little);
let value_ptr =
bx.phi(bx.type_ptr(), &[regsave_value_ptr, stack_value_ptr], &[from_regsave, from_stack]);
return bx.load(layout.llvm_type(bx), value_ptr, layout.align.abi);
}

pub(super) fn emit_va_arg<'ll, 'tcx>(
bx: &mut Builder<'_, 'll, 'tcx>,
addr: OperandRef<'tcx, &'ll Value>,
Expand Down Expand Up @@ -281,6 +373,7 @@ pub(super) fn emit_va_arg<'ll, 'tcx>(
let indirect: bool = target_ty_size > 8 || !target_ty_size.is_power_of_two();
emit_ptr_va_arg(bx, addr, target_ty, indirect, Align::from_bytes(8).unwrap(), false)
}
"xtensa" => emit_xtensa_va_arg(bx, addr, target_ty),
// For all other architecture/OS combinations fall back to using
// the LLVM va_arg instruction.
// https://llvm.org/docs/LangRef.html#va-arg-instruction
Expand Down

0 comments on commit 9805351

Please sign in to comment.