Linux Assembler-Programmierung

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.


Was ist ein Assembler ?

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 100 DM. Im nachfolgenden Teil setze ich grundlegende i386-Kenntnisse voraus.


Linux und Assembler ?

Linux läßt sich erstaunlich elegant auch in Assembler programmieren. In den nachfolgenden Text beziehe ich mich auf die Asmutils 0.14 von Konstantin Boldyshev.

Mit Asmutils 0.14 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.14 sind Freeware und stehen unter der GPL.

Asmutils 0.14 - 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 !!)


'Hello world' in C und Assembler

Erstmal in C:

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 6.1 mit gcc -o hello hello.c übersetzt.
 

Jetzt in Assembler:

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. Der nasm -Assembler befindet sich bei der SuSE 6.1-Distribution in der Serie d.

Das folgende 'Hello world' in nasm-Assemblersyntax stammt aus der Asmutils-Homepage:
 
 
section .text
global _start ; für den Linker (ld) notwendig 
msg db 'Hello world',0x0A ; unser Text incl. Zeilenumbruch LF (0x0A) 
len  equ $ - msg  ; Länge des Textes berechnen (12 Bytes)
_start: ; Programmstart für Linker 
mov eax,4  ; Systemaufruf Nr. 4 (sys_write) Textausgabe
mov ebx,1  ; Ausgabekanal Nr.1 = stdout
mov ecx,msg ; Adresse unseres Textes im Speicher
mov edx,len ; Länge des Textes in Bytes
int 0x80 ; Kernel aufrufen mit obigen System-Aufrufparametern
mov eax,1 ; Systemaufruf Nr. 1 (sys_exit) Programmende 
int 0x80 ; Kernel aufrufen mit obigen System-Aufrufparametern

 

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 aout-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


Systemaufrufe des Linux-Kernels

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.


Weitere Informationen

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.
 

Linux Assembly HOWTO -- (Online, im HTML-Format, sgml source)

Startup state of Linux/i386 ELF binary -- Erklärungen zum ELF-Dateiformat

List of Linux/i386 system calls -- Linux Systemaufrufe

A Whirlwind Tutorial on Creating Really Teensy ELF Executables for Linux


Links

Tiny Linux executables -- Linux-Assembler-Beispiele
Jan's Linux & Assembler HomePage -- schöne Website mit Assembler-Beispielen (meist für libc)
Assembly Programming Journal -- einige Artikel zur Linux Assemblerprogrammierung
Nasm Webpage -- Nasm Assembler Informationen
Linux Kernel -- alles über den Linux-Kernel

Literatur