Die ABI (Application Binary Interface) legt fest, wie Funktionen auf Binärebene kommunizieren: welche Register Argumente tragen, wie der Stack aufgebaut ist, wer Register sichert und wie der Rückgabewert transportiert wird. Lyx folgt den nativen Konventionen der jeweiligen Zielplattform, um Kompatibilität mit Betriebssystem-Syscalls und C-Bibliotheken ohne Wrapper zu gewährleisten.
Dieses Wissen ist notwendig für:
unsafe-Blöcken–call-graph-Reports bei DO-178C-ZertifizierungAlle Typen folgen ihrer natürlichen Ausrichtung (Alignment = Größe, maximal 8 Byte). Das gilt auf allen drei unterstützten Architekturen.
| Typ | Größe (Byte) | Alignment | Registerklasse |
|---|---|---|---|
bool | 1 | 1 | Integer (GPR), Wert 0 oder 1 |
int8 / uint8 | 1 | 1 | Integer (GPR) |
int16 / uint16 | 2 | 2 | Integer (GPR) |
int32 / uint32 | 4 | 4 | Integer (GPR) |
int64 / uint64 | 8 | 8 | Integer (GPR) |
isize / usize | 8 | 8 | Integer (GPR) |
f32 | 4 | 4 | Float (FPU/SIMD) |
f64 | 8 | 8 | Float (FPU/SIMD) |
qbool | 8 | 8 | Float (FPU) – wie f64 |
pchar / ^T | 8 | 8 | Integer (GPR) – Pointer = int64 |
char | 1 | 1 | Integer (GPR) |
qbool in der ABI:
Obwohlqboollogisch wie einboolwirkt, wird er alsf64behandelt. Wahrscheinlichkeitswerte fließen direkt durch die Floating-Point-Einheiten – ohne GPR-zu-FPU-Konvertierung.
Structs werden so ausgerichtet, dass jedes Feld sein natürliches Alignment erfüllt. Der Compiler fügt Padding-Bytes ein:
type Example = struct {
a: uint8; // Offset 0, 1 Byte
// 7 Byte Padding
b: int64; // Offset 8, 8 Byte
c: uint32; // Offset 16, 4 Byte
// 4 Byte Padding
};
// Gesamt: 24 Byte
Mit @packed entfällt das Padding (wichtig für Hardware-Register-Maps und Protokoll-Frames):
@packed
type PackedExample = struct {
a: uint8; // Offset 0
b: int64; // Offset 1
c: uint32; // Offset 9
};
// Gesamt: 13 Byte – kein Padding
→ Details: FFI – @packed und Struct-Layout
Die Standard-Konvention auf Linux und macOS. Lyx verwendet sie für alle Nicht-Windows-Targets.
Zuordnung der Integer-Register nach x86-64 System V ABI:
| Zweck | Register | Gesichert von |
|---|---|---|
| Argument 1 | RDI | Aufrufer (Caller-saved) |
| Argument 2 | RSI | Aufrufer |
| Argument 3 | RDX | Aufrufer |
| Argument 4 | RCX | Aufrufer |
| Argument 5 | R8 | Aufrufer |
| Argument 6 | R9 | Aufrufer |
| Argument 7+ | Stack | Aufrufer |
| Rückgabewert (primär) | RAX | Aufrufer |
| Rückgabewert (sekundär) | RDX | Aufrufer |
| Scratch (temporär) | R10, R11 | Aufrufer |
| Callee-saved | RBX, RBP, R12–R15 | Aufgerufener |
Zuordnung der Floating-Point-Register:
| Zweck | Register |
|---|---|
| Float-Argumente 1–8 | XMM0 – XMM7 |
| Float-Rückgabewert | XMM0 |
| Caller-saved | XMM0 – XMM15 |
Standard-Funktionsrahmen bei x86_64:
; Funktionsprolog (Lyx-generiert)
push rbp ; Sichert alten Frame-Pointer (Callee-saved)
mov rbp, rsp ; Neuer Frame-Pointer
sub rsp, N ; Platz für lokale Variablen (N = nächstes Vielfaches von 16)
; … Funktionskörper …
; Funktionsepilog
mov rsp, rbp ; Stack-Pointer wiederherstellen
pop rbp ; Frame-Pointer wiederherstellen
ret ; Springt zur Adresse auf dem Stack (von 'call' abgelegt)
fn Add(a: int64, b: int64): int64 {
return a + b;
}
Generierter Assembler:
Add:
push rbp
mov rbp, rsp
mov rax, rdi ; a kommt in RDI → ins Ergebnis-Register RAX
add rax, rsi ; b kommt in RSI → addieren
pop rbp
ret
Bei rekursiven Aufrufen muss nichts extra gesichert werden – call legt die Rücksprungadresse automatisch auf den Stack. RBP wird im Prolog gesichert.
Factorial:
push rbp
mov rbp, rsp
cmp rdi, 1
jle .base
push rdi ; RDI sichern (Caller-saved, wird durch rekursiven call überschrieben)
dec rdi
call Factorial ; RDI = n-1
pop rdi ; RDI (= n) wiederherstellen
imul rax, rdi ; Ergebnis = n * Factorial(n-1)
jmp .done
.base:
mov rax, 1
.done:
pop rbp
ret
Windows verwendet eine eigene Calling Convention. Der Hauptunterschied: nur vier Argument-Register, Shadow Space auf dem Stack.
Zuordnung der Integer-Register nach x86-64 System V ABI:
| Zweck | Register | Gesichert von |
|---|---|---|
| Argument 1 | RCX | Aufrufer |
| Argument 2 | RDX | Aufrufer |
| Argument 3 | R8 | Aufrufer |
| Argument 4 | R9 | Aufrufer |
| Argument 5+ | Stack | Aufrufer |
| Rückgabewert | RAX | Aufrufer |
| Callee-saved | RBX, RBP, RDI, RSI, R12–R15 | Aufgerufener |
Zuordnung der Floating-Point-Register:
| Zweck | Register |
|---|---|
| Float-Argumente 1–4 | XMM0 – XMM3 |
| Float-Rückgabewert | XMM0 |
| Caller-saved | XMM0 – XMM5 |
| Callee-saved | XMM6 – XMM15 |
Wichtiger Unterschied zu System V:
Auf Windows sind XMM6–XMM15 callee-saved. Wenn Lyx-Code diese Register nutzt (z. B. für f64-Operationen), muss der generierte Prolog sie sichern. Der Lyx-Compiler erledigt das automatisch beim Windows-Target.
Vor jedem Funktionsaufruf reserviert der Aufrufer 32 Byte Shadow Space auf dem Stack – direkt unterhalb der Rücksprungadresse. Windows-API-Funktionen nutzen diesen Bereich intern zum Sichern von RCX, RDX, R8, R9.
; Windows-Aufruf: Funktion mit 2 Argumenten
sub rsp, 40 ; 32 Byte Shadow Space + 8 Byte Alignment
mov rcx, arg1 ; Argument 1
mov rdx, arg2 ; Argument 2
call SomeCFunction
add rsp, 40 ; Shadow Space freigeben
Lyx generiert den Shadow Space automatisch bei allen @extern-Aufrufen unter Windows-Target.
@stdcall ist die Aufrufkonvention älterer Win32-APIs (vor Vista weit verbreitet). Der wesentliche Unterschied: Der Aufgerufene räumt den Stack auf (nicht der Aufrufer).
@extern @stdcall
fn MessageBoxA(hwnd: int64, text: pchar, caption: pchar, flags: uint32): int32;
ARM64 verwendet den AAPCS64-Standard (Procedure Call Standard for the Arm 64-bit Architecture). Dieser gilt auf Linux-ARM, Apple Silicon (macOS/iOS) und Embedded-ARM-Targets.
Zuordnung der Integer-Register nach x86-64 System V ABI:
| Register | Alias | Zweck | Gesichert von |
|---|---|---|---|
| X0–X7 | – | Argumente 1–8 / Rückgabewert 1–2 | Aufrufer |
| X8 | XR | Indirekter Ergebnis-Zeiger (große Structs) | Aufrufer |
| X9–X15 | – | Temporäre Register | Aufrufer |
| X16–X17 | IP0, IP1 | Intra-Procedure-Call Scratch | Aufrufer |
| X18 | PR | Platform-Register (OS-intern, nicht für Lyx) | – |
| X19–X28 | – | Callee-saved Register | Aufgerufener |
| X29 | FP | Frame-Pointer | Aufgerufener |
| X30 | LR | Link-Register (Rücksprungadresse) | Aufgerufener |
| SP | – | Stack-Pointer (16-Byte-Aligned) | – |
| Register | Zweck | Gesichert von |
|---|---|---|
| V0–V7 | Float-Argumente / Float-Rückgabewert | Aufrufer |
| V8–V15 | Callee-saved | Aufgerufener |
| V16–V31 | Temporäre SIMD-Register | Aufrufer |
; ARM64 Funktionsprolog
stp x29, x30, [sp, #-16]! ; Frame-Pointer (x29) und Link-Register (x30) sichern
; '!' = Pre-Decrement: SP wird zuerst um 16 verringert
mov x29, sp ; Neuen Frame-Pointer setzen
; … Funktionskörper …
; ARM64 Funktionsepilog
ldp x29, x30, [sp], #16 ; FP und LR wiederherstellen; SP += 16
ret ; Springt zu Adresse in X30
Häufigster ARM64-Bug bei Rekursion:
Das Link-Register X30 wird beimbl-Befehl (Branch with Link) mit der Rücksprungadresse des aktuellen Aufrufs überschrieben. Ohnestp x29, x30am Anfang verliert die Funktion ihre eigene Rücksprungadresse, sobald sie eine andere Funktion aufruft. Das führt zu einem Absturz beimret.
fn Clamp(val: int64, lo: int64, hi: int64): int64 {
if (val < lo) { return lo; }
if (val > hi) { return hi; }
return val;
}
Clamp:
; val → X0, lo → X1, hi → X2
stp x29, x30, [sp, #-16]!
mov x29, sp
cmp x0, x1
bge .check_hi
mov x0, x1 ; return lo
b .done
.check_hi:
cmp x0, x2
ble .done
mov x0, x2 ; return hi
.done:
ldp x29, x30, [sp], #16
ret
Structs bis 16 Byte werden in X0–X1 zurückgegeben (zwei 8-Byte-Hälften). Größere Structs: Der Aufrufer reserviert Platz und übergibt den Zeiger in X8 (Indirect Result Register). Der Aufgerufene schreibt das Ergebnis dorthin.
type BigResult = struct { a: int64; b: int64; c: int64; }; // 24 Byte
fn GetResult(): BigResult {
return BigResult { a: 1, b: 2, c: 3 };
// Compiler: Aufrufer reserviert 24 Byte, Adresse in X8
// Aufgerufener schreibt nach [X8]
}
Für RISC-V nutzt Lyx die offizielle RISC-V calling convention (psABI, Integer + FP extension).
| ABI-Name | Reg.-Nr. | Zweck | Gesichert von |
|---|---|---|---|
zero | x0 | Konstante 0 (hardwired) | – |
ra | x1 | Return Address (Rücksprungadresse) | Aufgerufener |
sp | x2 | Stack Pointer | Aufgerufener |
gp | x3 | Global Pointer (GOT-Zugriff) | – |
tp | x4 | Thread Pointer (TLS) | – |
t0–t2 | x5–x7 | Temporäre Register | Aufrufer |
s0 / fp | x8 | Saved Register / Frame Pointer | Aufgerufener |
s1 | x9 | Saved Register | Aufgerufener |
a0–a7 | x10–x17 | Argumente / Rückgabewerte (a0–a1) | Aufrufer |
s2–s11 | x18–x27 | Saved Register | Aufgerufener |
t3–t6 | x28–x31 | Temporäre Register | Aufrufer |
| ABI-Name | Zweck | Gesichert von |
|---|---|---|
fa0–fa7 | Float-Argumente / Rückgabe | Aufrufer |
fs0–fs11 | Saved Float-Register | Aufgerufener |
ft0–ft11 | Temporäre Float-Register | Aufrufer |
; RISC-V64 Funktionsprolog
addi sp, sp, -16 ; Stack-Pointer verringern
sd ra, 8(sp) ; Rücksprungadresse sichern
sd s0, 0(sp) ; Frame-Pointer sichern
addi s0, sp, 16 ; Neuen Frame-Pointer setzen
; … Funktionskörper …
; RISC-V64 Funktionsepilog
ld ra, 8(sp) ; Rücksprungadresse wiederherstellen
ld s0, 0(sp) ; Frame-Pointer wiederherstellen
addi sp, sp, 16 ; Stack freigeben
ret ; = jalr x0, ra, 0
Ähnlich wie ARM64: ra muss im Prolog gesichert werden, bevor eine andere Funktion aufgerufen wird.
Alle drei Architekturen verlangen ein 16-Byte-Alignment des Stack-Pointers zum Zeitpunkt eines Funktionsaufrufs. Der Lyx-Compiler stellt dies im Prolog sicher.
Höhere Adressen (Richtung Basis)
┌──────────────────────────────────┐
│ Argument 8+ (vom Aufrufer) │ ← RSP vor dem 'call'
├──────────────────────────────────┤
│ Rücksprungadresse (von 'call') │
├──────────────────────────────────┤ ← RBP zeigt hier
│ Gesicherter alter RBP │
├──────────────────────────────────┤
│ Lokale Variable 1 │
│ Lokale Variable 2 │
│ … │
├──────────────────────────────────┤
│ Padding (für 16-Byte-Alignment) │
└──────────────────────────────────┘ ← RSP (aktuelle Position)
Niedrigere Adressen (Stack wächst ↓)
In Modulen mit @integrity oder @redundant fügt der Lyx-Compiler zusätzliche Canary-Bytes zwischen Stack-Frames ein. Diese werden beim Epilog geprüft; eine Änderung signalisiert einen Pufferüberlauf oder Bit-Flip.
@integrity(mode: scrubbed)
@dal(B)
fn ProcessFrame(data: ^uint8) {
var buf: [128]uint8;
// Compiler: legt Canary-Wert oberhalb von 'buf', prüft ihn im Epilog
}
Konventionen für Rückgabewerte nach ABI:
| Typ | x86_64 | ARM64 | RISC-V64 |
|---|---|---|---|
int8…int64, Pointer | RAX | X0 | a0 |
f32, f64, qbool | XMM0 | V0 | fa0 |
| Zweiter Rückgabewert | RDX | X1 | a1 |
Lyx unterstützt Tupel-Rückgaben: (int64, bool). Beide Werte werden in den zwei primären Rückgabe-Registern transportiert:
fn Divide(a: int64, b: int64): (int64, bool) {
if (b = 0) { return (0, false); }
return (a / b, true);
}
; x86_64: RAX = Quotient, RDX = ok (0 oder 1)
; ARM64: X0 = Quotient, X1 = ok
; RISC-V: a0 = Quotient, a1 = ok
Structs über 16 Byte werden nicht in Registern zurückgegeben. Der Aufrufer alloziert den Speicher und übergibt einen versteckten Zeiger:
| Architektur | Zeiger-Register |
|---|---|
| x86_64 (System V) | RDI (erstes Argument – verschiebt alle anderen um +1) |
| ARM64 | X8 (Indirect Result Register – kein Argument verschoben) |
| RISC-V64 | a0 (erstes Argument – verschiebt alle anderen um +1) |
Variadische Funktionen (C-printf-Stil) erfordern in System V, dass AL (lower byte of RAX) die Anzahl der genutzten XMM-Register enthält, bevor die Funktion aufgerufen wird.
@extern @variadic
fn printf(fmt: pchar): int32;
; x86_64 System V: printf("Wert: %d\n", 42)
mov rdi, fmt_str ; Format-String
mov rsi, 42 ; Erstes variadisches Argument
xor eax, eax ; AL = 0 (keine XMM-Register genutzt)
call printf
Auf Windows (Microsoft x64) entfällt die AL-Konvention; variadische Argumente folgen den normalen Argument-Registern (RCX, RDX, R8, R9, dann Stack).
Syscalls nutzen eine eigene Konvention und umgehen die normale Calling Convention. std.os und std.fs kapseln diese Aufrufe.
| Aspekt | x86_64 (Linux) | ARM64 (Linux) | RISC-V64 (Linux) |
|---|---|---|---|
| Syscall-Nummer | RAX | X8 | a7 |
| Argument 1 | RDI | X0 | a0 |
| Argument 2 | RSI | X1 | a1 |
| Argument 3 | RDX | X2 | a2 |
| Argument 4 | R10 | X3 | a3 |
| Argument 5 | R8 | X4 | a4 |
| Argument 6 | R9 | X5 | a5 |
| Rückgabewert | RAX | X0 | a0 |
| Instruktion | syscall | svc #0 | ecall |
// std.os kapselt Syscalls – direkter Einsatz nur in unsafe + FFI:
unsafe {
// write(1, msg, len) → Syscall Nr. 1 auf x86_64
var result: int64;
// ... inline asm oder std.os.Write() verwenden
}
| Merkmal | System V (Linux/macOS) | Microsoft x64 | ARM64 (AAPCS64) | RISC-V64 |
|---|---|---|---|---|
| Argument-Register (Int) | 6 (RDI,RSI,RDX,RCX,R8,R9) | 4 (RCX,RDX,R8,R9) | 8 (X0–X7) | 8 (a0–a7) |
| Argument-Register (Float) | 8 (XMM0–XMM7) | 4 (XMM0–XMM3) | 8 (V0–V7) | 8 (fa0–fa7) |
| Rückgabe-Register | RAX (+RDX) | RAX | X0 (+X1) | a0 (+a1) |
| Float-Rückgabe | XMM0 | XMM0 | V0 | fa0 |
| Shadow Space | Nein | 32 Byte | Nein | Nein |
| Callee-saved XMM | Keine | XMM6–XMM15 | V8–V15 | fs0–fs11 |
| Rücksprung-Adresse | Stack (call) | Stack (call) | X30 (LR) | x1 (ra) |
| Stack-Alignment | 16 Byte | 16 Byte | 16 Byte | 16 Byte |
| Struct > 16 Byte | RDI (arg 0) | Stack | X8 (IR) | a0 (arg 0) |
Praktische Auswirkungen der ABI-Regeln auf Lyx-Programmcode:
Mehr als 6 (System V) / 4 (Windows) / 8 (ARM64/RISC-V) Integer-Argumente gehen auf den Stack – mit Aufruf-Overhead. Die Lyx-Empfehlung: Struct-Pointer übergeben.
// Schlecht: viele Parameter → Stack-Overflow-Risiko bei @flight_crit
fn ProcessData(a: int64, b: int64, c: int64, d: int64, e: int64, f: int64, g: int64) { ... }
// Besser: Struct-Pointer – ein Register, kein Stack-Argument
type DataParams = struct { a: int64; b: int64; c: int64; d: int64; e: int64; f: int64; g: int64; };
fn ProcessData(p: ^DataParams) { ... }
Werden Float- und Integer-Argumente gemischt, belegen sie separate Registerbänke und blockieren sich nicht gegenseitig:
fn Mix(i: int64, f: f64, j: int64): f64 {
// x86_64 System V: i→RDI, f→XMM0, j→RSI
// ARM64: i→X0, f→V0, j→X1
return f * (i as f64) + (j as f64);
}
Mit @export erzeugt der Lyx-Compiler ein C-kompatibles Symbol ohne Name-Mangling. Die Konvention folgt der Plattform-ABI:
@export
pub fn lyx_add(a: int64, b: int64): int64 {
return a + b;
}
// Verwendung aus C:
extern long lyx_add(long a, long b);
long result = lyx_add(3, 4); // 7
Bei DO-178C-Zertifizierung muss die ABI-Konformität nachgewiesen werden. Der –call-graph-Pass erzeugt einen Bericht über alle Funktionsaufrufe und deren Register-Belegung:
lyxc --call-graph --provenance --target arm64 main.lyx
Call-Graph-Auszug:
ProcessFlightData(val: f64)
Plattform: ARM64 (AAPCS64)
Argumente: val → V0 (f64, Float-Register)
Rückgabe: X0 (int64)
Callee-saved genutzt: X19, X20
Sichere Register: X19, X20 korrekt gesichert (stp/ldp im Prolog/Epilog)
unsafe-Blöcke: keine
@redundant-Variablen: heading (TMR-gesichert)
✅ ABI-konform
Der –target=macosx64-Backend erzeugt Mach-O-Binaries statt ELF. Die Calling Convention ist identisch mit System V AMD64 (Abschnitt 2), aber es gibt plattformspezifische Unterschiede im generierten Maschinencode und in den Syscall-Nummern.
Der macOS-_start-Stub ist 79 Bytes lang (Linux: 65 Bytes). Die drei eingebetteten Helper-Symbole liegen an exakt denselben Offset-Positionen wie auf Linux:
| Symbol | Offset im Binary |
|---|---|
strlen | Byte 79 |
printstr | Byte 94 |
printint | Byte 120 |
Der rel32-Patch für den call main-Sprung verwendet Bytes 63–66 (nicht 49–52 wie bei CG_ARGC — ein kritischer Bug in der ersten Implementierung, der falsche Sprungziele erzeugte).
cg.data (Strings und VMT-Tabellen) wird nach dem Code-Segment in die Mach-O-Datei geschrieben. VMT-Zeiger, die für Linux-Basisadressen berechnet wurden, werden beim Schreiben auf die macOS-Ladeadresse umgerechnet (VMT-Patching: Linux-Base → macOS-Base).
macOS x86_64 verwendet andere Syscall-Nummern als Linux. Die folgende Tabelle zeigt die 9 Socket-Syscalls, die der Backend-Code anpasst:
| Syscall | Linux x86_64 | macOS x86_64 |
|---|---|---|
socket | 41 | 97 |
connect | 42 | 98 |
bind | 49 | 104 |
listen | 50 | 106 |
accept | 43 | 30 |
sendto | 44 | 133 |
recvfrom | 45 | 29 |
setsockopt | 54 | 105 |
shutdown | 48 | 134 |
| Konstante | Linux | macOS |
|---|---|---|
MAP_ANON | 0x20 | 0x1002 |
Der Unterschied betrifft alle mmap-Aufrufe für anonyme Speicherzuordnungen (alloc, Heap, Stack-Extension). Ein falscher MAP_ANON-Wert führt zu EINVAL beim Systemaufruf.
Weiterführende Seiten: