Trackermodul-Engine (sehr einfach)

Benutzeravatar
zatzen
DOS-Kenner
Beiträge: 459
Registriert: Di 17. Apr 2012, 05:10
Wohnort: bei Köln
Kontaktdaten:

Re: Trackermodul-Engine (sehr einfach)

Beitragvon zatzen » Fr 6. Mär 2020, 18:06

Ich habe auch mal in der Pascal-Hilfe nachgesehen, da steht durch die Anweisung interrupt bzw. bei Interrupt-Routinen werden Register als Pseudo-Parameter übergeben, also möglicherweise bei Assembler-Prozeduren auch in Form von Locals bzw. Parameter, so dass BP und SP verändert werden und BP auf dem Stack liegt. Die "interrupt" Anweisung einfach weglassen geht wohl nicht, da viele Register durch die Player-Routine verändert werden. Wenn man nur ein Warteflag ändert in der Interrupt-Routine müsste man es ohne Register-Sichern machen können, jedenfalls habe ich das so früher schonmal gemacht in einer kleinen Demo in 100% Assembler. Naja gut, oder ich mache das eben alles "händisch", lasse interrupt weg und schreibe stattdessen pushf und pusha rein und unten die entsprechenden pops, oder vielleicht statt pusha explizit nur die Register die tatsächlich im Code verändert werden. pusha sichert nicht ES oder DS, daher wäre evtl. ein einzelnes Sichern besser. Ich denke ich werde mir auch mal das Kompilat dieser Interrupt-Routine ansehen...
Benutzeravatar
zatzen
DOS-Kenner
Beiträge: 459
Registriert: Di 17. Apr 2012, 05:10
Wohnort: bei Köln
Kontaktdaten:

Re: Trackermodul-Engine (sehr einfach)

Beitragvon zatzen » Fr 6. Mär 2020, 20:12

Okay, disassembliert, also, AX-DX, SI, DI, DS und ES werden gepusht, außerdem BP und mit SP zusammen ein Stackframe angelegt der später mit LEAVE aufgelöst wird, und danach werden auch die Register wieder vom Stack geholt. Danach folgt ein IRET. Also ich denke mal, das muss nicht unbedingt sein, dann lieber per Hand benutzte Register sichern. Ich habe nur in Pascal bisher noch nie Interrupt-Routinen ohne die Deklaration "Interrupt" gemacht. Kennt eigentlich jemand einen guten Disassembler? Ich habe jetzt für diese Codesegment https://defuse.ca/online-x86-assembler.htm benutzt, aber der interpretiert einiges nicht richtig, und ist generell auf 32 Bit minimum ausgelegt.
Naja, also zu obigem Vorhaben - ich verändere im Player alle oben aufgeführten Register, auch BP, nur SP nicht. Ich kann mir also nur den Stackframe sparen. SP müsste ich mir sparen können, keine der Unterroutinen haben einen Stackframe. BP allerdings auch, das wird innerhalb der Playerroutine gesichert. Probleme könnte es geben, wenn DS außerhalb verändert wird und dann die Interrupt-Routine plötzlich dazwischen kommt, dann sind die Datenzugriffe fehlerhaft. Demnach müsste man darauf achten dass DS niemals verändert wird... Oder man macht, was generell auch eigentlich vernünftiger wäre: In der Interrupt Routine nur den nächsten Tick flaggen, und alles andere in der Haupschleife erledigen. Alternativ könnte man zur Sicherheit den Ausgangswert von DS in einer Variable speichern und in der Interrupt-Routine dann zurück in DS schreiben. Aber nein - drüber nachgedacht, das geht ja gar nicht. Wie will ich auf eine Variable zugreifen wenn DS nicht mehr stimmt?! Dann bleibt wohl nur, dass man DS nicht mehr verändern darf sobald die Interrupt-Routine aktiviert ist.
DOSferatu
DOS-Übermensch
Beiträge: 1180
Registriert: Di 25. Sep 2007, 12:05
Kontaktdaten:

Re: Trackermodul-Engine (sehr einfach)

Beitragvon DOSferatu » Mo 9. Mär 2020, 20:49

Ich antworte später ausführlicher - aber ich bringe mal Licht in's Dunkel.
Es gibt einige Arten (5!) von RET - mit verschiedenen Opcodes.

1.)
einfacher (near) RET: (Opcode $C3)
Holt NUR IP (ein Word) vom Stack zurück. CS bleibt erhalten.

2.)
spezieller near RET: (Opcode $C2)
Holt IP (ein Word) vom Stack zurück. CS bleibt erhalten.
Danac holt es ein Word vom Stack. Dieses Word gibt an, daß soviele Bytes im Stack "übersprungen" werden sollen (bzw zurückgesprungen - wobei das beim Stack ja "nach oben" geht).
Das dient dazu, die auf dem Stack angelegten Parameter/Variablen etc. für die Subroutinen (von Hochsprachen) wieder freizugeben.

3.)
far RET, also RETF: (Opcode $CB)
Holt (in diesere Reihenfolge) IP und CS vom Stack (2 Words). (ist also das Gegenstück zu einem FAR CALL)

