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

[RISC-V] Update ABI doc #107286

Merged
merged 5 commits into from
Sep 16, 2024
Merged
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
12 changes: 11 additions & 1 deletion docs/design/coreclr/botr/clr-abi.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ The Linux System V x86_64 ABI is documented in [System V Application Binary Inte

The LoongArch64 ABI documentation is [here](https://github.com/loongson/LoongArch-Documentation/blob/main/docs/LoongArch-ELF-ABI-EN.adoc)

The RISC-V ABIs Specification: [latest release](https://github.com/riscv-non-isa/riscv-elf-psabi-doc/releases/latest), [latest draft](https://github.com/riscv-non-isa/riscv-elf-psabi-doc/releases), [document source repo](https://github.com/riscv-non-isa/riscv-elf-psabi-doc).

# General Unwind/Frame Layout

For all non-x86 platforms, all methods must have unwind information so the garbage collector (GC) can unwind them (unlike native code in which a leaf method may be omitted).
Expand Down Expand Up @@ -77,7 +79,9 @@ On ARM and ARM64, just like native, nothing is put in the floating point registe

However, unlike native varargs, all floating point arguments are not promoted to double (`R8`), and instead retain their original type (`R4` or `R8`) (although this does not preclude an IL generator like managed C++ from explicitly injecting an upcast at the call-site and adjusting the call-site-sig appropriately). This leads to unexpected behavior when native C++ is ported to C# or even just managed via the different flavors of managed C++.

Managed varargs are not supported in .NET Core.
Managed varargs are supported on Windows only.

Managed/native varargs are supported on Windows only. Support for managed/native varargs on non-Windows platforms is tracked by [this issue](https://github.com/dotnet/runtime/issues/82081).

## Generics

Expand All @@ -98,6 +102,10 @@ Just like native, AMD64 has implicit-byrefs. Any structure (value type in IL par

The AMD64 native calling conventions (Windows 64 and System V) require return buffer address to be returned by callee in RAX. JIT also follows this rule.

## RISC-V only: structs passed/returned according to hardware floating-point calling convention

Passing/returning structs according to hardware floating-point calling convention like native is currently [supported only up to 16 bytes](https://github.com/dotnet/runtime/issues/107386), ones larger than that differ from the standard ABI and are passed/returned according to integer calling convention (by implicit reference).

## Return buffers

The same applies to some return buffers. See `MethodTable::IsStructRequiringStackAllocRetBuf()`. When that returns `false`, the return buffer might be on the heap, either due to reflection/remoting code paths mentioned previously or due to a JIT optimization where a call with a return buffer that then assigns to a field (on the GC heap) are changed into passing the field reference as the return buffer. Conversely, when it returns true, the JIT does not need to use a write barrier when storing to the return buffer, but it is still not guaranteed to be a compiler temp, and as such the JIT should not introduce spurious writes to the return buffer.
Expand All @@ -122,6 +130,8 @@ Primitive value types smaller than 32-bits are widened to 32-bits: signed small

Small primitive arguments have undefined upper bits. This can be different from the standard calling conventions that may require normalization (e.g. on ARM32 and Apple ARM64).

On RISC-V small primitive arguments are extended according to standard calling conventions.
Copy link
Member

@jkotas jkotas Sep 3, 2024

Choose a reason for hiding this comment

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

The managed calling convention leaves the upper bits of small primitive arguments undefined across all architectures. It does not match the native calling convention wrt. passing small types on number of other architectures.

Is there a value in RISC-V to be special on this point? Would be better to unify it?

Copy link
Contributor Author

@tomeksowi tomeksowi Sep 4, 2024

Choose a reason for hiding this comment

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

It's natural for RISC-V to have small primitives extended, it's cheaper to require that of the caller (it's free in case of load instructions) than to do 2 shifts for each argument every time in the callee, which we couldn't optimize out otherwise. Some care was already taken to properly extend small integers, e.g. in #93665 (comment) so we're almost there anyway. Without it we would have to create a special calling convention variant just for managed.

Is there a benefit of unifying it given each architecture strives to follow their own platform calling conventions?

Copy link
Member

Choose a reason for hiding this comment

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

Ok, sounds reasonable to me. It is another one of those things that make RISCV different.


# PInvokes

The convention is that any method with an InlinedCallFrame (either an IL stub or a normal method with an inlined PInvoke) saves/restores all non-volatile integer registers in its prolog/epilog respectively. This is done so that the InlinedCallFrame can just contain a return address, a stack pointer and a frame pointer. Then using just those three it can start a full stack walk using the normal RtlVirtualUnwind.
Expand Down