UEFI, CMake und GCC

Manchmal sind es die Dinge, wo man denkt:

Ach, das sollte in 15 Minuten erledigt sein.

die einen dann den ganzen Tag (oder länger) beschäftigen.

Mir erging es mit der Einrichtung eines CMake Scripts für UEFI-Apps in einer GCC Linux Umgebung heute so.


Der MSVC hat es mir mit seinem EFI-Applications Build-Typ recht einfach gemacht, Apps ohne Betriebssystem zu erstellen.
Doch ein großes Problem stellen C++, seine Exception und RTTI dar, bei denen der MSVC Code generiert, der eine C-Runtime voraussetzt, die an den Windows Kernel angepasst ist … also etwas, was in UEFI nicht existiert.

Der GCC hingegen lässt sich leichter dazu bewegen, solche Datenstrukturen generisch zu erzeugen, und somit war mein erster Schritt schon festgelegt:

Ich möchte den EFI-App-Build in einer GCC Umgebung durchführen können.

Doch da der GCC unter Linux das ELF-Format einsetzt und UEFI APIs auf Microsofts COFF Spezifikation aufsetzen, gestaltet sich das Bauen von UEFI-Apps im GCC wesentlich schwieriger.

Makefile Analyse

Die GNU-EFI Bibliothek sieht folgende Prozedur vor:

  • Die EFI-Schnittstellen werden in eine statische libefi.a kompiliert.
  • Der (Assembler basierte) Startup Code kommt in die statische libgnuefi.a.
  • Die eigene EFI-App wird als shared library gebaut, die gegen die beiden vorherigen mit ein paar speziellen Flags linkt.
  • Das Tool objcopy extrahiert die Code und Datenbereiche aus der shared lib und baut daraus ein EFI-App-Binary mit dem richtigen Target-Subsystem zusammen.

Wichtige Compiler-Optionen hierfür sind:

  • -ffreestanding: Verzicht auf die übliche C-Standard-Bibliothek
  • -fpic: Shared-lib position independent code
  • -fshort-wchar: wchar_t als 16-bit wie bei Microsoft
  • Deaktivierung diverser Stack-Prüfungen, wie: -fno-stack-protector, -fno-stack-check, -mno-red-zone
  • und noch ein paar Kleinigkeiten (siehe CMake Beispiel)

Der Linker braucht folgende Zusätze:

  • -shared: erzeugt eine temporäre dynamische Lib
  • -nostdlib: Verzicht die übliche C-Standard-Bibliothek zu linken
  • -Bsymbolic: Verhaltensanpassung, wie Symbole im Code aufgelöst werden
  • -e_start: Festlegung des Funktionsnamens, der beim App-Start benutzt werden soll, also _start(), eine in Assembler geschriebene Einsprungfunktion aus libgnuefi.a.
    Sie initialisiert alles und ruft dann efi_main auf.
  • -T /path/to/gnuefi/elf_x86_64_efi.lds Pfad zum Linkerscript, das wichtige Details zum Linken der EFI-App beinhaltet.

Die Makefile Dateien, die gnu-efi mitliefert, dokumentieren, dass libgnuefi.a zwar gelinkt wird, aber dass das crt0 Objekt (*.o) trotzdem nochmal separat zur finalen EFI-App gelinkt wird.
So wird die darin enthaltene _start Funktion automatisch zum Einsprungpunkt.

Bei meinen Tests, wo ich die .o nicht mehr separat angab, musste ich den Einsprungpunkt eben mit -e_start festlegen, ansonsten wählte der Compiler selbsttätig immer efi_main aus.

CMake Umsetzung

Das Standard CMake Setup nutzt cc um seine Kompilate umzusetzen.
Da die originalen gnu-efi Sourcen Makefiles einsetzen, die explizit zwischen gcc und ld Aufrufen unterscheiden, muss man beim Wechsel zu CMake mit cc darauf achten, welche Kommandos für den Linker bestimmt sind.

Diese muss man dann mit -Wl, präfixen, z.B: -Wl,-nostdlib.

Man erhält nun eine EFI .so Datei, die man aber noch umwanden muss. Ein Custom-Build-Job mit dem Aufruf von objcopy führt zum gewünschten Ergebnis:

1objcopy -j .text -j .sdata -j .data -j .dynamic -j .dynsym -j .rel -j .rela -j .reloc --target=efi-app-x86_64 my-efi-app.so my-efi-app.efi

Die Datei my-efi-app.efi kann nun auf einen mit FAT32 formatierten USB Stick kopiert werden.
Bootet man in die UEFI-Shell, kann man mit fs0: (oder fsX: wenn mehrere Laufwerke erkannt werden) zum Stick wechseln und my-efi-app.efi einfach durch Eingabe des Dateinamens + ENTER ausführen lassen (DOS lässt grüßen).

Wer keine UEFI-Shell in seinem BIOS hat, kann die Datei auch als /EFI/BOOT/BOOTX64.EFI speichern und vom Stick booten.
Die EFI-App wird dann wie ein Bootloader ausgeführt … nur dass eben kein OS geladen wird, sondern nur unser Code in efi_main() ausgeführt wird.

Problem Calling Convention

… und dann sitze ich stundenlang vor meinem Bildschirm und stelle fest, dass die erzeugte EFI-App sofort zum Einfrieren des Systems führt.

Doch erst nach längerer Analyse der Assembler und C Sourcen wurde mir mein Fehler bewusst.

_start() versucht zuerst dynamische Symbole korrekt auszurichten (reloc()) und will am Ende die Funktion efi_main aufrufen, in der der eigene Code startet.

Und hier hatte ich die Signatur

1EFI_STATUS EFIAPI efi_main(EFI_HANDLE ImageHandle, EFI_SYSTEM_TABLE* SystemTable) 

gewählt. Und der Token EFIAPI war des Pudels Kern. Denn dieses Makro ist im GCC definiert als:

1#define EFIAPI __attribute__((ms_abi))

EFIAPI wird für die EFI-Schnittstellenfunktionen benutzt und ermahnt den GCC Compiler, die Aufrufe im Microsoft-Format (x86_64) durchzuführen.

Doch _start geht davon aus, dass efi_main eine “lokale” nicht-EFI-Funktion ist, die sich auch nicht an die Microsoft-Konvention halten muss. Stattdessen wird die übliche GCC x86_64 cdecl Konvention benutzt, bei der die Register anders belegt sind.

Microsoft’s cdecl nutzt die Register RCX und RDX für die ersten beiden Parameter und der GCC nutzt die System-V cdecl Spezifikation und übergibt die gleichen Parameter in den Register RDI und RSI.

Und so erhielt meine efi_main Funktion also zwei ungültige Registerwerte, was den Crash auslöste.
Die Entfernen von EFIAPI in efi_main vermochte mein Problem zu lösen.

Fazit

Nun wo der C-Teil bei GCC, CMake und EFI durchprobiert wurde, kann ich mich also in Zukunft mehr um die C++ Seiten kümmern.

Wie “cool” wäre es, wenn man nun auch vollständige C++ Routinen außerhalb eines OS nutzen kann?

Ich freue mich daher schon, diese neue Welt entdecken zu dürfen.

📧 📋 🐘 | 🔔
 

Meine Dokus über:
 
Weitere externe Links zu:
Alle extern verlinkten Webseiten stehen nicht in Zusammenhang mit opengate.at.
Für deren Inhalt wird keine Haftung übernommen.



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!