4.)
spezieller far RET (also RETF) (Opcode $CA)
(vor allem für Hochsprachen, die Stack-Parameter-Übergaben haben) : (Opcode
Holt (in diesere Reihenfolge) IP und CS vom Stack (2 Words), danach wieder so ein "Anzahl-Word", das wieder angibt, wieviele Bytes übersprungen werden sollen.

5.)
Interrupt-Return, also IRET (Opcode $CF)
Macht fast das Gleiche wie der far RET bei 3.), nur holt er danach noch FLAGS vom Stack.
Also holt er (in dieser Reihenfolge) vom Stack: IP, CS, FLAGS.

Um also eine Interrupt-Routine (wie z.B. die Original ISR vom Ticker oder Keyboard) so aufzurufen wie eine Subroutine, braucht man nur das tun:
PUSHF
CALL FAR (Adresse der ISR)

Weil die sich ja mit einem IRET beendet, erwartet die, daß da vor der Rücksprungadresse noch was liegt. Und wenn sie schon FLAGS erwartet, sollte man das auch tun.

Erklärung Interrupt: Wie man sieht, sichert eine ISR nichts weiter als FLAGS und die (FAR-)Rücksprungadresse.
(Man muß also FLAGS nicht pushen, weil das der Interruptaufruf selbst tut. Er sperrt dabei auch gleich das I-Flag.)
Alle anderen Register, die man benutzen will, muß man selbst sichern. Weil aber eine ISR kaum anders funktioniert wie eine normale FAR-Routine, braucht man auch nur die Register sichern, die man verändern will.

