cdecl dynamisch zusammenbauen
« | 06 Feb 2021 | »Nehmen wir mal an:
Wir lesen zur Laufzeit die Text-Deklaration einer beliebigen C Funktion ein. Können wir einen “generischen” Funktionsaufruf mit allen Argumenten zur Laufzeit zusammenstellen?
Ja natürlich! Mit Assembler!
Aber das wäre zu plattformspezifisch.
Geht es auch mit reinem C Code?
Egal ob leicht oder nicht, man hat nicht immer einen Assembler
parat und MSVC erlaubt für
64bit auch kein Inline-ASM mehr.
Und trotzdem wäre es doch “cool”, wenn man eine Funktion mit zur
Übersetzungszeit unbekannter Deklaration trotzdem korrekt aufrufen könnte.
Meine Erkenntnisse bisher:
Es funktioniert halbwegs, wenn man Abstriche macht und ABI-Details für jede Plattform separat implementiert.
Windows 32 Bit
Für X86 32 bit gilt, dass unter der C-decl Aufrufkonvention alle Parameter 32-bit ausgerichtet auf den Stack geschrieben werden und außerdem C-Strukturen auch als Ganzes auf den Stack gelegt werden, dann kann man jede Funktion in den Prototyp…
…casten, in dem die Parameter als Datenblock übergeben werden, wo man die
Werte der richtigen Parameter einfach an die richtige Stelle schreibt.
Ist die Größe aller Ziel-Stack-Elemente kleiner als der Block, müsste der
Aufruf korrekt funktionieren, weil der C-Compiler alles erforderliche
für uns macht.
Beim Return-Value müsste man noch zwischen <= 32 bit
und > 32 bit
unterscheiden, aber grundsätzlich wäre es das bereits.
Windows 64 Bit
Für X64 nutzt Microsoft
teils Register und danach den Stack, der ebenso speziell ausgerichtet sein
muss.
Mein Trick wäre es dann, die generische Funktion mit extrem vielen void*
Parametern auszustatten und deren Inhalt entsprechend dem Zielfunktionslayout
zu befüllen.
Das ist zwar viel lästiger als für 32 bit, aber geht auch.
Egal, ob man einen Pointer, einen char
, long
oder short
übergibt,
auf dem Stack ist alles an 64 Bits (also = void*
) ausgerichtet.
Natürlich belegt man hier viel mehr Stack als notwendig ist, aber nachdem der Aufrufer den Stack zurücksetzt, kann uns das egal sein.
Problematisch sind bei X64 double
und float
Typen, die in
Multimedia-Registern übergeben werden und von den “normalen” Registern
entkoppelt sind. Der void*
Trick funktioniert also nur bei allen Zeiger-
und Integer-Typen.
Fazit
Die meisten Funktionen bleiben unter 10 Parametern und nutzen nur CPU-Wort-breite Typen, alles andere wird meist auf Pointer abgebildet.
Nur die teuflischen C-struct
s, die als Wert übergeben werden sprengen hier
womöglich den Rahmen. Wenn man also auf diese Spezialfälle verzichtet (die
ja auch nur sehr selten als öffentliche C-API wo vorkommen), dann kann man
mit etwas Glück und Wohlwollen des Compilers ohne Assembler solche Hacks
anwenden.
Ob das nun unter Linux und unter nicht X86-Plattformen wie z.B. ARM auch noch geht … Tja, das sollte ich bei Gelegenheit mal untersuchen.
Das ist aber auf jeden Fall ein spannendes Thema.
Im GATE Framework ist dieses Beispiel für weitere Experimente nun in
gate/functions.h
verewigt.