
In Zeiten der Hochsprachen, wie C und C++ scheint die Programmierung in
Maschinensprache für viele Programmierer obsolet zu sein. Im blinden
Vertrauen auf die Optimierungen heutiger Compiler, die Taktfrequenzen der
CPU´s und riesiger Speicher wird in vielen Fällen bewußt
Leistung verschenkt.
Assembler kommt überall da zum Einsatz, wo Performance und Speichereffizienz ein absolutes Muß sind. Typisch sind Einsätze in den
Bereichen Spiele, Grafik, Meßtechnik und im innersten Teil von
Betriebssystemen (z.B. Scheduler).
Assemblerprogrammierung ist nicht ganz trivial, da jede CPU eine eigene
Maschinensprache besitzt. So lassen sich Assemblerprogramme nur schlecht
portieren, was einen aber nicht davon abhalten sollte die Maschinensprache
seines Rechners zumindest in den Grundzügen zu kennen.
Im Nachfolgenden werde ich auf die Grundzüge der Erstellung von
Assemblerprogrammen auf i386-kompatiblen PC's unter Linux eingehen. Die Maschinensprache von
i386-kompatiblen CPU´s ist nicht gerade umwerfend. Da sind CPU´s wie
Motorola 680x0 und
PowerPC wesentlich
eleganter. Egal, wovon uns schlecht wird, hier geht es um i386-Assembler unter
Linux.
Eine CPU versteht nur die Maschinensprache. Typische
Maschinenbefehle sind 1 - 20 Bytes lang. Der Prozessor lädt diese aus dem
Speicher in seine Dekodierlogik und führt sie dann aus. Die Befehle sind
meist recht primitiv. Typisch sind das Laden und Speichern von Werten,
Programmsprünge und einfache Rechenoperationen.
Alle Programme, die existieren, sind im Endeffekt reine Maschinenprogramme. Der
C-Compiler übersetzt den C-Quelltext für uns in die Maschinensprache.
Die Codequalität ist in vielen Fällen nicht schlecht. Will man mehr,
muß man in Assembler programmieren. Die Assembler-Befehle nennt man
Mnemonics. Ein
Assembler-Befehl entspricht genau einem echten Maschinenbefehl, also ist ein
Assemblerprogramm immer eine Einszueins-Übersetzung in Maschinensprache.
Beispiel i386-Befehl: mov ax,4
Der Befehl lädt das 16-bit CPU-Register ax mit dem Wert 4. Ein Prozessor
besitzt eine ganze Reihe von internen Speichervariablen, die man als
Register bezeichnet.
Als Assembler-Profi versucht man möglichst viele Operationen in den
CPU-Registern auszuführen. Register-Operationen sind turboschnell,
während jeder Speicherzugriff quälend langsam dagegen ist.
Die i386-Befehle werde ich Ihnen allerdings nicht erklären, die
müssen Sie sich schon selbst beibringen. Empfehlenswert ist der
Riesen-Wälzer 'Das Assembler-Buch' aus dem Addison-Wesley-Verlag. Leider
kostet der Spaß fast 50 Euro. Im nachfolgenden Teil setze ich grundlegende
i386-Kenntnisse voraus.
Linux läßt sich erstaunlich elegant auch in
Assembler programmieren. In den nachfolgenden Text beziehe ich mich auf die
Asmutils 0.17 von Konstantin
Boldyshev.
Mit Asmutils 0.17 werden
viele Unix-Kommandos in Assembler nachprogrammiert. Ziel ist es, Standard-Unix-Befehle
durch extrem kleine und schnelle Maschinenprogramme zu ersetzen. Nebenbei kann
jeder am Quellcode nachvollziehen, wie so ein Unix-Befehl mit Kernelaufrufen
implementiert werden muß. Jeder, der Lust hat, kann diesen Code weiter
optimieren. Der Autor freut sich über jede neue Optimierung und neue Unix-Befehle
für die Asmutils. Nicht umsonst bezeichnet man Assembler-Freaks als 'Microsecond-
und Memoryhunter'.
Die Asmutils 0.17 sind
Freeware und stehen unter der GPL.
Asmutils 0.17 - Homepage: http://linuxassembly.org/asmutils.html
Konstantin Boldyshev <konst@voshod.com>
Beispiel: cat-Kommando
Das Unix-Kommando cat dient zum Ausgeben von Dateiinhalten. Da
cat normalerweise ohne zusätzliche Optionen (s. man cat)
verwendet wird, kann man ein ganz kurzes und schnelles cat per
Assembler bauen.
| Unix-Kommando cat |
|---|
| Original cat Vers. 1.22 : 12840 Bytes |
| Asmutils cat : 187 Bytes (68x kleiner !!) |
Das Programm 'Hello world' ist meist das erste C-Programm, das man als
C-Anfänger schreibt.
#include <stdio.h>
int main( void
)
{
printf("Hello, world\n");
return 0;
}
Das Programm habe ich unter SuSE-Linux mit gcc
-o hello hello.c übersetzt.
Nun implementieren wir das gleiche i386-Assembler für Linux. Neben dem
eigentlichen Assembler brauchen wir noch einen Linker, der die Objektdatei des
Assembler ausführbar macht.
Wir benutzen allerdings nicht den gas (Gnu Assembler), da er die unangenehme A&T-Syntax verwendet.
Wir verwenden den Assembler nasm (Netwide Assember), der stattdessen die übliche
INTEL-Syntax verwendet.
Das folgende 'Hello world' in nasm-Assemblersyntax stammt Matthias Müller, der mir
freundlicherweise eine modifizierte Version des alten Beispiels von der Asmutils-Homepage geschickt hat:
| section .text | ; section Deklaration | |||
| global _start | ; Eintrittspunkt für den ELF-Linker-/Loader | |||
| _start: | ; Programmstart | |||
| mov | edx,len | ; drittes Argument, Textlänge | ||
| mov | ecx,msg | ; zweites Argument, Adresse unseres Textes im Speicher | ||
| mov | ebx,1 | ; erstes Argument, file handle stdout | ||
| mov | eax,4 | ; Systemaufruf Nr.4 = sys_write | ||
| int | 0x80 | ; Kernel aufrufen mit obigen System-Aufrufparametern | ||
| mov | ebx,0 | ; erstes Argument für sys_exit (Programm beenden) | ||
| mov | eax,1 | ; Systemaufruf Nr. 1 (sys_exit) Programmende | ||
| int | 0x80 | ; Kernel aufrufen mit obigen System-Aufrufparametern | ||
| section .data | ; section Deklaration | |||
| msg | db | 'Hello world',0x0A | ; unser Text incl. Zeilenumbruch LF (0x0A) | |
| len | equ | $ - msg | ; Länge des Textes berechnen (12 Bytes) |
Nachdem wir den Quelltext (hello.asm) mittels eines Texteditors eingegeben
haben, muß er jetzt übersetzt assembliert werden.
nasm -f elf hello.asm
Die Option -f elf gibt an, daß der Assembler eine
Objektdatei hello.o im ELF (Extended Linker Format) erzeugen soll (früher
wurde das veraltete a.out-Format in Unix verwendet).
Objektdateien sind vorübersetzte Assemblerdateien, die aber noch nicht
ausführbar sind. Erst ein Linker erzeugt aus ihnen ausführbare
Programmdateien.
Die Aufgabe übernimmt ld (GNU Linker). Das Programm ld ist Teil der
binutils-Programme. Diese sind in jeder Distribution vorinstalliert.
ld -s -o hello hello.o
Der Linker erzeugt jetzt ein ausführbares Programm hello. Die Option
-o hello gibt den Namen des ausführbaren Programms an.
Die Option -s weist den Linker an keine Symboltabelle an den
Code anzuhängen. So eine Symboltabelle mit den Bezeichnern (hier msg, len,
_start) braucht man nur, wenn man den Code später debuggen will.
| Hello world in C und Assembler |
|---|
| C-Version : 33030 Bytes |
| Asmutils Version : 400 Bytes |
Der Linux-Kernel kennt in der Version 2.2 rund 190
sogenannte Systemaufrufe. Der Kernel wird per Software-Interrupt 0x80
aufgerufen. Diese Verfahren ähnelt den Interrupt 0x21 von MS-DOS. Der
Kernel nutzt dabei bis zu 6 CPU- Register zur Parameterübergabe.
Das CPU-Register eax (32-bit) enthält dabei die Nummer
des Systemaufrufs (0 - 190).
Die CPU-Register ebx, ecx, edx, esi und edi enthalten je nach
Systemaufruf entsprechende Vorgaben.
Im obigen Beispiel braucht der Systemaufruf sys_write 4
Parameter, während der Systemaufruf sys_exit nur die
Funktionsnummer als Parameter braucht.
Dies ist nur ein kurzer Anriß der
Möglichkeiten mit denen man Assemblerprogramme in Linux schreiben kann.
Auf der Homepage der asmutils finden Sie reichlich Informationen um Linux
optimal in Assembler auszunutzen.
So finden Sie die Liste der Systemaufrufe, Informationen zum ELF-Format, zum
Aufbau des Stacks bei Parameterübergaben uvm. in dieser Homepage.
Tiny Linux
executables -- Linux-Assembler-Beispiele
Assembly Programming Journal
-- einige Artikel zur Linux Assemblerprogrammierung
Nasm Webpage -- Nasm Assembler
Informationen
Linux Kernel -- alles über den
Linux-Kernel