PDA

Archiv verlassen und diese Seite im Standarddesign anzeigen : Assembler Stack Technik Fragen


Capt'N Coax
2004-01-31, 18:41:50
Hey Leute,

Ich hab mal wieder eine Assembler Frage (Für den 6502, ist aber net soo wichtig).

Geht weniger darum was der Stack ist, als vielmehr um Arbeitsweisen, die mit Ihm betrieben werden können.

Konkret würde ich gerne folgendes wissen:
Wie funktioniert die Frame-Pointer Arbeitsweise? Ein Beispielprog oder ein gutes Tut wäre sehr hilfreich!

Außerdem ist mir der Sinn nicht ganz klar, weshalb Ergebnisse von Unterprogrammen (also per JSR) auf den Stack gespeichert werden sollen (auch wenn es IMO nicht nötig wäre).
Zur Verdeutlichung vielleicht ein Beispiel, bei der mir die Nutzung des Stacks nicht ganz einleuchtet:

--------------------
Ab Adresse a+1 stehen v Zahlen, wobei Anzahl v in a gespeichert ist (Diese Speicherart nennt man meines Wissen HFELD). Sinnvollerweise ist a auf einen Zeiger (ZeroPage) abgebildet, den ich einfach mal ZEIGER nenne.
Man kann auf v und a natürlich nur per ZEIGER zugreifen.

Jetzt sollen von mir aus alle geradzahligen Zahlen per Unterprogramm ermittelt werden (z.B. UP1) und per STAPEL AN DAS UNTERPROGRAMM ÜBERGEBEN WERDEN.
---------------------

Der markierte Teil ist mir nicht klar.

Also, ich würde mich freuen wenn mir einer folgendes erklärt:

- Frame-Pointer
- Markierter Teil
- Sonstige Stacktechniken

Ich hab im Kopf zwar halbgare Ideen, möchte aber gerne eine Absicherung haben ;)

THX,
de Capt'N

Demirug
2004-01-31, 20:49:53
Die Idee dahinter ist den Stack als Speicher für die übergabe von Werten an Unterprogramme zu verwenden. Bei Hochsprachencompielern ist das durchaus üblich.

Der Unterprogrammaufruf erfolgt dann so das man zuerst alle Parameter auf den Stack schiebt und als letztes den Aufruf ausführt welcher ja noch die Rücksprungadresse auf den Stack schiebt.

Innerhalb des Unterprogramms holt man sich nun zuerst einmal den Wert des Stackpointers. Dieser zeigt auf die erste freie Stelle. Davor liegt die Rücksprungadresse und alle Parameter. Mit einer entsprechenden relativen Addreessierung kann man jetzt auf alle Parameter zugreifen.

Nachdem das Unterprogramm beendet ist muss man natürlich alles was man auf den Stack geschoben hat auch wieder herunter holen. Auf diese Weise kann man auch Werte vom Unterprogramm zurückliefern.

Beim 6502 müsste das ganze wie folgt funktionieren.


Aufruf:

LDA irgendwas
PHA
LDA was anderes
PHA
JSR Sub1
PLA
PLA

Unterprogramm:

TSX ; Stack Pointer nach X
LDA $0103,X ; dort liegt was anderes
...
LDA $0104,X ; dort liegt irgendwas
...
RTS

zeckensack
2004-02-01, 22:55:34
ESP ist eigentliche stack pointer, der von der Hardware für Rücksprungaddressen genutzt wird (CALL/RET). Dieser muss immer am unteren Ende der aktuell benötigten temporären Variablen liegen (wenn diese auf dem Stack liegen), denn es kann jederzeit ein Interrupt ausgelöst werden, wodurch der Stack modifiziert wird. Liegen Nutzdaten unterhalb von ESP, werden sie dabei zerstört, was man natürlich nicht will.

Warum legt man Variablen auf dem Stack an? Weil der Stack viel schneller und viel unkomplizierter ist als malloc/free, und robuster als globale Variablen. Letztere erlauben nämlich keine Reentranz, was uA für Multithreading lebensnotwendig ist.

