cdecl dynamisch zusammenbauen

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…

1union x86_stack_args
2{
3        void*        dummy;
4        char        data[1024];
5}
6
7typedef void* (*generic_x86_func_t)(union x86_stack_args args);

…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.

1typedef void* (*generic_x64_func_t)(
2  void*, void*, void*, void*, void*, void*, void*, void*,
3  void*, void*, void*, void*, void*, void*, void*, void*,
4  void*, void*, void*, void*, void*, void*, void*, void*,
5  void*, void*, void*, void*, void*, void*, void*, void*,
6);

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 Pointern 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-structs, 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.


Wenn sich eine triviale Erkenntnis mit Dummheit in der Interpretation paart, dann gibt es in der Regel Kollateralschäden in der Anwendung.
frei zitiert nach A. Van der Bellen
... also dann paaren wir mal eine komplexe Erkenntnis mit Klugheit in der Interpretation!