Der Hello-World-Gruß
(thecoder08.github.io)-
Erkundung der Welt der Abstraktionen, die sich hinter modernen Hello-World-Programmen verbirgt
- Dieser Artikel behandelt ein Hello-World-Programm, das in C geschrieben ist. C ist unter den höheren Programmiersprachen, bei denen man sich vor der tatsächlichen Ausführung nicht damit beschäftigen muss, was die Sprache intern tut – etwa bei Interpreter/Compiler/JIT – noch die systemnaheste.
- Ursprünglich sollte der Text so geschrieben sein, dass ihn jede Person mit Programmiererfahrung verstehen kann, aber zumindest Kenntnisse in C oder Assembler sind wohl hilfreich.
-
Start des Hello-World-Programms
- Alle sind mit Hello-World-Programmen vertraut. In Python war das erste geschriebene Programm vermutlich etwas wie
print('Hello World!'). - In diesem Artikel schauen wir uns Hello World an, das in der Programmiersprache C geschrieben ist. In C kann man ein Programm nicht ausführen, indem man einfach einen Interpreter aufruft. Zuerst muss ein Compiler laufen, der es in Maschinencode übersetzt, den der Prozessor des Computers direkt ausführen kann.
- Alle sind mit Hello-World-Programmen vertraut. In Python war das erste geschriebene Programm vermutlich etwas wie
-
Analyse unseres Programms
- Analysiert man die kompilierte Programmdatei, erkennt man, dass es sich um eine ELF-Executable für die x86-64 Instruction Set Architecture handelt.
- Eine ELF-Executable ist unter Linux das Gegenstück zu einer .exe-Datei unter Windows.
- x86-64 ist die CPU-Architektur, die auf PCs verwendet wird, seit der IBM PC 1981 eingeführt wurde.
- Diese Datei enthält Maschinencode, die einzige Sprache, die die CPU versteht.
-
Analyse des Assembler-Codes
- Wir suchen den Entry Point, also die Startadresse des Programms, und analysieren den Assembler-Code.
- Assembler ist eine für Menschen lesbare Darstellung von Maschinencode.
- Man sieht Initialisierungscode, der automatisch vom Compiler (genauer: vom Linker) hinzugefügt wurde, sowie den Aufruf der Funktion
__libc_start_main. - Dieser Code ist jedoch nicht in unserem Programm definiert, sondern befindet sich irgendwo anders.
-
Die C-Standardbibliothek
- Die Funktion
__libc_start_mainist inlibc.so.6definiert, der Standard-C-Bibliothek unseres Systems. - Die C-Standardbibliothek ist eine Sammlung von Routinen und Funktionen, die von fast allen Programmen auf unserem Computer verwendet werden.
- Die C-Bibliothek führt Initialisierungsarbeiten aus und ruft die von uns geschriebene Funktion main() auf. Wenn main() zurückkehrt, beendet sie das Programm mit dem von uns gelieferten Exit-Code.
- Die Funktion
-
Analyse der Funktion main()
- In der Funktion main() wird ein Stack-Frame eingerichtet, dann die Adresse des Hello-World-Strings als Argument für den Funktionsaufruf gesetzt und anschließend
puts()aufgerufen. puts()wurde anstelle vonprintf()aufgerufen, weil der Compiler eine Optimierung vorgenommen hat.printf()ist komplex, währendputs()einfach nur einen unformatierten String ausgibt.
- In der Funktion main() wird ein Stack-Frame eingerichtet, dann die Adresse des Hello-World-Strings als Argument für den Funktionsaufruf gesetzt und anschließend
-
Der Hello-World-String
- Der String besteht aus "Hello World!" gefolgt von einem NULL-Terminator.
- In C gibt es bei Strings keine zugehörige Längeninformation, daher markiert der NULL-Terminator das Ende des Strings. Ohne NULL-Terminator würde das Programm in nicht erlaubten Speicher lesen und mit einem Segmentation Fault abstürzen.
- Wegen einer Compiler-Optimierung wurde der in
printf()verwendete Zeilenumbruch (\n) entfernt.puts()fügt nach der Ausgabe des Strings selbst einen Zeilenumbruch an.
-
Die Funktion puts()
- Die Funktion
puts()ruft wiederum Code innerhalb der Standardbibliothek auf. - Im Code von glibc kann man sehen, dass
_IO_puts -> _IO_new_file_xsputnaufgerufen wird, aber der Code ist komplex und daher schwer zu erklären. - Im Fall von musl libc ist es etwas einfacher. Dort erfolgt die Aufrufkette
puts -> fputs -> fwrite -> __fwritex -> __stdio_write -> syscall.
- Die Funktion
-
Systemaufrufe
- So groß die C-Bibliothek auch ist: Direkt mit der Hardware kommunizieren kann sie nicht. Das kann nur der Kernel.
- Daher endet ein Aufruf von
puts()letztlich damit, dass das Betriebssystem gebeten wird, etwas zu tun – hier also den String in einen Ausgabestream zu schreiben. - musl libc verwendet dafür den Systemaufruf
writev, der es erlaubt, mehrere Buffer auf einmal zu schreiben. - Ein Systemaufruf erfolgt, indem Parameter in Registern gesetzt und anschließend die Instruktion
syscallausgeführt wird. Dann geht die Kontrolle an den Kernel über, der die Parameter liest und den Systemaufruf ausführt.
-
Der Kernel
- Der Linux-Kernel muss die durch den Systemaufruf angeforderte Aktion ausführen. Der Systemaufruf
writeweist den Kernel an, in eine geöffnete Datei oder einen Stream des Dateisystems zu schreiben. writenimmt drei Parameter entgegen: den File Descriptor, in den geschrieben werden soll, den zu schreibenden Buffer und die Anzahl der zu schreibenden Bytes.- Wohin tatsächlich geschrieben wird, hängt von der Situation ab. Bei einem Terminal-Emulator erscheint es als virtuelles Terminal (pty), bei Remote-Login wird es an
sshdweitergereicht, bei einem physischen Terminal geht es an einen Serial-USB-Adapter. Bei einer Framebuffer-Konsole rendert der Kernel den Text und gibt ihn auf dem Display aus.
- Der Linux-Kernel muss die durch den Systemaufruf angeforderte Aktion ausführen. Der Systemaufruf
-
Fazit
- Moderne Softwaresysteme arbeiten auf der Hardware sehr komplex und hochgradig ausgefeilt, daher ist es letztlich sinnlos, jede kleine Aktion eines Computers vollständig verstehen zu wollen.
- Um alles zu erklären, mussten viele Teile weggelassen werden.
- Das Senden der Hello-World-Nachricht ist nur eines von unzähligen System Calls und Programmen, die gerade auf dem Computer laufen.
Meinung von GN⁺
- Dieser Text zeigt gut, wie jede Schicht eines Computing-Systems durch Abstraktion die Komplexität der darunterliegenden Schicht verbirgt und es Entwicklern dadurch ermöglicht, Anwendungen komfortabel zu entwickeln.
- Gleichzeitig macht er deutlich, wie viel unter der Haube passiert, damit eine einzige Zeile einer Anwendung ausgeführt werden kann, und warum Debugging so schwierig ist.
- Ich denke, jede Programmiererin und jeder Programmierer sollte zumindest die Systeme unterhalb der hauptsächlich verwendeten Sprache gut kennen. Man muss nicht alles wissen, aber es ist wichtig zu verstehen, wie die abstrahierten Teile tatsächlich funktionieren.
- Auch wenn man höhere Programmiersprachen verwendet, hilft es sehr, Konzepte der Systemprogrammierung wie Speicheraufbau, Stack und Heap oder Systemaufrufe zu lernen – sowohl beim Debugging als auch bei der Performance-Optimierung.
- Anwendungsentwickler werden Compiler oder C-Bibliotheken zwar kaum direkt anfassen, aber zu verstehen, wie ein selbst geschriebenes Programm letztlich das System nutzt, ist aus meiner Sicht unverzichtbar, um eine gute Programmiererin oder ein guter Programmierer zu werden.
Noch keine Kommentare.