Original geschrieben von Capt'N Coax
- Frame-Pointer
Die "AMD Athlon Processer x86 code optimization guide" (PDF) (http://www.amd.com/us-en/assets/content_type/white_papers_and_tech_docs/22007.pdf) beschreibt es treffend:
"A classical approach for referencing function arguments and
local variables inside a function is the use of a so-called frame
pointer. In x86 code, the EBP register is customarily used as a
frame pointer. In function prologue code, the frame pointer is
set up as follows:

PUSH EBP ;save old frame pointer
MOV EBP, ESP ;new frame pointer
SUB ESP, nnnnnnnn ;allocate local variables

Function arguments on the stack can now be accessed at
positive offsets relative to EBP, and local variables are
accessible at negative offsets relative to EBP. In the function
epilogue code, the following work is performed:

MOV ESP, EBP ;deallocate local variables
POP EBP ;restore old frame pointer"

Hier werden - wie angesprochen - die lokalen Variablen der Funktion auf dem Stack angelegt. ESP 'schützt' die Funktion nach unten hin vor Interrupts. Über dem Frame Pointer liegt zuerst die Rücksprungaddresse in die aufrufende Funktion, dann die Parameter der Funktion, und unterhalb des Frame Pointers (und damit über dem Stack Pointer) liegen die lokalen Variablen.

Das ganze ist im Grunde völlig unnötig, denn die Funktion weiss, wieviel Platz sie für ihre lokalen Variablen reserviert hat. EBP ist also um eine bekannte Konstante höher als ESP. Es gibt aber doch einen Grund: Debugger und die beliebten 'stack traces'. Um die Funktion zu finden, die die aktuelle (gecrashte?) Codestelle aufgerufen hat, muss man [EBP+4] lesen. Das ist der alte EIP.
Nun möchte man auch die lokalen Variablen der übergeordneten Funktion wissen. Die - bzw der Frame Pointer, mittels derer sie man sofort finden kann - erhält man wenn man [EBP] einliest. Währenddessen bleibt ESP an seinem Platz, und schützt weiterhin alle möglichen lokalen Variablen vor dem Überschreiben.

Das kann man dann bis auf die höchste Hierarchie-Ebene fortführen, bis man irgendwo im Kernel-Code landet, wo der Prozess erzeugt wurde.

Es kann sein, dass dieser Mechanismus auch für throw/catch genutzt wird, aber da kenne ich mich nicht so gut aus ...

- Markierter TeilDas ist eine Abwandlung der vararg Aufrufkonvention (http://www.google.com/search?q=vararg+calling+convention&sourceid=opera&num=0&ie=utf-8&oe=utf-8). Die aufrufende Funktion muss natürlich wissen, wieviele Parameter sie übergeben möchte, die aufgerufene Funktion braucht das allerdings nicht vorher zu wissen. Sie kann sich die Anzahl vom Stack holen, wo sowieso alle Parameter liegen - sofern die aufrufende Funktion die Information dort auch hinterlegt hat.

In C kann man Funktionen tatsächlich so deklariern:
void do_something(int x, ... );Man beachte die drei Punkte. printf ist zB so ein Kandidat, der beliebig viele Parameter fressen kann.


Original geschrieben von Demirug
<...>
PHA
<...>
PLA
Phush? Plop? :D

Demirug
2004-02-01, 23:03:40
Original geschrieben von zeckensack

Phush? Plop? :D


PHA = Push Accumulator
PLA = Pull Accumulator

liquid
2004-02-02, 17:31:04
Kurze Frage, gehört auch in gewisser Weise zum Thema. Wenn ich eine Funktion habe, die mit Gleitkommazahlen arbeitet also die ST(x) Register belegt und in dieser Funktion eine andere Funktion aufgerufen wird, was passiert dann vor dem Funktionsaufruf mit den ST(x) Registern?
Werden die auf dem Stack gespeichert, denn sie könnten ja von der anderen Funktion überschrieben werden? Oder muss sich darum die Funktion selber kümmern.

Also sagen wir mal wir haben Funktion A und Funktion B. In A wird B aufgerufen. A benutzt sowohl vor als nach dem Aufruf von B die ST-Register. B nutzt diese allerdings auch. Ist es nun Aufgabe von A oder von B die Register zu sichern? Weiss der Compiler wie er das machen soll (analysiert er die Funktionen) oder geht es immer davon aus, dass die Funktion alle Register manipuliert und speichert deshalb alles auf dem Stack?

Ich hab nämlich ein schlechtes Gefühl bei meinem memcpy-Funktionen, da diese die MMX-Register benutzen und die ja wiederum auf den ST-Registern liegen. Sprich wenn ich memcpy benutze ich nachher der Inhalt der ST-Register flöten. Was ist wenn die memcpy-aufrufende Funktion da vorher Daten drinhatte und die jetzt weiterverwenden will?

cya
liquid

Capt'N Coax
2004-02-02, 17:44:11
Wow, hat sich ja was getan!
Wollte schon eher mal schauen, aber momentan nur Stress.

Muss das alles erstmal verarbeiten, sprich Theorie verstehen und test0rn. Aber erstmal danke!

Wenn ich hier vorankomme meld ich mich wieder wie's ausschaut.

Ach ja:
Ihr 2 macht das aber Hobbymäßig/im Beruf?
Oder Studium Technische Inf.?

Mit meinem 1 Semester Billig- RS FH seh ich da ziemlich alt aus ;)

So long,
de Capt'N

EDIT: Gestern beim Umzug TIERISCH auf der Treppe ausgerutscht. Mit dem Hintern abgefangen. Der ist jetzt schwarz/Lila. Und komme im Stehen nicht so gut voran, rein programmtechnisch :(

Xmas
2004-02-02, 17:58:16
Original geschrieben von liquid
Kurze Frage, gehört auch in gewisser Weise zum Thema. Wenn ich eine Funktion habe, die mit Gleitkommazahlen arbeitet also die ST(x) Register belegt und in dieser Funktion eine andere Funktion aufgerufen wird, was passiert dann vor dem Funktionsaufruf mit den ST(x) Registern?
Werden die auf dem Stack gespeichert, denn sie könnten ja von der anderen Funktion überschrieben werden? Oder muss sich darum die Funktion selber kümmern.

Also sagen wir mal wir haben Funktion A und Funktion B. In A wird B aufgerufen. A benutzt sowohl vor als nach dem Aufruf von B die ST-Register. B nutzt diese allerdings auch. Ist es nun Aufgabe von A oder von B die Register zu sichern? Weiss der Compiler wie er das machen soll (analysiert er die Funktionen) oder geht es immer davon aus, dass die Funktion alle Register manipuliert und speichert deshalb alles auf dem Stack?

Ich hab nämlich ein schlechtes Gefühl bei meinem memcpy-Funktionen, da diese die MMX-Register benutzen und die ja wiederum auf den ST-Registern liegen. Sprich wenn ich memcpy benutze ich nachher der Inhalt der ST-Register flöten. Was ist wenn die memcpy-aufrufende Funktion da vorher Daten drinhatte und die jetzt weiterverwenden will?

cya
liquid
Das sollte auf die Calling Convention ankommen. Bei C/C++ ist AFAIK standardmäßig (__cdecl): Jede Funktion sichert zuerst genau so viele FP-Register auf den (Speicher-)Stack, wie sie selbst benötigt, und holt diese vor dem Rücksprung wieder zurück. Im Prinzip nicht anders als bei den General Purpose Registern.

liquid
2004-02-02, 18:43:03
Also müsste ich in meiner memcpy Funktion vorher noch alle ST-Register auf den Stack pushen? omg, das tut der Performance ja gar nicht gut...

cya
liquid

zeckensack
2004-02-02, 19:01:23
Original geschrieben von liquid
Also müsste ich in meiner memcpy Funktion vorher noch alle ST-Register auf den Stack pushen? omg, das tut der Performance ja gar nicht gut...

cya
liquid Nein, musst du nicht. Der Wechsel zwischen MMX und x87 erzeugt eine Exception, die ein entsprechender Handler im OS abfängt. Der kümmert sich darum dass
a)beim Übergang x87->MMX die x87-Register gesichert werden
b)beim Übergang MMX->x87 die Register wieder in den Ausgangszustand versetzt werden.

