Dieser Beitrag will die erschreckenden Dokumentationslöcher des Lattice-C-Entwicklungspakets (Version 5.50) zumindest ein wenig auffüllen helfen. Der Pferdefuß bei diesem ansonsten nämlich recht brauchbaren C-System ist das (deutsche) Benutzerhandbuch - es wirft zumindest für den professionellen Programmierer mehr Fragen auf, als es beantwortet.
Zur Ehrenrettung des deutschen Übersetzers möchte ich einmal annehmen, daß die Ursache des „Übels“ bei der von der englischen Firma HiSoft „verbrochenen“ Originalfassung dieses Handbuchs hegt (allerdings wirken manche Passagen im Handbuch wie mit einem simplen Übersetzungsprogramm erzeugt). Die hier geäußerte Kritik betrifft übrigens nicht die beiden sehr gut gestalteten Bibliotheks-Handbücher des Pakets, wenn man mal davon absieht, daß sie den Inhalt der Bibliotheken des Compiler-Systems in der Version 5.00 beschreiben (eine zusätzliche Beschreibung der Bibliotheksneuheiten der Version 5.50 in einer „Read Me“-Datei wäre ja wohl mindestens zu erwarten gewesen).
Fangen wir bei der Beschreibung des C-Compilers an. Wie in der allgemeinen „Read Me“-Datei richtig vermerkt wurde, hat sich bei den Compiler-Optionen und den Options-Dialogboxen der integrierten Oberfläche (LC5.PRG) einiges gegenüber den Angaben (und Abbildungen) im Benutzerhandbuch verändert. Leider wurde dabei die Gelegenheit versäumt, einige Fehler und Auslassungen bei den Compiler-Optionen zu korrigieren:
-mf erzwingt die Erzeugung von Standard-Stackframes bei allen übersetzten Funktionen, d.h., es werden LINK-u nd UNLK-Befehle benutzt, damit der Inhalt des Frame-Pointers (normalerweise A6) sich immer auf die aktuelle Funktion bezieht.
-rx Hiermit kann ein bestimmtes Adreßregister x (x = 2-6) als Frame-Pointer eingestellt werden. Die Default-Einstellung ist A6; sie sollte in der Regel nicht verändert werden!
-rb entspricht der Einstellung „Parameter: Beide“ in der Dialogbox „Compileroptionen - Generell“. Bei mit dieser Option übersetzten Funktionen können die Aufruf-Argumente sowohl per Register als auch über den Stack übergeben werden. Derartige Universalfunktionen besitzen hierfür zwei getrennte Einsprungstellen [Bennenung auf Linker-Ebene: C-Funktionsname einmal mit vorangestelltem Unterstrich (_) und einmal mit Klammeraffe (@)]. Diese Universalität hat allerdings ihren Preis: der erzeugte Programmcode wird deutlich länger, vor allem bei Funktionen mit langen Parameterlisten.
-s entspricht der Einstellung „Standard Sektionsnamen“ in der Dialogbox „Compileroptionen - Objekt“. Ohne diese Option erzeugt der Compiler „unbenannte“ Sektionen für die Bereiche Code, far DATA und far BSS. Bei gesetzter Option werden die Sektionsnamen „text“, „data“ und „udata“ vergeben. Die Sektionen für die Bereiche near DATA und near BSS tragen immer den Namen „__MERGED“.
-mc entspricht der Einstellung „Kein Stack mischen“ in der Dialogbox „Compileroptionen - Objekt“!? Es fragt sich allerdings, was diese Einstellung überhaupt bedeutet...
-as Diese Option wird vom Compiler zurückgewiesen!?
-aw Hier fehlen ein paar warnende Worte im Handbuch: Bei eingeschalteter Option werden nämlich char-Argumente als einzelne Bytes auf den Stack gelegt. Folgt darauf ein 16- oder 32-Bit-Wert, liegt dieser garantiert auf einer ungeraden Adresse. Eine mit dieser Option übersetzte Funktion erwartet bei char-Parametem allerdings jeweils ein nachfolgendes Null-Byte, damit der nächste Parameter auf dem Stack wieder auf gerader Adresse liegt. Daher: Verwenden Sie niemals diese Option, wenn Ihr Programm Funktionen bzw. Funktionsaufrufe mit char-Argumenten enthält (bzw. schreiben Sie die aufgerufenen Funktionen unter Berücksichtigung des korrekten Parameter-Interfaces in Assembler).
-y entspricht der Einstellung „Lade A4 mit JLinkerDB“ in der Dialogbox „Compileroptionen-Objekt“. Dadurch wird sichergestellt, daß beim Einsprung in jede unter dieser Option übersetzte Funktion der Basiszeiger zur 16-Bit-Adressierung von „near“-Daten (A4) tatsächlich auf den Beginn des „Near-DATA-Segments“ zeigt.
-dn Die diversen Debug-Optionen (n = 0-5) haben folgende Entsprechungen im Debug-Pop-Up-Menü der Dialogbox „Compileroptionen - Generell“: 0 - Nicht, 1 - Nur Zeile, 2 - Lokal, 3 -Lokal/bündig, 4 - Voll/bündig, 5 -Voll. Bei den „bündigen“ Varianten werden immer Standard-Stackframes erzeugt (siehe Option -mf), auch wenn sonst keine Optionen mit entsprechender Wirkung gesetzt sind. Was den Unterschied zwischen „Lokal“ und „Voll“ im Detail ausmacht, ließ sich leider noch nicht genau feststellen.
Als Entsprechung der Einstellung „Liste Svstemincludes“ in der Dialogbox „Compileroptionen - Listing“ wird im Handbuch -gi angegeben. Tatsächlich ist dies aber die Option für „Liste Userincludes“, die korrekte Angabe wäre -gh.
Wenig Genaues findet sich im Handbuch auch zum Thema „Konventionen des Funktionsaufrufs mit Parameterübergabe in Registern“, lediglich im Abschnitt „Schnittstelle C und Assembler“ im Assembler-Kapitel stehen ein paar Grundinformationen. Hier also das Fehlende: Das erste Zeigerargument (Reihenfolge von links nach rechts) wird in A0 übergeben, das zweite in Al usw., bis zur maximal eingestellten Anzahl von Adreß-Übergaberegistern. Das gleiche gilt analog für Argumente der Typen char, int und long, die der selben Reihenfolge nach in den Datenregistern D0, D1 usw. übergeben werden. Dabei ist es für die Registerbelegung gleichgültig, ob z.B. zwei Integer-von zwei Zeiger-Argumenten gefolgt werden, oder ob sich jeweils Integer- und Zeiger-Argumente in der Aufrufliste abwechseln! Alle „überzähligen“ Argumente (solche, für die keine Übergaberegister mehr „frei“ sind) werden standardgemäß in umgekehrter Reihenfolge auf dem Stack abgelegt, d.h. das letzte Argument in der Liste wird als erstes auf den Stack „gepusht“. Bei Funktionen mit variabler Argumentliste werden die Argumente grundsätzlich nur über den Stack übergeben!
Soweit die Optionen -ha2, -hd3 und -hf0 (A0, A1, D0, D1 und D3 zur Argumentübergabe) sowie -rr (bzw. das Schlüsselwort regargs) benutzt werden, entspricht diese Aufrufkonvention der Grundeinstellung beim großen Konkurrenten Pure C (vormals Turbo C). Leider gibt es jedoch ein paar kleine, aber sehr (!) wichtige Unterschiede: Während Pure C Zeigerergebnisse grundsätzlich in A0 zurückgibt, verwendet Lattice-C zu diesem Zweck immer D0! Außerdem ergänzt Lattice-C alle Funktionsnamen auf Objectcode-Ebene durch einen vorangestellten Klammeraffen (@) oder Unterstrich (), während Pure C alle Namen unverändert läßt. Mögen sich die Compiler-Bauer die Köpfe darüber heiß reden, welche Regelung denn nun die bessere sei - für den Benutzer beider C-Systeme bleibt als Fazit, daß vollständige Code-Kompatibilität zwischen Pure- und Lattice-Compilaten nicht erzielbar ist [mal ganz abgesehen davon, daß die Objectcode-Formate beider Systeme natürlich (! ?) inkompatibel zueinander sind].
Wissen Sie, welche Entdeckungen möglich sind, wenn man sich die Programmdatei eines Compilers mit einem Disk-Monitor oder geeigneten Editor genauer ansieht? Nun, zum Beispiel undokumentierte Schlüsselwörter, die auf „geheime“ Features des Systems hinweisen! Beispiele gefällig?
Das Schlüsselwort _chip (oder auch chip) kann vor jede Datendefinition gesetzt werden (so wie _near oder _far). Es bewirkt, daß die betreffenden Daten in einer „Far DATA“-Sektion mit dem Namen „chip“ untergebracht werden. Vor Funktionsdefinitionen ist _chip zwar auch zulässig, bewirkt aber anscheinend nichts. Daß die chip-Sektion etwas Besonderes für den Compiler darstellt, erkennt man schon daran, daß er sie in seiner Abschlußmeldung am Ende des Übersetzungsvorgangs individuell berücksichtigt. Diese Meldung hat das folgende Format (falls alle möglichen Datensektionstypen im Programm Vorkommen):
Module size
P=n1 D=n2 U=n3 C=n4 F=n5 UF=n6
Dabei stehen die großen Buchstaben vor den Gleichheitszeichen für den Typ der jeweiligen Programmsektion (P - Code, D - near DATA, U - near BSS, C - chip, F -far DATA, UF - far BSS) und die Angaben n1 bis n6 für die Längen der Sektionen in Bytes (hexadezimal). Was der Compiler zuläßt, verträgt der Linker aber noch lange nicht - Programmteile mit einer chip-Sektion werden als fehlerhaft zurückgewiesen! Es bleibt daher offen, welche praktische Bedeutung das chip-Schlüsselwort haben soll! In der Compiler-Programmdatei findet sich allerdings eine große Anzahl weiterer Strings, die auf den AMIGA-Ursprung des ganzen Systems hindeuten:
libcall, GfxBase, syscall, APTR, IntuitionBase, DOSBase, BPTR, Flags, LayersBase, ClistBase, MsgPort, Node, ExpansionBase, List, Message, MathTransBase, EXEC_NODES_H, EXEC_LISTS_H, EXEC_PORTS_H, CStringBase, EXEC_IO_H, Gadget, execportsh (alle Namen in der Originalreihenfolge).
Möglicherweise stammt die chip-Sektion aus dieser „Ecke“, und die HiSoft-Programmierer haben nur „vergessen“, dieses Feature in der ATARI-Version zu inaktivieren. Nebenbei: Jeder einigermaßen geübte C-Programmierer weiß, wie man mit den Mitteln der bedingten Übersetzung (#if/#elif/#else/#endif) Mehrversions-Quelltexte gestaltet, bei denen alle Daten/Anweisungen, die für die gewählte Version unerheblich sind, erst gar nicht im Übersetzungsergebnis (letztendlich der Programmdatei) landen. Die „Experten“ der Firma HiSoft scheinen davon jedoch noch nichts gehört zu haben, denn sonst dürfte man keine AMIGA-spezifischen Schlüsselwörter in der ATARI-Version des Compilers finden!
Außer den AMIGA-Relikten existieren aber noch andere, auch in der ATARI-Version zulässige und brauchbare Schlüsselwörter, die im Handbuch keine Erwähnung finden. Da gibt es z.B. die Attribute near, far und huge, die einfache Synonyme für ihre Namensvettern mit den zwei vorangestellten Unterstrichen (_near usw.) darstellen. Dann wäre da noch das Schlüsselwort _saveds, das ganz kurz im Linker-Kapitel erwähnt wird (natürlich ohne hinreichende Erklärung). Man benutzt es bei Funktionsdefinitionen, und es bewirkt, daß am Anfang der betreffenden Funktion der Prozessorbefehl LEA JLinkerDB,A4 eingefügt wird. Dadurch wird sichergestellt, daß der Basiszeiger für Datenadressierungen in den Near-Sektionen (A4) auf den Anfang dieser Sektionen zeigt. Von daher ist der Name _saveds natürlich etwas irreführend, denn es wird tatsächlich nichts „gesichert“ sondern im Gegenteil ein Registerwert „restauriert“. Schließlich existieren noch zwei undokumentierte #pragma-Unterdirektiven: Mit #pragma title <Titelstring> definiert man einen beliebigen String, der als Untertitel im Kopf jeder nachfolgenden Listing-Seite auftaucht (soweit überhaupt ein Compiler-Listing erzeugt wird). Sichtbar wird der neu eingestellte Untertitel aber immer erst auf der nächsten Listing-Seite. Einen Seitenvorschub im Listing (Beginn einer neuen Listing-Seite) kann man mit #pragma eject erzwingen.
Nun zu einem weiteren Schwachpunkt des Benutzerhandbuchs, der Beschreibung der Fehler- bzw. Warnungsmeldungen des Compilers. Die Erläuterungen der einzelnen Meldungen sind ganz offensichtlich mit maschineller Hilfe ohne jegliche menschliche Nachbearbeitung übersetzt worden. Daher ein Tip zur „Entzifferung“ der Meldungen: Stellen Sie sich die Texte Wort für Wort ins Englische übersetzt vor (ohne den Satzbau zu ändern), und versuchen Sie dann, diese englische Version mit etwas Sprachgespür wieder ins Deutsche zurückzuübersetzen. Ein Manko läßt sich aber auch damit nicht beseitigen - die Meldungen sind in vielen Fälle viel zu telegrammartig und daher kaum nachvollziehbar. Immerhin führte auch hier die gründliche Analyse der Meldungen und ausgiebiges Herumprobieren mit diversen Testprogrammen zu neuen Erkenntnissen. Folgende, vom ANSI-C-Standard abweichende bzw. darüber hinausgehende Features hält der Lattice-Compiler für den geneigten Benutzer bereit:
Lattice-C ermöglicht den Gebrauch von sogenannten Zeilenendkommentaren, wie sie in der ANSI-C-Nachfolge/Erweiterungssprache C++ eingeführt worden sind: Zwei Schrägstriche hintereinander (//) markieren den Beginn eines Kommentartextes, der automatisch durch das Zeilenende abgeschlossen wird.
In switch-Anweisungen ist als Auswahlwert (in den runden Klammem hinter switch) jeder numerische Ausdruck zulässig, der sich in einen short- bzw. long-Ausdruck konvertieren läßt (also z.B. auch ein Fließkommawert). Eine solche Umwandlung wird grundsätzlich ohne Warnungsmeldung durchgeführt! Übrigens unterscheidet Lattice-C je nach Typ des Auswahlwertes zwischen short- und long-switches (bei short-switches ist die Verzweigung schneller und meist auch kürzer im Programmcode). Hier ist Lattice-C außerdem „ANSI-kompatibler“ als Pure C, das ja bekanntlich keine switches mit long-Ausdrücken zuläßt!
Nach den Regeln des ANSI-C-Standards muß der Bedingungsausdruck in einer if-, while-, do- oder for-Anweisung immer in mnden Klammem eingefaßt sein. Lattice-C erlaubt hingegen, diese Klammem wegzulassen. Aus Gründen der Portabilität und des guten Programmierstils sollte man allerdings die Finger von diesem Feature lassen!
Die englischen Fehlermeldungen des Compilers lassen sich übrigens ohne großen Aufwand durch deutsche Texte ersetzen: Kopieren Sie einfach die Datei LC1.FRG aus dem EXTRA-Ordner des Lattice-Pakets in den BIN-Ordner und benennen Sie die Datei dabei in LC1 .LC um. Die sprachliche Qualität der eingedeutschten Meldungen ist allerdings alles andere als überzeugend, weshalb ich mir zum Privatgebrauch neue Fehlertexte geschrieben habe. Damit Sie es mir gleichtun können, hier kurz das Format der entsprechenden Dateien:
Es handelt sich um reine ASCII-Texte, bei denen das Zeilenende jedoch (à la UNIX) mit einem einzelnen Linefeed (ASCII $0A) markiert wird. Unter TOS (und auch MS-DOS) ist dagegen normalerweise die Folge Carriage Return Linefeed (ASCII $0D $0A) üblich. Sie benötigen also auf jeden Fall einen Texteditor, der mit diesem speziellen Zeilenformat umgehen kann (z.B. PKS-Edit). Jede Zeile enthält den Meldungstext zu jeweils einem Fehler, wobei die Zeilennummer auch der Fehlernummer entspricht (1 bis 171). Einige Zeilen enthalten am Ende ein Dollarzeichen ($), welches als Platzhalter für einen Namen anzusehen ist, der vom Compiler je nach Fehlerkontext eingesetzt wird. Solche Dollarzeichen dürfen ausschließlich am Ende der Fehlermeldung erscheinen! Zu Fehlernummern, denen derzeit keine Fehler- oder Warnungs-Meldungen zugeordnet sind, finden sich Zeilen wie „E115:$“ oder „E130“; diese Texte sollten nicht verändert werden.
Soviel zu den Dokumentationslücken des Compilers (was nicht heißen soll, daß es keine weiteren „Löcher“-zu stopfen gilt). Auch der Assembler wird im Handbuch sehr oberflächlich und unvollständig beschrieben. Gerade mal die Hälfte aller tatsächlich zulässigen Assembler-Direktiven wird überhaupt erwähnt, der Rest sei deshalb hier kurz aufgelistet:
COMM, EQU, IDNT, IFC, IFNC, INC-BIN, INCLUDE, LIST, MEXIT, NOLIST, OFFSET, OPSYN, PAGE, RORG, SECTION, SET, SPC, TTL
Hierbei läßt sich die Bedeutung der meisten Direktiven aus dem Vergleich mit anderen Assemblern ableiten. Dennoch bleiben einige Direktiven übrig, die sich hartnäckig der Deutung versagen (für mich waren das IFC, IFNC, INCBIN, IDNT und SPC). Lediglich der Gebrauch von OPSYN wird klar, wenn man sich die Datei AS68.SYN im EXTRA-Ordner des Lattice-Pakets ansieht. Dort werden nämlich mittels OPSYN einige Direktiven des uralten DRI-Assemblers AS68 als Synonyme für Direktiven des Lattice-Assemblers definiert. Fazit: Wer nicht bereit ist, den „großen“ DevPac-Assembler von HiSoft mitsamt dem (hoffentlich besseren) Handbuch zu kaufen, guckt beim Lattice-Assembler ganz schön in die Röhre!
Die Parameter reltype und relsize der Direktive CSECT können übrigens folgende Werte annehmen:
,0,4 - Absolute Adressierung (32 Bit)
,1,2 - PC-relative Adressierung (16 Bit)
,2,2 - Basis-relative Adressierung über A4 (16 Bit)
Bei der Beschreibung der Assembler-Aufrufoptionen fehlen auch diverse Angaben:
-d fügt Zeilennummern-Infos in die erzeugte Object-Datei ein (fürs Debuggen)
-l erzeugt eine Protokoll-Datei (Listing)
-lm erlaubt mehrfache Listing-Zeilen (was auch immer damit gemeint ist!)
-m9 aktiviert MC68851-Anweisungen
Schließlich konnte ich noch einige Informationen über die zulässigen Operatoren in Ausdrücken ermitteln. Neben den arithmetischen Grundoperatoren (+,-,*,/) existieren noch: % (Modulo), ! (ODER), & (UND), ~ (NICHT), » (Rechts-Shift) und « (Links-Shift).
Damit soll es vorläufig genug sein mit den Nachträgen und Korrekturen zum Benutzerhandbuch. Ergänzend möchte ich hier noch ein paar Anmerkungen über einige Bindings in den Lattice-Bibliotheken machen. Für jeden ambitionierten GEM-Programmierer ist es wichtig zu wissen, wie die Details der AES- und VDI-Inter-faces beim verwendeten Compiler-System aussehen. Die Lattice-GEM-Bindings trennen die Interfaces zum AES und zum VDI komplett, d.h., im Gegensatz zu manch anderem System gibt es keine gemeinsam genutzten Control-Arrays oder ähnliches. Fürs AES lauten die Namen der benutzten Arrays (nachzulesen in der Header-Datei AES.H): _AESpb, _AEScontrol, _AESglobal, _AESintin, _AESintout, _AESaddrin und _AESaddrout. Dabei ist _AESpb mit den Adressen der anderen Arrays vorinitialisiert. Um eine beliebige AES-Funktion aufzurufen, müssen zunächst die von der Funktion benötigten Eingangsdaten in _AESintin und evtl. _AESaddrin eingetragen werden. Danach ruft man die Funktion _AESif auf, die folgenden Prototypen besitzt:
int _regargs _AESif (unsigned int table_offset);
Der table_offset berechnet sich dabei zu (AES-Funktionsnummer -10) * 4. Es bietet sich daher an, folgendes Makro zu definieren:
#define CallAES(opcode)_
AESif((((unsigned int) (opcode)) - 10) << 2)
Diese Funktion erledigt alles weitere, so daß man am Schluß nur noch die Ergebnisdaten aus _AESintout und evtl. _AESaddrout auslesen muß. Beim VDI-Interface werden die Arrays _VDIpb, _VDIcontrl, _VDIintin, _VDIintout, _VDIptsin und _VDIptsout verwendet (in VDI.H zu finden), wobei auch hier _VDIpb mit den entsprechenden Adressen vorinitialisiert ist. Zum Aufruf irgend einer VDI-Funktion müssen zunächst die Eingangsdaten der Funktion in _VDIint in und _VDIptsin abgelegt werden. Je nach Funktion sind außerdem noch Eintragungen in den Feldern _VDIcontrl[5] (Unter-Opcode) und _VDIcontrl[7..] vorzunehmen. Dann ruft man die Funktion _VDIif auf, die mit dem folgenden Prototyp vereinbart ist:
int_regargs_VDIif (unsigned long arg1, unsigned long arg2);
Dabei sind alle für den eigentlichen VDI-Aufruf noch fehlenden Daten in komprimierter Form in den Argumenten arg1 und arg2 untergebracht:
arg1, obere 16 Bits: Flandle der (virtuellen) VDI-Workstation
arg1, untere 16 Bits: Anzahl der Werte in _VDIintin
arg2, obere 16 Bits: Opcode der VDI-Funktion
arg2, untere 16 Bits: Anzahl der Werte in _VVDIptsin
Auch hier bietet sich die Definition eines Makros zum vereinfachten Aufruf von _VDIif an:
#define CallVDI(handle. opcode, intin_count, ptsin_count)
\_VDIif((unsigned long)(((handle) << 16) | (intin_count)),
\(unsigned long) (((opcode) << 16) | (ptsin_count)))
Schlußendlich müssen die Funktionsergebnisse noch aus den Arrays _VDlcontrl, _VDIintout und _VDIptsout ausgelesen werden. Die Lattice-GEM-Bindings sind also sehr sauber implementiert worden (was man von ihren Gegenstücken beim Pure C leider nicht behaupten kann). Allerdings gehen sie „blind“ davon aus, daß alle (!) Register bei einem AES- oder VDI-Aufruf intern gerettet und wieder restauriert werden, was von ATARI nicht ausdrücklich garantiert worden ist. Für das ROM-VDI (und wohl auch NVDI) scheint diese Annahme immerhin zu stimmen.
Ein weiteres interessantes Detail der Lattice-Bibliotheken ist die Implementation der Eingabefunktionen aus der scanf-Familie. Diese durch den ANSI-C-Standard definierte Gruppe enthält die Funktionen scanf, sscanf und fscanf (für die Input-Kanäle Standardeingabe, Speicher und Datei). Zu jeder dieser Funktionen existiert ein Ausgabegegenstück in der printf-Gruppe (printf, sprintf und fprintf). Allerdings definiert ANSI-C noch drei weitere, sehr nützliche printf-Varianten: vprintf, vsprintf und vfprintf. Das v im Namen dieser Funktionen bedeutet, daß die auszugebenden Werte nicht über die Argumentliste, sondern indirekt über ein abschließendes Argument des Typs va_list übergeben werden. Man benötigt diese Varianten, um selbst Ausgabefunktionen zu schreiben, die einerseits eine variable Argumentenliste besitzen und andererseits den printf-Mechanismus benutzen sollen. Leider fehlen die entsprechenden Gegenstücke in der scanf-Gruppe. Deshalb wurden bei den Pure C-Bibliotheken diese (nicht im ANSI-Standard enthaltenen) Funktionen hinzugefügt. Vor allem die vsscanf-Funktion kann man öfters gut gebrauchen, deshalb will ich hier kurz vorführen, wie man sie sich selbst als Ergänzung der Lattice-Bibliotheken schreiben kann.
Eine Analyse der im Lattice-Paket enthaltenen scanf-Varianten zeigt, daß sie sich nur geringfügig unterscheiden. Das kommt daher, weil sie alle praktisch nur als Interface zu einer bibliotheksinternen „Kernfunktion“ realisiert worden sind. Diese Kernfunktion erledigt die eigentliche Arbeit und ist wie folgt definiert:
int _regargs_sf (int (*p_getc) (void),
void (*p_ungetc) (int c), int *p_count,
const char *format, va_list arglist);
Die Argumente haben dabei folgende Bedeutung: p_getc ist die Adresse einer Funktion, die pro Aufruf ein neues Eingabezeichen als Ergebnis liefert. Analog zur Standardfunktion getchar sollte von ihr der Wert EOF (-1) zurückgegeben werden, falls der Eingabekanal geschlossen wurde (z.B. Dateiende erreicht). Als Gegenstück dazu muß in p_ungetc die Adresse einer weiteren Funktion angegeben werden, die dazu dient, jeweils ein zuvor mit *p_getc eingelesenes Zeichen (c) wieder in den Eingabestrom „zurückzuschieben“ (à la ungetc). Beide Hilfsfunktionen zusammen müssen eine (modul-)globale Zählervariable vom Typ int verwalten (Inkrementieren beim Einlesen und Dekrementieren beim „Zurückstellen“ eines Zeichens), dessen Adresse an _sf übergeben werden muß (p_count). In dieser Variablen wird die Anzahl der bereits „verkonsumierten“ Zeichen aus dem Eingabestrom gezählt, weshalb sie am Anfang auch immer auf Null initialisiert werden muß. Die Adresse des von allen scanf-Varianten benötigten Format-Strings wird als format an _sf weitergeleitet. Das Argument arglist schließlich zeigt auf die Argumentenliste mit den Adressen der Variablen, in denen die eingelesenen Daten abgelegt werden sollen.
Das Listing „VSSCANF.C“ zeigt exemplarisch, wie man mit diesen Informationen eine nahezu beliebige scanf-Variante (hier eben vsscanf) schreiben kann. Sie können dieses Minimodul mit dem Lattice-Compiler übersetzen und mittels OML.TTP in die Grundbibliothek(en) (LCxxxxx.LIB) einbinden. Achten Sie dabei aber darauf, daß Sie die Bibliothek passend zu den beim Übersetzen eingestellten Compiler-Optionen wählen (int-Längen-Default, Argumentübergabe, Datenadressierung).
Im zweiten Teil dieses Beitrags werde ich den inneren Aufbau von Objectcode-Dateien beschrieben, wie sie der Lattice-C-Compiler in der Version 5.50 und der dazugehörende Assembler erzeugen. Auch dieses Format ist durch Lattice/HiSoft/ CCD (wie vieles andere) leider nicht dokumentiert worden, daher beruhen alle weiteren Informationen dazu auf ausgiebigen Analysen des Autors. Daraus ergibt sich natürlich ein „inoffizieller“ Charakter der Beschreibungen! Es sei daher von vornherein darauf hingewiesen, daß die Informationen in einigen Details lückenhaft sein können. Der interessierte Leser mag diesen Umstand als Ansporn verstehen, sich selbst weiter mit solchen Details auseinanderzusetzen und die dabei gewonnenen weiterführenden Erkenntnisse der Öffentlichkeit zugänglich zu machen.
Anlaß zur Analyse des HiSoft-Formates war ein zunächst sehr mysteriöser Fehler, der bei dem Versuch auftrat, eine längere Object-Datei in eine Bibliothek einzubinden. Diese Datei war das Übersetzungsergebnis eines großen Assembler-Quelltextes, der zuvor vom MadMac- ins Hisoft-Format umgeschrieben worden war (diese Umsetzung geriet wegen der Dokumentationslücken im Benutzerhandbuch komplizierter als zunächst erwartet). Der erwähnte Fehler manifestierte sich jedenfalls in der Meldung „Invalid Hunk Type“ des Bibliotheks-Verwaltungs-Programms OML.TTP (selbstverständlich ohne genaueren Hinweis auf Ort und Art der Fehlerentstehung). Es bestand also Klärungsbedarf: Was ist ein „Hunk“, wie findet man ihn in einer Object-Datei, und was ist in diesem Zusammenhang zulässig und was nicht? Folgendes offenbarten die Analysen dann:
Zunächst einmal zeigte sich schnell, daß alle (!) Einzelgrößen in einer HiSoft-Object-Datei aus Langwörtern (32 Bits) bestehen bzw. auf Langwortgrenzen ausgerichtet werden (mit Ausnahme einiger Angaben in den Debug-Tabellen). Daß dies eine nicht unbeträchtliche Platzverschwendung darstellt, erkennt man schon am drastischen Größenunterschied zwischen den Übersetzungsergebnissen (Objectcode) des Lattice- und des Pure C-Compilers - bis zu dreimal längere Compilate des Lattice-Systems bei identischen Quelltexten (jeweils mit maximaler Debugger-Unterstützung). Besonders negativ wirkt sich dies beim Anlegen von umfangreichen Bibliotheken aus, weil OML.TTP leider bei 256 KB Gesamtlänge schlapp macht (was als echter Designfehler des Programms zu betrachten ist)!
Die Platzverschwendung betrifft im besonderen auch die Darstellung von Namen (für Sektionen, Module, Symbole, Bezeichner, Dateien etc.). Vor dem eigentlichen Namens-String steht nämlich immer ein Langwort, dessen Wert die Länge des Strings in Langwörtem (!) angibt. Bei diesem Längenpräfix können im höchstwertigen Byte einzelne Bits gesetzt sein, die meistens zur Klassifizierung des Namens dienen. Der String selbst ward, soweit erforderlich, mit Null-Bytes bis auf die nächste Langwortgrenze aufgefüllt.
Jede HiSoft-Object-Datei ist (als Grobstruktur sozusagen) in die erwähnten „Hunks“ aufgeteilt. Jeder „Hunk“ beginnt mit einer eindeutigen Kennung (Langwort) und ist in der Regel aus diversen Untergruppen aufgebaut (die selbst wiederum jeweils mit einer speziellen Kennung anfangen). Dabei definiert ein „Hunk“ je eine Code- oder Datensektion (inklusive Relokationstabellen, Debug-Daten und Symbollisten). Das Ende jedes „Hunks“ wird ebenfalls durch eine eindeutige Kennung definiert (nicht so jedoch die Enden der einzelnen Untergruppen des „Hunks“, die stattdessen Längeninformationen beinhalten).
Zusätzlich zu den „Hunk“-Abschnitten befindet sich am Anfang jeder Datei die Definition des Modulnamens:
$000003E7 <Modulname>
Alle in spitze Klammern eingeschlossenen Bezeichnungen werden im folgenden (ä la Backus-Naur) als Metasymbole behandelt, d.h. sie werden noch genauer definiert bzw. beschrieben (soweit sie nicht selbsterklärend sind). Für alle Namen (wie hier den Modulnamen) gilt das weiter oben über Namensdarstellungen Gesagte. Der Compiler des Lattice-Pakets verwendet in der Regel den (kleingeschriebenen) Dateinamen des Quelltextes ohne Pfadanteil als Modulnamen, während der Assembler den kompletten Dateinamen mit Pfad, aber ohne Punkt und Extension einträgt.
Die durch die „Hunks“ definierten Sektionen werden in der Reihenfolge ihres Auftretens von Null beginnend durchnumeriert. Alle Bezüge innerhalb einer Sektion auf eine andere Sektion des Moduls benutzen diese Nummern! Und nun zum genaueren Aufbau der „Hunks“.
Jeder „Hunk“ beginnt optional mit der Definition eines Sektionsnamens:
$000003E8 <Sektionsname>
Fehlt diese Definition, ist die betreffende Sektion „unbenannt“. Bei den vom C-Compiler erzeugten „Hunks“ ist dies z.B. bei den Sektionen für Code, „Far DATA“ und „Far BSS“ der Fall, soweit nicht die Option -s gesetzt wurde.
Es folgt die Definition des eigentlichen Sektionsinhalts (Code oder Daten), wobei es drei Möglichkeiten gibt:
$000003E9 <Sektionslänge in Langwörtern> <Sektionsinhalt (Code)>
$000003EA <Sektionslänge in Langwörtern> <Sektionsinhalt (DATA)>
$000003EB <Sektionslänge in Langwörtern> <Sektionsinhalt (BSS)>
Der jeweilige Sektionsinhalt wird gegebenenfalls mit Null-Bytes auf Langwortgrenzen aufgefüllt. Die Sektionslänge bezieht sich auf die Daten nach (!) der Längenangabe selbst. Alle zu relozierenden Daten (16- und 32-Bit-Einträge) werden entweder als 0 (bei externen Referenzen) oder als Byte-Offset zum Anfang der dazugehörenden Sektion (bei modulinternen Referenzen) angegeben. Keine Regel ohne Ausnahme: Bei der ominösen chip-Sektion (die immer vom Typ DATA ist) wird als Kennung nicht wie erwartet $000003EA, sondern $400003EA eingetragen! Vermutlich ist der Wert $40 im Highbyte der Kennung so etwas wie ein Flag, um den „speziellen“ chip-Sektionstyp zu markieren. Diese Spezialkennung ist jedoch auch der Grund, warum der Lattice-Linker CLINK Object-Dateien, die eine solche Eintragung besitzen, mit der Fehlermeldung Nr. 509 „Unknown hunk type 1073742826 in Pass2“ abschmettert.
Meist folgt dann eine Relokationstabelle für 32-Bit-Referenzen (absolute Adressen), die mit der Kennung $000003EC eingeleitet wird. Ihr folgen eine oder mehrere Untertabellen mit folgendem Aufbau:
<Anzahl der zu relozierenden Referenzen>
<Modulinterne Nummer der Sektion, auf die sich die zu relozierenden Referenzen beziehen>
<Byte-Offset der zu relozierenden Stelle in der aktuellen Sektion>
Der letzten Angabe folgen evtl, weitere Offset-Angaben entsprechend der Anzahl der zu relozierenden Stellen. Nach einer solchen Untertabelle folgt entweder eine weitere Tabelle oder eine Null (Langwort) als Endmarke.
Alternativ oder zusätzlich kann auch eine Relokationstabelle für 16-Bit-Referenzen (PC-oder basis-relative Adressen) angegeben werden, die mit der Kennung S000003F8 beginnt und ansonsten den gleichen Aufbau wie eine 32-Bit-Tabelle besitzt.
Die nächste Untergruppe eines „Hunks“ definiert alle globalen Symbole, die im Modul enthalten sind oder referenziert werden. Sie beginnt immer mit der Kennung $000003EF. Danach folgt für jedes Symbol je eine Untertabelle (mit unterschiedlichem Aufbau je nach Symboltyp). Abgeschlossen wird eine solche symbolische Referenztabelle für den Linker durch eine Null (Langwort). Die Untertabelle für ein globales Symbol, das in der aktuellen Sektion definiert wurde, hat den einfachsten Aufbau:
<Symbolname ($01 im Highbyte des Längenpräfixes)>
<Byte-Offset der Definitionsstelle des Symbols in der aktuellen Sektion>
Bei externen Symbolreferenzen mit 32 Bit (absolute Adressierung) kommt ein anderes Untertabellenformat zur Anwendung:
<Symbolname ($81 im Highbyte des Längenpräfixes)>
<Anzahl der Referenzen auf dieses Symbol in der aktuellen Sektion>
<Byte-Offset einer Referenzierung des Symbols in der aktuellen Sektion>
Dem letzten Eintrag folgen entsprechend dem Wert im zweiten Eintrag evtl, weitere Offset-Angaben. Externe 16-Bit-Referenzen, die PC-relativ reloziert werden sollen, haben ein ganz ähnliches Untertabellenformat, wobei der einzige Unterschied darin besteht, daß im Highbyte vom Längenpräfix des Symbolnamens der Wert $83 steht. Gleiches gilt für externe, basisrelative Referenzen (ebenfalls 16 Bit), bei denen als Markierungswert im Highbyte $86 steht. Generell scheint also bei externen Referenzen das höchstwertige Bit im Längenpräfix des Symbolnamens gesetzt zu sein.
Während die soeben beschriebene „Hunk“-Untergruppe immer vorhanden ist, findet man die folgende Untergruppe nur dann, wenn irgendwelche Debug-Informationen in der Object-Datei enthalten sind. Bei „Erzeugnissen“ des Lattice-Compilers ist dies der Fall, wenn eine der Optionen -d1 bis -d5 benutzt wurde. Beim Hisoft-Assembler muß dazu die Option -d verwendet werden. Die fragliche Untergruppe definiert (nochmal) alle in der aktuellen Sektion definierten globalen Symbole (das sind alle Symbole, die exportiert werden). Die Untergruppe beginnt mit der Kennung $000003F0. Nachfolgend findet sich pro Symbol ein Eintrag der Form
<Symbolname> <Byte-Offset der Symboldefinition in der aktuellen Sektion>
wobei diesmal das Highbyte im Längenpräfix des Symbolnamens den Wert $00 hat! Abgeschlossen wird auch diese Tabelle durch ein Null-Langwort.
Die entscheidenden Debug-Informatio-nen finden sich aber in einer weiteren „Hunk“-Untergruppe, die ebenfalls nur unter den oben erwähnten Bedingungen in einer Object-Datei enthalten ist. Sie besitzt folgenden Header:
$000003F1 <Länge der nachfolgenden Debug-Daten in Langwörtern>
Was dann tatsächlich folgt, hängt davon ab, mit welchen Debug-Optionen compiliert bzw. assembliert wurde. Zunächst der einfachere Fall: „Zeilennummern-Infos“. Darunter versteht Lattice/HiSoft eine Cross-Referenz-Tabelle zwischen einzelnen Quelltextzeilen und Offsets in der aktuellen Code-Sektion. Wohlgemerkt: Code- und nicht Daten-Sektion, denn Quelltextzeilen, in denen Daten definiert werden, bleiben unberücksichtigt. Daher findet man diese Debug-Untergruppe auch ausschließlich in „Hunks“, die Code-Sektionen definieren! „Zeilennummern-Infos“ werden beim Assemblieren mit der Option -d und beim Compilieren mit der Option -d1 erzeugt. Die Debug-Daten haben dabei folgenden Vorspann:
$00000000 $4C494E45 <Dateiname des Quelltextes>
Der Wert $4C494E45 liest sich als String interpretiert ,LINE‘. Der Dateiname wird komplett mit Pfadanteil angegeben und groß geschrieben. Der Assembler setzt merkwürdigerweise hinter die normale Endung .S des Dateinamens noch das Anhängsel .a (mit kleinem A). Danach folgen die Cross-Referenzen, bestehend aus jeweils zwei Langwörtern pro Referenz:
<Zeilennummer in der Quelltextdatei> <Byte-Offset in der aktuellen Sektion>
Die Numerierung der Zeilen in der Quelltextdatei beginnt dabei mit Eins. Seltsamerweise enthalten die Ausgabetexte des Object-Datei-Disassemblers OMD.TTP Zeilennumerierungen, die bei Null loslaufen (man muß also immer noch Eins dazuaddieren, um auf die intern in der Object-Datei referenzierten Zeilennummern zu kommen). Die Zeileninfo-Daten werden nicht mit einer speziellen Marke abgeschlossen, weil die Gesamtlänge der Debug-Daten ja im Header der „Hunk“-Untergruppe eingetragen ist.
Wenn man einen C-Quelltext mit den Optionen -d2 bis -d5 compiliert, werden Debug-Daten in einem stark abweichenden Format erzeugt. Leider ist es mir nur zu einem sehr geringen Grad gelungen, dieses Format zu „knacken“ (Asche auf mein Haupt...). Auch hier gibt es einen Vorspann:
$00000000 $53524320 <32 Bytes mit dem Modulnamen (mit Null-Bytes aufgefüllt)>
Der Wert $53524320 liest sich als String interpretiert ,SRC mit angehängtem Leerzeichen‘. Der Modulname scheint eine Kopie des Eintrags am Anfang der Datei zu sein (aber ohne Längenpräfix und mit fester Maximallänge). Auf diesen Vorspann folgen drei Lang Wörter, deren Bedeutung nur teilweise zu entschlüsseln war.
<???> <Länge der später folgenden „Zeileninfos“ in Bytes>
<Länge der sich an die „Zeileninfos“ anschließenden Debug-Daten in Bytes>
Danach kommt die Angabe des Quelltext-Dateinamens in einem speziellen Format:
<Langwort: Länge des nachfolgenden Dateinamens in Bytes (immer gerade)>
<Dateiname (evtl, mit einem Null-Byte auf Wortgrenzen aufgefüllt)>
Der Dateiname wird immer komplett mit Pfad angegeben und großgeschrieben. Hinter dieser speziellen Namensdefinition folgt eine Tabelle mit den „Zeilennummern-Infos“, wie sie weiter oben bereits beschrieben wurde. Der einzige Unterschied besteht darin, daß diese (nach wie vor aus Langwörtern bestehende) Tabelle nur noch auf Wort- und nicht mehr Langwortgrenzen ausgerichtet ist.
An die Zeileninfos schließen sich weitere Debug-Daten an, deren Struktur mir allerdings bislang verschlossen geblieben ist. Das einzige, was sich als Hinweis für andere „Forschende“ sagen läßt: Diese Debug-Daten sind mit Sicherheit weder auf Langwort- noch auf Wortgrenzen ausgerichtet, sondern scheinen ein reiner Byte-Strom zu sein (d.h., Wort- bzw. Langw ortwerte fangen unter Umständen auch auf ungeraden Offsets an). Der vor der Angabe des Quelltext-Dateinamens zu findende Eintrag mit der Länge dieser Debug-Daten kann übrigens ebenfalls einen ungeraden Wert haben. Hinter den Daten wird aber, soweit notwendig, immer mit Null-Bytes auf die nächste Langwortgrenze aufgefüllt (damit die damit abgeschlossene „Hunk“-Untergruppe „sauber“ beendet wird)!
Was das normale Object-Datei-Format betrifft, wäre jetzt nur noch eine Kennung nachzutragen: Mit $000003F2 wird jeder „Hunk“ abgeschlossen. Dieser „Endekennung“ folgt entweder ein weiterer „Hunk“ oder aber das Dateiende.
Ach ja, was die seltsame Fehlermeldung „Invalid Hunk Type“ von OML betrifft, mit der alles angefangen hatte: Ursache war ein überzähliges Null-Byte, das durch den Assembler in eine Datensektion „hineingezaubert“ worden war. Dadurch endete die betreffende „Hunk“-Untergruppe in der Object-Datei auf einer ungeraden Adresse! Die Fehlermeldung war daher irreführend, denn nicht der „Hunk Type“ war unkorrekt, sondern eher die „Hunk-Syntax“, und außerdem wäre ein Hinweis auf das Modul und die betroffene Sektion durchaus möglich gewesen. Im Zuge meiner Nachforschungen zeigte sich dann noch, daß das Format der vom Bibliotheks-Verwaltungs-Programm OML.TTP erzeugten Library-Dateien lediglich eine spezielle Variante des normalen Object-Formats darstellt. Folgende Aktionen führt OML aus, wenn ein neues Modul in eine Library aufgenommen werden soll:
Anders als beim GST-Format besteht eine HiSoft-Library also nicht nur aus einer einfachen Aneinanderreihung der Module im Originalzustand. Der „Library-Header“ findet sich am Anfang der Datei und besteht aus zwei Lang Wörtern:
$000003FA <Gesamtlänge aller nachfolgenden Module in Langwörtern>
Hinter den Modulen schließt der „Library-Index“ die Datei ab. Er beginnt mit diesem Vorspann (ebenfalls zwei Langwörter):
$000003FB cLänge der nachfolgenden Indexdaten in Langwörtern>
Bemerkenswerterweise bestehen alle weitere Daten aus 16-Bit-Wörtern bzw. sind teilweise auf Wortgrenzen ausgerichtet! Diese Indexdaten sind grob in zwei aufeinanderfolgende Abschnitte aufgeteilt - eine Namenstabelle (die nichts anderes als Strings enthält) und eine Referenzliste (mit Verweisen in die Namenstabelle). Der Namenstabelle geht ein 16-Bit-Wert voraus, der die Länge der nachfolgenden Tabelle in Bytes angibt. Dieser Wert ist immer gerade (am Ende der Tabelle wird gegebenenfalls ein zusätzliches Null-Byte angehängt). Die Tabelle selbst besteht aus einer lückenlosen Folge von null-terminierten Strings. Den Anfang macht dabei ein einzelnes Null-Byte. Der so definierte „Leer-String“ wird für „unbenannte“ Sektionen verwendet.
Hinter dieser Namenstabelle folgt die relativ komplex strukturierte Referenzliste. Grob aufgeschlüsselt besteht sie aus einer Folge von modulbezogenen Referenzeinträgen (ein Eintrag pro Modul, in der selben Reihenfolge wie die Module selbst). Jeder dieser Einträge beginnt mit drei 16-Bit-Werten:
<String-Index des Modulnamens>
<Byte-Offset des Moduls in der Library>
<Anzahl der Hunks/Sektionen im Modul>
Wie bei allen anderen Namensangaben in der Referenzliste bezeichnet der <String-Index des Modulnamens> das erste Byte des entsprechenden Strings in der Namenstabelle (gerechnet ab dem Beginn der Tabelle). Der String-Index 0 bezeichnet also immer einen „Leer-String“ (kann bei Modulnamen allerdings nicht Vorkommen). Der Byte-Offset des Moduls bezieht sich auf den Beginn des ersten Moduls in der Library (das daher den Offset 0 hat). Um auf den absoluten Offset des Modulbeginns relativ zum Dateianfang zu kommen, muß man noch 8 (die Länge des Library-Headers in Bytes) dazuaddieren! Die weiter oben beklagte Beschränkung der Library-Länge auf maximal 256 KByte resultiert übrigens aus der Tatsache, daß der Byte-Offset ein 16-Bit-Wert ist!
Diesem Vorspann folgt jetzt eine entsprechende Anzahl von Sektionseinträgen, die jeweils folgenden Header besitzen (drei 16-Bit-Werte):
<String-Index des Sektionsnamens>
<Länge des Sektionsinhalts (Code oder Daten) in Langwörtern>
<Kennung des Sektionstyps>
Die <Kennungen des Sektionstyps> stimmen mit den schon bekannten Kennungen der entsprechenden „Hunk“-Untergruppen überein, allerdings sind sie auf 16-Bit-Werte verkürzt worden ($03E9 - Code, $03EA - DATA, $03EB - BSS). Der Sektionseintrag selbst fängt mit einer Auflistung der externen Referenzen in der jeweiligen Sektion an, bestehend aus einem 16-Bit-Wert, der angibt, wieviele externe Referenzen existieren, und einer einfachen Liste mit den String-Indizes der entsprechenden Namen. Bei diesen String-Indizes ist allerdings eine Kleinigkeit zu beachten: Steht an der bezeichneten Stelle in der Namenstabelle ein Null-Byte, handelt es sich nicht (!) um einen „Leer-String“ (würde bei einer symbolischen Referenz auch keinen Sinn ergeben), stattdessen beginnt der eigentliche Namens-String erst beim nächsten Byte in der Tabelle. Derart „kodierte“ Namen werden bei allen 16-Bit-Referenzen benutzt (PC-oder basis-relative Adressierungen). In diesem Sinne „unkodierte“ Namen bezeichnen im Gegensatz dazu 32-Bit-Referenzen (absolute Adressierungen).
Nach der Liste der externen Referenzen folgt die Angabe der aus der jeweiligen Sektion exportierten Symbole, deren Anzahl zu Beginn wieder als 16-Bit-Wert erscheint. Pro Symbol folgt darauf ein Eintrag der Form
<String-Index des Symbols>
<Byte-Offset der Symboldefinition relativ zum Sektionsanfang> $0001
Etwas rätselhaft ist dabei der jeweils dritte Wert ($0001). Ich konnte bisher noch keine Library finden, in der hier ein anderer Wert als 1 eingetragen wurde (meine erste Vermutung, daß es sich um einen „Zähler“ für mehrfach in der Library definierte Symbole handelt, erwies sich als falsch!).
So viel zum Aufbau des „Library-Indexes“, der übrigens am Schluß gegebenenfalls mit zwei Null-Bytes auf Langwortgrenzen aufgefüllt wird. Weitere Kennungen („Hunk“-Untergruppen) konnte ich im Objectcode/Library-Format von Hi-Soft nicht ausfindig machen, obwohl einige Lücken in der Numerierung der Kennungs- Langwörter darauf hin weisen könnten, daß es noch weitere Gruppen gibt. Da die Analyse der Indexdaten einer Lattice/ HiSoft-Library mit einem Diskmonitor oder binärdump-tauglichen Editor recht mühsam ist, habe ich sozusagen als „Zugabe“ ein kleines Dump-Programm für diese Daten geschrieben. Das Listing OMLINDEX.C enthält auch Hinweise zum Gebrauch dieses Hilfsprogramms, das sich übrigens wahlweise mit Pure C oder Lattice-C übersetzen läßt.
/* << OMLINDEX.C 1.0 06.05.92 19:00 >> */
/* Dieses Programm dient dazu, die "Index-Daten" einer Bibliotheksdatei im
Lattice/HiSoft-Format so ausführlich wie möglich aufzulisten. Der Aufruf
des Programms hat folgendes Format:
OMLINDEX.TTP <library file> [<library file>]
...
Die Ausgaben des Programms landen in der Standard-Ausgabe (Bildschirm) und
können daher in eine Datei umgeleitet werden, soweit der richtige
Startup-Code beim Linken verwendet wurde und das TOS ausnahmsweise mal
mitspielt!
(c) 1992 by MAXON Computer und Heiner Högel
History:
1.0 06.05.92 - Ersterstellung!
*/
/*------------------------------------------*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <portab.h>
/* Diverse library-interne Kennungen: */
#define LIB_ID 0x000003FAL
#define LIB_INDEX 0x00000SFBL
#define LI_CODE 0x03E9
#define LI_DATA 0x03EA
#define LI_BSS 0x03EB
/* Der main-Prototyp ist nur fur Lattice-C notwendig! */
int main (int argc, char *argv[]);
/* Prototypen modulinterner Funktionen: */
static int show_lib_index (FILE *lib);
static int read_error (void);
static char *print_symbol (char *symlist, DWORD sym offset);
static char *hunk_type (UWORD hunk_id, char *hunk_name);
/* =========================================== */
/* Globale Funktionen: */
/* =========================================== */
int main (int argc, char *argv[]) /*
----------------------------------*/
{
FILE *lib;
int i,
status;
if (argc <= 1)
{
puts("Usage: OMLINDEX.TTP <library file> [<library file>] ...");
return (EXIT_FAILURE);
}
for (i = 1; i < argc; i++)
{
if ((lib = fopen(argv[i], "rb")) == NULL)
{
status = EXIT_FAILURE;
printf("*** Cannot open libray file %s! ***\n", argv[i]);
continue;
}
printf("\nDumping library index of file %s:\n", argv[i]);
puts("============================================");
status = show_lib_index(lib);
fclose(lib);
}
return (status);
}
/* =========================================== */
/* Modulinterne Funktionen: */
/* =========================================== */
static int show_lib_index (FILE *lib) /*
-----------==============---------*/
{
struct
{
ULONG index_id;
ULONG index_len;
UWORD symlist_len;
} index_header;
char *symlist,
*buffer = NULL,
*str;
UWORD *module_index,
*index_end,
offset,
sym_offset,
n;
ULONG lib_header[2];
long index_start;
size_t buflen;
int hunks,
hunk_count,
symbols,
module_count;
/* Read library header and check it */
if (fread(lib_header, sizeof(ULONG) * 2, 1, lib) != 1)
return (read, error());
if (lib_header[0] != LIB_ID)
{
puts("*** This is not a HiSoft library file! ***");
return (EXIT_FAILURE);
}
/* Read index header and check it */
index_start = (long) (sizeof(ULONG) * (lib_header[1] + 2));
if (fseek(lib, index_start, SEEK_SET) != 0)
return (read_error());
if (fread(&index_header, sizeof(index_header), 1, lib) != 1)
return (read_error());
if (index_header.index_id != LIB_INDEX)
{
puts("*** Syntax error in library file! ****");
return (EXIT_FAILURE) ;
}
/* Allocate buffer and load index */
buflen = index_header.index_len * sizeof(ULONG) - sizeof(UWORD);
if ((buffer = malloc(buflen)) == NULL)
{
puts("*** Not enough memory to load library index! ***");
return (EXIT_FAILURE) ;
}
if (fread(buffer, buflen, 1, lib) != 1)
return (read_error());
symlist = buffer;
module_index = (UWORD *) (buffer + index_header.symlist_len);
index_end = (UWORD *) (buffer + buflen - 3 * sizeof(UWORD));
/* Walk through index as long as there is enough data left for
at least one module
*/
for (module_count = 0; module_index <= index_end; module_count++)
{
/* Dump header data for next module */
printf("\nModule: ");
print_symbol(symlist, *module_index++);
offset = *module index++ + (UWORD) sizeof(ULONG) * 2;
hunks = (int) *module_index++;
printf(" Library Offset: 0x%04X (%u),%d Hunk(s)\n", offset, offset, hunks);
/* Walk through hunks of module */
for (hunk_count = 0; hunk_count < hunks; hunk_count++)
{
/* Dump header data of hunk */
printf(" Section %d hunk, count);
str = print_symbol(symlist, *module_index++);
n = *module_index++ * (UWORD) sizeof(ULONG);
printf("\" 0x%X (%u) Bytes, <%s>\n",
n, n, hunk_type(*module.index++, str));
/* Dump external references */
if ((symbols = *module_index++) > 0)
{
printf(" %d external reference(s):\n", symbols);
for (; symbols > 0; symbols--)
{
printf(" ");
print_symbol(symlist, *module_index++);
putchar('\n');
}
}
/* Dump exported symbols */
if ((symbols = *module_index++) > 0)
{
printf (" %d exported symbol (s) : \n", symbols);
for (; symbols > 0; symbols--)
{
sym_offset = *module_index++;
offset = *module_index++;
printf(" Section offset: 0x%04X, Extra: 0x%04X, Symbol: ", offset, *module_index++);
print_symbol(symlist, sym_offset);
putchar('\n');
}
}
}
}
printf("\n----------------------------------------
"\n%d Modules in library file!\n", module_count);
if (buffer != NULL)
free(buffer);
return (EXIT_SUCCESS);
}
/*--------------------------------------------*/
static int read_error (void) /*
-----------==========--------*/
{
puts("*** Read error on library file! ***");
return (EXIT_FAILURE);
}
/*--------------------------------------------*/
static char *print_symbol (char *symlist, UWORD sym_offset) /*
-------------============----------------------------------*/
{
int special = FALSE;
char *symbol;
symbol = symlist + sym_offset;
if (*symbol == '\0' && sym_offset != 0)
{
special = TRUE;
symbol++;
}
printf("%s", symbol);
if (special == TRUE)
printf(" (16 bit ref)");
return (symbol);
}
/*--------------------------------------------*/
static char *hunk_type (UWORD hunk_id, char *hunk_name) /*
-------------=========---------------------------------*/
{
char *type;
int far_type = FALSE;
if (strcmp(hunk_name, "__MERGED"))
far_type = TRUE;
switch (hunk_id)
{
case LI_CODE: type = "Code"; break;
case LI_DATA: type = (far_type) ? "far DATA" : "near DATA"; break;
case LI_BSS: type = (far_type) ? “far BSS" : "near BSS"; break;
default: type = "Unknown hunk type"; break;
}
return (type) ;
}
/* =========================================== */
/* «< VSSCANF.C 1.0 06.05.92 14:30 >* */
/* Library: LCxxxx.LIB
Dieses Modul enthält die globale Funktion:
- vsscanf
(c) 1992 by MAXON Computer
und Heiner Högel
History:
1.0 06.05.92 - Ersterstellung (vsscanf)!
Dies ist eine Ergänzung der scanf-Funktionenfamilie,
um kompatibel zur Pure-C-Library zu sein. Die
Verwendung der libraryinternen
Funktion _sf ist von Lattice/Hisoft/CCD nicht
offiziell dokumentiert und beruht auf eigenen Analysen
der Library LCSR.LIB! (Heiner Högel)
*/
/* =========================================== */
#include <stdarg.h>
int vsscanf (const char *s, const char *format, va_list arglist);
extern int __regargs _sf (int (*p_getc) (void), void (*p_ungetc) (int c),
int *p_count, const char *format, va_list arglist);
#define EOF (-1)
static int count;
static const char *input;
/* =========================================== */
static int x_getc (void) /*
-----------======--------*/
{
unsigned char c;
count++;
if ((c = *input++) == '\0')
return (EOF);
else
return ((int) c);
}
static void x_ungetc (int c) /*
------------========---------*/
{
count--;
input--;
}
int vsscanf (const char *s, const char *format, va_list arglist) /*
----=======---------------------------------------*/
{
count = 0;
input = s;
return (_sf(x_getc, x_ungetc, &count, format, arglist));
}