Biff (Binary Interchange Format)
Schon seit langem reizt es mich, hinter die intimsten Geheimnisse von Excel
zu kommen, es sozusagen beim Liebesspiel zu beobachten. Was wäre dazu besser
geeignet, als sich die Struktur anzuschauen, in der Excel seine Daten speichert,
sozusagen die Aminosäuresequenzen auf Fileebene zu analysieren und
zu entschlüsseln?
Da man das Rad ja nicht jedesmal neu erfinden muss, begann ich also vor Jahren
mit der Suche nach Infos im großen weiten Netz. Ich fand auch einiges
zu diesem Thema, zu meiner Enttäuschung musste ich aber feststellen, das
nichts, aber auch gar nichts mit der Realität übereinstimmte. Keinen
einzigen beschriebenen Record konnte ich mit dem Hexeditor ausmachen und so
wurde das Thema erst mal abgehakt.
Vor kurzem las ich über die Unterschiede, in der Daten in Files gespeichert
werden. Von Little- und Bigendian war da die Rede. Die Tatsache an sich war
mir seit langem bekannt, aber erst jetzt fiel es mir plötzlich wie Schuppen
aus den Haaren. Das Low-Byte bei Intel und Co. ist vorne, also stimmt
die Darstellung in den Dokumentationen mit einem Hex-Dump nicht überein.
Sofort wurde der Hex-Editor angeschmissen und eine Excel-Datei angeschaut. Und
siehe, es ward Licht!
Vom Licht der Erkenntnis, aber auch vom Schatten soll im folgenden die Rede
sein:
Die Struktur von Excel in groben Zügen:
Eine Excel-Datei ist in viele verschiedene Abschnitte unterteilt.
Jeder Abschnitt repräsentiert ein Objekt, das kann ein Chart, ein Worksheet,
ein VBA Blatt oder auch ein Workbook sein.
Jeder Abschnitt besteht aus vielen Records, die jeweils eine Eigenschaft, einen
(Zell) Wert, eine Formel oder ein Format enthalten. Jeder Record besitzt einen
Header, der immer gleich aufgebaut ist. Die ersten beiden Bytes identifizieren
den Record, die nächsten zwei enthalten die Länge der folgenden Daten.
Die einzelnen Abschnitte beginnen mit einem BOF-Record und enden mit einem EOF-Record.
Der BOF-Record zeigt gleichzeitig an, um welche Objektart es sich in diesem
Abschnitt handelt.
Der Abschnitt „Workbook/Global“ sollte an erster Stelle stehen, dahinter kommen
die Tabellenblätter, dann Charts, VBA etc.. Die Position des ersten BOF’s
ist ein Offset für verschiedene andere Zeiger, so dass man sich diesen
merken sollte. In diesem Objekt „Workbook/Global“ sind Einstellungen des Workbooks
gespeichert, wie Mappenschutz, Kennwörter, Formate, benutzte Schriftarten
aber auch Daten wie die Shared-Stringgtabelle und die XF-Tabellen.
Ganz wichtig sollen auch die Boundsheet-Records sein, diese enthalten Namen,
Typ und Streampositionen der einzelnen Sheets. Zu den Streampositionen muss
als Offset die erste BOF-Position addiert werden und schon sollte man den BOF
des gewünschten Tabellenblatts haben.
In diesem „Blatt“ Objekt gibt es ziemlich zu Beginn einen IndexRecord, der Infos
über die erste/letzte benutzte Zeile und erste/letzte benutzte Spalte
enthält. Dieser enthält auch einen Zeiger (plus 1. BOF) auf den DBCell-Record.
Der DBCell-Record enthält einen Zeiger rückwärts
auf den ersten Row-Record, der Infos wie Zeilenhöhe- und Formate enthält.
Darüber hinaus enthält der DBCell-Record als Array die Offsets zu
den jeweils ersten Zellen einer Zeile, bezogen auf den Beginn des zweiten
Row-Records.
Kompliziert genug? Es kommt noch besser!
In den Zell-Records sind die Daten nicht immer direkt gespeichert. Strings sind
beispielsweise in der Shared-String Tabelle gespeichert, die Zell-Records enthalten
nur den Index darauf. Formate sind ein Index auf die XF Tabelle, die gemeinsam
benutzte Formate enthält.
Formeln werden nicht im Klartext gespeichert, wie er in Excel zu sehen ist,
sondern als Token-Sequenz.
Die einfache Formel
=A1+B1
steht dort etwa so:
(Token, um Operand auf Stack zu legen)(ZeileSpalte A1)
(Token, um Operand auf Stack zu legen)(ZeileSpalte B1)
(Token Operation) (Opcode +)
Die Funktion „Datum“ mit der Funktionsnummer 0041h ist in meiner Klasse umgesetzt,
andere können durch Probieren ermittelt werden. Falls eine Funktion unbekannt
ist, wird von meiner Klasse ein Hex-Dump geliefert.
Besondere Schwierigkeiten bereiten Zellen mit Zahlen als Inhalt.
IEEE Gleitpunktzahlen entsprechen noch einem Double, weshalb ein einfaches Kopieren
auf ein Double mittels der API-Funktion CopyMemory klappt.
Die Probleme beginnen bei den platzsparenden RK-Zahlenformaten, die von Excel
bevorzugt verwendet werden. Diese sind 4 Bytes lang, wobei 2 Bits davon die
Art der Zahlen kennzeichnen.
Es gibt zum einen Gleitkommazahlen mit 30 Bit Länge (18 Bit Mantisse, 11
Bit Exponent, 1 Vorzeichenbit) und es gibt 30 Bit Vorzeichenbehaftete Ganzzahlen.
Die Gleitkommazahlen lassen sich nicht mittels CopyMemory
in ein Single überführen und auch die Ganzzahlen bedürfen einer
besonderen Behandlung. Ich habe viele Stunden damit verbracht, die Probleme
dabei (vermeintlich) zu lösen. So ganz sicher, dass das unter allen Umständen
funzt, bin ich mir aber trotzdem nicht.
Weiter oben habe ich öfter das Wort soll benutzt. Warum?
Ganz einfach, Excel hält sich an keine Konventionen. Nach einer erneuten
Speicherung ist plötzlich nicht mehr der BOF „Workbook/Global“ an erster
Stelle, sondern ein ganz anderer. Ein Abschnitt am Ende enthält möglicherweise
keinen EOF mehr. Dieser, und ein Teil der Daten beginnen dann schon mal vor
dem ersten BOF. Die Zeiger, die eigentlich ein Schlüssel für hohe
Verarbeitungsgeschwindigkeit sind, kann man dann meistens vergessen. Sie
stimmen einfach nicht mehr. Wenn man mit diesen Zeigern einen Zellwert sucht,
dann gute Nacht. Viel Mühe musste ich aufwenden, um trotzdem an die Daten
zu kommen. Ich kann mir vorstellen, dass sogar Excel, der Verursacher, damit
Probleme bekommen kann. Zumindestens die Performance dürfte darunter nachhaltig
leiden, auch wenn Excel so fehlertolerant ist, mit dem selbst erzeugten Mist
noch klarzukommen.
Die vorliegende Mappe soll aufzeigen, wie man eine Arbeitsmappe analysieren
kann. Die Klassen und Funktionen sind nicht ausgiebig getestet und auch nicht
optimiert. Sie können und werden sicher auch den einen oder anderen Fehler
haben. Viele Recordarten sind gar nicht berücksichtigt und erscheinen als
unbekannt, obwohl sie sehr wohl bekannt sind, aber noch (oder auch nie) nicht
umgesetzt sind. Viele Mappen bringen beim Parsen einen Fehler. Am besten eine
neue Mappe angelegt, Werte eingetragen und geschaut was passiert. Das ist IMO
der einzige Weg, den Aufbau wirklich zu verstehen.
Ich habe auch noch nicht begonnen, mich mit den VBA Modulen zu beschäftigen,
es fehlt mir einfach etwas die Zeit.
Viel Spaß beim Probieren!
Beispieldatei (Biff.zip 163 KB)
P.S. Die Datei und der Code sind selbstverständlich nicht geschützt!
Und auch hier gilt: Sie können den Code uneingeschränkt benutzen,
eine Veröffentlichung ohne Rückfrage bitte ich, zu unterlassen.
Wenn euch die Homepage gefällt, hätte ich auch nichts gegen einen
Eintrag ins Gästebuch einzuwenden.