UEFI Apps erstellen

Der Traum meiner Kindheit rückt wieder näher: “Mein eigenes OS

Vor etwas über 20 Jahren hatten einige Idealisten wie ich noch die Idee, mit BIOS Interrupts und ein paar Assemblerzeilen Programme zu schreiben, die kein OS brauchten und direkt von der Diskette booten konnten.

Heute ist die Sache viel einfacher, denn heute haben wir UEFI.


Viele Sicherheitsprobleme entstehen auf Grund der Komplexität von modernen Betriebssystemen. Ein offener Webserver Port lässt dann über eine ungenutze Zusatzfunktion Viren hereinströmen, wo man sich immer fragt:

Wieso war diese Funktion überhaupt da?

Und dann haben wir Mikrocontroller wie den ESP32, der von solchen Problemen zwar nicht grundsätzlich verschont ist, der aber trotzdem einfache Dienst anbieten kann, die eben nicht durch Trillionen von Seiteneffekten kontaminiert werden.

Warum haben wir das nicht auch auf der PC Plattform?

EFI Apps als Lösung

Das Extensible Firmware Interface ist mehr als nur ein neues BIOS, denn es stellt eine wesentlich breitere Menge an Diensten bereit, als das alte BIOS, das lediglich einen Textbildschirm, eine Tastatur, Disketten und max. 8 GB Festplatten ansprechen konnte.

Es fügt nämlich Netzwerke, TCP, UDP, serielle Ports und Dateisysteme hinzu und damit haben wir eine Basis, mit der man ganz ohne ein Betriebssystem Code ausführen kann.
Man kompiliert sich ein EFI-Binary, packt es auf einen USB-Stick, bootet in die EFI-Shell, wechselt auf den USB-Datenträger und führt die Datei aus.
Und liegt die Datei im “richtigen” Verzeichnis, wird sie vom EFI Bootloader automatisch gestartet.

GNU-EFI und der MSVC

Das Projekt GNU-EFI liefert alle notwendigen Header und statischen Code, um primitive EFI-Anwendungen programmieren zu können.
Während die meisten Beispiele im Netz dazu für Linux bzw. den GCC ausgelegt sind, unterstützt auch Microsoft’s MSVC das EFI-Format als Zielplattform und mit ein paar Anpassungen, kann er gnu-efi auch benutzen.

Das ist auch kein Wunder, schließlich hat Microsoft den EFI-Standard mitdefiniert und dabei das unter Windows übliche PE-Format in EFI etabliert.

Man braucht also nur vom Subsystem WINDOWS bzw. CONSOLE zu EFI_APPLICATION wechseln und kann loslegen.
… allerdings muss man sich dafür von der C-Runtime und vielen hilfreichen Funktionen angefangen bei malloc() verabschieden.
Und C++ muss auch weichen … außer man hat eine Strategie für C++ ohne Exceptions und RTTI, was schwierig aber nicht gänzlich unmöglich ist.

CMake EFI Einstellungen für MSVC

Folgende Compiler Flags sind für EFI hilfreich und können in CMake definiert werden:
(Die C-Flags sind auch für C++ notwendig unter CMAKE_CXX_FLAGS)

  • string (REPLACE "/DWIN32" "" CMAKE_C_FLAGS ${CMAKE_C_FLAGS})
    Das WIN32 Symbol sollte verschwinden, damit keine Makros davon irritiert werden.
  • string (REPLACE "/D_WINDOWS" "" CMAKE_C_FLAGS ${CMAKE_C_FLAGS})
    Auch das automatisch generierte _WINDOWS Symbol sollte weg.
  • string (REPLACE "/EHsc" "" CMAKE_CXX_FLAGS ${CMAKE_CXX_FLAGS})
    string (REPLACE "/EHs" "" CMAKE_CXX_FLAGS ${CMAKE_CXX_FLAGS})
    string (REPLACE "/EHa" "" CMAKE_CXX_FLAGS ${CMAKE_CXX_FLAGS})
    C++ muss auf jegliche Art von Exceptions verzichten.
  • set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} /Gm-")
    Einstellung: Enable Minimal Rebuild sollte deaktiviert sein.
  • set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} /GS-")
    Security Checks müssen deaktiviert sein, weil sie auf die C-Runtime verweisen.
  • set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} /GR-")
    Run-Time Type Information (RTTI) braucht auch eine Runtime, die wir unter EFI nicht haben
  • set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} /TC")
    Die Option “Compile As: Compile As C Code” hilft bei nur-C Projekten
  • set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} /Gs16384")
    Diese Geheimoption lässt alle Stackvariablen 16 KB statt nur 4 KB groß sein.