Noch etwas: Wenn man eine Subroutine benutzt und entweder Parameter übergibt oder lokale Variablen benutzt (oder beides), sollte man nicht mit BP herumspielen, wenn man sich nicht sicher ist, was man tut. Jeder Zugriff auf eine lokale Variable (auch von ASM aus) wird umgewandelt in einen Zugriff auf SS:BP+Offset (wobei Offset eben dem Offset entspricht vom Aufruf.

Beim Aufruf einer Rotuine weiß man ja nicht, wo der Stackzeiger gerade ist, daher wird dieser nach BP gesichert, damit der Zugriff auf lokale Variablen oder übergebene Parameter relativ zum Stackzeiger sind. Wenn man also BP ändert und NICHT sichert/zurückändert und danach auf eine lokale Variable (übergebene Parameter werden eigentlich auch nur wie lokale Variablen behandelt) zugreift, geht das in die Hose.

Die "interrupt"-Anweisung hinter dem Prozedurkopf macht das was Du geschrieben hast: die Handvoll (16bit!) Register sichern, inkl. DS und ES und am Ende zurückholen -sowie ein Stackframe einrichten. Auf 8086 "manuell", ab 286er mit ENTER und LEAVE, wobei je nach Aufrufart manchmal trotzdem die manuelle Variante benutzt wird - hatte das schonmal erklärt. Das Ganze ist also nur etwas, um es Hochsprachen-Codern leichter zu machen - wenn man ASM kann, kann man das auch alles selbst.

Wichtig (daher das 16bit! da oben): Weil Turbo-Pascal sich der Existenz von 32bit-Systemen nicht bewußt ist, sichert diese obengenannte Automatik nur den 16bit-Teil von 32-Bit-Registern. Wenn man also z.B. in der ISR die oberen 16bit von EAX verändert, werden diese nicht zurückholt - außer man sichert es selbst.

So, genug geschwafelt. Hoffe, es hilft.
Sollte es dazu noch Fragen geben - bin ich wie immer bereit zu helfen.
(Freue mich ja so dermaßen, daß es "da draußen" noch andere Leute gibt, die programmieren - und auch noch hardwarenah und unter DOS. Das muß unterstützt werden.)
Benutzeravatar
zatzen
DOS-Kenner
Beiträge: 459
Registriert: Di 17. Apr 2012, 05:10
Wohnort: bei Köln
Kontaktdaten:

Re: Trackermodul-Engine (sehr einfach)

Beitragvon zatzen » Mo 9. Mär 2020, 22:10

Vielen Dank für diese Erläuterungen!

Es scheint mir, dass Pascal, auch wenn es dann ASM Routinen sind, automatisch je nach Situation die RETs in passende Opcodes umwandelt, denn ich habe überall nur RET geschrieben und nirgends RETF. Oder habe ich da nur Glück gehabt?

Alle Prozeduren die vom Player genutzt werden (bis auf die Initialisierungen) sind rein in Assembler gehalten und ohne Locals und Parameter. Daher habe ich BP für eigene Zwecke verwendet. Aber gut, dass Du nochmal darauf hinweist, wann genau man da aufpassen muss.

Und ja, eine Frage habe ich noch, nämlich mit dem Register DS. Ist das nicht eigentlich ein Dilemma im Zusammenhang mit Interrupt-Routinen? Man biegt DS ja gerne mal um z.B. für den Heap, aber wenn in der Zeit wo DS dann nicht mehr aufs Datensegment zeigt eine Interrupt Routine dazwischenfunkt die auf Variablen zugreifen will, dann haben wir doch ein Problem. Mir fällt dazu spontan nur ein, dass man Interrupts sperren muss (CLI) für die Zeit in der DS nicht aufs Datensegment zeigt.
DOSferatu
DOS-Übermensch
Beiträge: 1180
Registriert: Di 25. Sep 2007, 12:05
Kontaktdaten:

Re: Trackermodul-Engine (sehr einfach)

Beitragvon DOSferatu » Di 10. Mär 2020, 22:45

zatzen hat geschrieben:Vielen Dank für diese Erläuterungen!

Es scheint mir, dass Pascal, auch wenn es dann ASM Routinen sind, automatisch je nach Situation die RETs in passende Opcodes umwandelt, denn ich habe überall nur RET geschrieben und nirgends RETF. Oder habe ich da nur Glück gehabt?

Ja, Glück gehabt. Ich verlasse mich nicht auf sowas. Ich benutze immer RETN, wenn ich einen NEAR-Return haben will oder RETF, wenn ich einen FAR-Return will. In ASM kann man teilweise etwas "kreativ" zu Werke gehen und nicht immer ist dem Compiler klar, was man da angestellt hat, wenn man mit dem Stack herumspielt.

INNERHALB des gleichen Assemblerblocks, sind RET natürlich normalerweise NEAR. Da sollten die CALLs dann natürlich auch NEAR sein.

zatzen hat geschrieben:Alle Prozeduren die vom Player genutzt werden (bis auf die Initialisierungen) sind rein in Assembler gehalten und ohne Locals und Parameter. Daher habe ich BP für eigene Zwecke verwendet. Aber gut, dass Du nochmal darauf hinweist, wann genau man da aufpassen muss.

Kleiner Hinweis dazu: BP ist, genau wie die anderen 7 Register, ab 386er ja 32bit. Falls man von Registern nur die unteren 16bit benutzen will und den Rest sichern, kann man z.B. das tun:

Code: Alles auswählen

db $66;rol BP,16;

Das tauscht quasi die oberen und die unteren 16 Bit und somit kann man quasi beides haben - sowohl den ursprünglichen Wert als auch einen anderen. Außerdem ist es schneller als PUSH/POP.
Wenn man so ein Register übrigens sichern will und das neue soll auf 0 initialisiert werden, bietet sich stattdessen SHL an:

Code: Alles auswählen

db $66;shl BP,16;

So ist BP "nach oben" gesichert und gleichzeitig auf 0 gesetzt - mit EINEM Befehl.

zatzen hat geschrieben:Und ja, eine Frage habe ich noch, nämlich mit dem Register DS. Ist das nicht eigentlich ein Dilemma im Zusammenhang mit Interrupt-Routinen?

Ich kenne dieses Dilemma - habe dazu aber eine Lösung gefunden.

zatzen hat geschrieben:Man biegt DS ja gerne mal um z.B. für den Heap, aber wenn in der Zeit wo DS dann nicht mehr aufs Datensegment zeigt eine Interrupt Routine dazwischenfunkt die auf Variablen zugreifen will, dann haben wir doch ein Problem. Mir fällt dazu spontan nur ein, dass man Interrupts sperren muss (CLI) für die Zeit in der DS nicht aufs Datensegment zeigt.

Nicht nötig.
Ja, in manchen meiner Subroutinen verändere ich DS absichtlich nicht, weil ich andauernd auch auf globale Variablen zugreifen muß und dann müßte ich das jedes Mal wiederherstellen. In Interrupts allerdings ist es schon störend, nichts "außerhalb des Interrupts" nutzen zu können - daher sichere ich DS auf etwas elegante Weise.
Wir sind uns ja einig: Alles, worüber man sich sicher sein kann, wenn ein INT aufgerufen wird, ist, daß CS definiert ist, denn CS:IP werden ja schließlich automatisch geholt, um den INT auszuführen. Achja, und man weiß, daß FLAGS gesichert ist.

Also - dann benutzen wir doch CS. Nämlich, indem wir einfach IM CODE das DS sichern! Ich habe da mal so ein kleines Testprogramm geschrieben. Der Unfug, den es anstellt, dient nur dazu, zu zeigen, daß Interrupt und Hauptprogramm gleichzeitig laufen ohne sich gegenseitig zu behindern - im Hauptprogramm wird zwischendurch DS geändert...

Ich habe es getestet - es funktioniert.
(Weil ich es als "allgmeines Anschauungsmaterial" gemacht habe, habe ich da auch in Englisch so Kommentare reingetan.)

Code: Alles auswählen

var INT8PTR:Pointer absolute 0:32;
var OLDINT8:Pointer;
var TOPLAY:word;
var TIME:longint;
var TEXT:string;
var INDEX:byte;
{----------------------------------------------------------------------------}
procedure INTER; assembler;
asm
push DS
call near ptr @saveDS
dw CS:offset @saveDS
{here starts your code for interrupt - some silly example..........}
push ES
push AX
push SI
push DI
push BX
mov AX,$B800
mov ES,AX
lea SI,DS:TEXT
xor DI,DI
mov BX,DI
mov BL,INDEX
@loop1:
cmp BL,DS:[SI]
jbe @NoChange
mov BL,1
@NoChange:
mov AL,DS:[SI+BX]
mov ES:[DI],AL
inc DI
mov AX,DI
inc AX
shr AX,1
add ES:[DI],AL
and ES:byte ptr[DI],$F
inc BL
inc DI
cmp DI,160
jb @loop1
inc INDEX
mov AL,INDEX
cmp AL,DS:[SI]
jbe @NoWrap
mov INDEX,1
@NoWrap:
pop BX
pop DI
pop SI
pop AX
pop ES
{here ends your code for interrupt.................................}
  {here an example to call the old int 8}
   pushF {because the INT8 ISR ends with an IRET. Note that NOW I-Flag is clear!}
   call OLDINT8
  {end of the interrupt routine}
pop DS
push AX
mov AL,$20
out $20,AL
pop AX
IRET{here your interrupt ends}
{--the following is only executed once - and saves DS--}
@saveDS:
pop BX  {catch the address saved by the call above into BX}
mov CS:byte ptr[BX-3],$2E {write a "CS:" prefix where first CALL was}
mov CS:word ptr[BX-2],$1E8E {write a "MOV DS,@saveDS," after the CALL}
pop CS:word ptr @saveDS {pop DS into the place where "pop BX" stands}
retF {leave normally, because its called from main program}
end;
{----------------------------------------------------------------------------}
{MAIN PROGRAM}
begin
INTER;{1x, to save DS}

asm mov AX,3;int $10;end;{set 80x25, clear screen}
writeln;
writeln('upper line: done in int8, lower line: copied in main program, changing DS!');

INDEX:=1;
TEXT:='This is a Test * (C) Imperial Games 2020 * See how to save DS for interrupt with modified code * ';

{change INT 8 to your INTER routine}
asm CLI;end;
OLDINT8:=INT8PTR;
INT8PTR:=@INTER;
asm STI;end;

{a test loop to "do something while the interrupt runs"}
repeat
write(#13'Change DS to copy lines,');
asm
push DS
mov AX,$B800
mov DS,AX
add AX,30
mov ES,AX
xor SI,SI
xor DI,DI
mov CX,80
cld
rep movsw
pop DS
end;
write('...but INT uses original DS!');

TIME:=memL[64:108]*1080 div 19663;{get ticker and calculate into seconds}
write(' Time still works: ',TIME div 3600:2,':',TIME mod 3600 div 60:2,':',TIME mod 60:2);
until memW[64:26]<>memW[64:28];{wait until key pressed}

{get back your int 8}
asm CLI;end;
INT8PTR:=OLDINT8;
asm STI;end;

memW[64:26]:=memW[64:28];{clear keaboard buffer}
end.
{explanation:
after INTER; is called once from the main program (with DS set "normally")
the "nop;nop;call near ptr @saveDS" above is changed into:
"mov DS,CS:word ptr @saveDS"
and thus it gets the real DS everytime when an interrupt is called!}


Erklärung auf Deutsch, was das macht:
Damit einem im Interrupt nicht Zyklen durch sinnlose Sprünge verlorengehen habe ich die Routine "hybrid" gemacht. Bevor man den Interrupt aktiviert, muß man EINMALIG(!) die Routine wie eine normale Procedure aufrufen. In der Routine ist nach dem PUSH DS ein Sprung nach unten (HINTER das IRET). Dieser führt nur die Befehle aus, um den Code zu modifizieren, damit es danach im Interrupt etwas anderes macht.

Die Sequenz

Code: Alles auswählen

call near ptr @saveDS
dw CS:offset @saveDS

ist 5 Bytes lang. Der CALL ist 3 Bytes und der einzelnstehende WORD-Wert, der genau die Lage des Labels @saveDS angibt, ist 2 Bytes. Die Routine unten überschreibt den CALL mit $2F,$8E,$1E, das ist der Code für mov DS,CS:,,, - und das xxx sind die restlichen 2 Bytes. Cooler wäre zwar, DS direkt mit dem Wert zu laden - aber wie wir wissen, können Segmentregister nur entweder über ein normales Register oder über eine Speicherreferenz geladen werden. Unten (hinter dem IRET) das Ganze wird nur 1x benötigt. Deshalb überschreibt es praktischerweise gleich den POP BX Befehl mit dem Wert, den DS jetzt gerade hat. Das heißt, dieser Einmal-Aufruf der Routine sollte erfolgen, wenn DS gerade den RICHTiGEN Wert hat (also am besten gleich am Anfang des Programms, wenn noch nichts geändert wurde....).

Die INT-Routine hat durch die Modifikation dann keinen CALL (oder JUMP oder wasauchimmer) mehr drin, sondern hat als erste zwei Befehle quasi diese:

Code: Alles auswählen

push DS
mov DS,CS:word ptr @saveDS

Und das ist es ja, was man sowieso braucht: DS muß ja sowieso gesichert werden (weil man nicht weiß, ob es im Hauptprogramm gerade NICHT "original" ist (und das muß ja vom INT so wiederhergestellt werden!) und danach holt es das "originale" DS, was ganz zu Anfang durch diesen Einmal-Aufruf gesichert wurde quasi "aus seinem eigenen Code". Praktischerweise habe ich es HINTER das IRET getan - so muß nichts übersprungen werden oder so.

Den ganzen Schlunz in Pascal ringsherum habe ich nur gemacht, um zu zeigen, wie man das Ganze innerhalb von Pascal einbauen könnte.

Damit die Subroutine "irgendwas macht" habe ich diesen lustigen Scrolltext da hingemurkst - das ist nicht gerade schick gecodet, soll quasi nur zeigen, daß alles, was man da innerhalb des INTs macht, geht. Und: Man muß nur die Register sichern, die man wirklich benutzt.

Man KANN danach am Ende die Original-Routine (im Falle von INT8 also den Ticker) aufrufen, einfach mit einem CALL (und einem PUSHF davor! nicht vergessen!). Das IRET des normalen INT8 würde zwar INTs wieder am Ende freigeben - aber das PUSHF macht gleich 2 nützliche Dinge: Zum Einen sorgt es natürlich dafür, daß nicht abgestürzt wird - weil ja ein IRET IP,CS und FLAGS holt. Zum Anderen ist bei FLAGS ja (weil es INNERHALB des INT ist) zu der Zeit das INT-Flag gesperrt, d.h. das "zurückholen" von FLAGS von der Originalroutine wird trotzdem keine INTs freigeben (die vielleicht am Ende in "unseren neuen" reingrätschen können. Die Freigabe macht erst unser eigener INT mit seinem eigenen IRET.

Das POP DS am Ende ist wichtig - schließlich haben wir es zu Anfang gePUSHt.
Außerdem muß man noch (vor allem, falls man NICHT die Original-Routine aufruft!), dieses OUT $20,$20 machen (also über AL), damit setzt man den Interrupt-Controller zurück. (Wenn man das NICHT macht, kommen keine weiteren INTs!)
Die Original-Routinen haben natürlich selbst am Ende diese OUT-Sachen drin (sonst würden sie ja nicht funktionieren).

Achja, wieso da am Anfang ein CALL und kein JMP? Naja, JMP wäre auch gegangen, aber 1. weiß man nicht (weil es von der Länge des Codes im INT abhängt) ob der Compiler/Assembler einen SHORT oder NEAR JUMP macht. (Ein Short JUMP hat nur 2 Bytes, nicht drei. Dann muß man noch ein NOP davor machen, damit genug Platz für den neuen (MOV DS...) Befehl ist. Außerdem habe ich durch den CALL (den ich NICHT "returniere!") nämlich auf dem Stack jetzt die Offset-Adresse, die genau hinter dem Call ist (also die "Rücksprung-Adresse"). Die lade ich in BX und greife dann mit BX auf diese Stelle da oben zu, um den Code zu ändern. Natürlich kann man das auch einfach mit Labels machen und dann LABEL und LABEL+1 ... und damit den Befehl ändern. Aber man kann es ja auch mal auf die coole Art machen. Dann wird der Befehl geändert UND natürlich ist ein zweites POP da unten und das POPt den Inhalt von DS gleich in die Stelle hinter @saveDS - damit es von dort später geholt werden kann. Man kann ja nicht nur in Register POPen, sondern auch direkt in Speicher.

Daß ein CALL ein paar Zyklen mehr braucht als ein JMP, macht hier das Kraut nicht fett. Erstens wird der ja insgesamt nur EINMAL ausgeführt (danach wird er ja überschrieben) und zweitens wird das wohl unten wieder eingespart durch die Zugriffe per BX.

Ich hoffe, ich konnte das einigermaßen erklären. Es ist zwar modifizierter Code, aber ich finde, das ist eine einfache und sehr elegante Lösung, um das Ursprungs-DS zu retten, um es im INT benutzen zu können.

Wie immer: Falls noch Fragen auftreten, gerne fragen.
Benutzeravatar
zatzen
DOS-Kenner
Beiträge: 459
Registriert: Di 17. Apr 2012, 05:10
Wohnort: bei Köln
Kontaktdaten:

Re: Trackermodul-Engine (sehr einfach)

Beitragvon zatzen » Di 10. Mär 2020, 23:40

Danke nochmals für die schnelle Antwort!

Was mich bzgl. RET's etwas verwirrt ist, dass im Assembler86FAQ nur RET aufgeführt ist, und dass es dort so beschrieben ist als sei durch die Art des CALLs bedingt ob nur IP oder auch CS zurückgeholt werden. D.h. demnach müsste die CPU durch den CALL "wissen", wie sie den Return zu interpretieren hat.

Es freut mich, dass es eine Lösung zu dem DS Problem gibt. Grob kann ich es schon nachvollziehen, aber ich möchte es erst selber verwenden wenn ich es so verstanden habe dass ich es ohne nachzusehen selber coden kann. Bis dahin vermeide ich DS-Änderungen, womit man ja meistens sowieso am besten fährt, zumal man stattdessen auch FS und GS benutzen kann, wenn auch etwas kryptisch innerhalb Pascal. Noch eine Frage wäre noch, ob Pascal selbst den Hochsprachencode so kompiliert, dass eine zeitweise Änderung von DS vorkommt.

Übrigens habe ich Bedenken, ob ich bei den ZV2 Routinen mit den Registern hinkomme, d.h. mit SI, DI, BX und BP. Kombinieren kann man ja nur immer SI/DI mit BX/BP, wenn ich das richtig erfahren habe.
DOSferatu
DOS-Übermensch
Beiträge: 1180
Registriert: Di 25. Sep 2007, 12:05
Kontaktdaten:

Re: Trackermodul-Engine (sehr einfach)

Beitragvon DOSferatu » Mi 11. Mär 2020, 20:50

zatzen hat geschrieben:Danke nochmals für die schnelle Antwort!

Was mich bzgl. RET's etwas verwirrt ist, dass im Assembler86FAQ nur RET aufgeführt ist, und dass es dort so beschrieben ist als sei durch die Art des CALLs bedingt ob nur IP oder auch CS zurückgeholt werden. D.h. demnach müsste die CPU durch den CALL "wissen", wie sie den Return zu interpretieren hat.

Um Himmels Willen! - NEIN!
Auf GAR KEINEN FALL "wissen" die CPU-Befehle irgend etwas voneinander. Jeder Befehl steht für sich allein. Ein RET macht einfach nur das, was der Opcode ihm vorgibt. Man kann ein RET auch ausführen, ohne einen CALL gemacht zu haben! Man kann einen Offset (oder eine Adresse) auch einfach manuell auf den Stack schmeißen und dann RET ausführen. (Den Trick benutze ich gern, wenn ich mal irgendwo zu einer FAR-Adresse springen will und die gerade irgendwo in einem Register rumliegen habe.)

Was möglich ist, ist, einen NEAR-RET zu einem FAR-CALL zu machen, aber nur, wenn es einer ist, der keine zusätzlichen Bytes vom Stack holen soll.
D.h. man könnte so eine Routine dann NEAR oder FAR aufrufen. NEAR-RET holt nur IP, FAR-RET holt IP und CS. Allerdings wirft ein FAR-CALL ja auch CS und IP (in dieser Reihenfolge) auf den Stack - d.h. wenn man so eine Routine dann mit NEAR-RET beenden würde, müßte man danach noch das zusätzliche Word vom Stack holen (add SP,2). Aber dieses ganze Gefrickel macht überhaupt keinen Sinn - man spart dadurch quasi nix, wenn man dafür dann außenrum so Herummurksen muß - das lohnt nur, wenn man irgendeinen kreativen Trick/Hack einsetzen will.

Also, nochmal: Die Befehle machen nur für sich das, was sie machen sollen. ein FAR CALL wirft CS und IP auf den Stack und wechselt dann zum neuen Segment und dort in den neuen Offset. Ein NEAR CALL wirft nur IP auf den Stack und wechselt dann innerhalb des gleichen Segments (das in CS steht) zum neuen Offset. Ein FAR RET holt IP und CS vom Stack und wechselt dann zum neuen Segment (CS) und dort zum Offset (IP). Ein NEAR RET holt IP vom Stack und wechselt innerhalb des aktuellen Segments zum Offset (IP). Die beiden zusätzlichen "normalen" RETs sind für Hochsprachen gedacht, weil ja am Anfang einer Subroutine Parameter über den Header übergeben werden oder auch im Kopf (zwischen Header und BEGIN) noch lokale Variablen angelegt werden... Diese ganzen Dinge landen zusätzlich auf dem Stack - und beim Beenden muß der Stackpointer ja wieder da stehen, wo er vor dem CALL war, deshalb diese RETs, die zusätzlich einen Parameter haben, der entsprechend viele Bytes noch "überspringt".

Und der IRET - naja, bei dem macht es ja Sinn, u.a. daß FLAGS zu speichern (vor allem, weil ein INT ja selbst ein Bit darin ändert - das INT-Erlauben-Bit). Und weil man im Interrupt ja nicht NICHTS machen will, würde sich ja sowieso mindestens irgend etwas in FLAGS ändern - und das Hauptprogramm darf ja davon nichts mitbekommen...

zatzen hat geschrieben:Es freut mich, dass es eine Lösung zu dem DS Problem gibt. Grob kann ich es schon nachvollziehen, aber ich möchte es erst selber verwenden wenn ich es so verstanden habe dass ich es ohne nachzusehen selber coden kann.

Naja, es ist etwas mehr Code als nötig gewesen wäre - das ganze "Drumherum" habe ich nur gemacht um zu demonstrieren, wie man es anwendet und daß es funktioniert. In Wirklichkeit ist der einzige Witz daran, daß man einfach nur direkt irgendwo im Codebereich des INT (also irgendwo, wo CS:xx ist) das DS vorher hinterlegt, damit es der INT finden kann. Man MUß das auch nicht so elegant wie ich machen. Aber weil ja CS das einzige Segmentregister ist, was zum Zeitpunkt des Aufrufs des INT einen sichergestellten Wert enthält, ist das meiner Meinung nach die einzige Möglichkeit, innerhalb eines INT an das DS zu kommen.

Eigentlich ist das nicht ganz richtig. Der Assembler von Pascal bietet zwei Pseudo-Konstanten an: @DATA und @CODE.
mit SEG @DATA kann man das Haupt-Datensegment angeben, mit SEG @CODE das Haupt-Codesegment. Allerdings kann man ja leider einem Segment keine Konstante zuweisen (dafür gibts keinen Opcode). Also geht das dann wieder nur über ein Register: mov AX,SEG @DATA; mov DS,AX;

Bin aber nicht sicher, ob das immer funktioniert - normalerweise sollte es aber.

zatzen hat geschrieben:Bis dahin vermeide ich DS-Änderungen, womit man ja meistens sowieso am besten fährt, zumal man stattdessen auch FS und GS benutzen kann, wenn auch etwas kryptisch innerhalb Pascal. Noch eine Frage wäre noch, ob Pascal selbst den Hochsprachencode so kompiliert, dass eine zeitweise Änderung von DS vorkommt.

Naja, normalerweise würde ich nein sagen - andererseits KENNT Pascal ja noch kein FS/GS. Diese in ASM gern als "String-Operationen" bezeichneten Dinge (REP MOVSW und ähnliches) brauchen ja DS und ES. (Man kann dabei DS auch mit etwas anderem "überschreiben - wenn man hat! - aber da bleiben ja nur CS und SS, weil, wie gesagt, FS und GS benutzt Pascal von sich aus nicht.) Also... ja, es könnte entweder sein, daß Pascal DS auch mal ändert ODER es ändert, aber nur in CLI/STI eingegrenzt. Kann ich leider nicht sagen - habe noch nicht alle Befehle von Pascal quasi manuell überprüft.

zatzen hat geschrieben:Übrigens habe ich Bedenken, ob ich bei den ZV2 Routinen mit den Registern hinkomme, d.h. mit SI, DI, BX und BP. Kombinieren kann man ja nur immer SI/DI mit BX/BP, wenn ich das richtig erfahren habe.

So ist es: Es gibt diese Kombinationen:
[BX], [SI], [DI], [BX+SI], [BX+DI], [BP+SI], [BP+DI]
dann noch alle +Bytewert, alle +Wordwert (hier auch [BP+Bytewert] und [BP+Wordwert]) - nur [BP] einzeln gibt es nicht, das wird immer intern umgewandelt in [BP+0]. (Grund dafür ist, daß an der "Stelle", wo das von der Abfolge her stehen würde, der einfache Wert (ohne Indexregister) steht, also [Wert] - sonst hätte man das nicht umsetzen können.)

ABER: Das Ganze gilt nur für 16bit. Wir haben ja auch die Möglichkeit der 32-Bit-Adressierung! Wenn man damit im 16bit-Mode auf Offsets >65535 zugreift, stürzt zwar der Rechner ab - aber da muß man eben drauf achten, daß das nicht passiert! Indem man die oberen 16bit der Register =0 setzt. Denn in der 32-Bit-Adressierung kann man ALLE Register (außer natürlich die Segmentregister) zur Adressierung benutzen, diese werden dann aber in ihrer 32-Bit-Entsprechung benutzt (also EAX statt AX).

Dazu folgt dem normalen Mod-R/M-Byte, das den normalen Befehlen folgt, wenn sie das $67-Präfix haben, zusätzlich noch ein S-I-B Byte (Scale-Index-Base).

Das heißt so, weil man bei einem der beiden Register sogar angeben kann, daß es mit 1, 2, 4 oder 8 multipliziert wird (wenn man beide auf das gleiche Register setzt, sind so also auch Indezes wie 3, 5 und 9 möglich).

Das S-I-B Byte hat also in seinen 8 Bit: 2Bit, die angeben, ob *1, *2, *4 oder *8 (bei einem der Register) und dann 3Bit für das eine (Base) Register (das ist das, was multipliziert werden kann) und 3Bit für das andere (Index) Register (das immer *1 ist). Da gibt's auch so "Sonderfall" der auch an der Stelle steht, wo normal EBP wäre... also so Fall wo ohne Register sondern nur mit Wert bzw in Kombination mit Wert...

Die Register sind bei x86 ja bekanntlich "numeriert" in dieser Reihenfolge:
AX, CX, DX, BX, SP, BP, SI, DI - so sind dann also auch die Bitmuster:
(000, 001, 010, 011, 100, 101, 110, 111)
Das gilt dann auch für die Exx-Register: gleiche Reihenfolge.

Nun kennt ja TurboPascal keine 32bit-Sachen, deshalb muß man, WENN man diese Geschichte nutzen will, das natürlich alles schick manuell basteln - aber ich habe Tabellen da, falls man's braucht.

ja, diese super-indizierte Methode ist natürlich recht schick - allerdings, wie gesagt: Da immer dran denken, daß die oberen Bits =0 sind, bzw. daß das ERGEBNIS, also der "Offset" von der Konstruktion dann ein 32-Bit-Wert wird, der auch im 16-Bit-Mode NICHT "wraparoundet"! Und wenn dieses Ergebnis einen Wert >$FFFF erreicht, dann wird so ein Fehler-INT ausgelöst (weiß grad nicht auswendig, welcher - steht sicher irgendwo, z.B. in der ASM86FAQ) - was im Klartext bedeutet: Wenn man den nicht abgefangen hat (was man sowieso kaum tut): Absturz. Und WENN man ihn abfängt ... naja, dann hat man kaum Performance gewonnen durch die Konstruktion, weil wenn da jedesmal eine Fehler-INT-Bereinigungs-ISR laufen soll...

Wollte nur der Vollständigkeit halber drauf hinweisen, daß Dir im 32bit-Mode (bzw mit der 32-Bit-Address-Option) auch die anderen Register zur Indizierung zur Verfügung stehen.
Benutzeravatar
zatzen
DOS-Kenner
Beiträge: 459
Registriert: Di 17. Apr 2012, 05:10
Wohnort: bei Köln
Kontaktdaten:

Re: Trackermodul-Engine (sehr einfach)

Beitragvon zatzen » Sa 14. Mär 2020, 18:12

DOSferatu hat geschrieben:Eigentlich ist das nicht ganz richtig. Der Assembler von Pascal bietet zwei Pseudo-Konstanten an: @DATA und @CODE.
mit SEG @DATA kann man das Haupt-Datensegment angeben, mit SEG @CODE das Haupt-Codesegment. Allerdings kann man ja leider einem Segment keine Konstante zuweisen (dafür gibts keinen Opcode). Also geht das dann wieder nur über ein Register: mov AX,SEG @DATA; mov DS,AX;

Bin aber nicht sicher, ob das immer funktioniert - normalerweise sollte es aber.

Selbstdefinierte Konstanten liegen ja auch im Datensegment, mit denen kann es also nicht funktionieren. Auch im Hauptprogramm DS im Codesegment zu hinterlegen und dann mit "mov ax, cs:[offset @dsbackup]" darauf zuzugreifen wäre wahrscheinlich unsicher, da sich CS ändern kann. Aber wenn man diese Konstanten @DATA und @CODE als Compilerkonstanten betrachten könnte auf die referenziert werden kann unabhängig vom Registerinhalt von DS oder CS, dann könnte man daraus vielleicht wirklich eine simple Lösung zu dem Problem basteln, ohne modifizierten Code. Aber das lässt sich ja auch relativ einfach überprüfen.

DOSferatu hat geschrieben:ABER: Das Ganze gilt nur für 16bit. Wir haben ja auch die Möglichkeit der 32-Bit-Adressierung! Wenn man damit im 16bit-Mode auf Offsets >65535 zugreift, stürzt zwar der Rechner ab - aber da muß man eben drauf achten, daß das nicht passiert! Indem man die oberen 16bit der Register =0 setzt. Denn in der 32-Bit-Adressierung kann man ALLE Register (außer natürlich die Segmentregister) zur Adressierung benutzen, diese werden dann aber in ihrer 32-Bit-Entsprechung benutzt (also EAX statt AX).

Dann wohl am einfachsten "db 66h; xor ax, ax"? Klar, wenn ich den Wert behalten will wohl lieber "db 66h; ror ax, 16; xor ax, ax; ror ax, 16" - sind aber auf nem 486 direkt 5 Takte. Ach ich dumme Nuss - "db 66h; and ax, 0ffffh" geht ja auch.
Das sind natürlich tolle Möglichkeiten, gerade auch mit der Multiplikation, ich müsste mir nur wenn ich das "per Hand" bastle mir den Assembler-Code als Kommentar dazuschreiben. Ich brauche diese Übersichtlichkeit, deshalb rücke ich auch die Zeilen ein.

Danke für diesen Hinweis, vielleicht wird er bei ZVID2 sehr wichtig werden. Aber dazu muss dann auch erstmal die Sache mit dem Unchained Mode geklärt werden. Und irgendwie meine ich, dass die Idee mit dem nur teilweise wiederherstellen und nur veränderte Bereiche in den Grafikspeicher kopieren auch im Unchained Mode sinnvoll sein könnte, sogar mit Scrolling, wenn man das mittels vier Seiten macht. Aber alles noch Neuland...
Benutzeravatar
zatzen
DOS-Kenner
Beiträge: 459
Registriert: Di 17. Apr 2012, 05:10
Wohnort: bei Köln
Kontaktdaten:

Re: Trackermodul-Engine (sehr einfach)

Beitragvon zatzen » Do 26. Mär 2020, 20:06

Hallo DOSferatu!
Ich habe das mit dem @data einmal ausprobiert mit folgendem Code:

Code: Alles auswählen

  var ds1, ds2: word;
begin
  asm
    mov ds1, ds
    push ds
    xor ax, ax
    mov ds, ax
    mov ax, seg @data
    pop ds
    mov ds2, ax
  end;
  writeln(ds1, ' ', ds2);
end.

Ergebnis: Beide Variablen, ds1 und ds2, haben den gleichen Wert.
Das müsste eigentlich beweisen, dass "seg @data" zum einen auf das Datensegment zeigt bzw. DS entspricht, und zum anderen unabhängig von dem Register DS seinen Wert behält. Deine Code-modifizierende Interrupt Routine ist ein tolles Kunststück, aber ich würde das ganze dann einfach über "seg @data" lösen. Oder was meinst Du?
Benutzeravatar
zatzen
DOS-Kenner
Beiträge: 459
Registriert: Di 17. Apr 2012, 05:10
Wohnort: bei Köln
Kontaktdaten:

Re: Trackermodul-Engine (sehr einfach)

Beitragvon zatzen » Fr 27. Mär 2020, 18:53

Okay, das Problem scheint gelöst. Hier nur die kleinere der beiden Interrupt-Routinen:

Code: Alles auswählen

procedure interrupt_flagtick; assembler;
asm
  push ax
  push ds
  mov ax, seg @data
  mov ds, ax
  mov btplay_next_tick, 1
  mov ax, tickervalue
  add tickerwrap, ax
  pop ds
  jc @call_oldint
    mov al, 020h
    out 020h, al
    pop ax
    iret
  @call_oldint:
    pop ax
    pushf
    call old_intvec
    iret
end;

So wie ich das nachgelesen habe beeinflusst ein POP die Flags nicht, daher müsste hier JC noch funktionieren.

Wer ist online?

Mitglieder in diesem Forum: 0 Mitglieder und 3 Gäste