Eine Webseite in Assemblersprache schreiben

Published 4 July, 2012

Die meisten Webseiten werden in Hochsprachen geschrieben, welche weit von der Ebene des nativen Maschinencodes entfernt sind: PHP wird interpretiert, C# und Java wird von einer virtuellen Maschine aufgeführt, bestenfalls von einem JIT-Compiler zur Laufzeit zu nativen Code kompiliert und in C/C++ werden nur (noch) sehr selten Webseiten entwickelt (schade eigentlich...).

Theoretisch ist es aber auch möglich, eine Webseite direkt in Assembly Language zu schreiben. Wie das geht, möchte ich in diesem, nicht ganz ernst zu nehmenden, Artikel beschreiben. Das Beispiel ist für eine Linux Maschine unter IA-32 (x86, 32-Bit) entwickelt.

Als erstes brauchen wir einen Webserver, einen Assembler und einen Linker. Ich nutze dafür einen Apache, NASM und GNU ld. Um native Programme/Shell-Skripte zur Generierung einer Webseite einsetzten kann, gibt es das Verzeichnis cgi-bin (/usr/lib/cgi-bin). Wenn nun die URL http://localhost/cgi-bin/tollesProgramm aufgerufen wird, versucht der Apache das Programm unter /usr/lib/cgi-bin/tollesProgramm auszuführen und die Programmausgabe an den Browser weiterzuleiten.

Ein einfaches C++-Programm dafür könnte so aussehen:

#include <iostream>
using namespace std;

int main(int argc, char* argv[])
{
	cout << "Content-Type: text/plain" << endl << endl;
	cout << "Hello world!" << endl;
	return 0;
}

Als erstes wird der String "Content-Type: text/plain" ausgegeben. Dies ist eine Zeile von HTTP-Response-Header, um den MIME-Type der Seite anzugeben. Für CGI-Skripte ist dies der einzige notwendige Header. Anschließend folgen zwei Leerzeilen, um den Apache zu signalisieren, dass dort der Header zuende ist, und nun der Seiteninhalt kommt. Das "Hello world!" würde in diesem Fall im Browser als text/plain ausgegeben werden.

Um einigermaßen schnell entwickeln zu können, sollten wir ein kleines Skript schreiben, welches den Assembler und den Linker aufrufen kann (ein Makefile wäre natürlich die bessere Wahl):

#!/bin/sh
nasm -f elf program.asm                   # assemble
ld -m elf_i386 -s -o program program.o    # link
./program                                 # execute in terminal

Am besten legt man sich das Skript mit samt der Sourcen in das cgi-bin-Verzeichnis, um nicht ständig das Programm rüberkopieren zu müssen.

Nun können wir mit der Entwicklung der Seite in program.asm anfangen: Für ein ausführbares Programm braucht man immer mindestens zwei Sektionen: Den Code-Teil ".text" und den Daten-Teil ".data" (hier können statische String stehen), außerdem muss der Entry-Point definiert werden:

SECTION .data
SECTION .text
	global _stat

_start:

In den Datenteil schreiben wir nun ein paar Dinge rein: Den Default-Header (der Content-Type) sowie den Inhalt der Seite. Zu beiden Strings brauchen wir auch noch die Länge.

headerDefault:		db 'Content-Tyoe: text/plain', 10, 10
headerDefaultLen:	equ $-headerDefault
text:			db 'Hello wolrd!'
textLen:		equ $-text

NASM errechnet nun die Länge der Strings automatisch: Das $-Zeichen steht für die aktuelle Adresse, und das Label vom String steht für die Adresse des Strings. Das 2. subtrahiert vom 1. ergibt nun (wennblog2 beides direkt untereinander steht) die Länge des Strings, der genau zwischen den beiden Deklarationen steht.

Kommen wir nun zum eigentlichen Programm: Unter Linux macht man Syscalls über die Interrupt-Adresse 0x80, die Syscalls kann man u.A. hier finden. Der Syscall "write" hat die ID 0x04 und erwartet als Parameter den File Descriptor (dort wo es hingeschrieben werden soll), den String, der geschrieben werden soll und die länge des Strings. Der File Descriptor für den Standard-Output ist immer die 1. Deshalb machen wir den Syscall wie folgt:

mov eax, 4 // der syscall "write"
mov ebx, 1 // der file descriptor "standard output"
mov ecx, string	   // der string
mov edx, stringLen // die länge
int 0x80 // feuer!

Diesen Syscall müssen wir jetzt natürlich zweimal machen, einmal für den Header und für die eigentliche Seite.

Anschließend muss das Programm noch beendet werden, was auf gleicher Art und Weise mit einem Syscall funktioniert. Der komplette Sourcecode sieht nun so aus:

SECTION .data
  headerDefault: 	db 'Content-Type: text/plain', 10, 10
  headerDefaultLen:	equ $-headerDefault
  text:			db 'Hello world!'
  textLen:		equ $-text

SECTION .text
  global _start

_start:
  ;print header
  mov eax, 4
  mov ebx, 1
  mov ecx, headerDefault
  mov edx, headerDefaultLen
  int 0x80

  mov eax, 4
  mov ebx, 1
  mov ecx, text
  mov edx, textLen
  int 0x80

exit:
  mov eax, 1
  mov ebx, 0
  int 0x80

Wie man sieht, ist das ganze nicht mehr, als ein normales Assembler-Hello-World-Beispiel, mit der Ausnahme des Speicherortes und dem Response-Header am Anfang. :-)

Das ganze könnte man nun auch dynamisch machen, indem man z.B. die GET-Parameter ausliest und darauf reagiert. Die normalen CGI-Parameter (und damit auch die GET-Parameter,...) liegen auf dem Stack in Form von Umgebungsvariablen (unter den argv-Werten). Aber das würde den Umfang vom Eintrag sprengen ;-)