Die nachfolgenden Linker-Flags sind ganz besonders wichtig:

  • set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} /NODEFAULTLIB")
    Die C-Runtime und alle anderen Windows-Lasten werden nicht automatisch gelinkt
  • set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} /MANIFEST:NO")
    Ein Windows-Manifest braucht man in EFI auch nicht.
  • set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} /SUBSYSTEM:EFI_APPLICATION")
    Und ohne dem EFI-Subsystem bekommen wir sowieso keine EFI App heraus.
  • set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} /OPT:REF")
    Nicht benutzte Funktionen im Code sollen weg-optimiert werden.
  • set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} /ENTRY:efi_main")
    Wir setzen explizit einen anderen Einsprungpunkt namens efi_main.
  • set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} /NXCOMPAT:NO")
    Auf die Data Execution Protection wollen wir uns auch nicht berufen.
  • set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} /DYNAMICBASE:NO")
    Und zufällige Adressen (als Schutz gegen Windows-Viren) sind unter EFI ebenfalls nicht notwendig bzw. nicht möglich.

Man kann jetzt noch weitere Wichteleien anfügen, wie dass .efi und nicht .exe am Ende herauskommt … aber die genannten Einstellungen sollten für den Kompilier- und Link-Vorgang ausreichen, damit man das erste Testprojekt bauen kann.

Die gnu-efi Bibliothek (und auch alle anderen Libs) müssen natürlich statisch zur EFI-App gelinkt werden. (Soetwas wie DLLs gibt es in EFI-Apps nicht.)

Runtime und Stack-Spezialitäten

Nachdem keine C-Runtime vorhanden ist, fallen Funktionen für Trigonometrie und Rundungen weg. Das schränkt den Handlungsspielraum ein und man müsste diese Routinen per Assembler oder C selbst nachbauen, wenn man andere C Libs nutzen möchte, die darauf verweisen.

malloc und free sind vermutlich die wichtigsten Utensilien, doch zum Glück stellt EFI mit seinen “BootServices” auch einfache Blockallokierungs- Routinen bereit, die man als malloc Ersatz nutzen kann.

Erste Link-Test liefern gerne Fehler, wie

error LNK2019: unresolved external symbol __chkstk
error LNK2019: unresolved external symbol __fltused

_chkstk ist ein interessanter Funktionsaufruf, den der Compiler automatisch einfügt, wenn ein Stackframe über 4 KB groß wird. Der Aufruf soll unter Windows eine Memory-Exception auflösen, wenn der aktuell allokierte Stack ausgeht, worauf das OS den Stack nach Möglichkeit vergrößert.
Und nachdem _chkstk in der C Runtime implementiert ist, fehlt sie unter EFI. Ich weiß nicht, wieviel Stack in EFI generell zur Verfügung steht, aber das Compilerflag /Gs16384 erhöht die Toleranzgrenze auf 16 KB … und man könnte es noch höher drehen.

_fltused ist eine andere globale CRT Variable in Bezug auf Fließkommarechnungen, die im MSVC offenbar darauf zugreifen wollen. Ein “schneller Fix” dafür ist einfach ein:

1extern int _fltused = 0;

womit wir die Variable formal im Programm haben. (Ich fürchte aber, dass diverse weitere Fließkomma-Operationen Patches bräuchten, so bald man sie einsetzt.)

efi_main und EFI_SYSTEM_TABLE

Der EFI-Einsprungpunkt bekommt ein HANDLE zur laufenden App und einen Pointer zur EFI_SYSTEM_TABLE, die jede Menge Pointer zu Unterobjekten und Funktionen enthält, mit denen man programmieren kann.

Zusätzlich bietet die gnu-efi/efilib.h einige Funktionen an, die so ähnlich wie die C-Runtime aussehen, z.B.: Print() als Alternative zu printf(). Die meisten dieser Routinen sind aber nur Wrapper zur Funktionstabelle, die mit der EFI_SYSTEM_TABLE übermittelt wurde.

 1#include <efi.h>
 2#include <efilib.h>
 3
 4extern EFI_STATUS efi_main(EFI_HANDLE efi_handle, EFI_SYSTEM_TABLE* sys_tbl)
 5{
 6  UINTN efi_event;
 7  InitializeLib(efi_handle, sys_tbl);
 8  Print(L"Hello World\n");
 9  sys_tbl->ConIn->Reset(sys_tbl->ConIn, FALSE);
10  sys_tbl->BootServices->WaitForEvent(1, &sys_tbl->ConIn->WaitForKey, &efi_event);  
11  return EFI_SUCCESS;
12}

Ein gutes und vollständiges Beispiel findet man unter uefi-simple.

Fazit

Ich habe mir die Mühe gemacht und gate/memalloc und gate/stream mit einer EFI-Implementierung versehen. gate/threads und gate/sychronization nutzen die Dummy-Implementierung aus meinem DOS-Layer und schon konnte ich die erste GATE basierte App für EFI erstellen.

Es handelt sich dabei zwar nur um Unit-Tests für GATE-CORE Routinen, aber sie demonstrierten auf meinem Test-EFI-Mainboard, dass die Speicherverwaltung und Textausgabe bereits korrekt funktionieren.

Und jetzt bin ich echt aufgegeilt.
Denn über systbl->BootServices->LocateProtocol lassen sich über GUIDs Protokolle wie TCP und UDP einbinden, womit man über einen angeschlossenen Netzwerkadapter Daten verschicken kann …
… und all das ohne ein Betriebssystem.
Boah, das nenne ich mal Freiheit und Abenteuer-Feeling!