Hallo Besucher, der Thread wurde 5,9k mal aufgerufen und enthält 42 Antworten

letzter Beitrag von Claus am

Styleguide für Assembler

  • Gründe für den Linker [...]

    Hmm... Da habe ich mich wahrscheinlich nicht richtig ausgedrückt. Die Modularisierung eines Programms wird ja nicht durch die Verwendung des Linkers erzielt, sondern durch die Organisation des Programms an sich. Man kann ja durchaus ein Programm modular gestalten mit sauber getrennten Klassen, Bibliotheken usw. und trotzdem das ganze Programm in einem Rutsch assemblieren ohne dabei auf einen Linker zurückgreifen zu müssen. Wie gesagt: Da es von der Geschwindigkeit eh nichts ausmacht, ob man das ganze Programm oder nur einen Teil davon assembliert, frage ich mich: wozu dann noch den Linker?

  • Die Modularisierung eines Programms wird ja nicht durch die Verwendung des Linkers erzielt, sondern durch die Organisation des Programms an sich.

    Doch, an sich schon. Einzelne "Übersetzungseinheiten" sind eine gute Möglichkeit dafür, dazu braucht man dann einen Linker. Je nach verwendeter Toolchain kann es auch andere Möglichkeiten geben, allerdings:

    trotzdem das ganze Programm in einem Rutsch assemblieren ohne dabei auf einen Linker zurückgreifen zu müssen.

    dabei, selbst wenn es gut vom Assembler supported ist, sehe ich direkt mindestens ein Problem: Beim Assemblieren korreliert dann der Code im Quelltext mit der exakten Position in der Ausgabe. Dann sind wir wieder bei den "netten" Pseudo-Opcodes von anno dazumal wie .base oder .org und vermischen damit zwei komplett unterschiedliche Aspekte (Logik und Speicheraufteilung) in ein und derselben Datei. Schlimmstenfalls folgen noch händische Rechenaufgaben, während man mit einem gescheiten Linker einfach Segmente definieren und mit Code und Daten befüllen lassen kann.

  • Das gibt es schon seit Ewigkeiten, auch in C ist das so, und unter Unix-basierten Betriebssystemen sind auch "readme.txt" und "Readme.txt" und "README.txt" und "README.TXT" 4 verschiedene Dateien.


    Da ist(war?) ja auch mal ein Punkt am Anfang des Dateinamens erlaubt. Für beschränkte 70er-Systeme oder den C64 find ich simple Strings als Dateinamen ja auch voll OK, aber spätestens nach dem ersten RM.* finde ich Einschränkungen doch ganz gut, wenn sie Fehler vermeiden. Oder wenn Du Deiner Mutter am Telefon erklären musst, die Readme.txt zu öffnen.

    Gerade einem solchen Wildwuchs, wie Du ihn jedoch herbeifuerchtest, schiebt dies jedoch einen Riegel vor: Denn Du kannst nun eben NICHT einfach schreiben, wie es Dir passt, sondern Du sollst gefaelligst den korrekten Namen verwenden, naemlich so, wie die Variable definiert wurde.


    Einheitliche Schreibweise ist ja zunächst mal imho kein Wert an sich, sondern heute eine von der Sprache angeforderte Notwendigkeit. Man kann einheitliche Schreibweise und eindeutige Namen zur gleichen Zeit bekommen, das alte Visual Basic zum Beispiel. Editoren setzen ja heute Einrückung und die Positionen von {} durch, und sie könnten auch eine einheitliche Schreibweise durchdrücken - Wenn die Namen denn wie in VB6 eindeutig wären. Readme kann ich mir gut merken, aber ob es Readme, ReadMe, readMe, readme oder README war nicht mehr so gut. Um die Korrektur der Groß/Kleinschreibung kann sich der Computer viel besser kümmern als ich.

    Wenn man sich nun an einen (eigenen oder fremden) Styleguide haelt, dann ist das auch ueberhaupt kein Problem. Wenn Du beispielsweise Variablen immer klein schreibst, Konstanten immer GROSS, und Klassen- oder Typnamen immer mit GrossemAnfangsbuchstaben, dann wirst Du sehen, dass das ueberhaupt keine Herausforderung darstellt. Im Gegenteil, durch das Erzwingen der korrekten Schreibweise kannst Du nun sogar immer an einem Namen direkt ablesen, ob es sich dabei um eine variable, eine KONSTANTE oder einen TypNamen handelt.


    Mit diesen 3 Regeln kann ich mich durchaus anfreunden, aber die könnte gerne auch der Editor für mich durchsetzen. Ich scheine aber nicht der Einzige zu sein, der Groß/Klein schlecht zu merken findet. Die Auto-Vervollständigung ignoriert den Case sicher nicht versehentlich.

    Gibt es denn überhaupt jemanden, der sich traut, den gleichen Variablennamen zweimal zu verwenden - einmal groß geschrieben,einmal klein? Nur dann macht das Unterscheiden überhaupt Sinn... ;)


    Ich kenne durchaus Leute, denen ich das zutraue, denen übergibst Du ein Bit, und anschließend ist es kaputt :S . PHP brauchte keine Deklaration, da passierte sowas durchaus versehentlich mit komischen Konsequenzen. Das hätte echt viel Ärger erspart, wenn der Case ignoriert würde.

  • Ich werf mal meinen vor knapp zwei Jahren eröffneten Thread ein:


    http://www.forum64.de/sauberes…ieren-tipps-tricks-links/


    Die beiden Threads könnte man (meinetwegen) auch gut und gern verschmelzen.


    Bei lemon oder csdb.dk bin ich die Tage auch auf eine Diskussion bezüglich englisch vs. Landessprache gestoßen. Vielleicht findet das jemand auf die Schnelle und kann es verlinken. Bin grad auf Smartphone unterwegs. Danke.

  • BTT:Meine Empfehlung zu Macros: Besser nicht benutzen. Früher[tm] hatte ich auf dem Amiga einen Macroassembler, aber habe das Feature so gut wie nie genutzt und folglich später auf dem PC gar nicht mehr im Assembler implementiert. Macros habe ich seitdem nicht mehr verwendet und auch nie vermißt. Mein Eindruck bisher war viel mehr, daß
    - fremder Assemblercode mit Makros schlecht zu lesen ist,
    - bei einer optimierten Registerbelegung (insbesondere bei Prozessoren mit wenigen Registern wie dem 6502) feste Makros eher stören,
    - Programmcode, der erkennbar mit der Unterstüzung von Macros geschrieben wurde, weniger gut optimiert war (hinsichtlich Codelänge als auch Geschwindigkeit) wie handgeschriebener Code.

    Also bisher hab ich die Macros hauptsächlich für 16bit Operationen und VIC-Ram-Settings benutzt. Da gibt es nichts zu optimieren.

    So hab ich das bisher gemacht.


    EDIT: Ja, die Kommentare sind inkonsistent. Da beschwert sich der Compiler nicht. :D
    Daher auch die Frage nach Konventionen. ;)

  • Hoogo: Ich moechte da jetzt nicht allzu tief reingehen, aber ich sage mal so - ich fand das frueher auch befremdlich, weil ich eben gerade von so Sprachen wie Visual Basic her anderes gewohnt war. Inzwischen finde ich es aber gut. Vielleicht kann ich es nicht immer rational begruenden, aber es gibt sicherlich einige, die Dir etliche Vorteile aufzaehlen koennten (angefangen bei Name Clashes, Scopes, Verwendung von Fremdmodulen usw usf). Ich kann mich nur wiederholen daher versuche ich es jetzt erst gar nicht. Aber glaub mir, ich wuerde nichtmehr zum Visual Basic-maessigen Wildwuchs zurueck wollen. Und PHP ist nochmal so ein Fall fuer sich, da ist eh alles total durcheinandergewuerfelt, das sollte man jetzt vielleicht nicht unbedingt als Paradebeispiel anfuehren.


    Aber um nochmal auf das Thema "Ich kann mir nicht merken ob ReadMe, readMe, README oder readme" usw zurueckzukommen: Eben das entfaellt ja, wenn man weiss, was man tut bzw. sich an einen Styleguide haelt. Wir sind jetzt wieder beim Thema Dateisystem, dabei geht es ja jetzt eigentlich eher um Programmiersprachen. Und da gibt es diese Beliebigkeit einfach gar nicht erst, sofern man sich wie gesagt an einen Styleguide haelt. Da weiss ich dann auch wirklich zu jedem Zeitpunkt, dass das Ding z.B. "sayHello" heisst. Und nicht "SayHello" oder "say_hello". Eben aufgrund des von mir verwendeten Styleguides, ganz egal wie das darin dann im einzelnen definiert ist.


    Um mal noch ein anderes Beispiel zu bringen: Ich habe frueher auch nicht verstanden, warum es in manchen Programmiersprachen einen Unterschied zwischen = und == gibt. Das sind einfach so Dinge, die man erst mit der Zeit begreift, und heute ist es fuer mich ganz natuerlich und ich kann verstehen, warum es suboptimal ist, wenn das in Sprachen wie Visual Basic usw. eben nicht so ist. Oder warum man in manchen Sprachen die Variablen erst deklarieren muss bevor man auf sie zugreift. Vielleicht sind das fuer Dich ja ein wenig nachvollziehbarere Beispiele.

  • Dann sind wir wieder bei den "netten" Pseudo-Opcodes von anno dazumal

    Hmm... Das habe ich irgendwie nicht verstanden. Der 6502 kennt bis auf die Branchbefehle keine Unterstützung für relokatiblen Code. An irgendeiner Stelle muß man dem Assembler oder Linker sagen, für welche Adresse das Programm assembliert/gelinkt werden soll. Hinzukommt - Stichwort Speicherbelegung -, daß man auf dem C64 als Programmierer durchaus wissen sollte, von wo bis wo der Code im Speicher steht. Auf dem PC arbeite ich logischerweise auch nicht mit ORG, aber für einen notgedrungen vollinformierten C64-Programmierer sehe ich gar keine Möglichkeit, das ORG wegzulassen.

    ; **** ADD num 2 num in adresse
    !macro add_word_adr_num adr,num{

    Solch eine 16-Bit-Addition kam bislang in meinen Programmen so gut wie nie vor. Da war es eher so, daß ein Teil der Adresse in einem anderen Register lag oder das Ergebnis in ein Register geladen wurde usw. Nur mal als Beispiel:

    Code
    1. clc
    2. lda var
    3. adc #
    4. sta var
    5. bcc .skip
    6. inc var + 1
    7. .skip:

    Das ist eine übliche Variante, um bei Konstanten zwischen 0..255 die Addition zu verkürzen und zu beschleunigen. Ist von einer vorhergehenden Operation das Carry-Bit noch gelöscht, kann auch auf den Befehl CLC verzichtet werden. Und damit sind wir bei dem, was ich meine: Macros erzeugen häufig längeren und langsameren Code, besonders auf dem 6502.
    Was die Verwendung von Macros beim Setzen von VIC-Registern anbelangt, so halte ich die Befehlsfolge wie

    Code
    1. lda #$1b
    2. sta $d011 ; oder ein beliebiger Name für das Register

    für immer noch am schnellsten und übersichtlichsten.

    Code
    1. and #VIC_ECM_Off
    2. and #VIC_BMM_Off

    Und hier reicht ein "and" aus. ;)

  • Hmm... Das habe ich irgendwie nicht verstanden. Der 6502 kennt bis auf die Branchbefehle keine Unterstützung für relokatiblen Code. An irgendeiner Stelle muß man dem Assembler oder Linker sagen, für welche Adresse das Programm assembliert/gelinkt werden soll.

    Auch das ist auf dem PC in der Regel nicht anders. Assemblierter Objektcode enthält Symbol- und Relokationstabellen, nur der Linker muss wissen, wohin der Code letztendlich soll.

    Hinzukommt - Stichwort Speicherbelegung -, daß man auf dem C64 als Programmierer durchaus wissen sollte, von wo bis wo der Code im Speicher steht.

    Natürlich. Ich will es aber in der Regel nicht für jedes einzelne Modul wissen! In der Linker-Config habe ich einen Speicherbereich für allen Code, der wird dann automatisch beim Linken befüllt. In ein Modul, das z.b. Integer-Arithmetik implementiert, will ich doch keine feste Adresse schreiben. Für "spezielle" Module wie z.B. autostart-bootloader oder 1541-drivecode kann ich immer noch in der Linker-config eigene Segmente definieren.


    Klar könnte ich hingehen und allen Code per .include schon vor dem Assemblieren zusammenbauen, das ist aber nicht nur umständlicher und weniger übersichtlich, es macht auch alles global, also müssen z.B. Labels über verschiedene Module eindeutig sein und ich kann leicht aus Versehen in den privaten Daten eines anderen Moduls herumpfuschen. Wenn ich dagegen einen Linker verwende exportiere ich gezielt die Symbole, die die Schnittstelle meines Moduls darstellen, und importiere sie in anderen Modulen.


    Vielleicht war das Beispiel in meinem ersten Posting zu knapp -- du kannst ja gerne mal einen Blick auf mein aktuelles Projekt werfen, da gibt es kein einziges .org, die memory-map steht in der linker-config.

  • Irgendwie habe ich den Eindruck, wir reden aneinander vorbei.

    nur der Linker muss wissen, wohin der Code letztendlich soll.

    Eben. Einer muß es wissen. Dabei ist es egal, wo diese Angabe steht: in der Assemblerdatei oder der Linkerdatei. Nur bei einer Linkerdatei muß man eben noch eine zusätzliche Datei erzeugen. Für meinen Geschmack nicht einfacher, sondern umständlicher.

    Ich will es aber in der Regel nicht für jedes einzelne Modul wissen!

    Solltest Du aber. Denn nur dadurch weißt Du am Ende, welchen Platz Dein Programm im Speicher belegt, kannst danach den Speicher besser ausnutzen und beim Debuggen im Monitor Dein Programm verfolgen. Natürlich heißt das nicht, daß Du für jedes Modul getrennt angeben mußt, wo es hinkommen soll, denn die Module werden ja einfach aneinandergehängt. und da reicht zunächst (sofern keine große Speicherfragmentierung vorliegt) eine einmalige Angabe der Ladeadresse zu Beginn des Codes.

    Klar könnte ich hingehen und allen Code per .include schon vor dem Assemblieren zusammenbauen

    Ganz genau. Darum geht es. Und was die Segmente anbelangt, so sind diese keine primäre Eigenschaft des Linkers, sondern des Assemblers. Natürlich wird man auch ohne Linker ein Programm in einzelne Segmente zerlegen, z. B. für die Zeropage oder den nicht-initialisierten Datenbereich. Ein Linker wird dafür nicht benötigt.

    es macht auch alles global, also müssen z.B. Labels über verschiedene Module eindeutig sein

    Da es hier ja um Style geht: In einem Assemblerprogramm sollten alle Label stets eindeutig sein. Daher ist es üblich, globale Labels nur für Module (Bibliotheken/Klassen...) zu verwenden und für Unterroutinen, Variablen etc lokale Labels. Für Sprungmarken innerhalb von Unterroutinen bieten sich je nach Assembler entweder Mikes Methode (".label_02"), anonyme Labels (-, +) oder drittlevel-Bezeichner ("?01", "!01" usw.) an. Sollte jemals ein Label mit einem anderen kollidieren, hat man einen Fehler im Programm.

    ich kann leicht aus Versehen in den privaten Daten eines anderen Moduls herumpfuschen.

    Aus Versehen sicherlich nicht. Kann ich mir bei Dir nicht ernsthaft vorstellen. Dafür hast Du zuviel Programmiererfahrung. Das Herumpfuschen in fremden Modulen ist bekanntlich mehr als schlechter Stil. Assembler ist eine Sprache für Leute, die wissen, was sie tun, und dazu gehört auch ausreichende Selbstdisziplin, ansonsten bekommt man früher oder später Probleme, so oder so.


    Wenn ich das jetzt richtig verstanden habe, ist das einzige Argument, das für den Einsatz eines Linkers spricht, die Kapselung von Bezeichnern, die man als Assemblerprogrammierer aber ohnehin vornehmen sollte auch ganz ohne Zwang. Als nachteilig für den Einsatz eines Linkers sehe ich hingegen das Fehlen eines kompletten Assemblerlistings, d. h. vollständiger Code mit Adreßangabe sowie tabellarische Auflistung aller Variablen und Adressen, damit man beim Debuggen sich schnell orientieren und Programm und Werte vergleichen kann. Für mich persönlich kann ich daher keinen Vorteil in der Verwendung eines Linkers erkennen, eher Nachteile: zusätzliche Konfigurationsdateien, mehr Aufwand beim Buildprozeß und kein vollständiges Listing zur Kontrolle.

  • Code
    1. clc
    2. lda var
    3. adc #xx
    4. sta var
    5. bcc .skip
    6. inc var + 1
    7. .skip:

    Das ist eine übliche Variante, um bei Konstanten zwischen 0..255 die Addition zu verkürzen und zu beschleunigen.

    Also, der hintere Teil der Routine (BCC/INC) braucht 3 Zyklen, wenn kein Übertrag war, und sonst 2+6 = 8 Zyklen.


    In Fällen, wo nicht unbedingt klar ist, ob immer eine 8-Bit-Konstante ausreicht, oder wenn z.B. nicht erwünscht ist, daß der Code *unterschiedliche* Laufzeiten hat, bin ich mir auch nicht zu schade, das so anzuschreiben:

    Code
    1. CLC
    2. LDA var :ADC #xx:STA var
    3. LDA var+1:ADC #0 :STA var+1

    Spart außerdem noch eins der "generischen" Labels (hier: "skip") und natürlich läßt sich das Konstrukt für eine 32-Bit-Addition verallgemeinern. Und da sieht man dann auch, daß das CLC *gefälligst* vor die gesamte Gruppe gehört und nicht direkt vor das erste ADC (also nicht: LDA var:CLC:ADC #xx:STA var [...]!)

  • @Deus & zrs1:


    Ihr kommt mir ja wirklich irgendwie etwas 'Hochsprachen-verstrahlt' vor :) .


    Wofür muss denn jeder Wert/Adresse einen persönlichen Namen haben statt seiner selbst? Braucht's beim 64er sicherlich nicht und wird eher unverständlich statt lesbarer.


    Nun ja, man kann aus jedem POKE/STA auch eine Wissenschaft machen, wer mag. Bin aber auch schon ewig wieder weg von irgendwelchen kuriosen Makros, die ich mal gebastelt hatte. Bringt nix.

  • Und da sieht man dann auch, daß das CLC *gefälligst* vor die gesamte Gruppe gehört und nicht direkt vor das erste ADC

    Danke für den Hinweis. Das gehört sicherlich auch in einen Styleguide. Wenn ich noch ergänzen darf: Sollte man sich das vorhergehende CLC/SEC sparen können, weil das Carry bereits den gewünschten Wert hat, sollte man dies kommentieren, damit a) man später noch nachvollziehen kann, wie die Berechnung funktioniert und b) bei einer eventuellen späteren Änderung des Programms kein Fehler daraus wird. Also sowas wie

    Code
    1. ; clc bereits gelöscht
    2. lda var
    3. adc #<wert>
    4. sta var

    Das gilt auch für den Fall, daß vor einer Addition das Carry gesetzt ist:

    Code
    1. ; sec
    2. lda var
    3. adc #<wert - 1> ; + 1!
    4. sta var


    Kleine Frage in die Runde:
    Gibt es irgendwo eine Liste von "Tips und Tricks" wie die oben genannte Verkürzung einer Addition, die man gerade Einsteigern in Assembler nahelegen könnte?

  • Ich hab mir einfach angewoehnt ebenfalls keine macros oder allzu Assembler-spezifische pseudo opcodes zu verwenden.
    Damit bleibt man dann flexibel, andere koennen den code lesen, man selbst auch wenn man mal die Umgebung wechselt und ausserdem faellt dann soetwas dabei ab:


    ;-)

  • Ich bin da anders. Ich mag mein Assembler ein wenig versüsst durch Hochsprachenkonstrukte, z.b. { } Klammer Scopes mit impliziten break/continue labels, ein bisschen wie in C/C++. Ausserdem verwende ich viel automatisch generierte Makros, um mit Gameplay Objekten zu hantieren. Und lange, aussagekräftige Namen. Ich hab kein Problem mit tippen.
    Hier ein Beispiel aus Canabalt:


  • Ich bin da anders. Ich mag mein Assembler ein wenig versüsst durch Hochsprachenkonstrukte, z.b. { } Klammer Scopes mit impliziten break/continue labels, ein bisschen wie in C/C++. Ausserdem verwende ich viel automatisch generierte Makros, um mit Gameplay Objekten zu hantieren. Und lange, aussagekräftige Namen. Ich hab kein Problem mit tippen.
    Hier ein Beispiel aus Canabalt:

    Kann ich voll verstehen, ich hab zwar nur ein Stück Quelltext, weiß aber sofort das es um die Tauben auf den Dächern geht.
    So fängt man nicht an ähnlich Befehlsketten an einer anderen Stelle zu verwechseln, da die Namen sprechend sind.

  • Wenn ich richtig loslege, kommt auch schon mal so ein Code raus (ist für VC-20):

    Diese Routine schreibt eine 40 Zeichen lange Zeichenkette als ganzes in eine 160x192 Bitmap. Die Zeichen sind 4x8 Pixel groß, damit das aufgeht. Damit nicht zuerst das linke Zeichen gedruckt wird (wobei die Nibbles der rechten Hälfte erhalten werden müßten) und dann erst das rechte Zeichen (wobei die Nibbles der linken Hälfte erhalten werden müßten...!), druckt die Routine in der Schleife "TDraw_01" immer zwei Zeichen auf einmal! In "TCharset" stehen die Zeichendefinitionen so drin, daß die Zeichen im rechten und linken Nibble enthalten sind. So entfällt an dieser Stelle aufwendiges Shiften.


    Der größere Teil der "TDraw_00"-Schleife betreibt Adreß-Arithmetik um die Basis-Pointer "text_left" und "text_right" in den Zeichensatz zu berechnen. Ein Byte wird beim Shiften im Akku gehalten, das braucht dann nur 2 Zyklen statt 5 auf der Zeropage.


    Dadurch, daß die Routine immer gleich eine ganze Textzeile printet, müssen sich die Daten nicht durch 1-Zeichen-Programmierinterface (wie eben bei CHROUT) quetschen. Sie schafft fast 6000 Zeichen/Sekunde und wird in einem scrollenden ASCII-Text-Viewer eingesetzt!

  • Sorry, mrsid, aber da vergeht mir persönlich echt der Spaß fremden Code lesen zu können (oder müssen). Das ist schon hart zu deuten. Nun denn...

  • Das ist schon hart zu deuten.

    Code niemals für den Fairchild F8. Da steht nämlich nirgends dran, daß Dir schon ein einfaches JMP den Akku zerschießt (also den im Prozessor, nicht den im Laptop). Meide ebenso den TDL-Z80-Assembler. Da darfst Du erstmal die selbstgestrickten Intel-style Mnemonics für den erweiterten Befehlssatz lernen.


    Übrigens: das clc von neulich gehört gefälligst _nicht_ an den Anfang, auch wenn es eine hübsche Einrückung erzeugt. Die bekommt man aber auch mit einer leeren Kommentarzeile hin. Und das clear carry gehört nunmal inhaltlich zu der Addition. Erster Befehl muß vielmehr ein LDA #wert sein; nicht nur weil dann beide Zeropage-Zugriffe in Rechenbefehlen erfolgen (das ist eher akademischer Natur), sondern vor allem weil dann sowas geht:


    Damit ist die Routine flexibel verwendbar. Wer mag, ersetzt das .by $2c noch durch ein SKIP2-Makro. Ist da sverständlich? Nun, für damalige Programmierer ganz sicher, denn die kannten den Prozessor-Befehlssatz nnoch auf Bit- und Byte- Ebene; heute sind die meisten ja schon aufgeschmissen wenn der Hersteller keinen C-Compiler mitliefert...


    Und um das nächste Faß aufzumachen: Man muß seinen Assembler schon recht genau kennen um zu wissen, ob der Sternchen-Operand auf den Beginn der Zeile zeigt, oder (wie der PC bei Ausführung des Sprunges) auf den folgenden Opcode- oder aber, was leider nur selten vorkommt, ob man dem Assembler direkt den Operanden des relativen Sprungbefehls anbieten kann.


    Und wenn euch zwischen den Jahren langweilig wird: Wie müssen die Flags (vor allem das Carry) bei eienm Vergleichsbefehl gesetzt sein, wenn der Operand kleiner war als der Vergleichswert? Stichwort Z80...