UEFI Apps erstellen
« | 28 Nov 2021 | »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 ungenutzte 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})
DasWIN32
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 habenset(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} /TC")
Die Option “Compile As: Compile As C Code” hilft bei nur-C Projektenset(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 gelinktset(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 namensefi_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!