doc/de_DE.ISO8859-1/books/developers-handbook/x86/chapter.sgml
2012-09-14 17:47:48 +00:00

6100 lines
186 KiB
XML

<?xml version="1.0" encoding="iso-8859-1" standalone="no"?>
<!--
The FreeBSD Documentation Project
The FreeBSD German Documentation Project
This file is automatically generated. Please do not make commits
to this file. Updates should be sent to the author :
G. Adam Stanislav (adam@redprince.net)
This chapter is an exception to our general rule, and the author
retains the copyright. Among other things, this means that this
chapter should not be included in any printed version of the
Developer's Handbook without Adam's explicit permission.
Eventually we will have to replace this chapter or convince the
author to assign us the copyright. For now, it is valuable
content so it should stay.
$FreeBSD$
$FreeBSDde: de-docproj/books/developers-handbook/x86/chapter.sgml,v 1.24 2010/12/15 19:03:52 bcr Exp $
basiert auf: 1.19
-->
<chapter id="x86">
<title>x86-Assembler-Programmierung</title>
<para><emphasis>Dieses Kapitel wurde geschrieben von
&a.stanislav;.</emphasis></para>
<sect1 id="x86-intro">
<sect1info>
<authorgroup>
<author>
<firstname>Hagen</firstname>
<surname>Kühl</surname>
<contrib>Übersetzt von </contrib>
</author>
</authorgroup>
</sect1info>
<title>Synopsis</title>
<para>Assembler-Programmierung unter &unix; ist höchst
undokumentiert. Es wird allgemein angenommen, dass niemand sie
jemals benutzen will, da &unix;-Systeme auf verschiedenen
Mikroprozessoren laufen, und man deshalb aus Gründen der
Portabilität alles in C schreiben sollte.</para>
<para>In Wirklichkeit ist die Portabilität von C
größtenteils ein Mythos. Auch C-Programme müssen
angepasst werden, wenn man sie von einem &unix; auf ein anderes
portiert, egal auf welchem Prozessor jedes davon läuft.
Typischerweise ist ein solches Programm voller Bedingungen, die
unterscheiden für welches System es kompiliert wird.</para>
<para>Sogar wenn wir glauben, dass jede &unix;-Software in C, oder
einer anderen High-Level-Sprache geschrieben werden sollte,
brauchen wir dennoch Assembler-Programmierer: Wer sonst sollte
den Abschnitt der C-Bibliothek schreiben, die auf den Kernel
zugreift?</para>
<para>In diesem Kapitel möchte ich versuchen zu zeigen, wie
man Assembler-Sprache verwenden kann, um &unix;-Programme,
besonders unter FreeBSD, zu schreiben.</para>
<para>Dieses Kapitel erklärt nicht die Grundlagen der
Assembler-Sprache. Zu diesem Thema gibt es bereits genug Quellen
(einen vollständigen Online-Kurs finden Sie in Randall
Hydes <ulink url="http://webster.cs.ucr.edu/">Art of Assembly
Language</ulink>; oder falls Sie ein gedrucktes Buch bevorzugen,
können Sie einen Blick auf Jeff Duntemanns <ulink
url="http://www.int80h.org/cgi-bin/isbn?isbn=0471375233">Assembly
Language Step-by-Step</ulink> werfen). Jedenfalls sollte jeder
Assembler-Programmierer nach diesem Kapitel schnell und
effizient Programme für FreeBSD schreiben
können.</para>
<para>Copyright &copy; 2000-2001 G. Adam Stanislav. All rights
reserved.</para>
</sect1>
<sect1 id="x86-the-tools">
<sect1info>
<authorgroup>
<author>
<firstname>Hagen</firstname>
<surname>Kühl</surname>
<contrib>Übersetzt von </contrib>
</author>
</authorgroup>
</sect1info>
<title>Die Werkzeuge</title>
<sect2 id="x86-the-assembler">
<title>Der Assembler</title>
<para>Das wichtigste Werkzeug der Assembler-Programmierung ist
der Assembler, diese Software übersetzt Assembler-Sprache
in Maschinencode.</para>
<para>Für FreeBSD stehen zwei verschiedene Assembler zur
Verfügung. Der erste ist
<citerefentry><refentrytitle>as</refentrytitle><manvolnum>1</manvolnum></citerefentry>,
der die traditionelle &unix;-Assembler-Sprache verwendet.
Dieser ist Teil des Systems.</para>
<para>Der andere ist
<application>/usr/ports/devel/nasm</application>. Dieser
benutzt die Intel-Syntax und sein Vorteil ist, dass es Code
fü viele Vetriebssysteme übersetzen kann. Er muss
gesondert installiert werden, aber ist völlig
frei.</para>
<para>In diesem Kapitel wird die
<application>nasm</application>-Syntax verwendet. Einerseits
weil es die meisten Assembler-Programmierer, die von anderen
Systemen zu FreeBSD kommen, leichter verstehen werden. Und
offen gesagt, weil es das ist, was ich gewohnt bin.</para>
</sect2>
<sect2 id="x86-the-linker">
<title>Der Linker</title>
<para>Die Ausgabe des Assemblers muss, genau wie der Code jedes
Compilers, gebunden werden, um eine ausführbare Datei zu
bilden.</para>
<para>Der Linker
<citerefentry><refentrytitle>ld</refentrytitle><manvolnum>1</manvolnum></citerefentry>
ist der Standard und Teil von FreeBSD. Er funktioniert mit dem
Code beider Assembler.</para>
</sect2>
</sect1>
<sect1 id="x86-system-calls">
<sect1info>
<authorgroup>
<author>
<firstname>Hagen</firstname>
<surname>Kühl</surname>
<contrib>Übersetzt von </contrib>
</author>
</authorgroup>
</sect1info>
<title>Systemaufrufe</title>
<sect2 id="x86-default-calling-convention">
<title>Standard-Aufrufkonvention</title>
<para>Standardmäßig benutzt der FreeBSD-Kernel die
C-Aufrufkonvention. Weiterhin wird, obwohl auf den Kernel
durch <function role="opcode">int 80h</function> zugegriffen
wird, angenommen, dass das Programm eine Funktion aufruft, die
<function role="opcode">int 80h</function> verwendet, anstatt
<function role="opcode">int 80h</function> direkt
aufzurufen.</para>
<para>Diese Konvention ist sehr praktisch und der
&microsoft;-Konvention von <acronym>&ms-dos;</acronym>
überlegen. Warum? Weil es die &unix;-Konvention jedem
Programm, egal in welcher Sprache es geschrieben ist, erlaubt
auf den Kernel zuzugreifen.</para>
<para>Ein Assembler-Programm kann das ebenfalls. Beispielsweise
könnten wir eine Datei öffnen:</para>
<programlisting>kernel:
int 80h ; Call kernel
ret
open:
push dword mode
push dword flags
push dword path
mov eax, 5
call kernel
add esp, byte 12
ret</programlisting>
<para>Das ist ein sehr sauberer und portabler Programmierstil.
Wenn Sie das Programm auf ein anderes &unix; portieren, das
einen anderen Interrupt oder eie andere Art der
Parameterübergabe verwendet, müssen sie nur die
Prozedur kernel ändern.</para>
<para>Aber Assembler-Programmierer lieben es Taktzyklen zu
schinden. Das obige Beispiel benötigt eine <function
role="opcode">call/ret</function>-Kombination. Das können
wir entfernen, indem wir einen weiteren Parameter mit
<function role="opcode">push</function> übergeben:</para>
<programlisting>open:
push dword mode
push dword flags
push dword path
mov eax, 5
push eax ; Or any other dword
int 80h
add esp, byte 16</programlisting>
<para>Die Konstante <constant>5</constant>, die wir in <varname
role="register">EAX</varname> ablegen, identifiziert die
Kernel-Funktion, die wir aufrufen. In diesem Fall ist das
<function role="syscall">open</function>.</para>
</sect2>
<sect2 id="x86-alternate-calling-convention">
<title>Alternative Aufruf-Konvention</title>
<para>FreeBSD ist ein extrem flexibles System. Es bietet noch
andere Wege, um den Kernel aufzurufen. Damit diese
funktionieren muss allerdings die Linux-Emulation installiert
sein.</para>
<para>Linux ist ein &unix;-artiges System. Allerdings verwendet
dessen Kernel die gleiche Systemaufruf-Konvention, bei der
Parameter in Registern abgelegt werden, wie
<acronym>&ms-dos;</acronym>. Genau wie bei der
&unix;-Konvention wird die Nummer der Funktion in <varname
role="register">EAX</varname> abgelegt. Allerdings werden die
Parameter nicht auf den Stack gelegt, sondern in die Register
<varname role="register">EBX, ECX, EDX, ESI, EDI,
EBP</varname>:</para>
<programlisting>open:
mov eax, 5
mov ebx, path
mov ecx, flags
mov edx, mode
int 80h</programlisting>
<para>Diese Konvention hat einen großen Nachteil
gegenüber der von &unix;, was die
Assembler-Programmierung angeht: Jedesmal, wenn Sie einen
Kernel-Aufruf machen, müssen Sie die Register <function
role="opcode">push</function>en und sie später <function
role="opcode">pop</function>en. Das macht Ihren Code
unförmiger und langsamer. Dennoch lässt FreeBSD
ihnen die Wahl.</para>
<para>Wenn Sie sich für die Linux-Konvention entscheiden,
müssen Sie es das System wissen lassen. Nachdem ihr
Programm übersetzt und gebunden wurde, müssen Sie
die ausführbare Datei kennzeichnen:</para>
<screen>&prompt.user;
<userinput>brandelf -t Linux
<replaceable>filename</replaceable></userinput></screen>
</sect2>
<sect2 id="x86-use-geneva">
<title>Welche Konvention Sie verwenden sollten</title>
<para>Wenn Sie speziell für FreeBSD programmieren, sollten
Sie die &unix;-Konvention verwenden: Diese ist schneller, Sie
können globale Variablen in Registern ablegen, Sie
müssen die ausführbare Datei nicht kennzeichnen und
Sie erzwingen nicht die Installation der Linux-Emulation auf
dem Zielsystem.</para>
<para>Wenn Sie portablen Programmcode erzeugen wollen, der auch
unter Linux funktioniert, wollen Sie den FreeBSD-Nutzern
vielleicht dennoch den effizientesten Programmcode bieten, der
möglich ist. Ich werde Ihnen zeigen, wie Sie das
erreichen können, nachdem ich die Grundlagen erklärt
habe.</para>
</sect2>
<sect2 id="x86-call-numbers">
<title>Aufruf-Nummern</title>
<para>Um dem Kernel mitzuteilen welchen Dienst Sie aufrufen,
legen Sie dessen Nummer in <varname
role="register">EAX</varname> ab. Natürlich müssen
Sie dazu wissen welche Nummer die Richtige ist.</para>
<sect3 id="x86-the-syscalls-file">
<title>Die Datei <filename>syscalls</filename></title>
<para>Die Nummer der Funktionen sind in der Datei
<filename>syscalls</filename> aufgeführt. Mittels
<command>locate syscalls</command> finden Sie diese in
verschiedenen Formaten, die alle auf die gleiche Weise aus
<filename>syscalls.master</filename> erzeugt werden.</para>
<para>Die Master-Datei für die
&unix;-Standard-Aufrufkonvention finden sie unter
<filename>/usr/src/sys/kern/syscalls.master</filename>.
Falls Sie die andere Konvention, die im
Linux-Emulations-Modus implementiert ist, verwenden
möchten, lesen Sie bitte
<filename>/usr/src/sys/i386/linux/syscalls.master</filename>.</para>
<note><para>FreeBSD und Linux unterscheiden sich nicht nur in
den Aufrufkonventionen, sie haben teilweise auch
verschiedene Nummern für die gleiche
Funktion.</para></note>
<para><filename>syscalls.master</filename> beschreibt, wie der
Aufruf gemacht werden muss:</para>
<programlisting>0 STD NOHIDE { int nosys(void); } syscall nosys_args int
1 STD NOHIDE { void exit(int rval); } exit rexit_args void
2 STD POSIX { int fork(void); }
3 STD POSIX { ssize_t read(int fd, void *buf, size_t nbyte); }
4 STD POSIX { ssize_t write(int fd, const void *buf, size_t nbyte); }
5 STD POSIX { int open(char *path, int flags, int mode); }
6 STD POSIX { int close(int fd); }
etc...</programlisting>
<para>In der ersten Spalte steht die Nummer, die in <varname
role="register">EAX</varname> abgelegt werden muss.</para>
<para>Die Spalte ganz rechts sagt uns welche Parameter wir
<function role="opcode">push</function>en müssen. Die
Reihenfolge ist dabei <emphasis>von rechts nach
links</emphasis>.</para>
<informalexample><para>Um beispielsweise eine Datei mittels
<function>open</function> zu öffnen, müssen wir
zuerst den <varname>mode</varname> auf den Stack <function
role="opcode">push</function>en, danach die
<varname>flags</varname>, dann die Adresse an der der
<varname>path</varname> gespeichert
ist.</para></informalexample>
</sect3>
</sect2>
</sect1>
<sect1 id="x86-return-values">
<sect1info>
<authorgroup>
<author>
<firstname>Hagen</firstname>
<surname>Kühl</surname>
<contrib>Übersetzt von </contrib>
</author>
</authorgroup>
</sect1info>
<title>Rückgabewerte</title>
<para>Ein Systemaufruf wäre meistens nicht sehr
nützlich, wenn er nicht irgendeinen Wert zurückgibt:
Beispielsweise den Dateideskriptor einer geöffneten Datei,
die Anzahl an Bytes die in einen Puffer gelesen wurde, die
Systemzeit, etc.</para>
<para>Außerdem muss Sie das System informieren, falls ein
Fehler auftritt: Wenn eine Datei nicht existiert, die
Systemressourcen erschöpft sind, wir ein ungültiges
Argument übergeben haben, etc.</para>
<sect2 id="x86-man-pages">
<title>Manualpages</title>
<para>Der herkömmliche Ort, um nach Informationen über
verschiedene Systemaufrufe unter &unix;-Systemen zu suchen,
sind die Manualpages. FreeBSD beschreibt seine Systemaufrufe
in Abschnitt 2, manchmal auch Abschnitt 3.</para>
<para>In
<citerefentry><refentrytitle>open</refentrytitle><manvolnum>2</manvolnum></citerefentry>
steht beispielsweise:</para>
<blockquote><para>Falls erfolgreich, gibt
<function>open()</function> einen nicht negativen Integerwert,
als Dateideskriptor bezeichnet, zurück. Es gibt
<varname>-1</varname> im Fehlerfall zurück und setzt
<varname>errno</varname> um den Fehler
anzuzeigen.</para></blockquote>
<para>Ein Assembler-Programmierer, der neu bei &unix; und
FreeBSD ist, wird sich sofort fragen: Wo finde ich
<varname>errno</varname> und wie erreiche ich es?</para>
<note><para>Die Information der Manualpage bezieht sich auf
C-Programme. Der Assembler-Programmierer benötigt
zusätzliche Informationen.</para></note>
</sect2>
<sect2 id="x86-where-return-values">
<title>Wo sind die Rückgabewerde?</title>
<para>Leider gilt: Es kommt darauf an... Für die meisten
Systemaufrufe liegt er in <varname
role="register">EAX</varname>, aber nicht für alle. Eine
gute Daumenregel, wenn man zum ersten Mal mit einem
Systemaufruf arbeitet, ist in <varname
role="register">EAX</varname> nach dem Rückgabewert zu
suchen. Wenn er nicht dort ist, sind weitere Untersuchungen
nötig.</para>
<note><para>Mir ist ein Systemaufruf bekannt, der den
Rückgabewert in <varname role="register">EDX</varname>
ablegt: <function role="syscall">SYS_fork</function> Alle
anderen mit denen ich bisher gearbeitet habe verwenden
<varname role="register">EAX</varname>. Allerdings habe ich
noch nicht mit allen gearbeitet.</para></note>
<tip><para>Wenn Sie die Antwort weder hier, noch irgendwo anders
finden, studieren Sie den Quelltext von
<application>libc</application> und sehen sich an, wie es mit
dem Kernel zusammenarbeitet.</para></tip>
</sect2>
<sect2 id="x86-where-errno">
<title>Wo ist <varname>errno</varname>?</title>
<para>Tatsächlich, nirgendwo...</para>
<para><varname>errno</varname> ist ein Teil der Sprache C, nicht
des &unix;-Kernels. Wenn man direkt auf Kernel-Dienste
zugreift, wird der Fehlercode in <varname
role="register">EAX</varname> zurückgegeben, das selbe
Register in dem der Rückgabewert, bei einem erfolgreichen
Aufruf landet.</para>
<para>Das macht auch Sinn. Wenn kein Fehler auftritt, gibt es
keinen Fehlercode. Wenn ein Fehler auftritt, gibt es keinen
Rückgabewert. Ein einziges Register kann also beides
enthalten.</para>
</sect2>
<sect2 id="x86-how-to-know-error">
<title>Feststellen, dass ein Fehler aufgetreten ist</title>
<para>Wenn Sie die Standard FreeBSD-Aufrufkonvention verwenden
wird das <varname role="register">carry flag</varname>
gelöscht wenn der Aufruf erfolgreich ist und gesetzt wenn
ein Fehler auftritt.</para>
<para>Wenn Sie den Linux-Emulationsmodus verwenden ist der
vorzeichenbehaftete Wert in <varname
role="register">EAX</varname> nicht negativ, bei einem
erfolgreichen Aufruf. Wenn ein Fehler auftritt ist der Wert
negativ, also <varname>-errno</varname>.</para>
</sect2>
</sect1>
<sect1 id="x86-portable-code">
<sect1info>
<authorgroup>
<author>
<firstname>Hagen</firstname>
<surname>Kühl</surname>
<contrib>Übersetzt von </contrib>
</author>
</authorgroup>
</sect1info>
<title>Portablen Code erzeugen</title>
<para>Portabilität ist im Allgemeinen keine Stärke der
Assembler-Programmierung. Dennoch ist es, besonders mit
<application>nasm</application>, möglich
Assembler-Programme für verschiedene Plattformen zu
schreiben. Ich selbst habe bereits Assembler-Bibliotheken
geschrieben die auf so unterschiedlichen Systemen wie &windows;
und FreeBSD übersetzt werden können.</para>
<para>Das ist um so besser möglich, wenn Ihr Code auf zwei
Plattformen laufen soll , die, obwohl sie verschieden sind, auf
ähnlichen Architekturen basieren.</para>
<para>Beispielsweise ist FreeBSD ein &unix;, während Linux
&unix;-artig ist. Ich habe bisher nur drei Unterschiede zwischen
beiden (aus Sicht eines Assembler-Programmierers) erwähnt:
Die Aufruf-Konvention, die Funktionsnummern und die Art der
Übergabe von Rückgabewerten.</para>
<sect2 id="x86-deal-with-function-numbers">
<title>Mit Funktionsnummern umgehen</title>
<para>In vielen Fällen sind die Funktionsnummern die
selben. Allerdings kann man auch wenn sie es nicht sind
leicht mit diesem Problem umgehen: Anstatt die Nummern in
Ihrem Code zu verwenden, benutzen Sie Konstanten, die Sie
abhängig von der Zielarchitektur unterschiedlich
definieren:</para>
<programlisting>%ifdef LINUX
%define SYS_execve 11
%else
%define SYS_execve 59
%endif</programlisting>
</sect2>
<sect2 id="x86-deal-with-geneva">
<title>Umgang mit Konventionen</title>
<para>Sowohl die Aufrufkonvention, als auch die
Rückgabewerte (das <varname>errno</varname> Problem) kann
man mit Hilfe von Makros lösen:</para>
<programlisting>%ifdef LINUX
%macro system 0
call kernel
%endmacro
align 4
kernel:
push ebx
push ecx
push edx
push esi
push edi
push ebp
mov ebx, [esp+32]
mov ecx, [esp+36]
mov edx, [esp+40]
mov esi, [esp+44]
mov ebp, [esp+48]
int 80h
pop ebp
pop edi
pop esi
pop edx
pop ecx
pop ebx
or eax, eax
js .errno
clc
ret
.errno:
neg eax
stc
ret
%else
%macro system 0
int 80h
%endmacro
%endif</programlisting>
</sect2>
<sect2 id="x86-deal-with-other-portability">
<title>Umgang mit anderen
Portabilitätsangelegenheiten</title>
<para>Die oben genannte Lösung funktioniert in den meisten
Fällen, wenn man Code schreibt, der zwischen FreeBSD und
Linux portierbar sein soll. Allerdings sind die Unterschiede
bei einigen Kernel-Diensten tiefgreifender.</para>
<para>In diesem Fällen müssen Sie zwei verschiedene
Handler für diese Systemaufrufe schreiben und bedingte
Assemblierung benutzen, um diese zu übersetzen.
Glücklicherweise wird der größte Teil Ihres
Codes nicht den Kernel aufrufen und Sie werden deshalb nur
wenige solcher bedingten Abschnitte benötigen.</para>
</sect2>
<sect2 id="x86-portable-library">
<title>Eine Bibliothek benutzen</title>
<para>Sie können Portabilitätsprobleme im Hauptteil
ihres Codes komplett vermeiden, indem Sie eine Bibliothek
für Systemaufrufe schreiben. Erstellen Sie eine
Bibliothek für FreeBSD, eine für Linux und weitere
für andere Betriebssysteme.</para>
<para>Schreiben Sie in ihrer Bibliothek eine gesonderte Funktion
(oder Prozedur, falls Sie die traditionelle
Assembler-Terminologie bevorzugen) für jeden
Systemaufruf. Verwenden Sie dabei die C-Aufrufkonvention um
Parameter zu übergeben, aber verwenden Sie weiterhin
<varname role="register">EAX</varname>, für die
Aufrufnummer. In diesem Fall kann ihre FreeBSD-Bibliothek sehr
einfach sein, da viele scheinbar unterschiedliche Funktionen
als Label für denselben Code implementiert sein
können:</para>
<programlisting>sys.open:
sys.close:
[etc...]
int 80h
ret</programlisting>
<para>Ihre Linux-Bibliothek wird mehr verschiedene Funktionen
benötigen, aber auch hier können Sie Systemaufrufe,
welche die Anzahl an Parametern akzeptieren
zusammenfassen:</para>
<programlisting>sys.exit:
sys.close:
[etc... one-parameter functions]
push ebx
mov ebx, [esp+12]
int 80h
pop ebx
jmp sys.return
...
sys.return:
or eax, eax
js sys.err
clc
ret
sys.err:
neg eax
stc
ret</programlisting>
<para>Der Bibliotheks-Ansatz mag auf den ersten Blick unbequem
aussehen, weil Sie eine weitere Datei erzeugen müssen von
der Ihr Code abhängt. Aber er hat viele Vorteile: Zum
einen müssen Sie die Bibliothek nur einmal schreiben und
können sie dann in allen Ihren Programmen verwenden. Sie
können sie sogar von anderen Assembler-Programmierern
verwenden lassen, oder eine die von jemand anderem geschrieben
wurde verwenden. Aber der vielleicht größte Vorteil
ist, dass Ihr Code sogar von anderen Programmierer auf andere
Systeme portiert werden kann, einfach indem man eine neue
Bibliothek schreibt, völlig ohne Änderungen an Ihrem
Code.</para>
<para>Falls Ihnen der Gedanke eine Bibliothek zu nutzen nicht
gefällt, können Sie zumindest all ihre Systemaufrufe
in einer gesonderten Assembler-Datei ablegen und diese mit
Ihrem Hauptprogramm zusammen binden. Auch hier müssen
alle, die ihr Programm portieren, nur eine neue Objekt-Datei
erzeugen und an Ihr Hauptprogramm binden.</para>
</sect2>
<sect2 id="x86-portable-include">
<title>Eine Include-Datei verwenden</title>
<para>Wenn Sie ihre Software als (oder mit dem) Quelltext
ausliefern, können Sie Makros definieren und in einer
getrennten Datei ablegen, die Sie ihrem Code beilegen.</para>
<para>Porter Ihrer Software schreiben dann einfach eine neue
Include-Datei. Es ist keine Bibliothek oder eine externe
Objekt-Datei nötig und Ihr Code ist portabel, ohne dass
man ihn editieren muss.</para>
<note>
<para>Das ist der Ansatz den wir in diesem Kapitel verwenden
werden. Wir werden unsere Include-Datei
<filename>system.inc</filename> nennen und jedesmal, wenn
wir einen neuen Systemaufruf verwenden, den entsprechenden
Code dort einfügen.</para>
</note>
<para>Wir können unsere <filename>system.inc</filename>
beginnen indem wir die Standard-Dateideskriptoren
deklarieren:</para>
<programlisting>%define stdin 0
%define stdout 1
%define stderr 2</programlisting>
<para>Als Nächstes erzeugen wir einen symbolischen Namen
für jeden Systemaufruf:</para>
<programlisting>%define SYS_nosys 0
%define SYS_exit 1
%define SYS_fork 2
%define SYS_read 3
%define SYS_write 4
; [etc...]</programlisting>
<para>Wir fügen eine kleine, nicht globale Prozedur mit
langem Namen ein, damit wir den Namen nicht aus Versehen in
unserem Code wiederverwenden:</para>
<programlisting>section .text
align 4
access.the.bsd.kernel:
int 80h
ret</programlisting>
<para>Wir erzeugen ein Makro, das ein Argument erwartet, die
Systemaufruf-Nummer:</para>
<programlisting>%macro system 1
mov eax, %1
call access.the.bsd.kernel
%endmacro</programlisting>
<para>Letztlich erzeugen wir Makros für jeden Systemaufruf.
Diese Argumente erwarten keine Argumente.</para>
<programlisting>%macro sys.exit 0
system SYS_exit
%endmacro
%macro sys.fork 0
system SYS_fork
%endmacro
%macro sys.read 0
system SYS_read
%endmacro
%macro sys.write 0
system SYS_write
%endmacro
; [etc...]</programlisting>
<para>Fahren Sie fort, geben das in Ihren Editor ein und
speichern es als <filename>system.inc</filename>. Wenn wir
Systemaufrufe besprechen, werden wir noch Ergänzungen in
dieser Datei vornehmen.</para>
</sect2>
</sect1>
<sect1 id="x86-first-program">
<sect1info>
<authorgroup>
<author>
<firstname>Hagen</firstname>
<surname>Kühl</surname>
<contrib>Übersetzt von </contrib>
</author>
</authorgroup>
</sect1info>
<title>Unser erstes Programm</title>
<para>Jetzt sind wir bereit für unser erstes Programm, das
übliche <application>Hello, World!</application></para>
<programlisting> 1: %include 'system.inc'
2:
3: section .data
4: hello db 'Hello, World!', 0Ah
5: hbytes equ $-hello
6:
7: section .text
8: global _start
9: _start:
10: push dword hbytes
11: push dword hello
12: push dword stdout
13: sys.write
14:
15: push dword 0
16: sys.exit</programlisting>
<para>Hier folgt die Erklärung des Programms: Zeile 1
fügt die Definitionen ein, die Makros und den Code aus
<filename>system.inc</filename>.</para>
<para>Die Zeilen 3 bis 5 enthalten die Daten: Zeile 3 beginnt den
Datenabschnitt/das Datensegment. Zeile 4 enthält die
Zeichenkette "Hello, World!", gefolgt von einem Zeilenumbruch
(<constant>0Ah</constant>). Zeile 5 erstellt eine Konstante, die
die Länge der Zeichenkette aus Zeile 4 in Bytes
enthält.</para>
<para>Die Zeilen 7 bis 16 enthalten den Code. Beachten Sie
bitte, dass FreeBSD das Dateiformat <emphasis>elf</emphasis>
für diese ausführbare Datei verwendet, bei dem jedes
Programm mit dem Label <varname>_start</varname> beginnt (oder,
um genau zu sein, wird dies vom Linker erwartet). Diese Label
muss global sein.</para>
<para>Die Zeilen 10 bis 13 weisen das System an
<varname>hbytes</varname> Bytes der Zeichenkette
<varname>hello</varname> nach <varname>stdout</varname> zu
schreiben.</para>
<para>Die Zeilen 15 und 16 weisen das System an das Programm mit
dem Rückgabewert <constant>0</constant> zu beenden. Der
Systemaufruf <function role="syscall">SYS_exit</function> kehrt
niemals zurück, somit endet das Programm hier.</para>
<note>
<para>Wenn Sie von <acronym>&ms-dos;</acronym>-Assembler zu
&unix; gekommen sind, sind Sie es vielleicht gewohnt direktauf
die Video-Hardware zu schreiben. Unter FreeBSD müssen Sie
sich darum keine Gedanken machen, ebenso bei jeder anderen Art
von &unix;. Soweit es Sie betrifft schreiben Sie in eine Datei
namens <filename>stdout</filename>. Das kann der Bildschirm,
oder ein <application>telnet</application>-Terminal, eine
wirkliche Datei, oder die Eingabe eines anderen Programms
sein. Es liegt beim System herauszufinden, welches davon es
tatsächlich ist.</para>
</note>
<sect2 id="x86-assemble-1">
<title>Den Code assemblieren</title>
<para>Geben Sie den Code (außer den Zeilennummern) in
einen Editor ein und speichern Sie ihn in einer Datei namens
<filename>hello.asm</filename>. Um es zu assemblieren
benötigen Sie <application>nasm</application>.</para>
<sect3 id="x86-get-nasm">
<title><application>nasm</application> installieren</title>
<para>Wenn Sie <application>nasm</application> noch nicht
installiert haben geben Sie folgendes ein:</para>
<screen>&prompt.user; <userinput>su</userinput>
Password:<userinput><replaceable>your root password</replaceable></userinput>
&prompt.root; <userinput>cd /usr/ports/devel/nasm</userinput>
&prompt.root; <userinput>make install</userinput>
&prompt.root; <userinput>exit</userinput>
&prompt.user;</screen>
<para>Sie können auch <userinput>make install
clean</userinput> anstatt <userinput>make
install</userinput> eingeben, wenn Sie den Quelltext von
<application>nasm</application> nicht behalten
möchten.</para>
<para>Auf jeden Fall wird FreeBSD
<application>nasm</application> automatisch aus dem Internet
herunterladen, es kompilieren und auf Ihrem System
installieren.</para>
<note>
<para>Wenn es sich bei Ihrem System nicht um FreeBSD
handelt, müssen Sie <application>nasm</application>
von dessen <ulink
url="https://sourceforge.net/projects/nasm">Homepage</ulink>
herunterladen. Sie können es aber dennoch verwenden
um FreeBSD code zu assemblieren.</para>
</note>
<para>Nun können Sie den Code assemblieren, binden und
ausführen:</para>
<screen>&prompt.user; <userinput>nasm -f elf hello.asm</userinput>
&prompt.user; <userinput>ld -s -o hello hello.o</userinput>
&prompt.user; <userinput>./hello</userinput>
Hello, World!
&prompt.user;</screen>
</sect3>
</sect2>
</sect1>
<sect1 id="x86-unix-filters">
<sect1info>
<authorgroup>
<author>
<firstname>Hagen</firstname>
<surname>Kühl</surname>
<contrib>Übersetzt von </contrib>
</author>
</authorgroup>
</sect1info>
<title>&unix;-Filter schreiben</title>
<para>Ein häufiger Typ von &unix;-Anwendungen ist ein Filter
&mdash; ein Programm, das Eingaben von
<filename>stdin</filename> liest, sie verarbeitet und das
Ergebnis nach <filename>stdout</filename> schreibt.</para>
<para>In diesem Kapitel möchten wir einen einfachen Filter
entwickeln und lernen, wie wir von <filename>stdin</filename>
lesen und nach <filename>stdout</filename> schreiben. Dieser
Filter soll jedes Byte seiner Eingabe in eine hexadezimale Zahl
gefolgt von einem Leerzeichen umwandeln.</para>
<programlisting>%include 'system.inc'
section .data
hex db '0123456789ABCDEF'
buffer db 0, 0, ' '
section .text
global _start
_start:
; read a byte from stdin
push dword 1
push dword buffer
push dword stdin
sys.read
add esp, byte 12
or eax, eax
je .done
; convert it to hex
movzx eax, byte [buffer]
mov edx, eax
shr dl, 4
mov dl, [hex+edx]
mov [buffer], dl
and al, 0Fh
mov al, [hex+eax]
mov [buffer+1], al
; print it
push dword 3
push dword buffer
push dword stdout
sys.write
add esp, byte 12
jmp short _start
.done:
push dword 0
sys.exit</programlisting>
<para>Im Datenabschnitt erzeugen wir ein Array mit Namen
<varname>hex</varname>. Es enthält die 16 hexadezimalen
Ziffern in aufsteigender Reihenfolge. Diesem Array folgt ein
Puffer, den wir sowohl für die Ein- als auch für die
Ausgabe verwenden. Die ersten beiden Bytes dieses Puffers werden
am Anfang auf <constant>0</constant> gesetzt. Dorthin schreiben
wir die beiden hexadezimalen Ziffern (das erste Byte ist auch
die Stelle an die wir die Eingabe lesen). Das dritte Byte ist
ein Leerzeichen.</para>
<para>Der Code-Abschnitt besteht aus vier Teilen: Das Byte lesen,
es in eine hexadezimale Zahl umwandeln, das Ergebnis schreiben
und letztendlich das Programm verlassen.</para>
<para>Um das Byte zu lesen, bitten wir das System ein Byte von
<filename>stdin</filename> zu lesen und speichern es im ersten
Byte von <varname>buffer</varname>. Das System gibt die Anzahl
an Bytes, die gelesen wurden, in <varname
role="register">EAX</varname> zurück. Diese wird
<constant>1</constant> sein, wenn eine Eingabe empfangen wird
und <constant>0</constant>, wenn keine Eingabedaten mehr
verfügbar sind. Deshalb überprüfen wir den Wert
von <varname role="register">EAX</varname>. Wenn dieser
<constant>0</constant> ist, springen wir zu
<varname>.done</varname>, ansonsten fahren wir fort.</para>
<note>
<para>Zu Gunsten der Einfachheit ignorieren wir hier die
Möglichkeit eines Fehlers.</para>
</note>
<para>Die Umwandlungsroutine in eine Hexadezimalzahl liest das
Byte aus <varname>buffer</varname> in <varname
role="register">EAX</varname>, oder genaugenommen nur in
<varname role="register">AL</varname>, wobei die übrigen
Bits von <varname role="register">EAX</varname> auf null gesetzt
werden. Außerdem kopieren wir das Byte nach <varname
role="register">EDX</varname>, da wir die oberen vier Bits
(Nibble) getrennt von den unteren vier Bits umwandeln
müssen. Das Ergebnis speichern wir in den ersten beiden
Bytes des Puffers.</para>
<para>Als Nächstes bitten wir das System die drei Bytes in
den Puffer zu schreiben, also die zwei hexadezimalen Ziffern und
das Leerzeichen nach <filename>stdout</filename>. Danach
springen wir wieder an den Anfang des Programms und verarbeiten
das nächste Byte.</para>
<para>Wenn die gesamte Eingabe verarbeitet ist, bitten wie das
System unser Programm zu beenden und null zurückzuliefern,
welches traditionell die Bedeutung hat, dass unser Programm
erfolgreich war.</para>
<para>Fahren Sie fort und speichern Sie den Code in eine Datei
namens <filename>hex.asm</filename>. Geben Sie danach folgendes
ein (<userinput>^D</userinput> bedeutet, dass Sie die
Steuerungstaste drücken und dann <userinput>D</userinput>
eingeben, während Sie Steuerung gedrückt
halten):</para>
<screen>&prompt.user; <userinput>nasm -f elf hex.asm</userinput>
&prompt.user; <userinput>ld -s -o hex hex.o</userinput>
&prompt.user; <userinput>./hex</userinput>
<userinput>Hello, World!</userinput>
48 65 6C 6C 6F 2C 20 57 6F 72 6C 64 21 0A <userinput>Here I come!</userinput>
48 65 72 65 20 49 20 63 6F 6D 65 21 0A <userinput>^D</userinput> &prompt.user;</screen>
<note>
<para>Wenn Sie von <acronym>&ms-dos;</acronym> zu &unix;
wechseln, wundern Sie sich vielleicht, warum jede Zeile mit
<constant>0A</constant> an Stelle von <constant>0D
0A</constant> endet. Das liegt daran, dass &unix; nicht die
CR/LF-Konvention, sondern die "new line"-Konvention verwendet,
welches hexadezimal als <constant>0A</constant> dargestellt
wird.</para>
</note>
<para>Können wir das Programm verbessern? Nun, einerseits ist
es etwas verwirrend, dass die Eingabe, nachdem wir eine Zeile
verarbeitet haben, nicht wieder am Anfang der Zeile beginnt.
Deshalb können wir unser Programm anpassen um einen
Zeilenumbruch an Stelle eines Leerzeichens nach jedem
<constant>0A</constant> auszugeben:</para>
<programlisting>%include 'system.inc'
section .data
hex db '0123456789ABCDEF'
buffer db 0, 0, ' '
section .text
global _start
_start:
mov cl, ' '
.loop:
; read a byte from stdin
push dword 1
push dword buffer
push dword stdin
sys.read
add esp, byte 12
or eax, eax
je .done
; convert it to hex
movzx eax, byte [buffer]
mov [buffer+2], cl
cmp al, 0Ah
jne .hex
mov [buffer+2], al
.hex:
mov edx, eax
shr dl, 4
mov dl, [hex+edx]
mov [buffer], dl
and al, 0Fh
mov al, [hex+eax]
mov [buffer+1], al
; print it
push dword 3
push dword buffer
push dword stdout
sys.write
add esp, byte 12
jmp short .loop
.done:
push dword 0
sys.exit</programlisting>
<para>Wir haben das Leerzeichen im Register <varname
role="register">CL</varname> abgelegt. Das können wir
bedenkenlos tun, da &unix;-Systemaufrufe im Gegensatz zu denen
von &microsoft.windows; keine Werte von Registern ändern in
denen sie keine Werte zurückliefern.</para>
<para>Das bedeutet, dass wir <varname role="register">CL</varname>
nur einmal setzen müssen. Dafür haben wir ein neues
Label <varname>.loop</varname> eingefügt, zu dem wir an
Stelle von <varname>_start</varname> springen, um das
nächste Byte einzulesen. Außerdem haben wir das Label
<varname>.hex</varname> eingefügt, somit können wir
wahlweise ein Leerzeichen oder einen Zeilenumbruch im dritten
Byte von <varname>buffer</varname> ablegen.</para>
<para>Nachdem Sie <filename>hex.asm</filename> entsprechend der
Neuerungen geändert haben, geben Sie Folgendes ein:</para>
<screen>&prompt.user; <userinput>nasm -f elf hex.asm</userinput>
&prompt.user; <userinput>ld -s -o hex hex.o</userinput>
&prompt.user; <userinput>./hex</userinput>
<userinput>Hello, World!</userinput>
48 65 6C 6C 6F 2C 20 57 6F 72 6C 64 21 0A
<userinput>Here I come!</userinput>
48 65 72 65 20 49 20 63 6F 6D 65 21 0A
<userinput>^D</userinput> &prompt.user;</screen>
<para>Das sieht doch schon besser aus. Aber der Code ist ziemlich
ineffizient! Wir führen für jeden einzelne Byte
zweimal einen Systemaufruf aus (einen zum Lesen und einen um es
in die Ausgabe zu schreiben).</para>
</sect1>
<sect1 id="x86-buffered-io">
<sect1info>
<authorgroup>
<author>
<firstname>Hagen</firstname>
<surname>Kühl</surname>
<contrib>Übersetzt von </contrib>
</author>
</authorgroup>
</sect1info>
<title>Gepufferte Eingabe und Ausgabe</title>
<para>Wir können die Effizienz unseres Codes erhöhen,
indem wir die Ein- und Ausgabe puffern. Wir erzeugen einen
Eingabepuffer und lesen dann eine Folge von Bytes auf einmal.
Danach holen wir sie Byte für Byte aus dem Puffer.</para>
<para>Wir erzeugen ebenfalls einen Ausgabepuffer. Darin speichern
wir unsere Ausgabe bis er voll ist. Dann bitten wir den Kernel
den Inhalt des Puffers nach <filename>stdout</filename> zu
schreiben.</para>
<para>Diese Programm endet, wenn es keine weitere Eingaben gibt.
Aber wir müssen den Kernel immernoch bitten den Inhalt des
Ausgabepuffers ein letztes Mal nach <filename>stdout</filename>
zu schreiben, denn sonst würde ein Teil der Ausgabe zwar im
Ausgabepuffer landen, aber niemals ausgegeben werden. Bitte
vergessen Sie das nicht, sonst fragen Sie sich später warum
ein Teil Ihrer Ausgabe verschwunden ist.</para>
<programlisting>%include 'system.inc'
%define BUFSIZE 2048
section .data
hex db '0123456789ABCDEF'
section .bss
ibuffer resb BUFSIZE
obuffer resb BUFSIZE
section .text
global _start
_start:
sub eax, eax
sub ebx, ebx
sub ecx, ecx
mov edi, obuffer
.loop:
; read a byte from stdin
call getchar
; convert it to hex
mov dl, al
shr al, 4
mov al, [hex+eax]
call putchar
mov al, dl
and al, 0Fh
mov al, [hex+eax]
call putchar
mov al, ' '
cmp dl, 0Ah
jne .put
mov al, dl
.put:
call putchar
jmp short .loop
align 4
getchar:
or ebx, ebx
jne .fetch
call read
.fetch:
lodsb
dec ebx
ret
read:
push dword BUFSIZE
mov esi, ibuffer
push esi
push dword stdin
sys.read
add esp, byte 12
mov ebx, eax
or eax, eax
je .done
sub eax, eax
ret
align 4
.done:
call write ; flush output buffer
push dword 0
sys.exit
align 4
putchar:
stosb
inc ecx
cmp ecx, BUFSIZE
je write
ret
align 4
write:
sub edi, ecx ; start of buffer
push ecx
push edi
push dword stdout
sys.write
add esp, byte 12
sub eax, eax
sub ecx, ecx ; buffer is empty now
ret</programlisting>
<para>Als dritten Abschnitt im Quelltext haben wir
<varname>.bss</varname>. Dieser Abschnitt wird nicht in unsere
ausführbare Datei eingebunden und kann daher nicht
initialisiert werden. Wir verwenden <function
role="opcode">resb</function> anstelle von <function
role="opcode">db</function>. Dieses reserviert einfach die
angeforderte Menge an uninitialisiertem Speicher zu unserer
Verwendung.</para>
<para>Wir nutzen, die Tatsache, dass das System die Register nicht
verändert: Wir benutzen Register, wo wir anderenfalls
globale Variablen im Abschnitt <varname>.data</varname>
verwenden müssten. Das ist auch der Grund, warum die
&unix;-Konvention, Parameter auf dem Stack zu übergeben,
der von Microsoft, hierfür Register zu verwenden,
überlegen ist: Wir können Register für unsere
eigenen Zwecke verwenden.</para>
<para>Wir verwenden <varname role="register">EDI</varname> und
<varname role="register">ESI</varname> als Zeiger auf das
nächste zu lesende oder schreibende Byte. Wir verwenden
<varname role="register">EBX</varname> und <varname
role="register">ECX</varname>, um die Anzahl der Bytes in den
beiden Puffern zu zählen, damit wir wissen, wann wir die
Ausgabe an das System übergeben, oder neue Eingabe vom
System entgegen nehmen müssen.</para>
<para>Lassen Sie uns sehen, wie es funktioniert:</para>
<screen>&prompt.user; <userinput>nasm -f elf hex.asm</userinput>
&prompt.user; <userinput>ld -s -o hex hex.o</userinput>
&prompt.user; <userinput>./hex</userinput>
<userinput>Hello, World!</userinput>
<userinput>Here I come!</userinput>
48 65 6C 6C 6F 2C 20 57 6F 72 6C 64 21 0A
48 65 72 65 20 49 20 63 6F 6D 65 21 0A
<userinput>^D</userinput> &prompt.user;</screen>
<para>Nicht was Sie erwartet haben? Das Programm hat die Ausgabe
nicht auf dem Bildschirm ausgegeben bis sie
<userinput>^D</userinput> gedrückt haben. Das kann man
leicht zu beheben indem man drei Zeilen Code einfügt,
welche die Ausgabe jedesmal schreiben, wenn wir einen
Zeilenumbruch in <constant>0A</constant> umgewandelt haben. Ich
habe die betreffenden Zeilen mit &gt; markiert (kopieren Sie die
&gt; bitte nicht mit in Ihre
<filename>hex.asm</filename>).</para>
<programlisting>%include 'system.inc'
%define BUFSIZE 2048
section .data
hex db '0123456789ABCDEF'
section .bss
ibuffer resb BUFSIZE
obuffer resb BUFSIZE
section .text
global _start
_start:
sub eax, eax
sub ebx, ebx
sub ecx, ecx
mov edi, obuffer
.loop:
; read a byte from stdin
call getchar
; convert it to hex
mov dl, al
shr al, 4
mov al, [hex+eax]
call putchar
mov al, dl
and al, 0Fh
mov al, [hex+eax]
call putchar
mov al, ' '
cmp dl, 0Ah
jne .put
mov al, dl
.put:
call putchar
> cmp al, 0Ah
> jne .loop
> call write
jmp short .loop
align 4
getchar:
or ebx, ebx
jne .fetch
call read
.fetch:
lodsb
dec ebx
ret
read:
push dword BUFSIZE
mov esi, ibuffer
push esi
push dword stdin
sys.read
add esp, byte 12
mov ebx, eax
or eax, eax
je .done
sub eax, eax
ret
align 4
.done:
call write ; flush output buffer
push dword 0
sys.exit
align 4
putchar:
stosb
inc ecx
cmp ecx, BUFSIZE
je write
ret
align 4
write:
sub edi, ecx ; start of buffer
push ecx
push edi
push dword stdout
sys.write
add esp, byte 12
sub eax, eax
sub ecx, ecx ; buffer is empty now
ret</programlisting>
<para>Lassen Sie uns jetzt einen Blick darauf werfen, wie es
funktioniert.</para>
<screen>&prompt.user; <userinput>nasm -f elf hex.asm</userinput>
&prompt.user; <userinput>ld -s -o hex hex.o</userinput>
&prompt.user; <userinput>./hex</userinput>
<userinput>Hello, World!</userinput>
48 65 6C 6C 6F 2C 20 57 6F 72 6C 64 21 0A
<userinput>Here I come!</userinput>
48 65 72 65 20 49 20 63 6F 6D 65 21 0A
<userinput>^D</userinput> &prompt.user;</screen>
<para>Nicht schlecht für eine 644 Byte große
Binärdatei, oder?</para>
<note>
<para>Dieser Ansatz für gepufferte Ein- und Ausgabe
enthält eine Gefahr, auf die ich im Abschnitt <link
linkend="x86-buffered-dark-side">Die dunkle Seite des
Buffering</link> eingehen werde.</para>
</note>
<sect2 id="x86-ungetc">
<title>Ein Zeichen ungelesen machen</title>
<warning>
<para>Das ist vielleicht ein etwas fortgeschrittenes Thema,
das vor allem für Programmierer interessant ist, die
mit der Theorie von Compilern vertraut sind. Wenn Sie
wollen, können Sie <link linkend="x86-command-line">zum
nächsten Abschnitt springen</link> und das hier
vielleicht später lesen.</para>
</warning>
<para>Unser Beispielprogramm benötigt es zwar nicht, aber
etwas anspruchsvollere Filter müssen häufig
vorausschauen. Mit anderen Worten, sie müssen wissen was
das nächste Zeichen ist (oder sogar mehrere Zeichen).
Wenn das nächste Zeichen einen bestimmten Wert hat, ist
es Teil des aktuellen Tokens, ansonsten nicht.</para>
<para>Zum Beispiel könnten Sie den Eingabestrom für
eine Text-Zeichenfolge parsen (z.B. wenn Sie einen Compiler
einer Sprache implementieren): Wenn einem Buchstaben ein
anderer Buchstabe oder vielleicht eine Ziffer folgt, ist er
ein Teil des Tokens, das Sie verarbeiten. Wenn ihm ein
Leerzeichen folgt, oder ein anderer Wert, ist er nicht Teil
des aktuellen Tokens.</para>
<para>Das führt uns zu einem interessanten Problem: Wie
kann man ein Zeichen zurück in den Eingabestrom geben,
damit es später noch einmal gelesen werden kann?</para>
<para>Eine mögliche Lösung ist, das Zeichen in einer
Variable zu speichern und ein Flag zu setzen. Wir können
<function>getchar</function> so anpassen, dass es das Flag
überprüft und, wenn es gesetzt ist, das Byte aus der
Variable anstatt dem Eingabepuffer liest und das Flag
zurück setzt. Aber natürlich macht uns das
langsamer.</para>
<para>Die Sprache C hat eine Funktion
<function>ungetc()</function> für genau diesen Zweck.
Gibt es einen schnellen Weg, diese in unserem Code zu
implementieren? Ich möchte Sie bitten nach oben zu
scrollen und sich die Prozedur <function>getchar</function>
anzusehen und zu versuchen eine schöne und schnelle
Lösung zu finden, bevor Sie den nächsten Absatz
lesen. Kommen Sie danach hierher zurück und schauen sich
meine Lösung an.</para>
<para>Der Schlüssel dazu ein Zeichen an den Eingabestrom
zurückzugeben, liegt darin, wie wir das Zeichen
bekommen:</para>
<para>Als erstes überprüfen wir, ob der Puffer leer
ist, indem wir den Wert von <varname
role="register">EBX</varname> testen. Wenn er null ist, rufen
wir die Prozedur <function>read</function> auf.</para>
<para>Wenn ein Zeichen bereit ist verwenden wir <function
role="opcode">lodsb</function>, dann verringern wir den Wert
von <varname role="register">EBX</varname>. Die Anweisung
<function role="opcode">lodsb</function> ist letztendlich
identisch mit:</para>
<programlisting> mov al, [esi]
inc esi</programlisting>
<para>Das Byte, welches wir abgerufen haben, verbleibt im Puffer
bis <function>read</function> zum nächsten Mal aufgerufen
wird. Wir wissen nicht wann das passiert, aber wir wissen,
dass es nicht vor dem nächsten Aufruf von
<function>getchar</function> passiert. Daher ist alles was wir
tun müssen um das Byte in den Strom "zurückzugeben"
ist den Wert von <varname role="register">ESI</varname> zu
verringern und den von <varname role="register">EBX</varname>
zu erhöhen:</para>
<programlisting>ungetc:
dec esi
inc ebx
ret</programlisting>
<para>Aber seien Sie vorsichtig! Wir sind auf der sicheren Seite,
solange wir immer nur ein Zeichen im Voraus lesen. Wenn wir
mehrere kommende Zeichen betrachten und
<function>ungetc</function> mehrmals hintereinander aufrufen,
wird es meistens funktionieren, aber nicht immer (und es wird
ein schwieriger Debug). Warum?</para>
<para>Solange <function>getchar</function>
<function>read</function> nicht aufrufen muss, befinden sich
alle im Voraus gelesenen Bytes noch im Puffer und
<function>ungetc</function> arbeitet fehlerfrei. Aber sobald
<function>getchar</function> <function>read</function> aufruft
verändert sich der Inhalt des Puffers.</para>
<para>Wir können uns immer darauf verlassen, dass
<function>ungetc</function> auf dem zuletzt mit
<function>getchar</function> gelesenen Zeichen korrekt
arbeitet, aber nicht auf irgendetwas, das davor gelesen
wurde.</para>
<para>Wenn Ihr Programm mehr als ein Byte im Voraus lesen soll,
haben Sie mindestens zwei Möglichkeiten:</para>
<para>Die einfachste Lösung ist, Ihr Programm so zu
ändern, dass es immer nur ein Byte im Voraus liest, wenn
das möglich ist.</para>
<para>Wenn Sie diese Möglichkeit nicht haben, bestimmen Sie
zuerst die maximale Anzahl an Zeichen, die Ihr Programm auf
einmal an den Eingabestrom zurückgeben muss. Erhöhen
Sie diesen Wert leicht, nur um sicherzugehen, vorzugsweise auf
ein Vielfaches von 16&mdash;damit er sich schön
ausrichtet. Dann passen Sie den <varname>.bss</varname>
Abschnitt Ihres Codes an und erzeugen einen kleinen
Reserver-Puffer, direkt vor ihrem Eingabepuffer, in etwa
so:</para>
<programlisting>section .bss
resb 16 ; or whatever the value you came up with
ibuffer resb BUFSIZE
obuffer resb BUFSIZE</programlisting>
<para>Außerdem müssen Sie <function>ungetc</function>
anpassen, sodass es den Wert des Bytes, das zurückgegeben
werden soll, in <varname role="register">AL</varname>
übergibt:</para>
<programlisting>ungetc:
dec esi
inc ebx
mov [esi], al
ret</programlisting>
<para>Mit dieser Änderung können Sie sicher
<function>ungetc</function> bis zu 17 Mal hintereinander
gqapaufrufen (der erste Aufruf erfolgt noch im Puffer, die
anderen 16 entweder im Puffer oder in der Reserve).</para>
</sect2>
</sect1>
<sect1 id="x86-command-line">
<sect1info>
<authorgroup>
<author>
<firstname>Fabian</firstname>
<surname>Ruch</surname>
<contrib>Übersetzt von </contrib>
</author>
</authorgroup>
</sect1info>
<title>Kommandozeilenparameter</title>
<para>Unser <application>hex</application>-Programm wird
nützlicher, wenn es die Dateinamen der Ein- und Ausgabedatei
über die Kommandozeile einlesen kann, d.h., wenn es
Kommandozeilenparameter verarbeiten kann. Aber... Wo sind
die?</para>
<para>Bevor ein &unix;-System ein Programm ausführt, legt es
einige Daten auf dem Stack ab (<function
role="opcode">push</function>) und springt dann an das
<varname>_start</varname>-Label des Programms. Ja, ich sagte
springen, nicht aufrufen. Das bedeutet, dass auf die Daten
zugegriffen werden kann, indem <varname>[esp+offset]</varname>
ausgelesen wird oder die Daten einfach vom Stack genommen werden
(<function role="opcode">pop</function>).</para>
<para>Der Wert ganz oben auf dem Stack enthält die Zahl der
Kommandozeilenparameter. Er wird traditionell
<varname>argc</varname> wie "argument count" genannt.</para>
<para>Die Kommandozeilenparameter folgen einander, alle
<varname>argc</varname>. Von diesen wird üblicherweise als
<varname>argv</varname> wie "argument value(s)" gesprochen. So
erhalten wir <varname>argv[0]</varname>,
<varname>argv[1]</varname>, <varname>...</varname> und
<varname>argv[argc-1]</varname>. Dies sind nicht die eigentlichen
Parameter, sondern Zeiger (Pointer) auf diese, d.h.,
Speicheradressen der tatsächlichen Parameter. Die Parameter
selbst sind durch NULL beendete Zeichenketten.</para>
<para>Der <varname>argv</varname>-Liste folgt ein NULL-Zeiger, was
einfach eine <constant>0</constant> ist. Es gibt noch mehr, aber
dies ist erst einmal genug für unsere Zwecke.</para>
<note>
<para>Falls Sie von der
<acronym>&ms-dos;</acronym>-Programmierumgebung kommen, ist
der größte Unterschied die Tatsache, dass jeder
Parameter eine separate Zeichenkette ist. Der zweite
Unterschied ist, dass es praktisch keine Grenze gibt, wie
viele Parameter vorhanden sein können.</para>
</note>
<para>Ausgerüstet mit diesen Kenntnissen, sind wir beinahe
bereit für eine weitere Version von
<filename>hex.asm</filename>. Zuerst müssen wir jedoch
noch ein paar Zeilen zu <filename>system.inc</filename>
hinzufügen:</para>
<para>Erstens benötigen wir zwei neue Einträge in unserer
Liste mit den Systemaufrufnummern:</para>
<programlisting>%define SYS_open 5
%define SYS_close 6</programlisting>
<para>Zweitens fügen wir zwei neue Makros am Ende der Datei
ein:</para>
<programlisting>%macro sys.open 0
system SYS_open
%endmacro
%macro sys.close 0
system SYS_close
%endmacro</programlisting>
<para>Und hier ist schließlich unser veränderter
Quelltext:</para>
<programlisting>%include 'system.inc'
%define BUFSIZE 2048
section .data
fd.in dd stdin
fd.out dd stdout
hex db '0123456789ABCDEF'
section .bss
ibuffer resb BUFSIZE
obuffer resb BUFSIZE
section .text
align 4
err:
push dword 1 ; return failure
sys.exit
align 4
global _start
_start:
add esp, byte 8 ; discard argc and argv[0]
pop ecx
jecxz .init ; no more arguments
; ECX contains the path to input file
push dword 0 ; O_RDONLY
push ecx
sys.open
jc err ; open failed
add esp, byte 8
mov [fd.in], eax
pop ecx
jecxz .init ; no more arguments
; ECX contains the path to output file
push dword 420 ; file mode (644 octal)
push dword 0200h | 0400h | 01h
; O_CREAT | O_TRUNC | O_WRONLY
push ecx
sys.open
jc err
add esp, byte 12
mov [fd.out], eax
.init:
sub eax, eax
sub ebx, ebx
sub ecx, ecx
mov edi, obuffer
.loop:
; read a byte from input file or stdin
call getchar
; convert it to hex
mov dl, al
shr al, 4
mov al, [hex+eax]
call putchar
mov al, dl
and al, 0Fh
mov al, [hex+eax]
call putchar
mov al, ' '
cmp dl, 0Ah
jne .put
mov al, dl
.put:
call putchar
cmp al, dl
jne .loop
call write
jmp short .loop
align 4
getchar:
or ebx, ebx
jne .fetch
call read
.fetch:
lodsb
dec ebx
ret
read:
push dword BUFSIZE
mov esi, ibuffer
push esi
push dword [fd.in]
sys.read
add esp, byte 12
mov ebx, eax
or eax, eax
je .done
sub eax, eax
ret
align 4
.done:
call write ; flush output buffer
; close files
push dword [fd.in]
sys.close
push dword [fd.out]
sys.close
; return success
push dword 0
sys.exit
align 4
putchar:
stosb
inc ecx
cmp ecx, BUFSIZE
je write
ret
align 4
write:
sub edi, ecx ; start of buffer
push ecx
push edi
push dword [fd.out]
sys.write
add esp, byte 12
sub eax, eax
sub ecx, ecx ; buffer is empty now
ret</programlisting>
<para>In unserem <varname>.data</varname>-Abschnitt befinden
sich nun die zwei neuen Variablen <varname>fd.in</varname> und
<varname>fd.out</varname>. Hier legen wir die Dateideskriptoren
der Ein- und Ausgabedatei ab.</para>
<para>Im <varname>.text</varname>-Abschnitt haben wir die
Verweise auf <varname>stdin</varname> und
<varname>stdout</varname> durch <varname>[fd.in]</varname> und
<varname>[fd.out]</varname> ersetzt.</para>
<para>Der <varname>.text</varname>-Abschnitt beginnt nun mit
einer einfachen Fehlerbehandlung, welche nur das Programm mit
einem Rückgabewert von <constant>1</constant> beendet. Die
Fehlerbehandlung befindet sich vor <varname>_start</varname>,
sodass wir in geringer Entfernung von der Stelle sind, an der
der Fehler auftritt.</para>
<para>Selbstverständlich beginnt die
Programmausführung immer noch bei
<varname>_start</varname>. Zuerst entfernen wir
<varname>argc</varname> und <varname>argv[0]</varname> vom
Stack: Sie sind für uns nicht von Interesse (sprich, in
diesem Programm).</para>
<para>Wir nehmen <varname>argv[1]</varname> vom Stack und legen
es in <varname role="register">ECX</varname> ab. Dieses Register
ist besonders für Zeiger geeignet, da wir mit <function
role="opcode">jecxz</function> NULL-Zeiger verarbeiten
können. Falls <varname>argv[1]</varname> nicht NULL ist,
versuchen wir, die Datei zu öffnen, die der erste Parameter
festlegt. Andernfalls fahren wir mit dem Programm fort wie
vorher: Lesen von <varname>stdin</varname> und Schreiben nach
<varname>stdout</varname>. Falls wir die Eingabedatei nicht
öffnen können (z.B. sie ist nicht vorhanden), springen
wir zur Fehlerbehandlung und beenden das Programm.</para>
<para>Falls es keine Probleme gibt, sehen wir nun nach dem
zweiten Parameter. Falls er vorhanden ist, öffnen wir die
Ausgabedatei. Andernfalls schreiben wir die Ausgabe nach
<varname>stdout</varname>. Falls wir die Ausgabedatei nicht
öffnen können (z.B. sie ist zwar vorhanden, aber wir
haben keine Schreibberechtigung), springen wir auch wieder in
die Fehlerbehandlung.</para>
<para>Der Rest des Codes ist derselbe wie vorher, außer
dem Schließen der Ein- und Ausgabedatei vor dem Verlassen
des Programms und, wie bereits erwähnt, die Benutzung von
<varname>[fd.in]</varname> und
<varname>[fd.out]</varname>.</para>
<para>Unsere Binärdatei ist nun kolossale 768 Bytes
groß.</para>
<para>Können wir das Programm immer noch verbessern?
Natürlich! Jedes Programm kann verbessert werden. Hier
finden sich einige Ideen, was wir tun könnten:</para>
<itemizedlist>
<listitem>
<para>Die Fehlerbehandlung eine Warnung auf
<varname>stderr</varname> ausgeben lassen.</para>
</listitem>
<listitem>
<para>Den <function>Lese</function>- und
<function>Schreib</function>funkionen eine Fehlerbehandlung
hinzufügen.</para>
</listitem>
<listitem>
<para>Schließen von <varname>stdin</varname>, sobald wir
eine Eingabedatei öffnen, von <varname>stdout</varname>,
sobald wir eine Ausgabedatei öffnen.</para>
</listitem>
<listitem>
<para>Hinzufügen von Kommandozeilenschaltern wie zum
Beispiel <parameter>-i</parameter> und
<parameter>-o</parameter>, sodass wir die Ein- und
Ausgabedatei in irgendeiner Reihenfolge angeben oder
vielleicht von <varname>stdin</varname> lesen und in eine
Datei schreiben können.</para>
</listitem>
<listitem>
<para>Ausgeben einer Gebrauchsanweisung, falls die
Kommandozeilenparameter fehlerhaft sind.</para>
</listitem>
</itemizedlist>
<para>Ich beabsichtige, diese Verbesserungen dem Leser als
Übung zu hinterlassen: Sie wissen bereits alles, das Sie
wissen müssen, um die Verbesserungen
durchzuführen.</para>
</sect1>
<sect1 id="x86-environment">
<sect1info>
<authorgroup>
<author>
<firstname>Fabian</firstname>
<surname>Ruch</surname>
<contrib>Übersetzt von </contrib>
</author>
</authorgroup>
</sect1info>
<title>Die &unix;-Umgebung</title>
<para>Ein entscheidendes Konzept hinter &unix; ist die Umgebung,
die durch <emphasis>Umgebungsvariablen</emphasis> festgelegt
wird. Manche werden vom System gesetzt, andere von Ihnen und
wieder andere von der <application>shell</application> oder
irgendeinem Programm, das ein anderes lädt.</para>
<sect2 id="x86-find-environment">
<title>Umgebungsvariablen herausfinden</title>
<para>Ich sagte vorher, dass wenn ein Programm mit der
Ausführung beginnt, der Stack <varname>argc</varname>
gefolgt vom durch NULL beendeten <varname>argv</varname>-Array
und etwas Anderem enthält. Das "etwas Andere" ist die
<emphasis>Umgebung</emphasis> oder, um genauer zu sein, ein
durch NULL beendetes Array von Zeigern auf
<emphasis>Umgebungsvariablen</emphasis>. Davon wird oft als
<varname>env</varname> gesprochen.</para>
<para>Der Aufbau von <varname>env</varname> entspricht dem von
<varname>argv</varname>, eine Liste von Speicheradressen gefolgt
von NULL (<constant>0</constant>). In diesem Fall gibt es kein
<varname>"envc"</varname>&mdash;wir finden das Ende heraus,
indem wir nach dem letzten NULL suchen.</para>
<para>Die Variablen liegen normalerweise in der Form
<varname>name=value</varname> vor, aber manchmal kann der
<varname>=value</varname>-Teil fehlen. Wir müssen diese
Möglichkeit in Betracht ziehen.</para>
</sect2>
<sect2 id="x86-webvar">
<title>webvars</title>
<para>Ich könnte Ihnen einfach etwas Code zeigen, der die
Umgebung in der Art vom &unix;-Befehl
<application>env</application> ausgibt. Aber ich dachte, dass es
interessanter sei, ein einfaches CGI-Werkzeug in Assembler zu
schreiben.</para>
<sect3 id="x86-cgi">
<title>CGI: Ein kurzer Überblick</title>
<para>Ich habe eine <ulink
url="http://www.whizkidtech.redprince.net/cgi-bin/tutorial">detaillierte
<acronym>CGI</acronym>-Anleitung</ulink> auf meiner Webseite,
aber hier ist ein sehr kurzer Überblick über
<acronym>CGI</acronym>:</para>
<itemizedlist>
<listitem>
<para>Der Webserver kommuniziert mit dem
<acronym>CGI</acronym>-Programm, indem er
<emphasis>Umgebungsvariablen</emphasis> setzt.</para>
</listitem>
<listitem>
<para>Das <acronym>CGI</acronym>-Programm schreibt seine
Ausgabe auf <filename>stdout</filename>. Der Webserver
liest von da.</para>
</listitem>
<listitem>
<para>Die Ausgabe muss mit einem
<acronym>HTTP</acronym>-Kopfteil gefolgt von zwei
Leerzeilen beginnen.</para>
</listitem>
<listitem>
<para>Das Programm gibt dann den
<acronym>HTML</acronym>-Code oder was für einen
Datentyp es auch immer verarbeitet
aus.</para>
</listitem>
<listitem>
<note>
<para>Während bestimmte
<emphasis>Umgebungsvariablen</emphasis> Standardnamen
benutzen, unterscheiden sich andere, abhängig vom
Webserver. Dies macht <application>webvars</application>
zu einem recht nützlichen Werkzeug.</para>
</note>
</listitem>
</itemizedlist>
</sect3>
<sect3 id="x86-webvars-the-code">
<title>Der Code</title>
<para>Unser <application>webvars</application>-Programm muss
also den <acronym>HTTP</acronym>-Kopfteil gefolgt von etwas
<acronym>HTML</acronym>-Auszeichnung versenden. Dann muss es
die <emphasis>Umgebungsvariablen</emphasis> eine nach der
anderen auslesen und sie als Teil der
<acronym>HTML</acronym>-Seite versenden.</para>
<para>Nun der Code. Ich habe Kommentare und Erklärungen
direkt in den Code eingefügt:</para>
<programlisting>
;;;;;;; webvars.asm ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;
; Copyright (c) 2000 G. Adam Stanislav
; All rights reserved.
;
; Redistribution and use in source and binary forms, with or without
; modification, are permitted provided that the following conditions
; are met:
; 1. Redistributions of source code must retain the above copyright
; notice, this list of conditions and the following disclaimer.
; 2. Redistributions in binary form must reproduce the above copyright
; notice, this list of conditions and the following disclaimer in the
; documentation and/or other materials provided with the distribution.
;
; THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
; ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
; IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
; ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
; FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
; DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
; OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
; HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
; LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
; OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
; SUCH DAMAGE.
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;
; Version 1.0
;
; Started: 8-Dec-2000
; Updated: 8-Dec-2000
;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
%include 'system.inc'
section .data
http db 'Content-type: text/html', 0Ah, 0Ah
db '&lt;?xml version="1.0" encoding="UTF-8"?&gt;', 0Ah
db '&lt;!DOCTYPE html PUBLIC "-//W3C/DTD XHTML Strict//EN" '
db '"DTD/xhtml1-strict.dtd"&gt;', 0Ah
db '&lt;html xmlns="http://www.w3.org/1999/xhtml" '
db 'xml.lang="en" lang="en"&gt;', 0Ah
db '&lt;head&gt;', 0Ah
db '&lt;title&gt;Web Environment&lt;/title&gt;', 0Ah
db '&lt;meta name="author" content="G. Adam Stanislav" /&gt;', 0Ah
db '&lt;/head&gt;', 0Ah, 0Ah
db '&lt;body bgcolor="#ffffff" text="#000000" link="#0000ff" '
db 'vlink="#840084" alink="#0000ff"&gt;', 0Ah
db '&lt;div class="webvars"&gt;', 0Ah
db '&lt;h1&gt;Web Environment&lt;/h1&gt;', 0Ah
db '&lt;p&gt;The following &lt;b&gt;environment variables&lt;/b&gt; are defined '
db 'on this web server:&lt;/p&gt;', 0Ah, 0Ah
db '&lt;table align="center" width="80" border="0" cellpadding="10" '
db 'cellspacing="0" class="webvars"&gt;', 0Ah
httplen equ $-http
left db '&lt;tr&gt;', 0Ah
db '&lt;td class="name"&gt;&lt;tt&gt;'
leftlen equ $-left
middle db '&lt;/tt&gt;&lt;/td&gt;', 0Ah
db '&lt;td class="value"&gt;&lt;tt&gt;&lt;b&gt;'
midlen equ $-middle
undef db '&lt;i&gt;(undefined)&lt;/i&gt;'
undeflen equ $-undef
right db '&lt;/b&gt;&lt;/tt&gt;&lt;/td&gt;', 0Ah
db '&lt;/tr&gt;', 0Ah
rightlen equ $-right
wrap db '&lt;/table&gt;', 0Ah
db '&lt;/div&gt;', 0Ah
db '&lt;/body&gt;', 0Ah
db '&lt;/html&gt;', 0Ah, 0Ah
wraplen equ $-wrap
section .text
global _start
_start:
; First, send out all the http and xhtml stuff that is
; needed before we start showing the environment
push dword httplen
push dword http
push dword stdout
sys.write
; Now find how far on the stack the environment pointers
; are. We have 12 bytes we have pushed before "argc"
mov eax, [esp+12]
; We need to remove the following from the stack:
;
; The 12 bytes we pushed for sys.write
; The 4 bytes of argc
; The EAX*4 bytes of argv
; The 4 bytes of the NULL after argv
;
; Total:
; 20 + eax * 4
;
; Because stack grows down, we need to ADD that many bytes
; to ESP.
lea esp, [esp+20+eax*4]
cld ; This should already be the case, but let's be sure.
; Loop through the environment, printing it out
.loop:
pop edi
or edi, edi ; Done yet?
je near .wrap
; Print the left part of HTML
push dword leftlen
push dword left
push dword stdout
sys.write
; It may be tempting to search for the '=' in the env string next.
; But it is possible there is no '=', so we search for the
; terminating NUL first.
mov esi, edi ; Save start of string
sub ecx, ecx
not ecx ; ECX = FFFFFFFF
sub eax, eax
repne scasb
not ecx ; ECX = string length + 1
mov ebx, ecx ; Save it in EBX
; Now is the time to find '='
mov edi, esi ; Start of string
mov al, '='
repne scasb
not ecx
add ecx, ebx ; Length of name
push ecx
push esi
push dword stdout
sys.write
; Print the middle part of HTML table code
push dword midlen
push dword middle
push dword stdout
sys.write
; Find the length of the value
not ecx
lea ebx, [ebx+ecx-1]
; Print "undefined" if 0
or ebx, ebx
jne .value
mov ebx, undeflen
mov edi, undef
.value:
push ebx
push edi
push dword stdout
sys.write
; Print the right part of the table row
push dword rightlen
push dword right
push dword stdout
sys.write
; Get rid of the 60 bytes we have pushed
add esp, byte 60
; Get the next variable
jmp .loop
.wrap:
; Print the rest of HTML
push dword wraplen
push dword wrap
push dword stdout
sys.write
; Return success
push dword 0
sys.exit</programlisting>
<para>Dieser Code erzeugt eine 1.396-Byte große
Binärdatei. Das meiste davon sind Daten, d.h., die
<acronym>HTML</acronym>-Auszeichnung, die wir versenden
müssen.</para>
<para>Assemblieren Sie es wie immer:</para>
<screen>&prompt.user; <userinput>nasm -f elf webvars.asm</userinput>
&prompt.user; <userinput>ld -s -o webvars webvars.o</userinput></screen>
<para>Um es zu benutzen, müssen Sie
<filename>webvars</filename> auf Ihren Webserver hochladen.
Abhängig von Ihrer Webserver-Konfiguration, müssen
Sie es vielleicht in einem speziellen
<filename>cgi-bin</filename>-Verzeichnis ablegen oder es mit
einer <filename>.cgi</filename>-Dateierweiterung
versehen.</para>
<para>Schließlich benötigen Sie Ihren Webbrowser,
um sich die Ausgabe anzusehen. Um die Ausgabe auf meinem
Webserver zu sehen, gehen Sie bitte auf <ulink
url="http://www.int80h.org/webvars/"><filename>http://www.int80h.org/webvars/</filename></ulink>.
Falls Sie neugierig sind, welche zusätzlichen Variablen
in einem passwortgeschützten Webverzeichnis vorhanden
sind, gehen Sie auf <ulink
url="http://www.int80h.org/private/"><filename>http://www.int80h.org/private/</filename></ulink>
unter Benutzung des Benutzernamens <userinput>asm</userinput>
und des Passworts <userinput>programmer</userinput>.</para>
</sect3>
</sect2>
</sect1>
<sect1 id="x86-files">
<sect1info>
<authorgroup>
<author>
<firstname>Paul</firstname>
<surname>Keller</surname>
<contrib>Übersetzt von </contrib>
</author>
<author>
<firstname>Fabian</firstname>
<surname>Borschel</surname>
</author>
</authorgroup>
</sect1info>
<title>Arbeiten mit Dateien</title>
<para>Wir haben bereits einfache Arbeiten mit Dateien gemacht:
Wir wissen wie wir sie öffnen und schliessen, oder wie
man sie mit Hilfe von Buffern liest und schreibt. Aber &unix;
bietet viel mehr Funktionalität wenn es um Dateien geht.
Wir werden einige von ihnen in dieser Sektion untersuchen und
dann mit einem netten Datei Konvertierungs Werkzeug
abschliessen.</para>
<para>In der Tat, Lasst uns am Ende beginnen, also mit dem Datei
Konvertierungs Werkzeug. Es macht Programmieren immer einfacher,
wenn wir bereits am Anfang wissen was das End Produkt bezwecken
soll.</para>
<para>Eines der ersten Programme die ich für &unix; schrieb
war <ulink url="ftp://ftp.int80h.org/unix/tuc/"><application>
tuc</application></ulink>, ein Text-Zu-&unix; Datei Konvertierer.
Es konvertiert eine Text Datei von einem anderen Betriebssystem
zu einer &unix; Text Datei. Mit anderen Worten, es ändert
die verschiedenen Arten von Zeilen Begrenzungen zu der Zeilen
Begrenzungs Konvention von &unix;. Es speichert die Ausgabe in
einer anderen Datei. Optional konvertiert es eine &unix; Text
Datei zu einer <acronym>DOS</acronym> Text Datei.</para>
<para>Ich habe <application>tuc</application> sehr oft benutzt,
aber nur von irgendeinem anderen <acronym>OS</acronym> nach
&unix; zu konvertieren, niemals anders herum. Ich habe mir immer
gewünscht das die Datei einfach überschrieben wird
anstatt das ich die Ausgabe in eine andere Datei senden muss.
Meistens, habe ich diesen Befehl verwendet:</para>
<screen>&prompt.user; <userinput>tuc <replaceable>myfile tempfile</replaceable></userinput>
&prompt.user; <userinput>mv <replaceable>tempfile myfile</replaceable></userinput></screen>
<para>Es wäre schö ein <application>ftuc</application>
zu haben, also, <emphasis>fast tuc</emphasis>, und es so zu
benutzen:</para>
<screen>&prompt.user; <userinput>ftuc <replaceable>myfile</replaceable></userinput></screen>
<para>In diesem Kapitel werden wir dann, <application>ftuc
</application> in Assembler schreiben (das Original
<application>tuc</application> ist in C), und verschiedene
Datei-Orientierte Kernel Dienste in dem Prozess studieren.</para>
<para>Auf erste Sicht, ist so eine Datei Konvertierung sehr
simpel: Alles was du zu tun hast, ist die Wagenrückläufe
zu entfernen, richtig?</para>
<para>Wenn du mit ja geantwortet hast, denk nochmal darüber
nach: Dieses Vorgehen wird die meiste Zeit funktionieren
(zumindest mit <acronym>MSDOS</acronym> Text Dateien), aber
gelegentlich fehlschlagen.</para>
<para>Das Problem ist das nicht alle &unix; Text Dateien ihre
Zeilen mit einer Wagen Rücklauf / Zeilenvorschub Sequenz
beenden. Manche benutzen Wagenrücklauf ohne Zeilenvorschub.
Andere kombinieren mehrere leere Zeilen in einen einzigen
Wagenrücklauf gefolgt von mehreren Zeilenvorschüben.
Und so weiter.</para>
<para>Ein Text Datei Konvertierer muss dann also in der Lage sein
mit allen möglichen Zeilenenden umzugehen:</para>
<itemizedlist>
<listitem>
<para>Wagenrücklauf / Zeilenvorschub</para>
</listitem>
<listitem>
<para>Wagenrücklauf</para>
</listitem>
<listitem>
<para>Zeilenvorschub / Wagenrücklauf</para>
</listitem>
<listitem>
<para>Zeilenvorschub</para>
</listitem>
</itemizedlist>
<para>Es sollte außerdem in der Lage sein mit Dateien
umzugehen die irgendeine Art von Kombination der oben stehenden
Möglichkeiten verwendet. (z.B., Wagenrücklauf gefolgt
von mehreren Zeilenvorschüben).</para>
<sect2 id="x86-finite-state-machine">
<title>Endlicher Zustandsautomat</title>
<para>Das Problem wird einfach gelöst in dem man eine
Technik benutzt die sich <emphasis>Endlicher
Zustandsautomat</emphasis> nennt, ursprünglich wurde sie
von den Designern digitaler elektronischer Schaltkreise
entwickelt. Eine <emphasis>Endlicher Zustandsautomat</emphasis>
ist ein digitaler Schaltkreis dessen Ausgabe nicht nur von der
Eingabe abhängig ist sondern auch von der vorherigen
Eingabe, d.h., von seinem Status. Der Mikroprozessor ist ein
Beispiel für einen <emphasis>Endlichen Zustandsautomaten
</emphasis>: Unser Assembler Sprach Code wird zu
Maschinensprache übersetzt in der manche Assembler Sprach
Codes ein einzelnes Byte produzieren, während andere
mehrere Bytes produzieren. Da der Microprozessor die Bytes
einzeln aus dem Speicher liest, ändern manche nur seinen
Status anstatt eine Ausgabe zu produzieren. Wenn alle Bytes
eines OP Codes gelesen wurden, produziert der Mikroprozessor
eine Ausgabe, oder ändert den Wert eines Registers,
etc.</para>
<para>Aus diesem Grund, ist jede Software eigentlich nur eine
Sequenz von Status Anweisungen für den Mikroprozessor.
Dennoch, ist das Konzept eines <emphasis>Endlichen
Zustandsautomaten</emphasis> auch im Software Design sehr
hilfreich.</para>
<para>Unser Text Datei Konvertierer kann als
<emphasis>Endlicher Zustandsautomat</emphasis> mit 3
möglichen Stati desgined werden. Wir könnten diese
von 0-2 benennen, aber es wird uns das Leben leichter machen
wenn wir ihnen symbolische Namen geben:</para>
<itemizedlist>
<listitem>
<para><symbol>ordinary</symbol></para>
</listitem>
<listitem>
<para><symbol>cr</symbol></para>
</listitem>
<listitem>
<para><symbol>lf</symbol></para>
</listitem>
</itemizedlist>
<para>Unser Programm wird in dem <symbol>ordinary</symbol> Status
starten. Während dieses Status, hängt die Aktion des
Programms von seiner Eingabe wie folgt ab:</para>
<itemizedlist>
<listitem>
<para>Wenn die Eingabe etwas anderes als ein
Wagenrücklauf oder einem Zeilenvorschub ist, wird die
Eingabe einfach nur an die Ausgabe geschickt. Der Status
bleibt unverändert.</para>
</listitem>
<listitem>
<para>Wenn die Eingabe ein Wagenrücklauf ist, wird der
Status auf <symbol>cr</symbol> gesetzt. Die Eingabe wird
dann verworfen, d.h., es entsteht keine Ausgabe.</para>
</listitem>
<listitem>
<para>Wenn die Eingabe ein Zeilenvorschub ist, wird der
Status auf <symbol>lf</symbol> gesetzt. Die Eingabe wird
dann verworfen.</para>
</listitem>
</itemizedlist>
<para>Wann immer wir in dem <symbol>cr</symbol> Status sind,
ist das weil die letzte Eingabe ein Wagenrücklauf war,
welcher nicht verarbeitet wurde. Was unsere Software in
diesem Status macht hängt von der aktuellen Eingabe
ab:</para>
<itemizedlist>
<listitem>
<para>Wenn die Eingabe irgendetwas anderes als ein
Wagenrücklauf oder ein Zeilenvorschub ist, dann gib
einen Zeilenvorschub aus, dann gib die Eingabe aus und
dann ändere den Status zu
<symbol>ordinary</symbol>.</para>
</listitem>
<listitem>
<para>Wenn die Eingabe ein Wagenrücklauf ist, haben
wir zwei (oder mehr) Wagenrückläufe in einer
Reihe. Wir verwerfen die Eingabe, wir geben einen
Zeilenvorschub aus und lassen den Status
unverändert.</para>
</listitem>
<listitem>
<para>Wenn die Eingabe ein Zeilenvorschub ist, geben wir
den Zeilenvorschub aus und ändern den Status zu
<symbol>ordinary</symbol>. Achte darauf, dass das nicht
das gleiche wie in dem Fall oben drüber ist &ndash;
würden wir versuchen beide zu kombinieren,
würden wir zwei Zeilenvorschübe anstatt einen
ausgeben.</para>
</listitem>
</itemizedlist>
<para>Letztendlich, sind wir in dem <symbol>lf</symbol> Status
nachdem wir einen Zeilenvorschub empfangen haben der nicht
nach einem Wagenrücklauf kam. Das wird passieren wenn
unsere Datei bereits im &unix; Format ist, oder jedesmal wenn
mehrere Zeilen in einer Reihe durch einen einzigen
Wagenrücklauf gefolgt von mehreren Zeilenvorschüben
ausgedrückt wird, oder wenn die Zeile mit einer
Zeilenvorschub / Wagenrücklauf Sequenz endet. Wir
sollten mit unserer Eingabe in diesem Status folgendermaßen
umgehen:</para>
<itemizedlist>
<listitem>
<para>Wenn die Eingabe irgendetwas anderes als ein
Wagenrücklauf oder ein Zeilenvorschub ist, geben wir
einen Zeilenvorschub aus, geben dann die Eingabe aus und
ändern dann den Status zu <symbol>ordinary</symbol>.
Das ist exakt die gleiche Aktion wie in dem
<symbol>cr</symbol> Status nach dem Empfangen der selben
Eingabe.</para>
</listitem>
<listitem>
<para>Wenn die Eingabe ein Wagenrücklauf ist, verwerfen
wir die Eingabe, geben einen Zeilenvorschub aus und
ändern dann den Status zu <symbol>ordinary</symbol>.
</para>
</listitem>
<listitem>
<para>Wenn die Eingabe ein Zeilenvorschub ist, geben wir den
Zeilenvorschub aus und lassen den Status unverändert.
</para>
</listitem>
</itemizedlist>
<sect3 id="x86-final-state">
<title>Der Endgültige Status</title>
<para>Der obige <emphasis>Endliche Zustandsautomat</emphasis>
funktioniert für die gesamte Datei, aber lässt die
Möglichkeit das die letzte Zeile ignoriert wird. Das
wird jedesmal passieren wenn die Datei mit einem einzigen
Wagenrücklauf oder einem einzigen Zeilenvorschub endet.
Daran habe ich nicht gedacht als ich
<application>tuc</application> schrieb, nur um festzustellen,
daß das letzte Zeilenende gelegentlich weggelassen
wird.</para>
<para>Das Problem wird einfach dadurch gelöst, indem man
den Status überprüft nachdem die gesamte Datei
verarbeitet wurde. Wenn der Status nicht
<symbol>ordinary</symbol> ist, müssen wir nur den
letzten Zeilenvorschub ausgeben.</para>
<note>
<para>Nachdem wir unseren Algorithmus nun als einen
<emphasis>Endlichen Zustandsautomaten</emphasis> formuliert
haben, könnten wir einfach einen festgeschalteten
digitalen elektronischen Schaltkreis (einen "Chip")
designen, der die Umwandlung für uns übernimmt.
Natürlich wäre das sehr viel teurer, als ein
Assembler Programm zu schreiben.</para>
</note>
</sect3>
<sect3 id="x86-tuc-counter">
<title>Der Ausgabe Zähler</title>
<para>Weil unser Datei Konvertierungs Programm
möglicherweise zwei Zeichen zu einem kombiniert,
müssen wir einen Ausgabe Zähler verwenden. Wir
initialisieren den Zähler zu <constant>0</constant>
und erhöhen ihn jedes mal wenn wir ein Zeichen an die
Ausgabe schicken. Am Ende des Programms, wird der
Zähler uns sagen auf welche Grösse wir die Datei
setzen müssen.</para>
</sect3>
</sect2>
<sect2 id="x86-software-fsm">
<title>Implementieren von EZ als Software</title>
<para>Der schwerste Teil beim arbeiten mit einer
<emphasis>Endlichen Zustandsmaschine</emphasis> ist das
analysieren des Problems und dem ausdrücken als eine
<emphasis>Endliche Zustandsmaschine</emphasis>. That geschafft,
schreibt sich die Software fast wie von selbst.</para>
<para>In eine höheren Sprache, wie etwa C, gibt es mehrere
Hauptansätze. Einer wäre ein <function
role="statement">switch</function> Angabe zu verwenden die
auswählt welche Funktion genutzt werden soll. Zum
Beispiel,</para>
<programlisting>
switch (state) {
default:
case REGULAR:
regular(inputchar);
break;
case CR:
cr(inputchar);
break;
case LF:
lf(inputchar);
break;
}
</programlisting>
<para>Ein anderer Ansatz ist es ein Array von Funktions Zeigern
zu benutzen, etwa wie folgt:</para>
<programlisting>
(output[state])(inputchar);
</programlisting>
<para>Noch ein anderer ist es aus <varname>state</varname> einen
Funktions Zeiger zu machen und ihn zu der entsprechenden
Funktion zeigen zu lassen:</para>
<programlisting>
(*state)(inputchar);
</programlisting>
<para>Das ist der Ansatz den wir in unserem Programm verwenden
werden, weil es in Assembler sehr einfach und schnell geht.
Wir werden einfach die Adresse der Prozedur in <varname
role="register">EBX</varname> speichern und dann einfach das
ausgeben:</para>
<programlisting>
call ebx
</programlisting>
<para>Das ist wahrscheinlich schneller als die Adresse im Code
zu hardcoden weil der Mikroprozessor die Adresse nicht aus dem
Speicher lesen muss&mdash;es ist bereits in einer der Register
gespeichert. Ich sagte <emphasis>wahrscheinlich</emphasis>
weil durch das Cachen neuerer Mikroprozessoren beide Varianten
in etwa gleich schnell sind.</para>
</sect2>
<sect2 id="memory-mapped-files">
<title>Speicher abgebildete Dateien</title>
<para>Weil unser Programm nur mit einzelnen Dateien
funktioniert, können wir nicht den Ansatz verwedenden der
zuvor funktioniert hat, d.h., von einer Eingabe Datei zu lesen
und in eine Ausgabe Datei zu schreiben.</para>
<para>&unix; erlaubt es uns eine Datei, oder einen Bereich einer
Datei, in den Speicher abzubilden. Um das zu tun, müssen
wir zuerst eine Datei mit den entsprechenden Lese/Schreib
Flags öffnen. Dann benutzen wir den <function
role="syscall">mmap</function> system call um sie in den
Speicher abzubilden. Ein Vorteil von <function
role="syscall">mmap</function> ist, das es automatisch mit
virtuellem Speicher arbeitet: Wir können mehr von der
Datei im Speicher abbilden als wir überhaupt
physikalischen Speicher zur Verfügung haben, noch immer
haben wir aber durch normale OP Codes wie <function
role="opcode">mov</function>, <function
role="opcode">lods</function>, und <function
role="opcode">stos</function> Zugriff darauf. Egal welche
Änderungen wir an dem Speicherabbild der Datei vornehmen,
sie werden vom System in die Datei geschrieben. Wir
müssen die Datei nicht offen lassen: So lange sie
abgebildet bleibt, können wir von ihr lesen und in sie
schreiben.</para>
<para>Ein 32-bit Intel Mikroprozessor kann auf bis zu vier
Gigabyte Speicher zugreifen &ndash; physisch oder virtuell.
Das FreeBSD System erlaubt es uns bis zu der Hälfte
für die Datei Abbildung zu verwenden.</para>
<para>Zur Vereinfachung, werden wir in diesem Tutorial nur
Dateien konvertieren die in ihrere Gesamtheit im Speicher
abgebildet werden können. Es gibt wahrscheinlich nicht
all zu viele Text Dateien die eine Grösse von zwei
Gigabyte überschreiben. Falls unser Programm doch auf
eine trifft, wird es einfach eine Meldung anzeigen mit dem
Vorschlag das originale <application>tuc</application> statt
dessen zu verwenden.</para>
<para>Wenn du deine Kopie von
<filename>syscalls.master</filename> überprüfst,
wirst du zwei verschiedene Systemaufrufe
finden die sich <function role="syscall">mmap</function>
nennen. Das kommt von der Entwicklung von &unix;: Es gab das
traditionelle <acronym>BSD</acronym> <function
role="syscall">mmap</function>, Systemaufruf 71. Dieses wurde
durch das <acronym>&posix;</acronym> <function
role="syscall">mmap</function> ersetzt, Systemaufruf 197. Das
FreeBSD System unterstützt beide, weil ältere
Programme mit der originalen <acronym>BSD</acronym> Version
geschrieben wurden. Da neue Software die
<acronym>&posix;</acronym> Version nutzt, werden wir diese
auch verwenden.</para>
<para>Die <filename>syscalls.master</filename> Datei zeigt die
<acronym>&posix;</acronym> Version wie folgt:</para>
<programlisting>
197 STD BSD { caddr_t mmap(caddr_t addr, size_t len, int prot, \
int flags, int fd, long pad, off_t pos); }
</programlisting>
<para>Das weicht etwas von dem ab was
<citerefentry><refentrytitle>mmap</refentrytitle>
<manvolnum>2</manvolnum></citerefentry> sagt. Das ist weil
<citerefentry><refentrytitle>mmap</refentrytitle>
<manvolnum>2</manvolnum></citerefentry> die C Version
beschreibt.</para>
<para>Der Unterschiede liegt in dem <varname>long pad</varname>
Argument, welches in der C Version nicht vorhanden ist. Wie
auch immer, der FreeBSD Systemaufruf fügt einen 32-bit
Block ein nachdem es ein 64-Bit Argument auf den Stack
ge<function role="opcode">push</function>t hat. In diesem
Fall, ist <varname>off_t</varname> ein 64-Bit Wert.</para>
<para>Wenn wir fertig sind mit dem Arbeiten einer im Speicher
abgebildeten Datei, entfernen wir das Speicherabbild mit dem
<function role="syscall">munmap</function> Systemaufruf:</para>
<tip>
<para>Für eine detailliert Behandlung von <function
role="syscall">mmap</function>, sieh in W. Richard Stevens'
<ulink url="http://www.int80h.org/cgi-bin/isbn?isbn=0130810819">
Unix Network Programming, Volume 2, Chapter 12</ulink>
nach.</para>
</tip>
</sect2>
<sect2 id="x86-file-size">
<title>Feststellen der Datei Grösse</title>
<para>Weil wir <function role="syscall">mmap</function> sagen
müssen wie viele Bytes von Datei wir im Speicher abbilden
wollen und wir außerdem die gesamte Datei abbilden wollen,
müssen wir die Grösse der Datei feststellen.</para>
<para>Wir können den <function
role="syscall">fstat</function> Systemaufruf verwenden um alle
Informationen über eine geöffnete Datei zu erhalten
die uns das System geben kann. Das beinhaltet die Datei
Grösse.</para>
<para>Und wieder, zeigt uns <filename>syscalls.master</filename>
zwei Versionen von <function role="syscall">fstat</function>,
eine traditionelle (Systemaufruf 62), und eine
<acronym>&posix;</acronym> (Systemaufruf 189) Variante.
Natürlich, verwenden wir die <acronym>&posix;</acronym>
Version:</para>
<programlisting>
189 STD POSIX { int fstat(int fd, struct stat *sb); }
</programlisting>
<para>Das ist ein sehr unkomplizierter Aufruf: Wir
übergeben ihm die Adresse einer
<structname>stat</structname> Structure und den Deskriptor
einer geöffneten Datei. Es wird den Inhalt der
<structname>stat</structname> Struktur ausfüllen.</para>
<para>Ich muss allerdings sagen, das ich versucht habe die
<structname>stat</structname> Struktur in dem
<varname>.bss</varname> Bereich zu deklarieren, und
<function role="syscall">fstat</function> mochte es nicht:
Es setzte das Carry Flag welches einen Fehler anzeigt.
Nachdem ich den Code veränderte so dass er die Struktur
auf dem Stack anlegt, hat alles gut funktioniert.</para>
</sect2>
<sect2 id="x86-ftruncate">
<title>Ändern der Dateigrösse</title>
<para>Dadurch das unser Programm
Wagenrücklauf/Zeilenvorschub-Sequenzen in einfache
Zeilenvorschübe zusammenfassen könnte, könnte
unsere Ausgabe kleiner sein als unsere Eingabe. Und da wir die
Ausgabe in dieselbe Datei um, aus der wir unsere Eingabe
erhalten, müssen wir eventuell die Dateigrösse
anpassen.</para>
<para>Der Systemaufruf <function
role="syscall">ftruncate</function> erlaubt uns, dies zu tun.
Abgesehen von dem etwas unglücklich gewählten Namen
<function role="syscall">ftruncate</function> können wir
mit dieser Funktion eine Datei vergrössern, oder
verkleinern.</para>
<para>Und ja, wir werden zwei Versionen von <function
role="syscall">ftruncate</function> in
<filename>syscalls.master</filename> finden, eine ältere
(130) und eine neuere (201). Wir werden die neuere Version
verwenden:</para>
<programlisting>
201 STD BSD { int ftruncate(int fd, int pad, off_t length); }
</programlisting>
<para>Beachten Sie bitte, dass hier wieder <varname>int
pad</varname> verwendet wird.</para>
</sect2>
<sect2 id="x86-ftuc">
<title>ftuc</title>
<para>Wir wissen jetzt alles nötige, um
<application>ftuc</application> zu schreiben. Wir beginnen,
indem wir ein paar neue Zeilen der Datei
<filename>system.inc</filename> hinzufügen. Als erstes
definieren wir irgendwo am Anfang der Datei einige Konstanten
und Strukturen:</para>
<programlisting>
;;;;;;; open flags
%define O_RDONLY 0
%define O_WRONLY 1
%define O_RDWR 2
;;;;;;; mmap flags
%define PROT_NONE 0
%define PROT_READ 1
%define PROT_WRITE 2
%define PROT_EXEC 4
;;
%define MAP_SHARED 0001h
%define MAP_PRIVATE 0002h
;;;;;;; stat structure
struc stat
st_dev resd 1 ; = 0
st_ino resd 1 ; = 4
st_mode resw 1 ; = 8, size is 16 bits
st_nlink resw 1 ; = 10, ditto
st_uid resd 1 ; = 12
st_gid resd 1 ; = 16
st_rdev resd 1 ; = 20
st_atime resd 1 ; = 24
st_atimensec resd 1 ; = 28
st_mtime resd 1 ; = 32
st_mtimensec resd 1 ; = 36
st_ctime resd 1 ; = 40
st_ctimensec resd 1 ; = 44
st_size resd 2 ; = 48, size is 64 bits
st_blocks resd 2 ; = 56, ditto
st_blksize resd 1 ; = 64
st_flags resd 1 ; = 68
st_gen resd 1 ; = 72
st_lspare resd 1 ; = 76
st_qspare resd 4 ; = 80
endstruc
</programlisting>
<para>Wir definieren die neuen Systemaufrufe:</para>
<programlisting>
%define SYS_mmap 197
%define SYS_munmap 73
%define SYS_fstat 189
%define SYS_ftruncate 201
</programlisting>
<para>Wir fügen die Makros hinzu:</para>
<programlisting>
%macro sys.mmap 0
system SYS_mmap
%endmacro
%macro sys.munmap 0
system SYS_munmap
%endmacro
%macro sys.ftruncate 0
system SYS_ftruncate
%endmacro
%macro sys.fstat 0
system SYS_fstat
%endmacro
</programlisting>
<para>Und hier ist unser Code:</para>
<programlisting>
;;;;;;; Fast Text-to-Unix Conversion (ftuc.asm) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;
;; Started: 21-Dec-2000
;; Updated: 22-Dec-2000
;;
;; Copyright 2000 G. Adam Stanislav.
;; All rights reserved.
;;
;;;;;;; v.1 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
%include 'system.inc'
section .data
db 'Copyright 2000 G. Adam Stanislav.', 0Ah
db 'All rights reserved.', 0Ah
usg db 'Usage: ftuc filename', 0Ah
usglen equ $-usg
co db "ftuc: Can't open file.", 0Ah
colen equ $-co
fae db 'ftuc: File access error.', 0Ah
faelen equ $-fae
ftl db 'ftuc: File too long, use regular tuc instead.', 0Ah
ftllen equ $-ftl
mae db 'ftuc: Memory allocation error.', 0Ah
maelen equ $-mae
section .text
align 4
memerr:
push dword maelen
push dword mae
jmp short error
align 4
toolong:
push dword ftllen
push dword ftl
jmp short error
align 4
facerr:
push dword faelen
push dword fae
jmp short error
align 4
cantopen:
push dword colen
push dword co
jmp short error
align 4
usage:
push dword usglen
push dword usg
error:
push dword stderr
sys.write
push dword 1
sys.exit
align 4
global _start
_start:
pop eax ; argc
pop eax ; program name
pop ecx ; file to convert
jecxz usage
pop eax
or eax, eax ; Too many arguments?
jne usage
; Open the file
push dword O_RDWR
push ecx
sys.open
jc cantopen
mov ebp, eax ; Save fd
sub esp, byte stat_size
mov ebx, esp
; Find file size
push ebx
push ebp ; fd
sys.fstat
jc facerr
mov edx, [ebx + st_size + 4]
; File is too long if EDX != 0 ...
or edx, edx
jne near toolong
mov ecx, [ebx + st_size]
; ... or if it is above 2 GB
or ecx, ecx
js near toolong
; Do nothing if the file is 0 bytes in size
jecxz .quit
; Map the entire file in memory
push edx
push edx ; starting at offset 0
push edx ; pad
push ebp ; fd
push dword MAP_SHARED
push dword PROT_READ | PROT_WRITE
push ecx ; entire file size
push edx ; let system decide on the address
sys.mmap
jc near memerr
mov edi, eax
mov esi, eax
push ecx ; for SYS_munmap
push edi
; Use EBX for state machine
mov ebx, ordinary
mov ah, 0Ah
cld
.loop:
lodsb
call ebx
loop .loop
cmp ebx, ordinary
je .filesize
; Output final lf
mov al, ah
stosb
inc edx
.filesize:
; truncate file to new size
push dword 0 ; high dword
push edx ; low dword
push eax ; pad
push ebp
sys.ftruncate
; close it (ebp still pushed)
sys.close
add esp, byte 16
sys.munmap
.quit:
push dword 0
sys.exit
align 4
ordinary:
cmp al, 0Dh
je .cr
cmp al, ah
je .lf
stosb
inc edx
ret
align 4
.cr:
mov ebx, cr
ret
align 4
.lf:
mov ebx, lf
ret
align 4
cr:
cmp al, 0Dh
je .cr
cmp al, ah
je .lf
xchg al, ah
stosb
inc edx
xchg al, ah
; fall through
.lf:
stosb
inc edx
mov ebx, ordinary
ret
align 4
.cr:
mov al, ah
stosb
inc edx
ret
align 4
lf:
cmp al, ah
je .lf
cmp al, 0Dh
je .cr
xchg al, ah
stosb
inc edx
xchg al, ah
stosb
inc edx
mov ebx, ordinary
ret
align 4
.cr:
mov ebx, ordinary
mov al, ah
; fall through
.lf:
stosb
inc edx
ret
</programlisting>
<warning>
<para>Verwenden Sie dieses Programm nicht mit Dateien, die
sich auf Datenträgern befinden, welche mit
<acronym>&ms-dos;</acronym> oder &windows; formatiert
wurden. Anscheinend gibt es im Code von FreeBSD einen
subtilen Bug, wenn <function role="syscall">mmap</function>
auf solchen Datenträgern verwendet wird: Wenn die Datei
eine bestimmte Grösse überschreitet, füllt
<function role="syscall">mmap</function> den Speicher mit
lauter Nullen, und überschreibt damit anschliessend den
Dateiinhalt.</para>
</warning>
</sect2>
</sect1>
<sect1 id="x86-one-pointed-mind">
<sect1info>
<authorgroup>
<author>
<firstname>Daniel</firstname>
<surname>Seuffert</surname>
<contrib>Übersetzt von </contrib>
</author>
</authorgroup>
</sect1info>
<title>One-Pointed Mind</title>
<para>Als ein Zen-Schüler liebe ich die Idee eines
fokussierten Bewußtseins: Tu nur ein Ding zur gleichen
Zeit, aber mache es richtig.</para>
<para>Das ist ziemlich genau die gleiche Idee, welche &unix;
richtig funktionieren lässt. Während eine typische
&windows;-Applikation versucht alles Vorstellbare zu tun (und
daher mit Fehler durchsetzt ist), versucht eine
&unix;-Applikation nur eine Funktion zu erfüllen und das
gut.</para>
<para>Der typische &unix;-Nutzer stellt sich sein eigenes System
durch Shell-Skripte zusammen, die er selbst schreibt, und welche
die Vorteile bestehender Applikationen dadurch kombinieren,
indem sie die Ausgabe eines Programmes als Eingabe in ein
anderes Programm durch eine Pipe übergeben.</para>
<para>Wenn Sie ihre eigene &unix;-Software schreiben, ist es
generell eine gute Idee zu betrachten, welcher Teil der
Problemlösung durch bestehende Programme bewerkstelligt
werden kann. Man schreibt nur die Programme selbst, für die
keine vorhandene Lösung existiert.</para>
<sect2 id="x86-csv">
<title>CSV</title>
<para>Ich will dieses Prinzip an einem besonderen Beispiel
aus der realen Welt demonstrieren, mit dem ich kürzlich
konfrontiert wurde:</para>
<para>Ich mußte jeweils das elfte Feld von jedem
Datensatz aus einer Datenbank extrahieren, die ich von einer
Webseite heruntergeladen hatte. Die Datenbank war eine
<acronym>CSV</acronym>-Datei, d.h. eine Liste von
<emphasis>Komma-getrennten Werten</emphasis>. Dies ist ein
ziemlich gewöhnliches Format für den Code-Austausch
zwischen Menschen, die eine unterschiedliche
Datenbank-Software nutzen.</para>
<para>Die erste Zeile der Datei enthält eine Liste der
Felder durch Kommata getrennt. Der Rest der Datei enthält
die einzelnen Datensätze mit durch Kommata getrennten
Werten in jeder Zeile.</para>
<para>Ich versuchte <application>awk</application> unter
Nutzung des Kommas als Trenner. Da aber einige Zeilen durch in
Bindestriche gesetzte Kommata getrennt waren, extrahierte
<application>awk</application> das falsche Feld aus diesen
Zeilen.</para>
<para>Daher mußte ich meine eigene Software schreiben,
um das elfte Feld aus der <acronym>CSV</acronym>-Datei
auszulesen. Aber durch Anwendung der &unix;-Philosophie
mußte ich nur einen einfachen Filter schreiben, das
Folgende tat:</para>
<itemizedlist>
<listitem>
<para>Entferne die erste Zeile aus der Datei.</para>
</listitem>
<listitem>
<para>Ändere alle Kommata ohne Anführungszeichen
in einen anderen Buchstaben.</para>
</listitem>
<listitem>
<para>Entferne alle Anführungszeichen.</para>
</listitem>
</itemizedlist>
<para>Streng genommen könnte ich
<application>sed</application> benutzen, um die erste Zeile
der Datei zu entfernen, aber das zu Bewerkstelligen war in
meinem Programm sehr einfach, also entschloss ich mich dazu
und reduzierte dadurch die Größe der
Pipeline.</para>
<para>Unter Berücksichtigung aller Faktoren kostete mich
das Schreiben dieses Programmes ca. 20 Minuten. Das Schreiben
eines Programmes, welches jeweils das elfte Feld aus einer
<acronym>CSV</acronym>-Datei extrahiert hätte wesentlich
länger gedauert und ich hätte es nicht
wiederverwenden können, um ein anderes Feld aus irgendeiner
anderen Datenbank zu extrahieren.</para>
<para>Diesmal entschied ich mich dazu, etwas mehr Arbeit zu
investieren, als man normalerweise für ein typisches
Tutorial verwenden würde:</para>
<itemizedlist>
<listitem>
<para>Es parst die Kommandozeilen nach Optionen.</para>
</listitem>
<listitem>
<para>Es zeigt die richtige Nutzung an, falls es ein
falsches Argument findet.</para>
</listitem>
<listitem>
<para>Es gibt vernünftige Fehlermeldungen aus.</para>
</listitem>
</itemizedlist>
<para>Hier ist ein Beispiel für seine Nutzung:</para>
<screen>Usage: csv [-t&lt;delim&gt;] [-c&lt;comma&gt;] [-p] [-o &lt;outfile&gt;] [-i &lt;infile&gt;]</screen>
<para>Alle Parameter sind optional und können in beliebiger
Reihenfolge auftauchen.</para>
<para>Der <parameter>-t</parameter>-Parameter legt fest, was
zu die Kommata zu ersetzen sind. Der <constant>tab</constant>
ist die Vorgabe hierfür. Zum Beispiel wird
<parameter>-t;</parameter> alle unquotierten Kommata mit
Semikolon ersetzen.</para>
<para>Ich brauche die <parameter>-c</parameter>-Option nicht,
aber sie könnte zukünftig nützlich sein. Sie
ermöglicht mir festzulegen, daß ich einen anderen
Buchstaben als das Kommata mit etwas anderem ersetzen
möchte. Zum Beispiel wird der Parameter
<parameter>-c@</parameter> alle @-Zeichen ersetzen
(nützlich, falls man eine Liste von Email-Adressen in
Nutzername und Domain aufsplitten will).</para>
<para>Die <parameter>-p</parameter>-Option erhält die
erste Zeile, d.h. die erste Zeile der Datei wird nicht
gelöscht. Als Vorgabe löschen wir die erste Zeile,
weil die <acronym>CSV</acronym>-Datei in der ersten Zeile
keine Daten, sondern Feldbeschreibungen enthält.</para>
<para>Die Parameter <parameter>-i</parameter>- und
<parameter>-o</parameter>-Optionen erlauben es mir, die
Ausgabe- und Eingabedateien festzulegen. Vorgabe sind
<filename>stdin</filename> und <filename>stdout</filename>,
also ist es ein regulärer &unix;-Filter.</para>
<para>Ich habe sichergestellt, daß sowohl <parameter>-i
filename</parameter> und <parameter>-ifilename</parameter>
akzeptiert werden. Genauso habe ich dafür Sorge getragen,
daß sowohl Eingabe- als auch Ausgabedateien festgelegt
werden können.</para>
<para>Um das elfte Feld jeden Datensatzes zu erhalten kann ich
nun folgendes eingeben:</para>
<screen>&prompt.user; <userinput>csv '-t;' <replaceable>data.csv</replaceable> | awk '-F;' '{print $11}'</userinput></screen>
<para>Der Code speichert die Optionen (bis auf die
Dateideskriptoren) in <varname role="register">EDX</varname>:
Das Kommata in <varname role="register">DH</varname>, den
neuen Feldtrenner in <varname role="register">DL</varname> und
das Flag für die <parameter>-p</parameter>-Option in dem
höchsten Bit von <varname role="register">EDX</varname>.
Ein kurzer Abgleich des Zeichens wird uns also eine schnelle
Entscheidung darüber erlauben, was zu tun ist.</para>
<para>Hier ist der Code:</para>
<programlisting>
;;;;;;; csv.asm ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;
; Convert a comma-separated file to a something-else separated file.
;
; Started: 31-May-2001
; Updated: 1-Jun-2001
;
; Copyright (c) 2001 G. Adam Stanislav
; All rights reserved.
;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
%include 'system.inc'
%define BUFSIZE 2048
section .data
fd.in dd stdin
fd.out dd stdout
usg db 'Usage: csv [-t&lt;delim&gt;] [-c&lt;comma&gt;] [-p] [-o &lt;outfile&gt;] [-i &lt;infile&gt;]', 0Ah
usglen equ $-usg
iemsg db "csv: Can't open input file", 0Ah
iemlen equ $-iemsg
oemsg db "csv: Can't create output file", 0Ah
oemlen equ $-oemsg
section .bss
ibuffer resb BUFSIZE
obuffer resb BUFSIZE
section .text
align 4
ierr:
push dword iemlen
push dword iemsg
push dword stderr
sys.write
push dword 1 ; return failure
sys.exit
align 4
oerr:
push dword oemlen
push dword oemsg
push dword stderr
sys.write
push dword 2
sys.exit
align 4
usage:
push dword usglen
push dword usg
push dword stderr
sys.write
push dword 3
sys.exit
align 4
global _start
_start:
add esp, byte 8 ; discard argc and argv[0]
mov edx, (',' &lt;&lt; 8) | 9
.arg:
pop ecx
or ecx, ecx
je near .init ; no more arguments
; ECX contains the pointer to an argument
cmp byte [ecx], '-'
jne usage
inc ecx
mov ax, [ecx]
.o:
cmp al, 'o'
jne .i
; Make sure we are not asked for the output file twice
cmp dword [fd.out], stdout
jne usage
; Find the path to output file - it is either at [ECX+1],
; i.e., -ofile --
; or in the next argument,
; i.e., -o file
inc ecx
or ah, ah
jne .openoutput
pop ecx
jecxz usage
.openoutput:
push dword 420 ; file mode (644 octal)
push dword 0200h | 0400h | 01h
; O_CREAT | O_TRUNC | O_WRONLY
push ecx
sys.open
jc near oerr
add esp, byte 12
mov [fd.out], eax
jmp short .arg
.i:
cmp al, 'i'
jne .p
; Make sure we are not asked twice
cmp dword [fd.in], stdin
jne near usage
; Find the path to the input file
inc ecx
or ah, ah
jne .openinput
pop ecx
or ecx, ecx
je near usage
.openinput:
push dword 0 ; O_RDONLY
push ecx
sys.open
jc near ierr ; open failed
add esp, byte 8
mov [fd.in], eax
jmp .arg
.p:
cmp al, 'p'
jne .t
or ah, ah
jne near usage
or edx, 1 &lt;&lt; 31
jmp .arg
.t:
cmp al, 't' ; redefine output delimiter
jne .c
or ah, ah
je near usage
mov dl, ah
jmp .arg
.c:
cmp al, 'c'
jne near usage
or ah, ah
je near usage
mov dh, ah
jmp .arg
align 4
.init:
sub eax, eax
sub ebx, ebx
sub ecx, ecx
mov edi, obuffer
; See if we are to preserve the first line
or edx, edx
js .loop
.firstline:
; get rid of the first line
call getchar
cmp al, 0Ah
jne .firstline
.loop:
; read a byte from stdin
call getchar
; is it a comma (or whatever the user asked for)?
cmp al, dh
jne .quote
; Replace the comma with a tab (or whatever the user wants)
mov al, dl
.put:
call putchar
jmp short .loop
.quote:
cmp al, '"'
jne .put
; Print everything until you get another quote or EOL. If it
; is a quote, skip it. If it is EOL, print it.
.qloop:
call getchar
cmp al, '"'
je .loop
cmp al, 0Ah
je .put
call putchar
jmp short .qloop
align 4
getchar:
or ebx, ebx
jne .fetch
call read
.fetch:
lodsb
dec ebx
ret
read:
jecxz .read
call write
.read:
push dword BUFSIZE
mov esi, ibuffer
push esi
push dword [fd.in]
sys.read
add esp, byte 12
mov ebx, eax
or eax, eax
je .done
sub eax, eax
ret
align 4
.done:
call write ; flush output buffer
; close files
push dword [fd.in]
sys.close
push dword [fd.out]
sys.close
; return success
push dword 0
sys.exit
align 4
putchar:
stosb
inc ecx
cmp ecx, BUFSIZE
je write
ret
align 4
write:
jecxz .ret ; nothing to write
sub edi, ecx ; start of buffer
push ecx
push edi
push dword [fd.out]
sys.write
add esp, byte 12
sub eax, eax
sub ecx, ecx ; buffer is empty now
.ret:
ret</programlisting>
<para>Vieles daraus ist aus <filename>hex.asm</filename>
entnommen worden. Aber es gibt einen wichtigen Unterschied:
Ich rufe nicht länger <function>write</function> auf,
wann immer ich eine Zeilenvorschub ausgebe. Nun kann der Code
sogar interaktiv genutzt werden.</para>
<para>Ich habe eine bessere Lösung gefunden für das
Interaktivitätsproblem seit ich mit dem Schreiben dieses
Kapitels begonnen habe. Ich wollte sichergehen, daß jede
Zeile einzeln ausgegeben werden kann, falls erforderlich. Aber
schlussendlich gibt es keinen Bedarf jede Zeile einzeln
auszugeben, falls nicht-interaktiv genutzt.</para>
<para>Die neue Lösung besteht darin, die Funktion
<function>write</function> jedesmal aufzurufen, wenn ich den
Eingabepuffer leer vorfinde. Auf diesem Wege liest das
Programm im interaktiven Modus eine Zeile aus der Tastatur des
Nutzers, verarbeitet sie und stellt fest, ob deren
Eingabepuffer leer ist, dann leert es seine Ausgabe und liest
die nächste Zeile.</para>
<sect3 id="x86-buffered-dark-side">
<title>Die dunkle Seite des Buffering</title>
<para>Diese Änderung verhindert einen mysteriösen
Aufhänger in einem speziellen Fall. Ich bezeichne dies
als die <emphasis>dunkle Seite des Buffering</emphasis>,
hauptsächlich, weil es eine nicht offensichtliche
Gefahr darstellt.</para>
<para>Es ist unwahrscheinlich, daß dies mit dem
<application>csv</application>-Programm oben geschieht aber
lassen Sie uns einen weiteren Filter betrachten: Nehmen wir
an ihre Eingabe sind rohe Daten, die Farbwerte darstellen,
wie z.B. die Intensität eines Pixel mit den Farben
<emphasis>rot</emphasis>, <emphasis>grün</emphasis> und
<emphasis>blau</emphasis>. Unsere Ausgabe wird der negative
Wert unserer Eingabe sein.</para>
<para>Solch ein Filter würde sehr einfach zu schreiben
sein. Der größte Teil davon würde so
aussehen wie all die anderen Filter, die wir bisher
geschrieben haben, daher beziehe ich mich nur auf den Kern
der Prozedur:</para>
<programlisting>.loop:
call getchar
not al ; Create a negative
call putchar
jmp short .loop</programlisting>
<para>Da dieser Filter mit rohen Daten arbeitet ist es
unwahrscheinlich, daß er interaktiv genutzt werden
wird.</para>
<para>Aber das Programm könnte als
Bildbearbeitungssoftware tituliert werden. Wenn es nicht
<function>write</function> vor jedem Aufruf von
<function>read</function> durchführt, ist die
Möglichkeit gegeben, das es sich aufhängt.</para>
<para>Dies könnte passieren:</para>
<procedure>
<step>
<para>Der Bildeditor wird unseren Filter laden mittels der
C-Funktion <function>popen()</function>.</para>
</step>
<step>
<para>Er wird die erste Zeile von Pixeln laden aus einer
Bitmap oder Pixmap.</para>
</step>
<step>
<para>Er wird die erste Zeile von Pixeln geschrieben in
die <emphasis>Pipe</emphasis>, welche zur Variable
<varname>fd.in</varname> unseres Filters
führt.</para>
</step>
<step>
<para>Unser Filter wird jeden Pixel auslesen von der
Eingabe, in in seinen negativen Wert umkehren und ihn in
den Ausgabepuffer schreiben.</para>
</step>
<step>
<para>Unser Filter wird die Funktion
<function>getchar</function> aufrufen, um das
nächste Pixel abzurufen.</para>
</step>
<step>
<para>Die Funktion <function>getchar</function> wird einen
leeren Eingabepuffer vorfinden und daher die Funktion
<function>read</function> aufrufen.</para>
</step>
<step>
<para><function>read</function> wird den Systemaufruf
<function role="syscall">SYS_read</function>
starten.</para>
</step>
<step>
<para>Der <emphasis>Kernel</emphasis> wird unseren Filter
unterbrechen, bis der Bildeditor mehr Daten zur Pipe
sendet.</para>
</step>
<step>
<para>Der Bildedior wird aus der anderen Pipe lesen,
welche verbunden ist mit <varname>fd.out</varname>
unseres Filters, damit er die erste Zeile des
auszugebenden Bildes setzen kann
<emphasis>bevor</emphasis> er uns die zweite Zeile der
Eingabe einliest.</para>
</step>
<step>
<para>Der <emphasis>Kernel</emphasis> unterbricht den
Bildeditor, bis er eine Ausgabe unseres Filters
erhält, um ihn an den Bildeditor
weiterzureichen.</para>
</step>
</procedure>
<para>An diesem Punkt wartet unser Filter auf den
Bildeditor, daß er ihm mehr Daten zur Verarbeitung
schicken möge. Gleichzeitig wartet der Bildeditor
darauf, daß unser Filter das Resultat der Berechnung
ersten Zeile sendet. Aber das Ergebnis sitzt in unserem
Ausgabepuffer.</para>
<para>Der Filter und der Bildeditor werden fortfahren bis in
die Ewigkeit aufeinander zu warten (oder zumindest bis sie
per kill entsorgt werden). Unsere Software hat den eine
<link linkend="secure-race-conditions">Race Condition</link>
erreicht.</para>
<para>Das Problem tritt nicht auf, wenn unser Filter seinen
Ausgabepuffer leert <emphasis>bevor</emphasis> er vom
<emphasis>Kernel</emphasis> mehr Eingabedaten
anfordert.</para>
</sect3>
</sect2>
</sect1>
<sect1 id="x86-fpu">
<sect1info>
<authorgroup>
<author>
<firstname>Fabian</firstname>
<surname>Borschel</surname>
<contrib>Übersetzt von </contrib>
</author>
</authorgroup>
</sect1info>
<title>Die <acronym>FPU</acronym> verwenden</title>
<para>Seltsamerweise erwähnt die meiste Literatur zu
Assemblersprachen nicht einmal die Existenz der
<acronym>FPU</acronym>, oder
<emphasis>floating point unit</emphasis>
(Fließkomma-Recheneinheit), geschweige denn, daß
auf die Programmierung mit dieser eingegangen wird.</para>
<para>Dabei kann die Assemblerprogrammierung gerade bei
hoch optimiertem <acronym>FPU</acronym>-Code, der
<emphasis>nur</emphasis> mit einer Assemblersprache realisiert
werden kann, ihre große Stärke ausspielen.</para>
<sect2 id="x86-fpu-organization">
<title>Organisation der <acronym>FPU</acronym></title>
<para>Die <acronym>FPU</acronym> besteht aus 8 80&ndash;bit
Fließkomma-Registern. Diese sind in Form eines
Stacks organisiert&mdash;Sie können einen Wert durch
den Befehl <function>push</function> auf dem
<acronym>TOS</acronym> (<emphasis>top of stack</emphasis>)
ablegen, oder durch <function>pop</function> von diesem
holen.</para>
<para>Da also die Befehle <function role="opcode">push</function>
und <function role="opcode">pop</function> schon verwendet
werden, kann es keine op-Codes in Assemblersprache mit diesen
Namen geben.</para>
<para>Sie können mit einen Wert auf dem
<acronym>TOS</acronym> ablegen, indem Sie
<function role="opcode">fld</function>, <function
role="opcode">fild</function>, und <function
role="opcode">fbld</function> verwenden. Mit weiteren op-Codes
lassen sich <emphasis>Konstanten</emphasis>&mdash;wie z.B.
<emphasis>Pi</emphasis>&mdash;auf dem <acronym>TOS</acronym>
ablegen.</para>
<para>Analog dazu können Sie einen Wert
holen, indem Sie <function role="opcode">fst</function>,
<function role="opcode">fstp</function>, <function
role="opcode">fist</function>, <function
role="opcode">fistp</function>, und <function
role="opcode">fbstp</function> verwenden. Eigentlich
holen (<function>pop</function>) nur die op-Codes, die auf
<emphasis>p</emphasis> enden, einen Wert, während die
anderen den Wert irgendwo speichern (<function>store</function>)
ohne ihn vom <acronym>TOS</acronym> zu entfernen.</para>
<para>Daten können zwischen dem <acronym>TOS</acronym>
und dem Hauptspeicher als 32&ndash;bit, 64&ndash;bit oder
80&ndash;bit <emphasis>real</emphasis>, oder als 16&ndash;bit,
32&ndash;bit oder 64&ndash;bit <emphasis>Integer</emphasis>,
oder als 80&ndash;bit <emphasis>packed decimal</emphasis>
übertragen werden.</para>
<para>Das 80&ndash;bit <emphasis>packed decimal</emphasis>-Format
ist ein Spezialfall des
<emphasis>binary coded decimal</emphasis>-Formates,
welches üblicherweise bei der Konvertierung zwischen der
<acronym>ASCII</acronym>- und
<acronym>FPU</acronym>-Darstellung von Daten verwendet wird.
Dieses erlaubt die Verwendung von 18 signifikanten
Stellen.</para>
<para>Unabhängig davon, wie Daten im Speicher dargestellt
werden, speichert die <acronym>FPU</acronym> ihre Daten immer
im 80&ndash;bit <emphasis>real</emphasis>-Format in den
Registern.</para>
<para>Ihre interne Genauigkeit beträgt mindestens 19
Dezimalstellen. Selbst wenn wir also Ergebnisse im
<acronym>ASCII</acronym>-Format mit voller 18&ndash;stelliger
Genauigkeit darstellen lassen, werden immer noch korrekte
Werte angezeigt.</para>
<para>Des weiteren können mathematische Operationen auf
dem <acronym>TOS</acronym> ausgeführt werden: Wir
können dessen <emphasis>Sinus</emphasis> berechnen, wir
können ihn <emphasis>skalieren</emphasis> (z.B.
können wir ihn mit dem Faktor 2 Multiplizieren oder
Dividieren), wir können dessen
<emphasis>Logarithmus</emphasis> zur Basis 2 nehmen, und viele
weitere Dinge.</para>
<para>Wir können auch <acronym>FPU</acronym>-Register
<emphasis>multiplizieren</emphasis>,
<emphasis>dividieren</emphasis>, <emphasis>addieren</emphasis>
und <emphasis>subtrahieren</emphasis>, sogar einzelne
Register mit sich selbst.</para>
<para>Der offizielle Intel op-Code für den
<acronym>TOS</acronym> ist <varname
role="register">st</varname> und für die
<emphasis>Register</emphasis> <varname
role="register">st(0)</varname>&ndash; <varname
role="register">st(7)</varname>. <varname
role="register">st</varname> und <varname
role="register">st(0)</varname> beziehen sich dabei auf das
gleiche Register.</para>
<para>Aus welchen Gründen auch immer hat sich der
Originalautor von <application>nasm</application> dafür
entschieden, andere op-Codes zu verwenden, nämlich
<varname role="register">st0</varname>&ndash; <varname
role="register">st7</varname>. Mit anderen Worten, es gibt
keine Klammern, und der <acronym>TOS</acronym> ist immer
<varname role="register">st0</varname>, niemals einfach nur
<function role="opcode">st</function>.</para>
<sect3 id="x86-fpu-packed-decimal">
<title>Das Packed Decimal-Format</title>
<para>Das <emphasis>packed decimal</emphasis>-Format verwendet
10 Bytes (80 Bits) zur Darstellung von 18 Ziffern. Die so
dargestellte Zahl ist immer ein
<emphasis>Integer</emphasis>.</para>
<tip>
<para>Sie können durch Multiplikation des
<acronym>TOS</acronym> mit Potenzen von 10 die einzelnen
Dezimalstellen verschieben.</para>
</tip>
<para>Das höchste Bit des höchsten Bytes (Byte 9)
ist das <emphasis>Vorzeichenbit</emphasis>:
Wenn es gesetzt ist, ist die Zahl
<emphasis>negativ</emphasis>, ansonsten
<emphasis>positiv</emphasis>. Die restlichen Bits dieses
Bytes werden nicht verwendet bzw. ignoriert.</para>
<para>Die restlichen 9 Bytes enthalten die 18 Ziffern der
gespeicherten Zahl: 2 Ziffern pro Byte.</para>
<para>Die <emphasis>signifikantere Ziffer</emphasis> wird in
der <emphasis>oberen Hälfte</emphasis> (4 Bits) eines
Bytes gespeichert, die andere in der
<emphasis>unteren Hälfte</emphasis>.</para>
<para>Vielleicht würden Sie jetzt annehmen, das
<constant>-1234567</constant> auf die folgende Art im
Speicher abgelegt wird (in hexadezimaler Notation):</para>
<programlisting>80 00 00 00 00 00 01 23 45 67</programlisting>
<para>Dem ist aber nicht so! Bei Intel werden alle Daten im
<emphasis>little&ndash;endian</emphasis>-Format gespeichert,
auch das <emphasis>packed decimal</emphasis>-Format.</para>
<para>Dies bedeutet, daß <constant>-1234567</constant>
wie folgt gespeichert wird:</para>
<programlisting>67 45 23 01 00 00 00 00 00 80</programlisting>
<para>Erinnern Sie sich an diesen Umstand, bevor Sie sich
aus lauter Verzweiflung die Haare
ausreißen.</para>
<note>
<para>Das lesenswerte Buch&mdash;falls Sie es finden
können&mdash;ist Richard Startz' <ulink
url="http://www.int80h.org/cgi-bin/isbn?isbn=013246604X">
8087/80287/80387 for the IBM PC &amp; Compatibles</ulink>.
Obwohl es anscheinend die Speicherung der <emphasis>packed
decimal</emphasis> im little&ndash;endian-Format für
gegeben annimmt. Ich mache keine Witze über meine
Verzweiflung, als ich den Fehler im unten stehenden Filter
gesucht habe, <emphasis>bevor</emphasis> mir einfiel,
daß ich einfach mal versuchen sollte, das
little&ndash;endian-Format, selbst für diesen Typ von
Daten, anzuwenden.</para>
</note>
</sect3>
</sect2>
<sect2 id="x86-pinhole-photography">
<title>Ausflug in die Lochblendenphotographie</title>
<para>Um sinnvolle Programme zu schreiben, müssen wir nicht
nur unsere Programmierwerkzeuge beherrschen, sondern auch das
Umfeld, für das die Programme gedacht sind.</para>
<para>Unser nächster Filter wird uns dabei helfen, wann
immer wir wollen, eine <emphasis>Lochkamera</emphasis> zu
bauen. Wir brauchen also etwas Hintergrundwissen über
die <emphasis>Lochblendenphotographie</emphasis>, bevor wir
weiter machen können.</para>
<sect3 id="x86-camera">
<title>Die Kamera</title>
<para>Die einfachste Form, eine Kamera zu beschreiben, ist
die eines abgeschlossenen, lichtundurchlässigen
Raumes, in dessen Abdeckung sich ein kleines Loch
befindet.</para>
<para>Die Abdeckung ist normalerweise fest (z.B. eine
Schachtel), manchmal jedoch auch flexibel (z.B. ein Balgen).
Innerhalb der Kamera ist es sehr dunkel. Nur durch ein
kleines Loch kann Licht von einem einzigen Punkt aus in den
Raum eindringen (in manchen Fällen sind es mehrere
Löcher). Diese Lichtstrahlen kommen von einem Bild,
einer Darstellung von dem was sich außerhalb der
Kamera, vor dem kleinen Loch, befindet.</para>
<para>Wenn ein lichtempfindliches Material (wie z.B. ein Film)
in der Kamera angebracht wird, so kann dieses das Bild
einfangen.</para>
<para>Das Loch enthält häufig eine
<emphasis>Linse</emphasis>, oder etwas linsenartiges,
häufig auch einfach <emphasis>Objektiv</emphasis>
genannt.</para>
</sect3>
<sect3 id="x86-the-pinhole">
<title>Die Lochblende</title>
<para>Streng genommen ist die Linse nicht notwendig: Die
ursprünglichen Kameras verwendeten keine Linse, sondern
eine <emphasis>Lochblende</emphasis>. Selbst heutzutage
werden noch <emphasis>Lochblenden</emphasis> verwendet,
zum einen, um die Funktionsweise einer Kamera zu erlernen,
und zum anderen, um eine spezielle Art von Bildern zu
erzeugen.</para>
<para>Das Bild, das von einer <emphasis>Lochblende</emphasis>
erzeugt wird, ist überall scharf. Oder unscharf. Es
gibt eine ideale Größe für eine Lochblende:
Wenn sie größer oder kleiner ist, verliert das
Bild seine Schärfe.</para>
</sect3>
<sect3 id="x86-focal-length">
<title>Brennweite</title>
<para>Dieser ideale Lochblendendurchmesser ist eine Funktion
der Quadratwurzel der <emphasis>Brennweite</emphasis>,
welche dem Abstand der Lochblende von dem Film
entspricht.</para>
<programlisting> D = PC * sqrt(FL)</programlisting>
<para>Hier ist <varname>D</varname> der ideale Durchmesser der
Lochblende, <varname>FL</varname> die Brennweite und
<constant>PC</constant> eine Konstante der Brennweite. Nach
Jay Bender hat die Konstante den Wert
<constant>0.04</constant>, nach Kenneth Connors
<constant>0.037</constant>. Andere Leute
haben andere Werte vorgeschlagen. Des weiteren gelten diese
Werte nur für Tageslicht: Andere Arten von
Licht benötigen andere konstante Werte, welche nur
durch Experimente bestimmt werden können.</para>
</sect3>
<sect3 id="x86-f-number">
<title>Der f&ndash;Wert</title>
<para>Der f&ndash;Wert ist eine sehr nützliche
Größe, die angibt, wieviel Licht den Film erreicht.
Ein Belichtungsmesser kann dies messen, um z.B. für
einen Film mit einer Empfindlichkeit von f5.6 eine
Belichtungsdauer von 1/1000 Sekunden auszurechnen.</para>
<para>Es spielt keine Rolle, ob es eine 35&ndash;mm- oder
eine 6x9cm-Kamera ist, usw. Solange wir den f&ndash;Wert
kennen, können wir die benötigte Belichtungszeit
berechnen.</para>
<para>Der f&ndash;Wert läßt sich einfach wie folgt
berechnen:</para>
<programlisting> F = FL / D</programlisting>
<para>Mit anderen Worten, der f&ndash;Wert ergibt sich aus
der Brennweite (FL), dividiert durch den Durchmesser (D) der
Lochblende. Ein großer f&ndash;Wert impliziert also
entweder eine kleine Lochblende, oder eine große
Brennweite, oder beides. Je größer also der
f&ndash;Wert ist, um so länger muß die
Belichtungszeit sein.</para>
<para>Des weiteren sind der Lochblendendurchmesser und die
Brennweite eindimensionale Meßgrößen,
während der Film und die Lochblende an sich
zweidimensionale Objekte darstellen. Das bedeutet, wenn
man für einen f&ndash;Wert <varname>A</varname>
eine Belichtungsdauer <varname>t</varname> bestimmt hat,
dann ergibt sich daraus für einen f&ndash;Wert
<varname>B</varname> eine Belichtungszeit von:</para>
<programlisting> t * (B / A)&#178;</programlisting>
</sect3>
<sect3 id="x86-normalized-f-number">
<title>Normalisierte f&ndash;Werte</title>
<para>Während heutige moderne Kameras den Durchmesser
der Lochblende, und damit deren f&ndash;Wert, weich und
schrittweise verändern können, war dies
früher nicht der Fall.</para>
<para>Um unterschiedliche f&ndash;Werte einstellen zu
können, besaßen Kameras typischerweise eine
Metallplatte mit Löchern unterschiedlichen
Durchmessers als Lochblende.</para>
<para>Die Durchmesser wurden entsprechend obiger Formel
gewählt, daß der resultierende f&ndash;Wert
ein fester Standardwert war, der für alle Kameras
verwendet wurde. Z.B. hat eine sehr alte Kodak Duaflex
IV Kamera in meinem Besitz drei solche Löcher
für die f&ndash;Werte 8, 11 und 16.</para>
<para>Eine neuere Kamera könnte f&ndash;Werte wie 2.8,
4, 5.6, 8, 11, 16, 22, und 32 (und weitere) besitzen.
Diese Werte wurden nicht zufällig ausgewählt:
Sie sind alle vielfache der Quadratwurzel aus 2, wobei
manche Werte gerundet wurden.</para>
</sect3>
<sect3 id="x86-f-stop">
<title>Der f&ndash;Stopp</title>
<para>Eine typische Kamera ist so konzipiert, daß die
Nummernscheibe bei den normalisierten f&ndash;Werten
einrastet. Die Nummernscheibe <emphasis>stoppt</emphasis>
an diesen Positionen. Daher werden diese Positionen auch
f&ndash;Stopps genannt.</para>
<para>Da die f&ndash;Werte bei jedem Stopp vielfache der
Quadratwurzel aus 2 sind, verdoppelt die Drehung der
Nummernscheibe um einen Stopp die für die gleiche
Belichtung benötigte Lichtmenge. Eine Drehung
um 2 Stopps vervierfacht die benötigte Belichtungszeit.
Eine Drehung um 3 Stopps verachtfacht sie, etc.</para>
</sect3>
</sect2>
<sect2 id="x86-pinhole-software">
<title>Entwurf der Lochblenden-Software</title>
<para>Wir können jetzt festlegen, was genau unsere
Lochblenden-Software tun soll.</para>
<sect3 id="xpinhole-processing-input">
<title>Verarbeitung der Programmeingaben</title>
<para>Da der Hauptzweck des Programms darin besteht, uns
beim Entwurf einer funktionierenden Lochkamera zu
helfen, wird die <emphasis>Brennweite</emphasis>
die Programmeingabe sein. Dies ist etwas, das wir ohne
zusätzliche Programme feststellen können:
Die geeignete Brennweite ergibt sich aus der
Größe des Films und der Art des Fotos, ob
dieses ein "normales" Bild, ein Weitwinkelbild oder
ein Telebild sein soll.</para>
<para>Die meisten bisher geschriebenen Programme arbeiteten
mit einzelnen Zeichen, oder Bytes, als Eingabe: Das
<application>hex</application>-Programm konvertierte
einzelne Bytes in hexadezimale Werte, das
<application>csv</application>-Programm ließ entweder
einzelne Zeichen unverändert, löschte oder
veränderte sie, etc.</para>
<para>Das Programm <application>ftuc</application> verwendete
einen Zustandsautomaten, um höchstens zwei gleichzeitig
eingegebene Bytes zu verarbeiten.</para>
<para>Das <application>pinhole</application>-Programm dagegen
kann nicht nur mit einzelnen Zeichen arbeiten, sondern
muß mit größeren syntaktischen Einheiten
zurrecht kommen.</para>
<para>Wenn wir z.B. möchten, daß unser Programm den
Lochblendendurchmesser (und weitere Werte, die wir
später noch diskutieren werden) für die Brennweiten
<constant>100 mm</constant>, <constant>150 mm</constant> und
<constant>210 mm</constant> berechnet, wollen wir etwa
folgendes eingeben:</para>
<screen><userinput>100, 150, 210</userinput></screen>
<para>Unser Programm muß mit der gleichzeitigen Eingabe
von mehr als nur einem einzelnen Byte zurecht kommen. Wenn
es eine <constant>1</constant> erkennt, muß es wissen,
daß dies die erste Stelle einer dezimalen Zahl ist.
Wenn es eine <constant>0</constant>, gefolgt von einer
weiteren <constant>0</constant> sieht, muß es wissen,
daß dies zwei unterschiedliche Stellen mit der
gleichen Zahl sind.</para>
<para>Wenn es auf das erste Komma trifft, muß es wissen,
daß die folgenden Stellen nicht mehr zur ersten
Zahl gehören. Es muß die Stellen der ersten
Zahl in den Wert <constant>100</constant> konvertieren
können. Und die Stellen der zweiten Zahl müssen
in den Wert <constant>150</constant> konvertiert werden.
Und die Stellen der dritten Zahl müssen in den Wert
<constant>210</constant> konvertiert werden.</para>
<para>Wir müssen festlegen, welche Trennsymbole
zulässig sind: Sollen die Eingabewerte durch Kommas
voneinander getrennt werden? Wenn ja, wie sollen zwei
Zahlen behandelt werden, die durch ein anderes Zeichen
getrennt sind?</para>
<para>Ich persönlich mag es einfach. Entweder etwas ist
eine Zahl, dann wird es verarbeitet, oder es ist keine
Zahl, dann wird es verworfen. Ich mag es nicht, wenn sich der
Computer bei der <emphasis>offensichtlichen</emphasis>
Eingabe eines zusätzlichen Zeichens beschwert.
Duh!</para>
<para>Zusätzlich erlaubt es mir, die Monotonie des
Tippens zu durchbrechen, und eine Anfrage anstelle einer
simplen Zahl zu stellen:</para>
<screen><userinput>Was ist der beste Lochblendendurchmesser
bei einer Brennweite von 150?</userinput></screen>
<para>Es gibt keinen Grund dafür, die Ausgabe mehrerer
Fehlermeldungen aufzuteilen:</para>
<screen>Syntax error: Was
Syntax error: ist
Syntax error: der
Syntax error: beste</screen>
<para>Et cetera, et cetera, et cetera.</para>
<para>Zweitens mag ich das <constant>#</constant>-Zeichen, um
Kommentare zu markieren, die ab dem Zeichen bis zum Ende der
jeweiligen Zeile gehen. Dies verlangt nicht viel
Programmieraufwand, und ermöglicht es mir, Eingabedateien
für meine Programme als ausführbare Skripte zu
handhaben.</para>
<para>In unserem Fall müssen wir auch entscheiden, in
welchen Einheiten die Dateneingabe erfolgen soll: Wir
wählen <emphasis>Millimeter</emphasis>, da die meisten
Photographen die Brennweite in dieser Einheit messen.</para>
<para>Letztendlich müssen wir noch entscheiden, ob wir die
Verwendung des dezimalen Punktes erlauben (in diesem Fall
müssen wir berücksichtigen, daß in vielen
Ländern der Welt das dezimale <emphasis>Komma</emphasis>
verwendet wird).</para>
<para>In unserem Fall würde das Zulassen eines dezimalen
Punktes/Kommas zu einer fälschlicherweise angenommenen,
höheren Genauigkeit führen: Der Unterschied
zwischen den Brennweiten <constant>50</constant> und
<constant>51</constant> ist fast nicht wahrnehmbar. Die
Zulassung von Eingaben wie <constant>50.5</constant> ist
also keine gute Idee. Beachten Sie bitte, das dies meine
Meinung ist. In diesem Fall bin ich der Autor des Programmes.
Bei Ihren eigenen Programmen müssen Sie selbst solche
Entscheidungen treffen.</para>
</sect3>
<sect3 id="x86-pinhole-options">
<title>Optionen anbieten</title>
<para>Das wichtigste, was wir zum Bau einer Lochkamera
wissen müssen, ist der Durchmesser der Lochblende. Da
wir scharfe Bilder schießen wollen, werden wir obige
Formel für die Berechnung des korrekten Durchmessers zu
gegebener Brennweite verwenden. Da Experten mehrere
Werte für die <constant>PC</constant>-Konstante
anbieten, müssen wir uns hier für einen Wert
entscheiden.</para>
<para>In der Programmierung unter &unix; ist es üblich,
zwei Hauptvarianten anzubieten, um Parameter an Programme zu
übergeben, und des weiteren eine Standardeinstellung
für den Fall zu haben, das der Benutzer gar keine
Parameter angibt.</para>
<para>Warum zwei Varianten, Parameter anzugeben?</para>
<para>Ein Grund ist, eine (relativ) <emphasis>feste</emphasis>
Einstellung anzubieten, die automatisch bei jedem
Programmaufruf verwendet wird, ohne das wir diese
Einstellung immer und immer wieder mit angeben
müssen.</para>
<para>Die feste Einstellung kann in einer Konfigurationsdatei
gespeichert sein, typischerweise im Heimatverzeichnis des
Benutzers. Die Datei hat üblicherweise denselben Namen
wie das zugehörige Programm, beginnt jedoch mit einem
Punkt. Häufig wird <emphasis>"rc"</emphasis> dem
Dateinamen hinzugefügt. Unsere Konfigurationsdatei
könnte also <filename>~/.pinhole</filename> oder
<filename>~/.pinholerc</filename> heißen. (Die
Zeichenfolge <filename>~/</filename> steht für das
Heimatverzeichnis des aktuellen Benutzers.)</para>
<para>Konfigurationsdateien werden häufig von Programmen
verwendet, die viele konfigurierbare Parameter besitzen.
Programme, die nur eine (oder wenige) Parameter anbieten,
verwenden häufig eine andere Methode: Sie erwarten die
Parameter in einer <emphasis>Umgebungsvariablen</emphasis>.
In unserem Fall könnten wir eine Umgebungsvariable mit
dem Namen <varname>PINHOLE</varname> benutzen.</para>
<para>Normalerweise verwendet ein Programm entweder die eine,
oder die andere der beiden obigen Methoden. Ansonsten
könnte ein Programm verwirrt werden, wenn eine
Konfigurationsdatei das eine sagt, die Umgebungsvariable
jedoch etwas anderes.</para>
<para>Da wir nur <emphasis>einen</emphasis> Parameter
unterstützen müssen, verwenden wir die zweite
Methode, und benutzen eine Umgebungsvariable mit dem
Namen <varname>PINHOLE</varname>.</para>
<para>Der andere Weg erlaubt uns, <emphasis>ad hoc</emphasis>
Entscheidungen zu treffen: <emphasis>"Obwohl ich
normalerweise einen Wert von 0.039 verwende, will ich dieses
eine Mal einen Wert von 0.03872 anwenden."</emphasis>
Mit anderen Worten, dies erlaubt uns, die Standardeinstellung
außer Kraft zu setzen.</para>
<para>Diese Art der Auswahl wird häufig über
Kommandozeilenparameter gemacht.</para>
<para>Schließlich braucht ein Programm
<emphasis>immer</emphasis> eine
<emphasis>Standardeinstellung</emphasis>. Der Benutzer
könnte keine Parameter angeben. Vielleicht weiß
er auch gar nicht, was er einstellen sollte. Vielleicht will
er es "einfach nur ausprobieren". Vorzugsweise wird die
Standardeinstellung eine sein, die die meisten Benutzer
sowieso wählen würden. Somit müssen diese
keine zusätzlichen Parameter angeben, bzw. können
die Standardeinstellung ohne zusätzlichen Aufwand
benutzen.</para>
<para>Bei diesem System könnte das Programm
widersprüchliche Optionen vorfinden, und auf die
folgende Weise reagieren:</para>
<procedure>
<step>
<para>Wenn es eine <emphasis>ad
hoc</emphasis>-Einstellung vorfindet (z.B. ein
Kommandozeilenparameter), dann sollte es diese
Einstellung annehmen. Es muß alle vorher
festgelegten sowie die standardmäßige
Einstellung ignorieren.</para>
</step>
<step>
<para><emphasis>Andererseits</emphasis>, wenn es eine
festgelegte Option (z.B. eine Umgebungsvariable)
vorfindet, dann sollte es diese akzeptieren und die
Standardeinstellung ignorieren.</para>
</step>
<step>
<para><emphasis>Ansonsten</emphasis> sollte es die
Standardeinstellung verwenden.</para>
</step>
</procedure>
<para>Wir müssen auch entscheiden, welches
<emphasis>Format</emphasis> unsere
<constant>PC</constant>-Option haben soll.</para>
<para>Auf den ersten Blick scheint es einleuchtend, das
Format <varname>PINHOLE=0.04</varname> für die
Umgebungsvariable, und <parameter>-p0.04</parameter>
für die Kommandozeile zu verwenden.</para>
<para>Dies zuzulassen wäre eigentlich eine
Sicherheitslücke. Die <constant>PC</constant>-Konstante
ist eine sehr kleine Zahl. Daher würden wir unsere
Anwendung mit verschiedenen, kleinen Werten für
<constant>PC</constant> testen. Aber was würde
passieren, wenn jemand das Programm mit einem sehr
großen Wert aufrufen würde?</para>
<para>Es könnte abstürzen, weil wir das Programm
nicht für den Umgang mit großen Werten
entworfen haben.</para>
<para>Oder wir investieren noch weiter Zeit in das Programm,
so daß dieses dann auch mit großen Zahlen
umgehen kann. Wir könnten dies machen, wenn wir
kommerzielle Software für computertechnisch
unerfahrene Benutzer schreiben würden.</para>
<para>Oder wir könnten auch sagen <emphasis>"Pech gehabt!
Der Benutzer sollte es besser wissen."</emphasis></para>
<para>Oder wir könnten es für den Benutzer
unmöglich machen, große Zahlen einzugeben. Dies
ist die Variante, die wir verwenden werden: Wir nehmen einen
<emphasis>impliziten 0.</emphasis>-Präfix an.</para>
<para>Mit anderen Worten, wenn der Benutzer den Wert
<constant>0.04</constant> angeben will, so muß er
entweder <parameter>-p04</parameter> als Parameter angeben,
oder <varname>PINHOLE=04</varname> als Variable in seiner
Umgebung definieren. Falls der Benutzer
<parameter>-p9999999</parameter> angibt, so wird dies als
<constant>0.9999999</constant> interpretiert&mdash;zwar
immer noch sinnlos, aber zumindest sicher.</para>
<para>Zweitens werden viele Benutzer einfach die Konstanten
von Bender oder Connors benutzen wollen. Um es diesen
Benutzern einfacher zu machen, werden wir
<parameter>-b</parameter> als <parameter>-p04</parameter>,
und <parameter>-c</parameter> als
<parameter>-p037</parameter> interpretieren.</para>
</sect3>
<sect3 id="x86-pinhole-output">
<title>Die Ausgabe</title>
<para>Wir müssen festlegen, was und in welchem Format
unsere Anwendung Daten ausgeben soll.</para>
<para>Da wir als Eingabe beliebig viele Brennweiten
erlauben, macht es Sinn, die Ergebnisse in Form
einer traditionellen Datenbank&ndash;Ausgabe darzustellen,
bei der zeilenweise zu jeder Brennweite der
zugehörige berechnete Wert, getrennt durch ein
<constant>tab</constant>-Zeichen, ausgegeben wird.</para>
<para>Optional sollten wir dem Benutzer die Möglichkeit
geben, die Ausgabe in dem schon beschriebenen
<acronym>CSV</acronym>-Format festzulegen. In diesem Fall
werden wir zu Beginn der Ausgabe eine Zeile einfügen,
in der die Beschreibungen der einzelnen Felder, durch
Kommas getrennt, aufgelistet werden, gefolgt von der Ausgabe
der Daten wie schon beschrieben, wobei das
<constant>tab</constant>-Zeichen durch ein
<constant>Komma</constant> ersetzt wird.</para>
<para>Wir brauchen eine Kommandozeilenoption für das
<acronym>CSV</acronym>-Format. Wir können nicht
<parameter>-c</parameter> verwenden, da diese Option bereits
für <emphasis>verwende Connors Konstante</emphasis>
steht. Aus irgendeinem seltsamen Grund bezeichnen
viele Webseiten <acronym>CSV</acronym>-Dateien als
<emphasis>"Excel Kalkulationstabelle"</emphasis> (obwohl das
<acronym>CSV</acronym>-Format älter ist als Excel). Wir
werden daher <parameter>-e</parameter> als Schalter für
die Ausgabe im <acronym>CSV</acronym>-Format
verwenden.</para>
<para>Jede Zeile der Ausgabe wird mit einer Brennweite
beginnen. Dies mag auf den ersten Blick überflüssig
erscheinen, besonders im interaktiven Modus: Der Benutzer
gibt einen Wert für die Brennweite ein, und das Programm
wiederholt diesen.</para>
<para>Der Benutzer kann jedoch auch mehrere Brennweiten
in einer Zeile angeben. Die Eingabe kann auch aus einer Datei,
oder aus der Ausgabe eines anderen Programmes, kommen. In
diesen Fällen sieht der Benutzer die Eingabewerte
überhaupt nicht.</para>
<para>Ebenso kann die Ausgabe in eine Datei umgelenkt werden,
was wir später noch untersuchen werden, oder sie
könnte an einen Drucker geschickt werden, oder auch
als Eingabe für ein weiteres Programm dienen.</para>
<para>Es macht also wohl Sinn, jede Zeile mit einer durch den
Benutzer eingegebenen Brennweite beginnen zu lassen.</para>
<para>Halt! Nicht, wie der Benutzer die Daten eingegeben hat.
Was passiert, wenn der Benutzer etwas wie folgt
eingibt:</para>
<screen><userinput>00000000150</userinput></screen>
<para>Offensichtlich müssen wir die führenden Nullen
vorher abschneiden.</para>
<para>Wir müssen also die Eingabe des Benutzers
sorgfältig prüfen, diese dann in der
<acronym>FPU</acronym> in die binäre Form konvertieren,
und dann von dort aus ausgeben.</para>
<para>Aber...</para>
<para>Was ist, wenn der Benutzer etwas wie folgt eingibt:</para>
<screen><userinput>17459765723452353453534535353530530534563507309676764423</userinput></screen>
<para>Ha! Das packed decimal-Format der <acronym>FPU</acronym>
erlaubt uns die Eingabe einer 18&ndash;stelligen
Zahl. Aber der Benutzer hat mehr als 18 Stellen eingegeben.
Wie gehen wir damit um?</para>
<para>Wir <emphasis>könnten</emphasis> unser Programm
so modifizieren, daß es die ersten 18 Stellen liest,
der <acronym>FPU</acronym> übergibt, dann weitere
18 Stellen liest, den Inhalt des <acronym>TOS</acronym>
mit einem Vielfachen von 10, entsprechend der Anzahl der
zusätzlichen Stellen multipliziert, und dann beide
Werte mittels <function>add</function> zusammen
addiert.</para>
<para>Ja, wir könnten das machen. Aber in
<emphasis>diesem</emphasis> Programm wäre es
unnötig (in einem anderen wäre es vielleicht
der richtige Weg): Selbst der Erdumfang in Millimetern ergibt
nur eine Zahl mit 11 Stellen. Offensichtlich können wir
keine Kamera dieser Größe bauen (jedenfalls jetzt
noch nicht).</para>
<para>Wenn der Benutzer also eine so große Zahl eingibt,
ist er entweder gelangweilt, oder er testet uns, oder er
versucht, in das System einzudringen, oder er spielt&mdash;
indem er irgendetwas anderes macht als eine Lochkamera
zu entwerfen.</para>
<para>Was werden wir tun?</para>
<para>Wir werden ihn ohrfeigen, gewissermaßen:</para>
<screen>17459765723452353453534535353530530534563507309676764423 ??? ??? ??? ??? ???</screen>
<para>Um dies zu erreichen, werden wir einfach alle
führenden Nullen ignorieren. Sobald wir eine Ziffer
gefunden haben, die nicht Null ist, initialisieren wir
einen Zähler mit <constant>0</constant> und
beginnen mit drei Schritten:</para>
<procedure>
<step>
<para>Sende die Ziffer an die Ausgabe.</para>
</step>
<step>
<para>Füge die Ziffer einem Puffer hinzu, welchen wir
später benutzen werden, um den packed decimal-Wert
zu erzeugen, den wir an die
<acronym>FPU</acronym> schicken können.</para>
</step>
<step>
<para>Erhöhe den Zähler um eins.</para>
</step>
</procedure>
<para>Während wir diese drei Schritte wiederholen,
müssen wir auf zwei Bedingungen achten:</para>
<itemizedlist>
<listitem>
<para>Wenn der Zähler den Wert 18 übersteigt,
hören wir auf, Ziffern dem Puffer hinzuzufügen.
Wir lesen weiterhin Ziffern und senden sie an die
Ausgabe.</para>
</listitem>
<listitem>
<para>Wenn, bzw. <emphasis>falls</emphasis>, das
nächste Eingabezeichen keine Zahl ist, sind wir mit
der Bearbeitung der Eingabe erst einmal fertig.</para>
<para>Übrigends können wir einfach Zeichen, die
keine Ziffern sind, verwerfen, solange sie kein
<constant>#</constant>-Zeichen sind, welches wir an den
Eingabestrom zurückgeben müssen. Dieses Zeichen
markiert den Beginn eines Kommentars. An dieser Stelle
muß die Erzeugung der Ausgabe fertig sein, und wir
müssen mit der Suche nach weiteren Eingabedaten
fortfahren.</para>
</listitem>
</itemizedlist>
<para>Es bleibt immer noch eine Möglichkeit
unberücksichtigt: Wenn der Benutzer eine Null (oder
mehrere) eingibt, werden wir niemals eine von Null
verschiedene Zahl vorfinden.</para>
<para>Wir können solch einen Fall immer anhand des
Zählerstandes feststellen, welcher dann
immer bei <constant>0</constant> bleibt. In diesem Fall
müssen wir einfach eine <constant>0</constant> an
die Ausgabe senden, und anschließend dem Benutzer
erneut eine "Ohrfeige" verpassen:</para>
<screen>0 ??? ??? ??? ??? ???</screen>
<para>Sobald wir die Brennweite ausgegeben, und die
Gültigkeit dieser Eingabe verifiziert haben,
(größer als <constant>0</constant> und kleiner
als 18 Zahlen) können wir den Durchmesser der
Lochblende berechnen.</para>
<para>Es ist kein Zufall, daß
<emphasis>Lochblende</emphasis> das Wort
<emphasis>Loch</emphasis> enthält. In der Tat ist eine
Lochblende buchstäblich eine
<emphasis>Loch Blende</emphasis>, also eine Blende, in die
mit einer Nadel vorsichtig ein kleines Loch gestochen
wird.</para>
<para>Daher ist eine typische Lochblende sehr klein. Unsere
Formel liefert uns das Ergebnis in Millimetern. Wir werden
dieses mit <constant>1000</constant> multiplizieren, so
daß die Ausgabe in <constant>Mikrometern</constant>
erfolgt.</para>
<para>An dieser Stelle müssen wir auf eine weitere Falle
achten: <emphasis>Zu hohe Genauigkeit.</emphasis></para>
<para>Ja, die <acronym>FPU</acronym> wurde für
mathematische Berechnungen mit hoher Genauigkeit entworfen.
Unsere Berechnungen hier erfordern jedoch keine solche
mathematische Genauigkeit. Wir haben es hier mit Physik zu
tun (Optik, um genau zu sein).</para>
<para>Angenommen, wir wollten aus eine Lastkraftwagen eine
Lochkamera bauen (wir wären dabei nicht die
ersten, die das versuchen würden!). Angenommen, die
Länge des Laderaumes beträgt <constant>12</constant>
Meter lang, so daß wir eine Brennweite von
<constant>12000</constant> hätten. Verwenden wir
Benders Konstante, so erhalten wir durch Multiplizieren
von <constant>0.04</constant> mit der Quadratwurzel aus
<constant>12000</constant> einen Wert von
<constant>4.381780460</constant> Millimetern, oder
<constant>4381.780460</constant> Micrometern.</para>
<para>So oder so ist das Rechenergebnis absurd präzise.
Unser Lastkraftwagen ist nicht <emphasis>genau</emphasis>
<constant>12000</constant> Millimeter lang. Wir haben
diese Länge nicht mit einer so hohen Genauigkeit
gemessen, weswegen es falsch wäre zu behaupten,
unser Lochblendendurchmesser müsse exakt
<constant>4.381780460</constant> Millimeter sein. Es
reicht vollkommen aus, wenn der Durchmesser
<constant>4.4</constant> Millimeter beträgt.</para>
<note>
<para>Ich habe in obigem Beispiel das Rechenergebnis "nur"
auf 10 Stellen genau angegeben. Stellen Sie sich vor,
wie absurd es wäre, die vollen uns zur Verfügung
stehenden, 18 Stellen anzugeben!</para>
</note>
<para>Wir müssen also die Anzahl der signifikanten Stellen
beschränken. Eine Möglichkeit wäre, die
Mikrometer durch eine ganze Zahl darzustellen. Unser
Lastkraftwaren würde dann eine Lochblende mit einem
Durchmesser von <constant>4382</constant> Mikrometern
benötigen. Betrachten wir diesen Wert, dann stellen wir
fest, das <constant>4400</constant> Mikrometer, oder
<constant>4.4</constant> Millimeter, immer noch genau
genug ist.</para>
<para>Zusätzlich können wir noch, unabhängig von
der Größe eines Rechenergebnisses, festlegen,
daß wir nur vier signifikante Stellen anzeigen wollen
(oder weniger). Leider bietet uns die <acronym>FPU</acronym>
nicht die Möglichkeit, das Ergebnis automatisch bis
auf eine bestimmte Stelle zu runden (sie sieht die Daten ja
nicht als Zahlen, sondern als binäre Daten an).</para>
<para>Wir müssen also selber einen Algorithmus entwerfen,
um die Anzahl der signifikanten Stellen zu reduzieren.</para>
<para>Hier ist meiner (ich denke er ist peinlich&mdash;wenn
Ihnen ein besserer Algorithmus einfällt, verraten sie
ihn mir <emphasis>bitte</emphasis>):</para>
<procedure>
<step>
<para>Initialisiere einen Zähler mit
<constant>0</constant>.</para>
</step>
<step>
<para>Solange die Zahl größer oder gleich
<constant>10000</constant> ist, dividiere die Zahl durch
<constant>10</constant>, und erhöhe den Zähler
um eins.</para>
</step>
<step>
<para>Gebe das Ergebnis aus.</para>
</step>
<step>
<para>Solange der Zähler größer als
<constant>0</constant> ist, gebe eine
<constant>0</constant> aus, und reduziere den Zähler
um eins.</para>
</step>
</procedure>
<note>
<para>Der Wert <constant>10000</constant> ist nur für
den Fall, daß Sie <emphasis>vier</emphasis>
signifikante Stellen haben wollen. Für eine andere
Anzahl signifikanter Stellen müssen Sie den Wert
<constant>10000</constant> mit <constant>10</constant>,
hoch der Anzahl der gewünschten signifikanten Stellen,
ersetzen.</para>
</note>
<para>Wir können so den Lochblendendurchmesser, auf vier
signifikante Stellen gerundet, ausgeben.</para>
<para>An dieser Stellen kennen wir nun die <emphasis>Brennweite
</emphasis> und den
<emphasis>Lochblendendurchmesser</emphasis>. Wir haben also
jetzt genug Informationen, um den
<emphasis>f&ndash;Wert</emphasis> zu bestimmen.</para>
<para>Wir werden den f&ndash;Wert, auf vier signifikante
Stellen gerundet, ausgeben. Es könnte passieren,
daß diese vier Stellen recht wenig aussagen. Um die
Aussagekraft des f&ndash;Wertes zu erhöhen, könnten
wir den nächstliegenden, <emphasis>normalisierten
f&ndash;Wert</emphasis> bestimmen, also z.B. das
nächstliegende Vielfache der Quadratwurzel aus 2.</para>
<para>Wir erreichen dies, indem wir den aktuellen f&ndash;Wert
mit sich selbst multiplizieren, so daß wir dessen
Quadrat (<function>square</function>) erhalten.
Anschließend berechnen wir den Logarithmus zur Basis 2
von dieser Zahl. Dies ist sehr viel einfacher, als direkt den
Logarithmus zur Basis der Quadratwurzel aus 2 zu berechnen!
Wir runden dann das Ergebnis auf die nächstliegende
ganze Zahl. Genau genommen können wir mit Hilfe der
<acronym>FPU</acronym> diese Berechnung beschleunigen: Wir
können den op-Code
<function role="opcode">fscale</function> verwenden, um eine
Zahl um 1 zu "skalieren", was dasselbe ist, wie eine Zahl
mittels <function role="opcode">shift</function> um eine
Stelle nach links zu verschieben. Am Ende berechnen wir noch
die Quadratwurzel aus allem, und erhalten dann den
nächstliegenden, normalisierten f&ndash;Wert.</para>
<para>Wenn das alles jetzt viel zu kompliziert wirkt&mdash;oder
viel zu aufwendig&mdash;wird es vielleicht klarer, wenn man
den Code selber betrachtet. Wir benötigen insgesamt
9 op-Codes:</para>
<programlisting>fmul st0, st0
fld1
fld st1
fyl2x
frndint
fld1
fscale
fsqrt
fstp st1</programlisting>
<para>Die erste Zeile,
<function role="opcode">fmul st0, st0</function>, quadriert
den Inhalt des <acronym>TOS</acronym> (Top Of Stack, was
dasselbe ist wie <varname role="register">st</varname>, von
<application>nasm</application> auch
<varname role="register">st0</varname> genannt). Die Funktion
<function role="opcode">fld1</function> fügt eine
<constant>1</constant> dem <acronym>TOS</acronym>
hinzu.</para>
<para>Die nächste Zeile, <function role="opcode">fld
st1</function>, legt das Quadrat auf dem
<acronym>TOS</acronym> ab. An diesem Punkt befindet sich das
Quadrat sowohl in <varname role="register">st</varname> als
auch in <varname role="register">st(2)</varname> (es wird
sich gleich zeigen, warum wir eine zweite Kopie auf dem
Stack lassen.) <varname role="register">st(1)</varname>
enthält die <constant>1</constant>.</para>
<para>Im nächsten Schritt, <function
role="opcode">fyl2x</function>, wird der Logarithmus von
<varname role="register">st</varname> zur Basis 2 berechnet,
und anschließend mit <varname
role="register">st(1)</varname> multipliziert. Deshalb haben
wir vorher die <constant>1</constant> in <varname
role="register">st(1)</varname> abgelegt.</para>
<para>An dieser Stelle enthält
<varname role="register">st</varname> den gerade berechneten
Logarithmus, und <varname role="register">st(1)</varname>
das Quadrat des aktuellen f&ndash;Wertes, den wir für
später gespeichert haben.</para>
<para><function role="opcode">frndint</function> rundet den
<acronym>TOS</acronym> zur nächstliegenden ganzen Zahl.
<function role="opcode">fld1</function> legt eine
<constant>1</constant> auf dem Stack ab.
<function role="opcode">fscale</function> shiftet die
<constant>1</constant> auf dem <acronym>TOS</acronym> um
<varname role="register">st(1)</varname> Stellen, wodurch im
Endeffekt eine 2 in <varname role="register">st(1)</varname>
steht.</para>
<para>Schließlich berechnet
<function role="opcode">fsqrt</function> die Quadratwurzel
des Rechenergebnisses, also des nächstliegenden,
normalisierten f&ndash;Wertes.</para>
<para>Wir haben nun den nächstliegenden, normalisierten
f&ndash;Wert auf dem <acronym>TOS</acronym> liegen, den auf
den Logarithmus zur Basis 2 gerundeten, nächstliegenden
ganzzahligen Wert in <varname
role="register">st(1)</varname>, und das Quadrat des
aktuellen f&ndash;Wertes in <varname
role="register">st(2)</varname>. Wir speichern den Wert
für eine spätere Verwendung in <varname
role="register">st(2)</varname>.</para>
<para>Aber wir brauchen den Inhalt von
<varname role="register">st(1)</varname> gar nicht mehr. Die
letzte Zeile, <function role="opcode">fstp st1</function>,
platziert den Inhalt von <varname role="register">st</varname>
in <varname role="register">st(1)</varname>, und erniedrigt
den Stackpointer um eins. Dadurch ist der Inhalt von
<varname role="register">st(1)</varname> jetzt
<varname role="register">st</varname>, der Inhalt von
<varname role="register">st(2)</varname> jetzt
<varname role="register">st(1)</varname> usw. Der neue
<varname role="register">st</varname> speichert jetzt den
normalisierten f&ndash;Wert. Der neue
<varname role="register">st(1)</varname> speichert das
Quadrat des aktuellen f&ndash;Wertes für die
Nachwelt.</para>
<para>Jetzt können wir den normalisierten f&ndash;Wert
ausgeben. Da er normalisiert ist, werden wir ihn nicht auf
vier signifikante Stellen runden, sondern stattdessen
mit voller Genauigkeit ausgeben.</para>
<para>Der normalisierte f&ndash;Wert ist nützlich, solange
er so klein ist, daß wir ihn auf einem Photometer
wiederfinden können. Ansonsten brauchen wir eine andere
Methode, um die benötigten Belichtungsdaten zu
bestimmen.</para>
<para>Wir haben weiter oben eine Formel aufgestellt, über
die wir einen f&ndash;Wert mit Hilfe eines anderen
f&ndash;Wertes und den zugehörigen Belichtungsdaten
bestimmen können.</para>
<para>Jedes Photometer, das ich jemals gesehen habe, konnte
die benötigte Belichtungszeit für f5.6 berechnen.
Wir werden daher einen
<emphasis>"f5.6 Multiplizierer"</emphasis> berechnen, der
uns den Faktor angibt, mit dem wir die bei f5.6 gemessene
Belichtungszeit für unsere Lochkamera multiplizieren
müssen.</para>
<para>Durch die Formel wissen wir, daß dieser Faktor
durch Dividieren unseres f&ndash;Wertes (der aktuelle Wert,
nicht der normalisierte) durch <constant>5.6</constant>
und anschließendes Quadrieren, berechnen
können.</para>
<para>Mathematisch äquivalent dazu wäre, wenn wir
das Quadrat unseres f&ndash;Wertes durch das Quadrat von
<constant>5.6</constant> dividieren würden.</para>
<para>Numerisch betrachtet wollen wir nicht zwei Zahlen
quadrieren, wenn es möglich ist, nur
eine Zahl zu quadrieren. Daher wirkt die erste Variante
auf den ersten Blick besser.</para>
<para>Aber...</para>
<para><constant>5.6</constant> ist eine
<emphasis>Konstante</emphasis>. Wir müssen nicht
wertvolle Rechenzeit der <acronym>FPU</acronym> verschwenden.
Es reicht aus, daß wir die Quadrate der einzelnen
f&ndash;Werte durch den konstanten Wert
<constant>5.6&#178;</constant> dividieren. Oder wir
können den jeweiligen f&ndash;Wert durch
<constant>5.6</constant> dividieren, und dann das Ergebnis
quadrieren. Zwei Möglichkeiten, die gleich
erscheinen.</para>
<para>Aber das sind sie nicht!</para>
<para>Erinnern wir uns an die Grundlagen der Photographie
weiter oben, dann wissen wir, daß sich die
Konstante <constant>5.6</constant> aus dem 5-fachen der
Quadratwurzel aus 2 ergibt. Eine
<emphasis>irrationale</emphasis> Zahl. Das Quadrat dieser
Zahl ist <emphasis>exakt</emphasis>
<constant>32</constant>.</para>
<para><constant>32</constant> ist nicht nur eine ganze Zahl,
sondern auch ein Vielfaches von 2. Wir brauchen also
gar nicht das Quadrat eines f&ndash;Wertes durch
<constant>32</constant> zu teilen. Wir müssen lediglich
mittels <function role="opcode">fscale</function> den
f&ndash;Wert um fünf Stellen nach rechts shiften.
Aus Sicht der <acronym>FPU</acronym> müssen wir also
<function role="opcode">fscale</function> mit
<varname role="register">st(1)</varname>, welcher gleich
<constant>-5</constant> ist, auf den f&ndash;Wert anwenden.
Dies ist <emphasis>sehr viel schneller</emphasis> als die
Division.</para>
<para>Jetzt wird es auch klar, warum wir das Quadrat des
f&ndash;Wertes ganz oben auf dem Stack der
<acronym>FPU</acronym> gespeichert haben. Die Berechnung
des f5.6 Multiplizierers ist die einfachste Berechnung
des gesamten Programmes! Wir werden das Ergebnis auf vier
signifikante Stellen gerundet ausgeben.</para>
<para>Es gibt noch eine weitere nützliche Zahl, die wir
berechnen können: Die Anzahl der Stopps, die unser
f&ndash;Wert von f5.6 entfernt ist. Dies könnte
hilfreich sein, wenn unser f&ndash;Wert außerhalb des
Meßbereiches unseres Photometers liegt, wir aber eine
Blende haben, bei der wir unterschiedliche Geschwindigkeiten
einstellen können, und diese Blende Stopps
benutzt.</para>
<para>Angenommen, unser f&ndash;Wert ist 5 Stopps von f5.6
entfernt, und unser Photometer sagt uns, daß wir eine
Belichtungszeit von 1/1000 Sek. einstellen sollen. Dann
können wir unsere Blende auf die Geschwindigkeit 1/1000
einstellen, und unsere Skala um 5 Stopps verschieben.</para>
<para>Diese Rechnung ist ebenfalls sehr einfach. Alles, was
wir tun müssen, ist, den Logarithmus des f5.6
Multiplizierers, den wir schon berechnet haben (wobei wir
dessen Wert vor der Rundung nehmen müssen) zur Basis 2
zu nehmen. Wir runden dann das Ergebnis zur nächsten
ganzen Zahl hin, und geben dies aus. Wir müssen uns
nicht darum kümmern, ob wir mehr als vier signifikante
Stellen haben: Das Ergebnis besteht
höchstwahrscheinlich nur aus einer oder zwei
Stellen.</para>
</sect3>
</sect2>
<sect2 id="x86-fpu-optimizations">
<title>FPU Optimierungen</title>
<para>In Assemblersprache können wir den Code für die
<acronym>FPU</acronym> besser optimieren, als in einer der
Hochsprachen, inklusive C.</para>
<para>Sobald eine C-Funktion die Berechnung einer
Fließkommazahl durchführen will, lädt sie erst
einmal alle benötigten Variablen und Konstanten in die
Register der <acronym>FPU</acronym>. Dann werden die
Berechnungen durchgeführt, um das korrekte Ergebnis zu
erhalten. Gute C-Compiler können diesen Teil des Codes
sehr gut optimieren.</para>
<para>Das Ergebnis wird "zurückgegeben", indem dieses auf
dem <acronym>TOS</acronym> abgelegt wird. Vorher wird
aufgeräumt. Sämtliche Variablen und Konstanten, die
während der Berechnung verwendet wurden, werden dabei
aus der <acronym>FPU</acronym> entfernt.</para>
<para>Was wir im vorherigen Abschnitt selber getan haben, kann so
nicht durchgeführt werden: Wir haben das Quadrat des
f&ndash;Wertes berechnet, und das Ergebnis für eine
weitere Berechnung mit einer anderen Funktion auf dem Stack
behalten.</para>
<para>Wir <emphasis>wußten</emphasis>, daß wir
diesen Wert später noch einmal brauchen würden. Wir
wußten auch, daß auf dem Stack genügend Platz
war (welcher nur Platz für 8 Zahlen bietet), um den Wert
dort zu speichern.</para>
<para>Ein C-Compiler kann nicht wissen, ob ein Wert auf dem
Stack in naher Zukunft noch einmal gebraucht wird.</para>
<para>Natürlich könnte der C-Programmierer dies wissen.
Aber die einzige Möglichkeit, die er hat, ist, den Wert
im verfügbaren Speicher zu halten.</para>
<para>Das bedeutet zum einen, daß der Wert mit der
<acronym>FPU</acronym>-internen, 80-stelligen
Genauigkeit in einer normalen C-Variable vom Typ
<emphasis>double</emphasis> (64 Bit) oder vom Typ
<emphasis>single</emphasis> (32 Bit) gespeichert wird.</para>
<para>Dies bedeutet außerdem, daß der Wert aus dem
<acronym>TOS</acronym> in den Speicher verschoben werden
muß, und später wieder zurück. Von allen
Operationen mit der <acronym>FPU</acronym> ist der Zugriff
auf den Speicher die langsamste.</para>
<para>Wann immer also mit der <acronym>FPU</acronym> in einer
Assemblersprache programmiert wird, sollte nach
Möglichkeiten gesucht werden, Zwischenergebnisse auf dem
Stack der <acronym>FPU</acronym> zu lassen.</para>
<para>Wir können mit dieser Idee noch einen Schritt weiter
gehen! In unserem Programm verwenden wir eine
<emphasis>Konstante</emphasis> (die wir <constant>PC</constant>
genannt haben).</para>
<para>Es ist unwichtig, wieviele Lochblendendurchmesser wir
berechnen: 1, 10, 20, 1000, wir verwenden immer dieselbe
Konstante. Daher können wir unser Programm so optimieren,
daß diese Konstante immer auf dem Stack belassen
wird.</para>
<para>Am Anfang unseres Programmes berechnen wir die oben
erwähnte Konstante. Wir müssen die Eingabe für
jede Dezimalstelle der Konstanten durch <constant>10</constant>
dividieren.</para>
<para>Multiplizieren geht sehr viel schneller als Dividieren.
Wir teilen also zu Beginn unseres Programmes
<constant>1</constant> durch <constant>10</constant>, um
<constant>0.1</constant> zu erhalten, was wir auf dem Stack
speichern: Anstatt daß wir nun für jede einzelne
Dezimalstelle die Eingabe wieder durch <constant>10</constant>
teilen, multiplizieren wir sie stattdessen mit
<constant>0.1</constant>.</para>
<para>Auf diese Weise geben wir <constant>0.1</constant> nicht
direkt ein, obwohl wir dies könnten. Dies hat einen Grund:
Während <constant>0.1</constant> durch nur eine einzige
Dezimalstelle dargestellt werden kann, wissen wir nicht,
wieviele <emphasis>binäre</emphasis> Stellen benötigt
werden. Wir überlassen die Berechnung des binären
Wertes daher der <acronym>FPU</acronym>, mit dessen eigener,
hoher Genauigkeit.</para>
<para>Wir verwenden noch weitere Konstanten: Wir multiplizieren
den Lochblendendurchmesser mit <constant>1000</constant>, um
den Wert von Millimeter in Micrometer zu konvertieren. Wir
vergleichen Werte mit <constant>10000</constant>, wenn wir
diese auf vier signifikante Stellen runden wollen. Wir
behalten also beide Konstanten, <constant>1000</constant>
und <constant>10000</constant>, auf dem Stack. Und
selbstverständlich verwenden wir erneut die gespeicherte
<constant>0.1</constant>, um Werte auf vier signifikante
Stellen zu runden.</para>
<para>Zu guter letzt behalten wir <constant>-5</constant> noch
auf dem Stack. Wir brauchen diesen Wert, um das Quadrat des
f&ndash;Wertes zu skalieren, anstatt diesen durch
<constant>32</constant> zu teilen. Es ist kein Zufall, daß
wir diese Konstante als letztes laden. Dadurch wird diese
Zahl die oberste Konstante auf dem Stack. Wenn später
das Quadrat des f&ndash;Wertes skaliert werden muß,
befindet sich die <constant>-5</constant> in
<varname role="register">st(1)</varname>, also genau da, wo
die Funktion <function role="opcode">fscale</function> diesen
Wert erwartet.</para>
<para>Es ist üblich, einige Konstanten per Hand zu erzeugen,
anstatt sie aus dem Speicher zu laden. Genau das machen wir
mit der <constant>-5</constant>:</para>
<programlisting>
fld1 ; TOS = 1
fadd st0, st0 ; TOS = 2
fadd st0, st0 ; TOS = 4
fld1 ; TOS = 1
faddp st1, st0 ; TOS = 5
fchs ; TOS = -5</programlisting>
<para>Wir können all diese Optimierungen in einer Regel
zusammenfassen: <emphasis>Behalte wiederverwendbare Werte auf
dem Stack!</emphasis></para>
<tip>
<para><emphasis>&postscript;</emphasis> ist eine
Stack-orientierte Programmiersprache. Es gibt weit mehr
Bücher über &postscript;, als über die
Assemblersprache der <acronym>FPU</acronym>: Werden Sie
in &postscript; besser, dann werden Sie auch automatisch
in der Programmierung der <acronym>FPU</acronym>
besser.</para>
</tip>
</sect2>
<sect2 id="x86-pinhole-the-code">
<title><application>pinhole</application>&mdash;Der Code</title>
<programlisting>
;;;;;;; pinhole.asm ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;
; Find various parameters of a pinhole camera construction and use
;
; Started: 9-Jun-2001
; Updated: 10-Jun-2001
;
; Copyright (c) 2001 G. Adam Stanislav
; All rights reserved.
;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
%include 'system.inc'
%define BUFSIZE 2048
section .data
align 4
ten dd 10
thousand dd 1000
tthou dd 10000
fd.in dd stdin
fd.out dd stdout
envar db 'PINHOLE=' ; Exactly 8 bytes, or 2 dwords long
pinhole db '04,', ; Bender's constant (0.04)
connors db '037', 0Ah ; Connors' constant
usg db 'Usage: pinhole [-b] [-c] [-e] [-p &lt;value&gt;] [-o &lt;outfile&gt;] [-i &lt;infile&gt;]', 0Ah
usglen equ $-usg
iemsg db "pinhole: Can't open input file", 0Ah
iemlen equ $-iemsg
oemsg db "pinhole: Can't create output file", 0Ah
oemlen equ $-oemsg
pinmsg db "pinhole: The PINHOLE constant must not be 0", 0Ah
pinlen equ $-pinmsg
toobig db "pinhole: The PINHOLE constant may not exceed 18 decimal places", 0Ah
biglen equ $-toobig
huhmsg db 9, '???'
separ db 9, '???'
sep2 db 9, '???'
sep3 db 9, '???'
sep4 db 9, '???', 0Ah
huhlen equ $-huhmsg
header db 'focal length in millimeters,pinhole diameter in microns,'
db 'F-number,normalized F-number,F-5.6 multiplier,stops '
db 'from F-5.6', 0Ah
headlen equ $-header
section .bss
ibuffer resb BUFSIZE
obuffer resb BUFSIZE
dbuffer resb 20 ; decimal input buffer
bbuffer resb 10 ; BCD buffer
section .text
align 4
huh:
call write
push dword huhlen
push dword huhmsg
push dword [fd.out]
sys.write
add esp, byte 12
ret
align 4
perr:
push dword pinlen
push dword pinmsg
push dword stderr
sys.write
push dword 4 ; return failure
sys.exit
align 4
consttoobig:
push dword biglen
push dword toobig
push dword stderr
sys.write
push dword 5 ; return failure
sys.exit
align 4
ierr:
push dword iemlen
push dword iemsg
push dword stderr
sys.write
push dword 1 ; return failure
sys.exit
align 4
oerr:
push dword oemlen
push dword oemsg
push dword stderr
sys.write
push dword 2
sys.exit
align 4
usage:
push dword usglen
push dword usg
push dword stderr
sys.write
push dword 3
sys.exit
align 4
global _start
_start:
add esp, byte 8 ; discard argc and argv[0]
sub esi, esi
.arg:
pop ecx
or ecx, ecx
je near .getenv ; no more arguments
; ECX contains the pointer to an argument
cmp byte [ecx], '-'
jne usage
inc ecx
mov ax, [ecx]
inc ecx
.o:
cmp al, 'o'
jne .i
; Make sure we are not asked for the output file twice
cmp dword [fd.out], stdout
jne usage
; Find the path to output file - it is either at [ECX+1],
; i.e., -ofile --
; or in the next argument,
; i.e., -o file
or ah, ah
jne .openoutput
pop ecx
jecxz usage
.openoutput:
push dword 420 ; file mode (644 octal)
push dword 0200h | 0400h | 01h
; O_CREAT | O_TRUNC | O_WRONLY
push ecx
sys.open
jc near oerr
add esp, byte 12
mov [fd.out], eax
jmp short .arg
.i:
cmp al, 'i'
jne .p
; Make sure we are not asked twice
cmp dword [fd.in], stdin
jne near usage
; Find the path to the input file
or ah, ah
jne .openinput
pop ecx
or ecx, ecx
je near usage
.openinput:
push dword 0 ; O_RDONLY
push ecx
sys.open
jc near ierr ; open failed
add esp, byte 8
mov [fd.in], eax
jmp .arg
.p:
cmp al, 'p'
jne .c
or ah, ah
jne .pcheck
pop ecx
or ecx, ecx
je near usage
mov ah, [ecx]
.pcheck:
cmp ah, '0'
jl near usage
cmp ah, '9'
ja near usage
mov esi, ecx
jmp .arg
.c:
cmp al, 'c'
jne .b
or ah, ah
jne near usage
mov esi, connors
jmp .arg
.b:
cmp al, 'b'
jne .e
or ah, ah
jne near usage
mov esi, pinhole
jmp .arg
.e:
cmp al, 'e'
jne near usage
or ah, ah
jne near usage
mov al, ','
mov [huhmsg], al
mov [separ], al
mov [sep2], al
mov [sep3], al
mov [sep4], al
jmp .arg
align 4
.getenv:
; If ESI = 0, we did not have a -p argument,
; and need to check the environment for "PINHOLE="
or esi, esi
jne .init
sub ecx, ecx
.nextenv:
pop esi
or esi, esi
je .default ; no PINHOLE envar found
; check if this envar starts with 'PINHOLE='
mov edi, envar
mov cl, 2 ; 'PINHOLE=' is 2 dwords long
rep cmpsd
jne .nextenv
; Check if it is followed by a digit
mov al, [esi]
cmp al, '0'
jl .default
cmp al, '9'
jbe .init
; fall through
align 4
.default:
; We got here because we had no -p argument,
; and did not find the PINHOLE envar.
mov esi, pinhole
; fall through
align 4
.init:
sub eax, eax
sub ebx, ebx
sub ecx, ecx
sub edx, edx
mov edi, dbuffer+1
mov byte [dbuffer], '0'
; Convert the pinhole constant to real
.constloop:
lodsb
cmp al, '9'
ja .setconst
cmp al, '0'
je .processconst
jb .setconst
inc dl
.processconst:
inc cl
cmp cl, 18
ja near consttoobig
stosb
jmp short .constloop
align 4
.setconst:
or dl, dl
je near perr
finit
fild dword [tthou]
fld1
fild dword [ten]
fdivp st1, st0
fild dword [thousand]
mov edi, obuffer
mov ebp, ecx
call bcdload
.constdiv:
fmul st0, st2
loop .constdiv
fld1
fadd st0, st0
fadd st0, st0
fld1
faddp st1, st0
fchs
; If we are creating a CSV file,
; print header
cmp byte [separ], ','
jne .bigloop
push dword headlen
push dword header
push dword [fd.out]
sys.write
.bigloop:
call getchar
jc near done
; Skip to the end of the line if you got '#'
cmp al, '#'
jne .num
call skiptoeol
jmp short .bigloop
.num:
; See if you got a number
cmp al, '0'
jl .bigloop
cmp al, '9'
ja .bigloop
; Yes, we have a number
sub ebp, ebp
sub edx, edx
.number:
cmp al, '0'
je .number0
mov dl, 1
.number0:
or dl, dl ; Skip leading 0's
je .nextnumber
push eax
call putchar
pop eax
inc ebp
cmp ebp, 19
jae .nextnumber
mov [dbuffer+ebp], al
.nextnumber:
call getchar
jc .work
cmp al, '#'
je .ungetc
cmp al, '0'
jl .work
cmp al, '9'
ja .work
jmp short .number
.ungetc:
dec esi
inc ebx
.work:
; Now, do all the work
or dl, dl
je near .work0
cmp ebp, 19
jae near .toobig
call bcdload
; Calculate pinhole diameter
fld st0 ; save it
fsqrt
fmul st0, st3
fld st0
fmul st5
sub ebp, ebp
; Round off to 4 significant digits
.diameter:
fcom st0, st7
fstsw ax
sahf
jb .printdiameter
fmul st0, st6
inc ebp
jmp short .diameter
.printdiameter:
call printnumber ; pinhole diameter
; Calculate F-number
fdivp st1, st0
fld st0
sub ebp, ebp
.fnumber:
fcom st0, st6
fstsw ax
sahf
jb .printfnumber
fmul st0, st5
inc ebp
jmp short .fnumber
.printfnumber:
call printnumber ; F number
; Calculate normalized F-number
fmul st0, st0
fld1
fld st1
fyl2x
frndint
fld1
fscale
fsqrt
fstp st1
sub ebp, ebp
call printnumber
; Calculate time multiplier from F-5.6
fscale
fld st0
; Round off to 4 significant digits
.fmul:
fcom st0, st6
fstsw ax
sahf
jb .printfmul
inc ebp
fmul st0, st5
jmp short .fmul
.printfmul:
call printnumber ; F multiplier
; Calculate F-stops from 5.6
fld1
fxch st1
fyl2x
sub ebp, ebp
call printnumber
mov al, 0Ah
call putchar
jmp .bigloop
.work0:
mov al, '0'
call putchar
align 4
.toobig:
call huh
jmp .bigloop
align 4
done:
call write ; flush output buffer
; close files
push dword [fd.in]
sys.close
push dword [fd.out]
sys.close
finit
; return success
push dword 0
sys.exit
align 4
skiptoeol:
; Keep reading until you come to cr, lf, or eof
call getchar
jc done
cmp al, 0Ah
jne .cr
ret
.cr:
cmp al, 0Dh
jne skiptoeol
ret
align 4
getchar:
or ebx, ebx
jne .fetch
call read
.fetch:
lodsb
dec ebx
clc
ret
read:
jecxz .read
call write
.read:
push dword BUFSIZE
mov esi, ibuffer
push esi
push dword [fd.in]
sys.read
add esp, byte 12
mov ebx, eax
or eax, eax
je .empty
sub eax, eax
ret
align 4
.empty:
add esp, byte 4
stc
ret
align 4
putchar:
stosb
inc ecx
cmp ecx, BUFSIZE
je write
ret
align 4
write:
jecxz .ret ; nothing to write
sub edi, ecx ; start of buffer
push ecx
push edi
push dword [fd.out]
sys.write
add esp, byte 12
sub eax, eax
sub ecx, ecx ; buffer is empty now
.ret:
ret
align 4
bcdload:
; EBP contains the number of chars in dbuffer
push ecx
push esi
push edi
lea ecx, [ebp+1]
lea esi, [dbuffer+ebp-1]
shr ecx, 1
std
mov edi, bbuffer
sub eax, eax
mov [edi], eax
mov [edi+4], eax
mov [edi+2], ax
.loop:
lodsw
sub ax, 3030h
shl al, 4
or al, ah
mov [edi], al
inc edi
loop .loop
fbld [bbuffer]
cld
pop edi
pop esi
pop ecx
sub eax, eax
ret
align 4
printnumber:
push ebp
mov al, [separ]
call putchar
; Print the integer at the TOS
mov ebp, bbuffer+9
fbstp [bbuffer]
; Check the sign
mov al, [ebp]
dec ebp
or al, al
jns .leading
; We got a negative number (should never happen)
mov al, '-'
call putchar
.leading:
; Skip leading zeros
mov al, [ebp]
dec ebp
or al, al
jne .first
cmp ebp, bbuffer
jae .leading
; We are here because the result was 0.
; Print '0' and return
mov al, '0'
jmp putchar
.first:
; We have found the first non-zero.
; But it is still packed
test al, 0F0h
jz .second
push eax
shr al, 4
add al, '0'
call putchar
pop eax
and al, 0Fh
.second:
add al, '0'
call putchar
.next:
cmp ebp, bbuffer
jb .done
mov al, [ebp]
push eax
shr al, 4
add al, '0'
call putchar
pop eax
and al, 0Fh
add al, '0'
call putchar
dec ebp
jmp short .next
.done:
pop ebp
or ebp, ebp
je .ret
.zeros:
mov al, '0'
call putchar
dec ebp
jne .zeros
.ret:
ret</programlisting>
<para>Der Code folgt demselben Aufbau wie alle anderen Filter,
die wir bisher gesehen haben, bis auf eine Kleinigkeit:</para>
<blockquote>
<para>Wir nehmen nun nicht mehr an, daß das Ende
der Eingabe auch das Ende der nötigen Arbeit bedeutet,
etwas, das wir für <emphasis>zeichenbasierte</emphasis>
Filter automatisch angenommen haben.</para>
<para>Dieser Filter verarbeitet keine Zeichen. Er verarbeitet
eine <emphasis>Sprache</emphasis> (obgleich eine sehr
einfache, die nur aus Zahlen besteht).</para>
<para>Wenn keine weiteren Eingaben vorliegen, kann das zwei
Ursachen haben:</para>
<itemizedlist>
<listitem>
<para>Wir sind fertig und können aufhören. Dies
ist dasselbe wie vorher.</para>
</listitem>
<listitem>
<para>Das Zeichen, das wir eingelesen haben, war eine Zahl.
Wir haben diese am Ende unseres <acronym>ASCII</acronym>
&ndash;zu&ndash;float Kovertierungspuffers gespeichert.
Wir müssen nun den gesamten Pufferinhalt in eine
Zahl konvertieren, und die letzte Zeile unserer Ausgabe
ausgeben.</para>
</listitem>
</itemizedlist>
<para>Aus diesem Grund haben wir unsere <function>getchar
</function>- und <function>read</function>-Routinen
so angepaßt, daß sie das
<varname role="register">carry flag</varname>
<emphasis>clear</emphasis> immer dann zurückgeben, wenn
wir ein weiteres Zeichen aus der Eingabe lesen, und das
<varname role="register">carry flag</varname>
<emphasis>set</emphasis> immer dann zurückgeben, wenn
es keine weiteren Eingabedaten gibt.</para>
<para>Selbstverständlich verwenden wir auch hier die
Magie der Assemblersprache! Schauen Sie sich
<function>getchar</function> näher an. Dieses gibt
<emphasis>immer</emphasis> das
<varname role="register">carry flag</varname>
<emphasis>clear</emphasis> zurück.</para>
<para>Dennoch basiert der Hauptteil unseres Programmes auf dem
<varname role="register">carry flag</varname>, um diesem eine
Beendigung mitzuteilen&mdash;und es funktioniert.</para>
<para>Die Magie passiert in <function>read</function>. Wann
immer weitere Eingaben durch das System zur Verfügung
stehen, ruft diese Funktion <function>getchar</function>
auf, welche ein weiteres Zeichen aus dem Eingabepuffer
einliest, und anschließend das
<varname role="register">carry flag</varname>
<emphasis>clear</emphasis>t.</para>
<para>Wenn aber <function>read</function> keine weiteren
Eingaben von dem System bekommt, ruft dieses
<emphasis>nicht</emphasis> <function>getchar</function>
auf. Stattdessen addiert der op-Code
<function role="opcode">add esp, byte 4</function>
<constant>4</constant> zu
<varname role="register">ESP</varname> hinzu,
<emphasis>setzt</emphasis> das
<varname role="register">carry flag</varname>, und
springt zurück.</para>
<para>Wo springt diese Funktion hin? Wann immer ein Programm
den op-Code <function role="opcode">call</function>
verwendet, <function role="opcode">push</function>t der
Mikroprozessor die Rücksprungandresse, d.h. er
speichert diese ganz oben auf dem Stack (nicht auf dem
Stack der <acronym>FPU</acronym>, sondern auf dem Systemstack,
der sich im Hauptspeicher befindet). Wenn ein Programm den
op-Code <function role="opcode">ret</function> verwendet,
<function role="opcode">pop</function>t der
Mikroprozessor den Rückgabewert von dem Stack, und
springt zu der Adresse, die dort gespeichert wurde.</para>
<para>Da wir aber <constant>4</constant> zu
<varname role="register">ESP</varname> hinzuaddiert haben
(welches das Register der Stackzeiger ist), haben wir
effektiv dem Mikroprzessor eine kleine
<emphasis>Amnesie</emphasis> verpaßt: Dieser erinnert
sich nun nicht mehr daran, daß
<function>getchar</function> durch
<function>read</function> aufgerufen wurde.</para>
<para>Und da <function>getchar</function> nichts vor dem
Aufruf von <function>read</function> auf dem Stack abgelegt
hat, enthält der Anfang des Stacks nun die
Rücksprungadresse von der Funktion, die
<function>getchar</function> aufgerufen hat. Soweit es den
Aufrufer betrifft, hat dieser <function>getchar</function>
ge<function role="opcode">call</function>t, welche mit
einem gesetzten
<varname role="register">carry flag</varname>
<function role="opcode">ret</function>urned.</para>
</blockquote>
<para>Des weiteren wird die Routine <function>bcdload</function>
bei einem klitzekleinen Problem zwischen der Big&ndash;Endian-
und Little&ndash;Endian-Codierung aufgerufen.</para>
<para>Diese konvertiert die Textrepräsentation einer
Zahl in eine andere Textrepräsentation: Der Text wird
in der Big&ndash;Endian-Codierung gespeichert, die
<emphasis>packed decimal</emphasis>-Darstellung jedoch in der
Little&ndash;Endian-Codierung.</para>
<para>Um dieses Problem zu lösen haben wir vorher den
op-Code <function>std</function> verwendet. Wir machen diesen
Aufruf später mittels <function>cld</function> wieder
rückgängig: Es ist sehr wichtig, daß wir keine
Funktion mittels <function>call</function> aufrufen, die von
einer Standardeinstellung des
<emphasis>Richtungsflags</emphasis> abhängig ist,
während <function>std</function> ausgeführt
wird.</para>
<para>Alles weitere in dem Programm sollte leicht zu verstehen
sein, vorausgesetzt, daß Sie das gesamte vorherige
Kapitel gelesen haben.</para>
<para>Es ist ein klassisches Beispiel für das Sprichwort,
daß das Programmieren eine Menge Denkarbeit, und nur
ein wenig Programmcode benötigt. Sobald wir uns über
jedes Detail im klaren sind, steht der Code fast schon
da.</para>
</sect2>
<sect2 id="x86-pinhole-using">
<title>Das Programm <application>pinhole</application>
verwenden</title>
<para>Da wir uns bei dem Programm dafür entschieden haben,
alle Eingaben, die keine Zahlen sind, zu ignorieren (selbst
die in Kommentaren), können wir jegliche
<emphasis>textbasierten Eingaben</emphasis> verarbeiten. Wir
<emphasis>müssen</emphasis> dies nicht tun, wir
<emphasis>könnten</emphasis> aber.</para>
<para>Meiner bescheidenen Meinung nach wird ein Programm durch
die Möglichkeit, anstatt einer strikten Eingabesyntax
textbasierte Anfragen stellen zu können, sehr viel
benutzerfreundlicher.</para>
<para>Angenommen, wir wollten eine Lochkamera für einen
4x5 Zoll Film bauen. Die standardmäßige Brennweite
für diesen Film ist ungefähr 150mm.
Wir wollen diesen Wert <emphasis>optimieren</emphasis>, so
daß der Lochblendendurchmesser eine möglichst
runde Zahl ergibt. Lassen Sie uns weiter annehmen, daß
wir zwar sehr gut mit Kameras umgehen können, dafür
aber nicht so gut mit Computern. Anstatt das wir nun eine
Reihe von Zahlen eingeben, wollen wir lieber ein paar
<emphasis>Fragen</emphasis> stellen.</para>
<para>Unsere Sitzung könnte wie folgt aussehen:</para>
<screen>&prompt.user; <userinput>pinhole
Computer,
Wie groß müßte meine Lochblende bei einer Brennweite
von 150 sein?</userinput>
150 490 306 362 2930 12
<userinput>Hmmm... Und bei 160?</userinput>
160 506 316 362 3125 12
<userinput>Laß uns bitte 155 nehmen.</userinput>
155 498 311 362 3027 12
<userinput>Ah, laß uns 157 probieren...</userinput>
157 501 313 362 3066 12
<userinput>156?</userinput>
156 500 312 362 3047 12
<userinput>Das ist es! Perfekt! Vielen Dank!
^D</userinput></screen>
<para>Wir haben herausgefunden, daß der
Lochblendendurchmesser bei einer Brennweite von 150 mm
490 Mikrometer, oder 0.49 mm ergeben würde. Bei einer fast
identischen Brennweite von 156 mm würden wir
einen Durchmesser von genau einem halben Millimeter
bekommen.</para>
</sect2>
<sect2 id="x86-pinhole-scripting">
<title>Skripte schreiben</title>
<para>Da wir uns dafür entschieden haben, das Zeichen
<constant>#</constant> als den Anfang eines Kommentares zu
interpretieren, können wir unser
<application>pinhole</application>-Programm auch als
<emphasis>Skriptsprache</emphasis> verwenden.</para>
<para>Sie haben vielleicht schon einmal
<application>shell</application><emphasis>-Skripte</emphasis>
gesehen, die mit folgenden Zeichen begonnen haben:</para>
<programlisting>#! /bin/sh</programlisting>
<para>...oder...</para>
<programlisting>#!/bin/sh</programlisting>
<para>... da das Leerzeichen hinter dem <function>#!</function>
optional ist.</para>
<para>Wann immer &unix; eine Datei ausführen soll, die mit
einem <function>#!</function> beginnt, wird angenommen, das
die Datei ein Skript ist. Es fügt den Befehl an das Ende
der ersten Zeile an, und versucht dann, dieses
auszuführen.</para>
<para>Angenommen, wir haben unser Programm
<application>pinhole</application> unter
<application>/usr/local/bin/</application> installiert, dann
können wir nun Skripte schreiben, um unterschiedliche
Lochblendendurchmesser für mehrere Brennweiten
zu berechnen, die normalerweise mit 120er Filmen verwendet
werden.</para>
<para>Das Skript könnte wie folgt aussehen:</para>
<programlisting>#! /usr/local/bin/pinhole -b -i
# Find the best pinhole diameter
# for the 120 film
### Standard
80
### Wide angle
30, 40, 50, 60, 70
### Telephoto
100, 120, 140</programlisting>
<para>Da ein 120er Film ein Film mittlerer Größe ist,
könnten wir die Datei <application>medium</application>
nennen.</para>
<para>Wir können die Datei ausführbar machen und dann
aufrufen, als wäre es ein Programm:</para>
<screen>&prompt.user; <userinput>chmod 755 medium</userinput>
&prompt.user; <userinput>./medium</userinput></screen>
<para>&unix; wird den letzten Befehl wie folgt
interpretieren:</para>
<screen>&prompt.user; <userinput>/usr/local/bin/pinhole -b -i ./medium</userinput></screen>
<para>Es wird den Befehl ausführen und folgendes
ausgeben:</para>
<screen>80 358 224 256 1562 11
30 219 137 128 586 9
40 253 158 181 781 10
50 283 177 181 977 10
60 310 194 181 1172 10
70 335 209 181 1367 10
100 400 250 256 1953 11
120 438 274 256 2344 11
140 473 296 256 2734 11</screen>
<para>Lassen Sie uns nun das folgende eingeben:</para>
<screen>&prompt.user; <userinput>./medium -c</userinput></screen>
<para>&unix; wird dieses wie folgt behandeln:</para>
<screen>&prompt.user; <userinput>/usr/local/bin/pinhole -b -i ./medium -c</userinput></screen>
<para>Dadurch erhält das Programm zwei
widersprüchliche Optionen: <parameter>-b</parameter> und
<parameter>-c</parameter> (Verwende Benders Konstante und
verwende Connors Konstante). Wir haben unser Programm so
geschrieben, daß später eingelesene Optionen die
vorheringen überschreiben&mdash;unser Programm wird also
Connors Konstante für die Berechnungen verwenden:</para>
<screen>80 331 242 256 1826 11
30 203 148 128 685 9
40 234 171 181 913 10
50 262 191 181 1141 10
60 287 209 181 1370 10
70 310 226 256 1598 11
100 370 270 256 2283 11
120 405 296 256 2739 11
140 438 320 362 3196 12</screen>
<para>Wir entscheiden uns am Ende doch für Benders
Konstante. Wir wollen die Ergebnisse im CSV-Format in
einer Datei speichern:</para>
<screen>&prompt.user; <userinput>./medium -b -e &gt; bender</userinput>
&prompt.user; <userinput>cat bender</userinput>
focal length in millimeters,pinhole diameter in microns,F-number,normalized F-number,F-5.6 multiplier,stops from F-5.6
80,358,224,256,1562,11
30,219,137,128,586,9
40,253,158,181,781,10
50,283,177,181,977,10
60,310,194,181,1172,10
70,335,209,181,1367,10
100,400,250,256,1953,11
120,438,274,256,2344,11
140,473,296,256,2734,11
&prompt.user;</screen>
</sect2>
</sect1>
<sect1 id="x86-caveats">
<sect1info>
<authorgroup>
<author>
<firstname>Daniel</firstname>
<surname>Seuffert</surname>
<contrib>Übersetzt von </contrib>
</author>
</authorgroup>
</sect1info>
<title>Vorsichtsmassnahmen</title>
<para>Assembler-Programmierer, die aufwuchsen mit
<acronym>&ms-dos;</acronym> und windows &windows; neigen oft
dazu Shotcuts zu verwenden. Das Lesen der Tastatur-Scancodes und
das direkte Schreiben in den Grafikspeicher sind zwei klassische
Beispiele von Gewohnheiten, die unter
<acronym>&ms-dos;</acronym> nicht verpönt sind, aber nicht
als richtig angesehen werden.</para>
<para>Warum dies? Sowohl das <acronym>PC-BIOS</acronym> als auch
<acronym>&ms-dos;</acronym> sind notorisch langsam bei der
Ausführung dieser Operationen.</para>
<para>Sie mögen versucht sein ähnliche Angewohnheiten
in der &unix;-Umgebung fortzuführen. Zum Beispiel habe ich
eine Webseite gesehen, welche erklärt, wie man auf einem
beliebten &unix;-Ableger die Tastatur-Scancodes
verwendet.</para>
<para>Das ist generell eine <emphasis>sehr schlechte
Idee</emphasis> in einer &unix;-Umgebung! Lassen Sie mich
erklären warum.</para>
<sect2 id="x86-protected">
<title>&unix; ist geschützt</title>
<para>Zum Einen mag es schlicht nicht möglich sein.
&unix; läuft im Protected Mode. Nur der Kernel und
Gerätetreiber dürfen direkt auf die Hardware
zugreifen. Unter Umständen erlaubt es Ihnen ein
bestimmter &unix;-Ableger Tastatur-Scancodes auszulesen, aber
ein wirkliches &unix;-Betriebssystem wird dies zu verhindern
wissen. Und falls eine Version es Ihnen erlaubt wird es eine
andere nicht tun, daher kann eine sorgfältig erstellte
Software über Nacht zu einem überkommenen
Dinosaurier werden.</para>
</sect2>
<sect2 id="x86-abstraction">
<title>&unix; ist eine Abstraktion</title>
<para>Aber es gibt einen viel wichtigeren Grund, weshalb Sie
nicht versuchen sollten, die Hardware direkt anzusprechen
(natürlich nicht, wenn Sie einen Gerätetreiber
schreiben), selbst auf den &unix;-ähnlichen Systemen, die
es Ihnen erlauben:</para>
<para><emphasis>&unix; ist eine Abstraktion!</emphasis></para>
<para>Es gibt einen wichtigen Unterschied in der
Design-Philosophie zwischen <acronym>&ms-dos;</acronym> und
&unix;. <acronym>&ms-dos;</acronym> wurde entworfen als
Einzelnutzer-System. Es läuft auf einem Rechner mit einer
direkt angeschlossenen Tastatur und einem direkt
angeschlossenem Bildschirm. Die Eingaben des Nutzers kommen
nahezu immer von dieser Tastatur. Die Ausgabe Ihres Programmes
erscheint fast immer auf diesem Bildschirm.</para>
<para>Dies ist NIEMALS garantiert unter &unix;. Es ist sehr
verbreitet für ein &unix;, daß der Nutzer seine
Aus- und Eingaben kanalisiert und umleitet:</para>
<screen>&prompt.user; <userinput>program1 | program2 | program3 &gt; file1</userinput></screen>
<para>Falls Sie eine Anwendung
<application>program2</application> geschrieben haben, kommt
ihre Eingabe nicht von der Tastatur, sondern von der Ausgabe
von <application>program1</application>. Gleichermassen geht
Ihre Ausgabe nicht auf den Bildschirm, sondern wird zur
Eingabe für <application>program3</application>, dessen
Ausgabe wiederum in <filename>file1</filename> endet.</para>
<para>Aber es gibt noch mehr! Selbst wenn Sie sichergestellt
haben, daß Ihre Eingabe und Ausgabe zum Terminal kommt
bzw. gelangt, dann ist immer noch nicht garantiert, daß
ihr Terminal ein PC ist: Es mag seinen Grafikspeicher nicht
dort haben, wo Sie ihn erwarten, oder die Tastatur könnte
keine <acronym>PC</acronym>-ähnlichen Scancodes erzeugen
können. Es mag ein &macintosh; oder irgendein anderer
Rechner sein.</para>
<para>Sie mögen nun den Kopf schütteln: Mein
Programm ist in <acronym>PC</acronym>-Assembler geschrieben,
wie kann es auf einem &macintosh; laufen? Aber ich habe nicht
gesagt, daß Ihr Programm auf &macintosh; läuft, nur
sein Terminal mag ein &macintosh; sein.</para>
<para>Unter &unix; muß der Terminal nicht direkt am
Rechner angeschlossen sein, auf dem die Software läuft,
er kann sogar auf einem anderen Kontinent sein oder sogar auf
einem anderen Planeten. Es ist nicht ungewöhnlich,
daß ein &macintosh;-Nutzer in Australien sich auf ein
&unix;-System in Nordamerika (oder sonstwo) mittels
<application>telnet</application> verbindet. Die Software
läuft auf einem Rechner während das Terminal sich
auf einem anderen Rechner befindet: Falls Sie versuchen
sollten die Scancodes auszulesen werden Sie die falschen
Eingaben erhalten!</para>
<para>Das Gleiche gilt für jede andere Hardware: Eine
Datei, welche Sie einlesen, mag auf einem Laufwerk sein, auf
das Sie keinen direkten Zugriff haben. Eine Kamera, deren
Bilder Sie auslesen, befindet sich möglicherweise in
einem Space Shuttle, durch Satelliten mit Ihnen
verbunden.</para>
<para>Das sind die Gründe, weshalb Sie niemals unter
&unix; Annahmen treffen dürfen, woher Ihre Daten kommen
oder gehen. Lassen Sie immer das System den physischen Zugriff
auf die Hardware regeln.</para>
<note>
<para>Das sind Vorsichtsmassnahmen, keine absoluten Regeln.
Ausnahmen sind möglich. Wenn zum Beispiel ein
Texteditor bestimmt hat, daß er auf einer lokalen
Maschine läuft, dann mag er die Tastatur-Scancodes
direkt auslesen, um eine bessere Kontrolle zu
gewährleisten. Ich erwähne diese
Vorsichtsmassnahmen nicht, um Ihnen zu sagen, was sie tun
oder lassen sollen, ich will Ihnen nur bewusst machen,
daß es bestimmte Fallstricke gibt, die Sie erwarten,
wenn Sie soeben ihn &unix; von <acronym>&ms-dos;</acronym>
angelangt sind. Kreative Menschen brechen oft Regeln und das
ist in Ordnung, solange sie wissen welche Regeln und
warum.</para>
</note>
</sect2>
</sect1>
<sect1 id="x86-acknowledgements">
<sect1info>
<authorgroup>
<author>
<firstname>Daniel</firstname>
<surname>Seuffert</surname>
<contrib>Übersetzt von </contrib>
</author>
</authorgroup>
</sect1info>
<title>Danksagungen</title>
<para>Dieses Handbuch wäre niemals möglich gewesen
ohne die Hilfe vieler erfahrener FreeBSD-Programmierer aus
&a.hackers;. Viele dieser Personen haben geduldig meine Fragen
beantwortet und mich in die richtige Richtung gewiesen bei
meinem Versuch, die tieferen liegenden Mechanismen der
&unix;-Systemprogrammierung zu erforschen im Allgemeinen und bei
FreeBSD im Besonderen.</para>
<para>Thomas M. Sommers öffnete die Türen für
mich. Seine <ulink
url="http://www.codebreakers-journal.com/content/view/262/27/">Wie
schreibe ich "Hallo Welt" in FreeBSD-Assembler?</ulink> Webseite
war mein erster Kontakt mit Assembler-Programmierung unter
FreeBSD.</para>
<para>Jake Burkholder hat die Tür offen gehalten durch das
bereitwillige Beantworten all meiner Fragen und das
Zurverfügungstellen von Assembler-Codebeispielen.</para>
<para>Copyright &copy; 2000-2001 G. Adam Stanislav. Alle Rechte
vorbehalten.</para>
</sect1>
</chapter>