Aber immer schön EMMS ans Ender deiner MMX-Funktionen setzen ..., sonst kann's dir passieren, dass b) nicht stattfindet. Wenn du das beachtest, dann geht der Rest allerdings automatisch.

liquid
2004-02-02, 20:12:27
Ne, ist klar, das emms hab ich schon drin. Das klingt ja echt super, dann wirds wohl auch so gehen. *juhuu*

cya
liquid

Capt'N Coax
2004-03-06, 17:05:30
So, bin jetzt endlich mal dazu gekommen mir eure Hilfe mal anzuschauen :)

Ich hab mal was geproggt, bezieht sich auf eine Klausuraufgabenstellung, dann hat wenigstens jeder den selben Ansatz hier. Ich poste jetzt mal die Aufgabenstellung, und zeig euch dann mal meine Lösung.

Am Ende frag ich dann ein paar Sachen die mir noch nicht klar sind, das bedeutet möglicherweise am wenigsten Aufwand für euch, ist ja Wochenende.

Aufgabe:
Ab einer Adresse a+1 seien 255 >= v <= 0 Zahlen im Byte-Format gespeichert, wobei v in a stehe (es liege also ein "Hfeld" vor).
a sei ab der Seite-0-Adresse ZEIGER gespeichert.

Auf v und die Zahlen kann nur über diesen Zeiger zugegriffen werden.

