Assembler Programmierung MIPS Architektur

  • Hi, zwei Fragen (könnten irgendwann mehr werden :) ) zur MIPS Programmierung, da mir Google dazu nicht die gewünschten Ergebnisse liefert und insgesamt die Fragen zu formulieren bezüglich Register eher schwer ist, sodass Google mir gibt was ich will:


    1)


    Wenn ich einen Systemfunktionsaufruf starte, zB


    li $v0, 1 #1 is the print function for integer
    lw $a0, var1 #loading integer 5 (word type) into the argument register
    syscall


    wird dabei $v0 gecleared? Ist keine Frage von besonderer Relevanz, da ich um sicher zu sein, einfach li $v0, 1 eingeben kann um den Befehl erneut ins Register zu schreiben, aber kann man den Befehl wiederholen ohne diese Operation, oder wird er nach dem syscall erased?


    2)


    Warum wird das Speichern im Index eines Arrays genau andersherum initiiert wie das Speichern in einem Register? Ist das bedingt dadurch, dass das Speichern in den .data Bereich geht?


    Beispiele (kopiere mal aus meinen Tests):


    lw $t0, var1 #loading from RAM destination into a register


    sw $t0, var1 #storing content of $t0 in the RAM destination var1 defined in .data


    sw $t1, 4($t0) #storing content of t1 in the array whose address is stored in $t0 (?), usng index 4 as 0,1,2,3 are already in use


    Anmerkung:


    Letzteres ist ein Array, das in .data als .space 16 definiert wurde und dessen Addresse in $t0 vorliegt. Ich intepretiere es so, dass bei lw, sw, lb, sb usw. immer erst das Register und dann der RAM-Platz vorliegt, statt wie bei den arithmetischen Befehlen (3-Address) immer erst Ziel, dann Operanden. Ist die Annahme richtig?


    Danke im Voraus, ist für jemanden der sich auskennt ja alles relativer Kindergarten, aber ich grabe mich da gerade fast enthusiastisch durch. :)


    Möglicherweise kommen mehr Fragen mit der Zeit.



    qaLa



    EDIT: Wenn ich das richtig sehe, wird $v0 durch einen syscall Aufruf schonmal nicht gecleared.

  • Irgendwie ist das schon wieder viel zu lange her, dass ich mich mit MIPS beschäftigt habe, aber ich versuch's mal. Natürlich ohne Gewähr. :p


    Sofern der Syscall nicht selbst irgendeinen Antwort-Wert hat, wird das Register nicht überschrieben (afair würde sonst das Ergebnis des Aufrufs in diesem Register stehen).


    Der Unterschied zwischen signed und unsigned ist (immer), dass bei unsigned Integern ein Bit mehr zur Verfügung steht, welches bei signed Integern für das Vorzeichen benötigt würde. Dadurch ändert sich der Wertebereich. In den gängigen Sprachen geht ein signed char von -128 bis 127, ein unsigned char dagegen von 0 bis 255.
    Der Unterschied zwischen den beiden Operationen ist, dass man bei add Fehler beim Overflow bekommen kann (am Beispiel des signed char also z.B. dann, wenn das Ergebnis einer Operation größergleich 128 wäre).


    Und ja, ich glaube, bei den Store- und Load-Operationen war die Reihenfolge tatsächlich umgekehrt. Eine MIPS-Referenz kann dir da aber wahrscheinlich besser Aufschluss geben als ich.^^

  • Wenn ich einen Systemfunktionsaufruf starte wird dabei $v0 gecleared?


    In der Regel kannst du davon ausgehen, dass die Register so erhalten bleiben, da Befehle normalerweise keine unnötigen Operationen durchführen. "Gecleared", also zurückgesetzt, werden Register sowieso nicht. Das einzige was passieren kann ist, dass sie mit etwas anderem "Sinnvollen" überschrieben werden. Dann ist das aber bei den jeweiligen Befehlen beschrieben. Beachte, dass manche Befehle auch nicht offensichtliche Register im Hintergrund nutzen.


    Warum wird das Speichern im Index eines Arrays genau andersherum initiiert wie das Speichern in einem Register?
    [...]


    Code
    1. lw $t0, var1 #loading from RAM destination into a register
    2. sw $t0, var1 #storing content of $t0 in the RAM destination var1 defined in .data
    3. sw $t1, 4($t0) #storing content of t1 in the array whose address is stored in $t0 (?), usng index 4 as 0,1,2,3 are already in use


    [...]
    Ich intepretiere es so, dass bei lw, sw, lb, sb usw. immer erst das Register und dann der RAM-Platz vorliegt, statt wie bei den arithmetischen Befehlen (3-Address) immer erst Ziel, dann Operanden. Ist die Annahme richtig?


    Bei Datentransfer Operationen wird das Register immer zu erst angegeben. Bei Ladeoperationen ist es das Ziel, bei Speicheroperationen die Quelle. In deinem Beispiel enthält "var1" die Adresse des Arrays im Speicher. Damit du mit der Adresse arbeiten kannst, musst du sie in ein Register laden ($t0) und kannst dann dieses Register für die Adressierung nutzen (mit dem Index).


    Was genau ist der Unterschied zwischen add und addu, bzw was heißt in dem Fall unsigned?


    addu behandelt die Zahlen als vorzeichenlos, während add das Vorzeichen berücksichtigt. Vorzeichenlose Zahlen haben ein Bit mehr und können daher doppelt so groß sein, dafür aber nicht negativ. Grund dafür ist die binäre Darstellung der Zahlen, die eigentlich klar sein sollte (wenn nicht, erklär ich dir das gerne noch), wo das erste Bit als Vorzeichenbit genutzt wird.
    Je nach Anwendungsfall ist es manchmal entscheidend eine vorzeichenlose Zahl zu haben, wenn man die große Kapazität braucht - oder eben umgekehrt. Da die Zahlen aber von außen nicht unterscheidbar sind, muss man das als Entwickler selbst berücksichtigen.

  • Also im Grunde alles bestätigt. Mit gecleared war auch gemeint, dass der Wert nicht "verschwindet", sondern 0 annimmt. Das ist aber wie es aussieht nicht der Fall.


    Das mit unsigned hilft mir gut weiter, es geht also einfach nur um diese zwei:


    0.0000000


    vs


    00000000


    Ersteres nutzt das erste Bit als Vorzeichen, - <=> 1, zweiteres bewegt sich nur im Bereich des Betrags, kann aber Werte von 2^(n+1) statt von 2^n annehmen, insofern vielen Dank an euch beide, was das angeht.


    Macht euch auch keine Sorgen, was die Aussagengenauigkeit angeht, ich suche ja eher Denkanstöße, als irgendwelche Musterlösungen, insofern ist das perfekt.


    Mir brannte letztens noch eine Frage auf der Zunge, fällt mir aber nicht mehr ein, vielleicht kommt das ja noch später.




    qaLa

  • Mal wieder ein paar Fragen, werden in den nächsten Tagen noch mehr werden, hatte keine Zeit für Assembler die letzten Wochen.


    1) Worin manifestiert sich der eigentliche Unterschied zwischen den s und den t Registern?


    2) Ich habe lh mit einer Zahl 12345678 getestet (gespeihert in einem 32Bit Word). Ausgabe der Zahl in der Konsole nach dem Befehl war 24910. Liege ich richtig damit, dass lh die 4 Bytes in 2 Hälften aufspaltet und dann eine Hälfte davon nimmt, in einem neuen Word speichert und den rest mit 0en auffüllt? Was sind die praktischen Anwendungen für den Befehl?


    3) Was kann ich mir unter dem Befehl lui vorstellen? Was ich bisher zu dem Befehl lese leuchtet mir nicht wirklich ein, aber eventuell muss sich das erst setzen.


    4) Wenn ich mit einem Conditional Branch Befehl einen Sprung zu einem Label mache, wie kann ich aus dem Label an die Stelle im Programm zurückkehren? In meinem praktischen Beispiel benutze ich eine beliebige Zahl, diese wird, sofern bne, $s0, $zero (s enthält die Zahl) an das Label loop übergeben. Dort erfolgt die schrittweise-Reduktion um 1 und Ausgabe des Ergebnisses, kurz ein Countdown. Probleme bereitet mir die Rückkehr. Ich habe es mit Sprungbefehlen an die Addresse versucht. $ra scheint beim Labelsprung zum loop auf dem default-Wert zu bleiben. Erzwinge ich einen Sprung zu der Addresse des bne Befehls kommt das Programm nicht zum Ende, obwohl dieses als nächster Befehl in der main-Methode ist.


    Daher, welche Methoden gibt es, um nach einem conditional Branch an die Stelle des Branches aus dem Label zurückzukehren? Also jal, jr, j, etc.. und wieso funktioniert ein einfacher Sprung zur Addresse nicht?


    Habe jetzt nach weiterem Testen die Addresse des nächsten Befehls nach dem Branch Befehl als Pointeraddresse (also die Addresse zu der gesprungen werden soll) angegeben, siehe da, das Programm läuft. Vermute anderenfalls triggerte ich eine Endlosschleife, da ich in einem Zwischenlabel die Übergabe vom s an ein temporary-Register gemacht habe, wodurch der Wert im s-Register erhalten blieb und womöglich erneut den Loop triggerte. Die Frage nach den jump-Befehlen juckt mich aber nach wie vor.


    5) In meinem MIPS-Editor sehe ich für jede Befehlszeile zwei Hexadezimalzahlen. Die eine ist mit der Überschrift Address, die andere mit Code versehen. Was ist Code? Wie steht dieser im Zusammenhang mit dem Programmzeiger?


    6) Was genau steckt hinter .globl? Beziehe mich auf diesen Code:


    Code
    1. # addthree.asm --- read in three integers and print their sum.text .globl mainmain: jal pread # read first integer move $s0,$v0 # save it in $s0 jal pread # read second integer move $s1,$v0 # save it in $s1 jal pread # read third integer move $s3,$v0 # save it in $s2 add $s2,$s0,$s1 # compute the sum add $a0,$s3,$s2 move $s3,$a0 # save it in $s3 jal pwrite li $v0,10 syscall# pread -- prompt for and read an integer# on entry:# $ra -- return address## on exit:# $v0 -- the integer .text .globl preadpread: la $a0,prompt1 # print string li $v0,4 # service 4 syscall li $v0,5 # read int into $v0 syscall # service 5 jr $ra # return pwrite: la $a0,prompt2 # print string li $v0,4 # service 4 syscall move $a0, $s3 # restore from $s3 to $a0 li $v0,1 # read int into $v0 syscall # service 1 jr $ra # return.dataprompt1: .asciiz "Enter an integer: "prompt2: .asciiz "The sum is: "


    Mit ist klar, was bei diesem Code passiert, doch wieso ist die .globl, main, .text Struktur so wie sie ist? Wieso gibt es 2x .text und in diesen .text jeweils verschiedene .globl Methoden? Gibt es einfache Faustregeln, die man sich dazu merken kann? Auch warum wird häufig das .data Segment des Arbeitsspeichers unter die Programmbefehle geschrieben, hat diese Konvention einen Sinn?
    Zum Vergleich, hier ist der Code für einen einfachen Counter, den ich gerade programmiert habe, die Struktur ist anders als in dem Beispielcode, ich versuche zu verstehen wieso. Der Code ist im Simulator MARS v.4.1 ebenfalls vollständig lauffähig:


    Wenn ihr keine Lust habt auf alles einzugehen, beantwortet bitte einfach einzelne Fragen nach eurer Laune. Besser eine Frage beantwortet als keine, es muss sich jetzt aber auch niemand durch alle Fragen durchquälen.


    Danke im Voraus!



    qaLa

  • Ich versuchs mal, bin allerdings schon ne Weile ausm Thema raus.



    1) Worin manifestiert sich der eigentliche Unterschied zwischen den s und den t Registern?


    Hm, also soweit ich weiß, ist es nur eine Konvention die besagt, dass man beim Aufruf von Unterprogrammen, die t-Register nicht sicher muss, s-Register dagegen schon, da irgendwelche wichtigen Zwischenergebnisse drinstehen (z.B. für Rekursion).


    2) Ich habe lh mit einer Zahl 12345678 getestet (gespeihert in einem 32Bit Word). Ausgabe der Zahl in der Konsole nach dem Befehl war 24910. Liege ich richtig damit, dass lh die 4 Bytes in 2 Hälften aufspaltet und dann eine Hälfte davon nimmt, in einem neuen Word speichert und den rest mit 0en auffüllt? Was sind die praktischen Anwendungen für den Befehl?


    Bei MIPS sind Worte erstmal immer 32bit lang. Mit lw und sw, wird das ganze Wort geladen/gespeichert.


    Daneben gibt es halt noch lh/sh (load/store half), für die niederen 16bit und lb/sb (load/store byte), für die niederen 8bit eines Wortes.


    Der Nutzen besteht meist darin mit dem char-Datentyp zu arbeiten. In C is dieser 8bit breit und Java 16bit.


    3) Was kann ich mir unter dem Befehl lui vorstellen? Was ich bisher zu dem Befehl lese leuchtet mir nicht wirklich ein, aber eventuell muss sich das erst setzen.


    lui = load upper immediate
    Damit lädst du eine Konstante in die oberen 16bit eines Registers.



    4) [...] Die Frage nach den jump-Befehlen juckt mich aber nach wie vor.


    jal speichert beim Aufruf die Adresse das darauffolgenden Befehls automatisch im Register ra.


    Also:


    Code
    1. 0000: jal 1234 # Springt zu Adresse 1234 und speichert 0001 automatisch in Register ra (return adress)
    2. 0001:
    3. ...
    4. 1234: ...
    5. 1235: jr $ra # springt in diesem Fall zu 0001 zurück


    Generell gilt: Willst du Registerinhalte sichern, nutze immer den Stack!
    s-Register in t-Registern zu sichern geht zwar, fliegt dir spätestens bei rekursiven Aufrufen um die Ohren, weil du da oftmals neben mehreren Parametern auch logischerweise die Rücksprungadresse sicher musst.



    5) In meinem MIPS-Editor sehe ich für jede Befehlszeile zwei Hexadezimalzahlen. Die eine ist mit der Überschrift Address, die andere mit Code versehen. Was ist Code? Wie steht dieser im Zusammenhang mit dem Programmzeiger?


    Code entspricht dem "Maschinencode" der jeweiligen Instruktion. Dabei unterscheidet MIPS drei Instruktionsformate (R-, I- und J-Typ), die alle 32bit lang sind (=8-stellige hexadezimal Zahl)


    beispielsweise setzt sich die Instruktion add $t0, $s1, $s2 (R-Typ) folgendermaßen zusammen:
    op code: 00000 definiert in diesem Fall einen Befehl für die ALU (es soll gerechnet werden)
    register des ersten Operanden: 10001, also t0
    register des zweiten Operanden: 10010, also s1
    und register des dritten Operanden: 01000, s2
    der shift amount ist in dem fall = 00000, da gerechnet werden soll
    und zum schluß der funct-code = 10000, welcher der ALU nun genau sagt, ob addiert oder subtrahiert werden soll.


    Nochmal der R-Typ zusammengefasst: op | 1. Operand | 2. | 3. | shift amount | funct


    dabei sind der op- und der funct-Code 6bit lang und der Rest 5bit, was zusammen dann der 32bit bzw 8-stelligen HexZahl entspricht.


    So werden alle Befehle in MIPS kodiert und eigentlich stehen diese nicht wirklich im Zusammenhang mit den Adressen.




    6) [...]


    Warum die Struktur da so ist, wie sie ist, kann ich dir nicht sagen;) Weiß auch grad nicht obs da ne Konvention für gab... Ich meine mich aber zu erinnern, dass wir den .data Teil immer vor den .text Teil geschrieben haben. Einfach mal ausprobieren... das zweite .text scheint eigentlich unnötig.



    Hoffe ich konnte dir wenigstens n bissl helfen ;)



    Gruß,


    fey


  • Bei MIPS sind Worte erstmal immer 32bit lang. Mit lw und sw, wird das ganze Wort geladen/gespeichert.


    Daneben gibt es halt noch lh/sh (load/store half), für die niederen 16bit und lb/sb (load/store byte), für die niederen 8bit eines Wortes.


    Der Nutzen besteht meist darin mit dem char-Datentyp zu arbeiten. In C is dieser 8bit breit und Java 16bit.


    Könnte ich diesen Befehl für char-Arrays nutzen?


    Quote

    lui = load upper immediate
    Damit lädst du eine Konstante in die oberen 16bit eines Registers.


    Ich weiß nicht wieso, aber das hilft mir nicht so recht weiter. Anwendungsbeispiele evtl?


    Quote


    jal speichert beim Aufruf die Adresse das darauffolgenden Befehls automatisch im Register ra.


    Also:


    Code
    1. 0000: jal 1234 # Springt zu Adresse 1234 und speichert 0001 automatisch in Register ra (return adress)
    2. 0001:
    3. ...
    4. 1234: ...
    5. 1235: jr $ra # springt in diesem Fall zu 0001 zurück


    Dass es ein Register weiter springt ist hilfreich, das heißt also wenn ein Abgleich einen Sprung bewirkt hat wird dieser nicht erneut getätigt.


    Quote


    Generell gilt: Willst du Registerinhalte sichern, nutze immer den Stack!
    s-Register in t-Registern zu sichern geht zwar, fliegt dir spätestens bei rekursiven Aufrufen um die Ohren, weil du da oftmals neben mehreren Parametern auch logischerweise die Rücksprungadresse sicher musst.


    Hast du dafür ein Codebeispiel? Gerade was Rekursion angeht?



    Wenn ich das also richtig verstehe, kann man mit Code die Befehle im Grunde dekodieren. Wie ergeben sich explizit diese Werte für den add-Befehl? Sind da fiktive Werte dabei oder folgt das Ganze einem festen System? ZB den Binärcode von $s1 habe ich auf dessen Registernummer zurückgeführt, bei $s2 machte das bereits keinen Sinn mehr.
    Worin unterscheiden sich die 3 von dir genannten Typen, J, I, R? Gibt es dazu eine Informationsquelle im Web, wo ich mich einwenig einlesen könnte?




    Quote


    Warum die Struktur da so ist, wie sie ist, kann ich dir nicht sagen;) Weiß auch grad nicht obs da ne Konvention für gab... Ich meine mich aber zu erinnern, dass wir den .data Teil immer vor den .text Teil geschrieben haben. Einfach mal ausprobieren... das zweite .text scheint eigentlich unnötig.


    Wundert mich, der Code sollte korrekt sein.



    Konntest du auf jeden Fall, ich bedanke mich für deine Hilfe! :)



    qaLa

  • Ich habe mich mit dieser Struktur jetzt etwas weiter befasst. Das heißt, diese 3 Formate sind im Grunde einfach nur Befehlskonventionen. Je nach Format wird der Befehl wohl entsprechend interpretiert und verarbeitet.


    Ich habe ein Beispiel für einen add Befehl gefunden und mich damit auseinandergesetzt, hier meine Interpretation:


    Angenommen wir haben add $t0, $s1, $s2.


    Die ersten 6 Bits seien der Opcode. Diese sind scheinbar 000000, wobei ich nicht weiß warum. Ist der add-Befehl zufällig 000000 oder liegt dem eine andere Konvention zugrunde? Na ja. Anyway:


    Die nächsten 5 Bits sind das erste Parameterregister, hier s1. s1 ist das 17. Register, also folgt binär 10001.


    Weitere 5 Bits fürs zweite Parameterregister, Nr. 18, also 10010.


    Dann das Zielregister, t0. Dieses ist Nr. 8, also 01000 (ich nehme übrigens an, dass in deinem Beispiel, fey, der falsche Code für das $t0 angegeben ist, korrigier mich wenn dem nicht so ist).


    Die letzten zwei Dinge bestehend aus 11 Bit leuchten mir nicht ein. Was das shamt Register machen soll ist mir völlig schleierhaft. Ich habe es auf 00000 gesetzt, da es in den Beispielen bei add nicht genutzt wird.


    Das funct Register ist auf 100000 gesetzt, ich weiß aber wiederum nicht wieso. In den Quellen die ich soweit gefunde habe, soll funct scheinbar der ALU irgendwelche Informationen übermitteln. Aber was genau macht nun funct? Ich denke was zu tun ist bestimmt schon Opcode?



    Na gut, nachdem trotz einiger Rätsel ich jetzt soweit gekommen bin ergibt sich folgender Binärcode für den Befehl:


    000000 10001 10010 01000 00000 100000


    Folgendes ist meine Interpretation, ich hoffe dies ist korrekt, ansonsten bitte ebenfalls berichtigen!


    Ich wollte auf den Hexacode des Befehls schließen. Da dieser immer aus einem 0x und dann 8 Hexziffern besteht habe ich meinen Binärcode in 8er Häppchen unterteilt und jedes Häppchen von binär in hexadezimal umgerechnet. Das heißt mein umstrukturierter Binärcode wäre:

    0000 0010 0011 0010 0100 0000 0010 0000


    Woraus dieser Hexadezimalcode folgen sollte:


    0x02324020



    Ist dies ein mögliches Verfahren für Operationen der R-Form? Ich nehme an jede der drei Operationsformen hat da ihr eigenes Schema wie man zu strukturieren hat.




    qaLa

  • 1) Bump (brauche so viel input wie möglich, daher bumpe ich das bis ich nicht mehr mag, solangs max. 1x/Tag ist :) )


    2) Ich hänge gerade wieder an was ganz banalem habe ich das Gefühl.


    Ich habe zwei Strings:


    string1: "0123456789abcdef"
    string2: "00000000"


    Ziel ist es, einen bestimmten Index im String zu ermitteln und das Element an diesem Index zu ersetzen. Nur sind hier Funktionen wie lw und lb scheinbar fehl am Platz. Load address lädt mir zwar die Addresse im Arbeitsspeicher, ich finde jedoch kein store Equivalent dazu. Oder abgekürzt:


    Wenn ihr jetzt einfach nur die 7. Null aus string2 mit dem a aus string1 ersetzen wollen würdet, was würdet ihr tun?



    EDIT:


    Scheinbar geht es über load byte. Wusste allerdings nicht, dass man 1er Schritte im String macht. Habe das folgendermaßen gelöst:



    Dez -> Hex Rechner.




    qaLa