Hallo Besucher, der Thread wurde 3,5k mal aufgerufen und enthält 22 Antworten

letzter Beitrag von BlackJack am

Nachladen von PRGs: wie zyklische Abhängigkeiten behandeln?

  • Ich ärgere mich schon seit längerem mit dem Problem herum, dass man zyklische Abhängigkeiten bekommen kann, wenn man mit PRGs arbeitet, die andere PRGs nachladen. Ein Beispiel:

    • PRG1 ist ein Programm, das PRG2 nachlädt und dann Unterroutinen aus PRG2 aufruft
    • PRG2 soll als Startadresse die Adresse direkt hinter dem Ende von PRG1 bekommen


    Jetzt ergibt sich eine zyklische Abhängigkeit:

    • PRG1 kann nur kompiliert werden, wenn PRG2 vorher kompiliert wurde, da es ja die Adressen der Unterroutinen aus PRG2 wissen muss
    • PRG2 kann nur kompiliert werden, wenn PRG1 vorher kompiliert wurde, weil seine Startadresse ja von der Länge von PRG1 abhängt


    Bisher habe ich das mit scheußlichen Konstruktionen gelöst wie Printouts beim Kompilieren, die dann wiederum für Symbole in anderen Sourcefiles eingefügt werden müssen. Das wird aber schnell unübersichtlich und mühselig, wenn es mehr als zwei Files sind. Meine Hoffnung war eigentlich, dass das durch Verwendung von Objectfiles und einen Linker gelöst werden könnte (ca65 und ld65). Aber jetzt wollte ich es mal ausprobieren und habe festgestellt, dass das ja auch nicht so einfach ist. Ld65 kann natürlich sowas prima behandeln, wenn ein einziges Binary aus allen Objectfiles erzeugt werden soll. Aber fürs Nachladen müsste man dem Linker im Beispiel oben PRG1.obj und PRG2.obj übergeben, damit er die Symbole auflösen kann, aber ihm sagen, dass er getrennte PRG files für PRG1 und PRG2 erzeugen soll.


    Mir scheint, dass das ein lösbares Problem ist, aber irgendwie habe ich keine Ahnung, wie man es praktisch angeht. Das müssen doch andere vor mir schon gemacht haben?

  • Mir fällt auf die Schnelle auch keine schöne/elegante/einfache Lösung mit existierenden Tools ein, aber lösbar ist es natürlich: In ACME z.B. müsste ich nur einen "!save FILE, VON, BIS"-Pseudo Opcode hinzufügen, der dann statt dem kompletten Binary nur einen Teil speichert (von Label ABC bis Label XYZ). Assembliert wird dann also alles zusammen, so dass das Auflösen der Referenzen kein Problem ist; nur das Aufteilen auf mehrere Outputfiles wäre neu.
    Irgendwo steht das auch auf meiner "Könnte man mal machen, aber wer braucht das schon"-Liste, aber - nun ja - wer braucht das schon? ;)
    Bisher scheint wirklich niemand diese Möglichkeit vermisst zu haben. Vermutlich weil:
    a) das Programm eh am Stück vorliegt, oder
    b) die nachgeladenen Teile immer nur einen Einsprungpunkt haben, z.B. ganz am Anfang, oder
    c) die nachgeladenen Teile mit einer festen JMP-Leiste beginnen.


    Wieviele Funktionen des zweiten Programmteils rufst Du denn auf, und warum liegt das Programm in mehreren Teilen vor? Wie gesagt, man kann das Problem auch durch Verändern der Tools lösen, aber evtl. gibt es wirklich eine viel einfachere Lösung.

  • Klingt mir auchnam ehesten nach einem error-by-Design. Warum muss das File nachgeladen werden? Wenn die Einsprungadressen schon fix bekannt sein müssen kann man es auch gleich in ein File gießen.
    Willst du ein flexibles Modul System haben müssen die Module so geschrieben sein dass man sie problemfrei im ram verschieben kann.
    Dann legt man jedem Modul eine Tabelle mit einsprungadressen ab Start und id für die sub Routinen bei.
    Das Haupt Programm erhält also Zugriff über einen Index.allerdings wird das vergleichsweise langsam werden.

  • Lässt sich das nicht durch anderes Design lösen? Bottom-Up, also mit den rudimentären Unterprogrammen anfangen, die von allen anderen gebraucht werden
    Oder falls da größere Logikteile ausgetauscht werden sollen, wie wäre es mit einer Sprungtabelle am Anfang von Prog2, ähnlich wie im Kernal?

  • Ich hab sowas bei Hyperion. Es gibt einen Hauptteil mit Basiscode, und die jeweiligen Loads, die dann auf die Routinen aus dem Hauptteil zugreifen. Anfangs hatte ich da einen Jump-Table, aber das artete ganz schnell in einen Riesentable aus.


    Da habe ich in das letzte Update von C64Studio eingebaut, das Symbole eines Kompilats in ein anderes übernommen werden können.


    Das Problem mit der Startadresse muss man dann noch selber lösen, aber das ist meiner Meinung nach das geringere.


    Ich habe also einen Hauptteil mit Basis-Code, der in allen Loads verwendet wird. Und die jeweilige Load-spezifischen Code-Teile haben als Abhängigkeit das Hauptprogramm und übernehmen zusätzlich dessen Symbole.
    Und als finales Kompilat habe ich das Disk-Element. Da habe ich nur ein Dummy-ASM-File mit Abhängigkeiten auf alle Loads. Im Post-Build-Event importiere ich alle Loads, das Hauptprogramm sowie die Daten in ein Disk-Image.

  • Das Problem mit dem Nachladen kenne ich auch. Es tritt vorzugsweise in komplexeren Spielen auf, bei denen levelspezifischer Code ausgeführt werden muß (vgl. Drol oder Prince of Persia), oder in RPGs, um bestimmte Handlungen wie Zaubern oder das Benutzen von Objekten einzeln umzusetzen. Daamals (Plagiat) hat man das zumeist mit Jump-Tabellen ermöglicht (z. B. Ultima oder Masquerade). Nachteil: Es gab oft eine Lücke zwischen dem eigentlichen Hauptprogramm und dem nachgeladenen Segment, weil die Assemblierung der Segmente direkt hinter das Hauptprogramm, wie von dir geschildert, sehr umständlich war. Besonders aus heutiger Sicht ist solch eine Vorgehensweise auch eher unbefriedigend, da, wie bereits erwähnt wurde, Jump-Tabellen sowohl langsam sind als auch unnötig Platz im Speicher kosten. Solltest du also in Assembler programmieren, so ist wahrscheinlich die von Mac Bacon vorgeschlagene Lösung die beste: Nimm dir einen Assembler, der dein gesamtes Programm mit allen Segmenten in einem Rutsch assembliert und daraus getrennte PRGs erzeugt. Auf heutigen Rechnern dauert sowas nur ein paar Sekunden, und das Resultat ist ein schöner kompakter Code. Es lohnt sich also.

  • @M.J.: exakt das ist mein Use-Case :). Und genau Deine Begründung, warum JMP-Tables doof sind, hat mich auch bewogen, nach einer anderen Lösung zu suchen. Es ist ja prinzipiell alle Information vorhanden, um das "schön" zu machen, nur die Tools, die ich bisher probiert habe, machen es nicht.


    Nimm gleich Assembler.


    Das ist leider ein prinzipielles Problem, die Wahl der Toolchain macht da keinen Unterschied (ich nehme übrigens auch Assembler :))

    Wieviele Funktionen des zweiten Programmteils rufst Du denn auf, und warum liegt das Programm in mehreren Teilen vor?


    Ja, so weit bin ich noch nicht. Ich baue gerade das prinzipielle Softwaredesign auf und will gleich alles richtig machen. Aber ich gehe fest davon aus, dass das Ding am Ende riesig wird und nicht auskommen wird, ohne Codeteile nachzuladen und das möglichst flexibel.

    Willst du ein flexibles Modul System haben müssen die Module so geschrieben sein dass man sie problemfrei im ram verschieben kann.


    Ne, sooo flexibel muss es zum Glück nicht sein. Die Codeteile werden schon immer an die selbe Adresse geladen werden.

    Da habe ich in das letzte Update von C64Studio eingebaut, das Symbole eines Kompilats in ein anderes übernommen werden können.


    So habe ich das bisher (unter anderem) auch gemacht, aber dann muss man die Abhängigkeiten im Makefile explizit schreiben und das finde ich schon wieder zu kompliziert und fehlerträchtig...


    Per PM habe ich auch einen Hinweis auf Overlays in ld65 bekommen, das löst zumindest das Problem mit den mehreren Outputfiles. Allerdings gibt es die wohl nur pro Memory Area, und deren Start/Größe muss man scheinbar wieder explizit angeben (und kann keine Label einsetzen).


    Gibt es denn andere Tools außer der cc65 Toolchain, die getrennte PRGs aus einem Haufen Sourcefiles erzeugen?

  • Beim C64-Studio ist das eigentlich nur ein Häkchen bei einer Abhängigkeit zu setzen. Alle anderen Sub-Dependencies werden vom Compiler aufgelöst.


    @MacBacon: Wie gehst du denn bei einem Multi-Load-Spiel vor? Du kannst ja nicht einfach mehrere Loads in einem assemblieren? Die Loads wären ja an der selben Adresse, nur der Hauptcode bleibt statisch.

  • Mit Offset-Assemblierung: beim Assemblieren liegen die Einzelteile hintereinander im Outputbuffer, werden aber jeweils für die gleiche Adresse assembliert. D.h. wenn alles zusammen größer als 64 KiB wird, hätte man immer noch ein Problem. Aber wenn man eh ein "!save" dranstrickt, könnte man ja auch den Outbuffer vergrößern. ;)
    Sauberer wäre es aber vermutlich, dem Assembler sagen zu können, mehrere voneinander getrennte Projekte auf einmal zu assemblieren, die sich nur die Symbole teilen. Man würde also in jedem Pass versuchen, jedes Projekt zu assemblieren. Sobald alle Referenzen aufgelöst sind, hat man das fertige Set von Einzelfiles. Ich schreib es mir mal auf die TODO-Liste... :whistling:

  • Noch ein Einwand: Prog 2 soll ja nachgeladen werden, weil man manchmal auch Prog3 braucht. Das klingt in meinen Ohren doch sehr nach Modulen, die dem Hauptprogramm 1 die "gleichen" Funktionen zur Verfügung stellen sollen, so im Sinne von "Bewege Endboss", "zeichne Endboss" u.Ä. MUSS natürlich nicht sein, ist nur so ein Gefühl. Falls das so ist: Sprungtabelle machen.

  • Jetzt ergibt sich eine zyklische Abhängigkeit:
    PRG1 kann nur kompiliert werden, wenn PRG2 vorher kompiliert wurde, da es ja die Adressen der Unterroutinen aus PRG2 wissen muss
    PRG2 kann nur kompiliert werden, wenn PRG1 vorher kompiliert wurde, weil seine Startadresse ja von der Länge von PRG1 abhängt


    Richtige Programmiersysteme verwenden dazu einen Linker. Hilfsweise setzt man die Startadresse von PRG2 auf einen empirisch ermittelten Wert, der größer ist als das bisher erreichte Programmende von PRG1 und genug Platz für Erweiterungen läßt, um nicht jedes Mal neu kompilieren zu müssen. Release-Versionen werden dann händisch auf das derzeit gültige Ende von PRG1 gesetzt und beide Programmteile ein letztes Mal kompiliert/assembliert.


    Alternativ:


    Definiere eine Sprungleiste am Anfang von PRG2, die am Ende von PRG1 mit Dummy-Sprungzielen definiert wird. Dann muß deren Startadresse 'nur noch' als ORG für PRG2 übernommen werden.

  • Versuch macht kluch: ich habe die Lösung gefunden! Ich hatte ja oben schon geschrieben, dass ich ca65 (der Object Files erzeugt) und ld65 als Linker dazu ausprobiert hatte. Ld65 kann auch getrennte Files schreiben, allerdings nur pro "Memory Area". Bislang dachte ich, diese Memory Areas müssten im Linker-Configfile explizit definiert werden, also mit einer festen Adresse für Start und Länge. Gestern bin ich mal mit dem Debugger durch ld65 gesteppt, um zu schauen, ob man da nicht was ändern kann. Und siehe da: die Funktion, die das Linker-Configfile parst, benutzt eine sehr generische Routine zum Parsen des Startwerts, die neben einfacher Arithmetik auch (TADA!) Labels interpretiert! Also habe ich zum Spaß folgendes Configfile geschrieben:



    ...und dachte mich tritt ein Pferd: der Linker schluckt es und macht tatsächlich exakt, was ich will. Zur Erklärung: MAIN ist die Section, von der aus Code aus den Sections OVERLAY1 und OVERLAY2 (die zur Laufzeit nachgeladen werden) aufgerufen werden soll. Die Symbole __xxx__LOAD und __xxx_SIZE__ erzeugt der Linker automatisch, das sind Start und Länge des Segments xxx. Alternativ könnte man wohl auch im Sourcecode ein Label am Ende des Main-Codes setzen und das dann benutzen. Die komischen -2 in den Startadressen der Memory Areas sind da, um die Startadresse im PRG-File richtig zu schreiben.


    Prima, ich dachte mir schon, dass es irgendwie gehen muss ^^ !

  • Beim C64-Studio ist das eigentlich nur ein Häkchen bei einer Abhängigkeit zu setzen. Alle anderen Sub-Dependencies werden vom Compiler aufgelöst.


    Gäb es dazu mal eine nähere Beschreibung. Das wäre toll!

  • Der Fall ist schon etwas besonderes. Normalerweise würde man zum Nachladen von Code wohl eine Shared Library benutzen, die dann vom Loader relocated wird (wie auch immer das auf Deutsch heißen mag). Das wäre für den C64 aber wohl deutlich zu aufwändig. Ich kann mir kaum vorstellen, dass ein Linker typischerweise alle Adressen schon vorher statisch berechnen und dennoch den Code in mehrere Teile aufteilen kann, wenn es solche Abhängigkeiten zwischen den Adressen gibt.

  • Gäb es dazu mal eine nähere Beschreibung. Das wäre toll!


    Die Version ist ja noch nicht freigegeben :)


    Ich versuche aber, das zu beschreiben. Ein bißchen habe ich an der Doku aufgeräumt. Es ist ziemlich schwierig, eine gute Doku zu bauen, wenn man selbst alle Eigenheiten kennt :)