(a) Es ist ein Unterprogramm zu entwickeln, das die Anzahl v der Zahlen in der Tabelle ermittelt, die geradzahlig sind. Das Ergebnis ist an das aufrufende Programm
auf dem Stapel zu übergeben.

(b) Schreiben Sie den Teil des aufrufenden Programms, das das Unterprogramm gemäß (a) aufruft und das Ergebnis zum Inhalt einer Speicherzelle ANZ_GERADE addiert.

**********************

Da Code:

.ORG $4000 ;Speicher ab 4000 nutzen

;Main

Zeiger=$00 ;Zeiger ist ZeroPage

lda STAB
STA Zeiger ;Tabelle zeigern
lda STAB+1
STA Zeiger+1

LDA #0 ;Platz für Variable
PHA ;auf dem Stack reservieren

JSR UP ;Unterprogramm Jump
PLA ;GeradzahlAnteil speichern
STA ANZ_GERADE ;in Erg (und Stack saeubern)

RTS ;Main Ende

UP:
LDY #0
LDA (Zeiger),y
TAY
Loop:
CLC
LDA (Zeiger),y
LSR ;shiften Carry an/aus
BCS NOTADD ;Wenn ungerade nicht addieren

TSX ;StackPointer -> X
LDA $0103,x ;Geradzahl Anteil
ADC #1 ;erhoehen und auf Stack
STA $0103,x ;speichern
NOTADD:
CLC
DEY ;Y Decrementieren
CPY #0 ;Y mit 0 Vergleichen
BNE Loop ;nein, Schleife
RTS ;ja, Hauptprogramm


;Tabelle ********************************************
Tab: .BYTE $02 ;Anzahl v (HFeld)
.BYTE $04 ;1. Eintrag
.BYTE $03 ;2. Eintrag

STAB: .WORD Tab
;****************************************************

;Variable zum speichern der Anzahl der Geradzahligen
;Tabelleneintraege, hier also 1.
ANZ_GERADE: .BYTE $00

.end



So, aufgefallen ist mir folgendes, bzw. habe ich euch oben so verstanden:


* Sollen Werte per Stapel übergeben werden (in einem UP), muss der Platz für einen Wert VOR dem UP- Aufruf auf dem Stapel reserviert sein.

*Auf dem Stapel wird per Absoluter Adresse + Offset zugegriffen, wobei der Offset den temporär gespeicherten Stackpointer zeigt (beim 6502 auf die nächste freie Stackstelle btw.) und die Absolute Adresse die Variablenanzahl darstellt (0103 für die erste Variable, da gilt: Stackpointer+(01)03, da zwei Plätze für die Rücksprungadresse gespeichert werden).
Ist das die einzige Methode des Stackzugriffs?

*Benutze ich hier eine indirekte- Nachindizierte Methode?
IMO ja.

*Ist das Programm Wiedereintrittsfähig? Bitte begründen. Und bedeutet wiedereintrittsfähig die tmp. Speicherung aller nötigen Register auf dem Stack?

*Was ist mies an dem Programm?

*Benutzt man RTS auch als letzte Anweisung im Hauptprogramm? Es ist ja keine Rücksprungadresse auf dem Stack mehr vorhanden?

Zu Zeckensacks Erklärung kann ich mich noch nicht äussern, da ich mit x86 Assembl0r noch rein garnichts gemacht habe und den Vergleich erstmal nachvollziehen muss.

BTW:
Den Simulator den ich genutzt habe gibt es Chier:
http://home.pacbell.net/michal_k/6502.html