Mir ist vorhin beim Arbeiten mit Pointern ein typischer Off-by-One
Fehler unterlaufen. Dabei kam mir die Frage auf was ein Zugriff auf
ungültige Adressen bei AVRs bzw. ATmegas denn für Konsequenzen hat.
Konkret geht es um einen ATmega328P, wobei das vermutlich (?) für die
ganze Serie gleich sein dürfte und letztendlich ist das natürlich eine
reine Interessensfrage.
Weder im Datenblatt noch im Instruction Set konnte ich diesbezüglich
Informationen finden, nicht einmal ein Hinweis auf das ansonsten
beliebte "undefinierte" Verhalten. Bei meiner etwas komplexeren
Mikrocontroller-Anwendung war die Konsequenz, dass der Mikrocontroller
gar nicht mehr ansprechbar war. Leider steht mir derzeit auch kein
Debugger oder ähnliches zur Verfügung.
In meinem Fall ging es um das Beschreiben des Speichers mit einem
bestimmten Bitmuster - beginnend beim Start des Heaps (__heap_start) bis
zum Ende des RAMs (RAMEND). Dafür wollte ich memset verwenden, und habe
dies zunächst so gelöst:
Im Assembler-Listing von memset sieht man, dass das Ganze auf die
Instruktion "st" zurückfällt:
1
00003e74 <memset>:
2
3e74: dc 01 movw r26, r24
3
3e76: 01 c0 rjmp .+2 ; 0x3e7a <memset+0x6>
4
3e78: 6d 93 st X+, r22
5
3e7a: 41 50 subi r20, 0x01 ; 1
6
3e7c: 50 40 sbci r21, 0x00 ; 0
7
3e7e: e0 f7 brcc .-8 ; 0x3e78 <memset+0x4>
8
3e80: 08 95 ret
Leider konnte ich keine Informationen zu st erlangen, die mir verraten
was bei einer ungültigen Adressierung passiert. Kann jemand das Rätsel
mit Verweisen auf entsprechende Dokumente lösen ;)? Gibt es zumindest
Erfahrungswerte? Führt das immer zum Absturz/Neustart?
Ich sitze mittlerweile wahrscheinlich schon zu lange vor der Röhre, und
auch auf die Gefahr hin mich hier extremst lächerlich zu machen, aber so
ganz nachvollziehen kann ich die o.g. "-1" nämlich nicht nachvollziehen.
Die war eher per Trial & Error (;)) bestimmt worden und selbst beim
Aufmalen auf Papier bin ich der Meinung, dass das eigentlich nicht
notwendig wäre.
Ein kleines Beispiel:
1
+---+---+---+---+---+---+---+---+---+
2
| 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
3
+---+---+---+---+---+---+---+---+---+
4
^ ^
5
__heap_start RAMEND
6
0x2 0x8
Der Bereich zwischen __heap_start und RAMEND ist ja genau 7 Byte groß
(2, 3, 4, 5, 6, 7, 8). RAMEND - __heap_start = 8 - 2 = 6, d.h. ich
müsste sogar noch 1 dazu addieren, um aus den beiden Variablen die Größe
des Bereichs zu berechnen. Ich bin mir ziemlich sicher, dass das Problem
vor dem Monitor sitzt, aber könnte mir da bitte jemand auf die Sprünge
helfen, warum memset nur mit o.g. Argument funktionieren mag?
Danke!
holger schrieb:> Kein Wunder wenn du dir den Stack platt machst.
Ja, das erklärt mein o.g. Problem tatsächlich. Habe gar nicht mehr an
den Stack gedacht bzw. dass dieser verwendet wird, obwohl die
entsprechende Funktion in in der "init3" Sektion steht. Aber klar, ein
Blick ins Listing hat verraten, dass memset per "call" angesprungen
wird. Danke für diesen wertvollen Tipp.
Aktuell sieht es nun so aus:
Das funktioniert zwar, aber basiert nun auf der Annahme, dass es nur die
eine "call" Instruktion gibt, welche den Stack modifiziert. Wenn jetzt
memcpy einen eigenen Stack-Frame anlegen würde, dann stehe ich vor dem
selben Problem.
Gibt es hierfür eine Lösung in C? Oder ist die Verwendung von Inline
Assembler hier tatsächlich sinnvoller, um sich solche Probleme zu
ersparen?
memset schrieb:> Gibt es hierfür eine Lösung in C?
Wofür? Warum willst den den Speicher auf 0 setzen?? Warum initialisiert
du nicht, wie es sich gehört, alle globalen und statischen Variablen auf
0 (bzw. initialisiert sie gar nicht, dann ist 0 der Default) und hast
somit alles auf 0?
Programmierer schrieb:> Wofür? Warum willst den den Speicher auf 0 setzen??
Ich setzte hier nichts auf 0. Ich schreibe ein bestimmtes Bit-Muster
hinein. Das dient dem Abschätzen der Stäckgröße während der Laufzeit. So
wird das auch hier beschrieben
(http://rn-wissen.de/wiki/index.php/Speicherverbrauch_bestimmen_mit_avr-gcc).
Ich wollte nur auf den Inline Assembler verzichten und memset()
verwenden.
Der Zugriff, zu mindest bei Prozessoren wie dem ATMega, hat soweit mir
bekannt keine Nebeneffekte.
Natürlich wird, beim Überschreiten des erlaubten Bereiches, Blödsinn
gelesen. Das Schreiben ins Nirwana hat keine mir bekannten Folgen.
... aber beim Schreiben in vorhandene Bereiche wird es richtig
interessant.
Das Schreiben in vorhandene, interne Register bewirkt genau dass, was
der Hersteller auch in seinen Datenblättern beschreibt.
Schreiben im RAM hat auch nur die schon vielfach beschriebenen Folgen
wie: Variablen und Zähler durcheinander bringen.
Den Stack zu überschreiben hat wohl ähnliche Folgen, wie wenn Du Dir den
Stuhl unter dem Hintern wegziehen lässt.
Das Lesen der jeweiligen Speicherstellen gibt deren aktuellen Zustand
zurück. Teilweise auch Blödsinn, wenn das Lesen nicht vorgesehen ist.
Alles in allem würde ich sagen: Genau das was zu erwarten ist.
memset schrieb:> aber basiert nun auf der Annahme, dass es nur die> eine "call" Instruktion gibt, welche den Stack modifiziert.
Du weißt aber schon, daß man das SP Register auch auslesen darf.
Im Stack ist nicht nur die Returnadresse, sondern auch lokale Variablen.
Peter Dannegger schrieb:> Im Stack ist nicht nur die Returnadresse, sondern auch lokale Variablen.
Dafür sollte es aber einen Variablenstack geben, oder ?
Marc Vesely schrieb:> Peter Dannegger schrieb:>> Im Stack ist nicht nur die Returnadresse, sondern auch lokale Variablen.>> Dafür sollte es aber einen Variablenstack geben, oder ?
Auf einem mC wie z.B. einem AVR gibt es nur einen Stack, Punkt.
So etwas wie stack fuer variablen, stack fuer returnadressen usw gibt es
nicht: ein Stack, punkt um!
@memset
In - was auch immer ein richtiges "C" ist - ist es zwar unerwünscht den
Stack zu beschreiben, aber erlaubt.
Zu Mindest früher wurden lokale Variablen auf dem Stack abgelegt. In
normalem "C" gibt es keine Sperre, die Dir verbietet, die mit "char
Etwas [ 2 ];" angelegte lokale Variable an der Stelle Etwas [ 2 ] zu
befummeln. Sowohl im erlaubten Bereich (0..1), als auch darüber hinaus,
ist das ein Stackzugriff.
Wie auch Peter bereits gesagt hat:
Bei manchen Mikroprozessoren sind sogar der Stack-Pointer und manchmal
der Programm-Counter ein normales Register.
Marc Vesely schrieb:> Dafür sollte es aber einen Variablenstack geben, oder ?
Das kann jeder Compilerbauer so halten, wie er lustig ist.
AVR-GCC hat keinen, IAR hat einen.
Peter Dannegger schrieb:> Du weißt aber schon, daß man das SP Register auch auslesen darf.
Ja, das tue ich doch mit o.g. Code schon.
Peter Dannegger schrieb:> Im Stack ist nicht nur die Returnadresse, sondern auch lokale Variablen.
Eben, und daher ist jeder Funktionsaufruf in diesem Kontext
problematisch, weil ich die Größe der involvierten Stack-Frames nicht
kenne und diese ggf. überschreibe.
Marc Vesely schrieb:> Dafür sollte es aber einen Variablenstack geben, oder ?
Nein, nicht bei den AVRs bzw. AVR GCC.
Sabro schrieb:> Fuer Fragen zur AVR Code ausfuehrung wuerde ich den Im AVR Studio> enthaltenen Simulator empfehlen.
Das Studio gibt es für meine Plattform (Linux) nicht.
Leo C. schrieb:> Niemand zwingt Dich dazu, memset(), oder sonst irgendeine Funktion zu> benutzen.
Ok, das ist eine Lösung. Danke.
Amateur schrieb:> Wie auch Peter bereits gesagt hat:> Bei manchen Mikroprozessoren sind sogar der Stack-Pointer und manchmal> der Programm-Counter ein normales Register.
Ja, und inwiefern bringt mich das weiter? Scheinbar übersehe ich hier
etwas ...
Peter Dannegger schrieb:> Das kann jeder Compilerbauer so halten, wie er lustig ist.> AVR-GCC hat keinen, IAR hat einen.
Ich kenne mich beim C nicht so genau aus, deswegen meine Frage. Ein
Variablenstack scheint mir aber viel besser und vor allem sicherer
zu sein...
Kaj schrieb:> So etwas wie stack fuer variablen, stack fuer returnadressen usw gibt es> nicht: ein Stack, punkt um!
Warum schreist du den ?
Ein bißchen rumlesen um sich über Compiler und die Art wie die
funktionieren zu informieren, wäre auch nicht schlecht...
Was glaubst du wozu Index Register gut sind ?
Marc Vesely schrieb:> Ein> Variablenstack scheint mir aber viel besser und vor allem sicherer> zu sein...
Was soll daran besser sein? Man ist nur unnötig damit beschäftigt beide
"synchron" zu halten. Inwiefern das sicherer sein soll erschließt sich
mir auch nicht. Sobald ein Stack "kaputt" ist, kann man mit den Daten
des anderen kaum mehr etwas anfangen, zumindest nicht im normalen
Programmablauf.
Und zu Bufferüberläufe durch Böslinge kann es es in beiden Fällen
kommen. Oder glaubst du ernsthaft, dass die ganze Computerindustrie
nicht schon lange auf dieses Modell umgestiegen wäre, wenn dem nicht so
ist?
matrixstorm schrieb:> Ich habe auch noch eine schoenere Version
Auch Assembler ;).
matrixstorm schrieb:> Bei Bedarf einfach melden:> matrixstorm@gmx.de
Warum stellst du es nicht einfach direkt rein?
memset schrieb:> Was soll daran besser sein? Man ist nur unnötig damit beschäftigt beide> "synchron" zu halten. Inwiefern das sicherer sein soll erschließt sich> mir auch nicht. Sobald ein Stack "kaputt" ist, kann man mit den Daten
Das stimmt schon mal nicht, ist eher umgekehrt.
memset schrieb:> Und zu Bufferüberläufe durch Böslinge kann es es in beiden Fällen> kommen. Oder glaubst du ernsthaft, dass die ganze Computerindustrie> nicht schon lange auf dieses Modell umgestiegen wäre, wenn dem nicht so> ist?
Variablenstack ist schon lange in Gebrauch, sogar mit Runtime checking.
Und es ging nicht um Computerindustrie, sondern um Compiler.
>> Sabro schrieb:>> Fuer Fragen zur AVR Code ausfuehrung wuerde ich den Im AVR Studio>> enthaltenen Simulator empfehlen.>Das Studio gibt es für meine Plattform (Linux) nicht.
Man kann sich das Leben vorsaetzlich schwer machen...
Marc Vesely schrieb:> Ich kenne mich beim C nicht so genau aus, deswegen meine Frage. Ein> Variablenstack scheint mir aber viel besser und vor allem sicherer> zu sein...
Kannst du mal erläutern, was da "viel besser" und "sicherer" sein soll?
Ich sehe nur Nachteile:
- man blockiert sich ein weiteres Indexregister als Stackpointer
- die beiden Stacks können jetzt asynchron werden
- man hat jetzt zwei Speicherbereiche (zusammen mit dem Heap: drei)
deren Wachstum nicht vorhersagbar ist
Speziell für µC mit dem notorisch knappen RAM ist der dritte Punkt
wahrscheinlich der wichtigste. Während man Heap und Stack von entgegen-
gesetzten Enden des RAMs aufeinander zu wachsen lassen kann ohne dabei
an Flexibilität einzubüßen (es knallt wirklich erst dann, wenn das
Programm mehr Speicher braucht als da ist), stellt sich die Frage wo man
denn bitte den Variablenstack hinlegen soll.
Legt man ihn zu nahe an den Stack, riskiert man einen Stacküberlauf
obwohl noch RAM zwischen Heap und Variablenstack frei wäre. Dito wenn
man ihn zu nah an den Heap legt. Die optimiale Position ist u.U. gar
nicht festlegbar, weil sie von den verarbeiteten Daten abhängen kann
(Stichwort Rekursion).
XL
Axel Schwenke schrieb:> - man blockiert sich ein weiteres Indexregister als Stackpointer
Aha. Und was glaubst du, wie werden die übergebenen Argumente sonst
von der aufgerufenen Routine geholt ?
> - die beiden Stacks können jetzt asynchron werden
Soll heissen ?
> - man hat jetzt zwei Speicherbereiche (zusammen mit dem Heap: drei)> deren Wachstum nicht vorhersagbar ist> Speziell für µC mit dem notorisch knappen RAM ist der dritte Punkt> wahrscheinlich der wichtigste.
Und deshalb hat man weniger RAM ?
Ein richtiges Heap und uC gehen schon mal nicht zusammen.
Heapverwaltung ist Codefressend, langsam und bei uC ungefähr so
nützlich wie die Objektprogrammierung.
Bei uC ist Heap praktisch nichts anderes als Variablenstack. Nur
Morons arbeiten bei uC mit malloc und soviel ich weiss, arbeitet
alloca wiederum nur mit Stack.
Aber wie ich das auch sehe und begründe ist ja unwichtig, ich arbeite
nicht mit C, es riecht schon nach Streit, also ja, du hast Recht.
Axel Schwenke schrieb:> Kannst du mal erläutern, was da "viel besser" und "sicherer" sein soll?
Bei der klassischen Methode, weist du genau wo die Rücksprungadresse
liegt, du kannst also gezielt dort eine falsche Rücksprungadresse
eintragen und gezielt springen. Also ein mögliches Angriffsszenario. Bei
einem getrennten Rücksprungstack, ist es ggf schwieriger herauszufinden
wo er ist.
Beim Atmega spielt das aber keinerlei Rolle, da dort kein fremder Code
ausgeführt werden kann, Harvard usw und die Gründe die Du schon genannt
hast.
Marc Vesely schrieb:> Aha. Und was glaubst du, wie werden die übergebenen Argumente sonst> von der aufgerufenen Routine geholt ?
Zu mindestens nicht von dem zusätzlichen Speicherbereich, der dazu
benötigt wird den zusätlichen Stack zu verwalten.
Es geht hier darum einen getrennten Stack für Variablen und
Rücksprungadressen zu führen, also 2 Stack statt einen. Der Heap kommt
da noch zusätzlich.
Amateur schrieb:> @memset> In - was auch immer ein richtiges "C" ist - ist es zwar unerwünscht den> Stack zu beschreiben, aber erlaubt.
Auf C-Ebene gibt es gar keinen Stack. Der ist nur ein Konzept der
darunterliegenden Implementation.
> Zu Mindest früher wurden lokale Variablen auf dem Stack abgelegt.
Oder in Registern.
> In normalem "C" gibt es keine Sperre, die Dir verbietet, die mit "char> Etwas [ 2 ];" angelegte lokale Variable an der Stelle Etwas [ 2 ] zu> befummeln.
C verbietet eine solche Sperre nicht. Tastsächlich erlaubt es jedes
beliebige Verhalten.
Und es gibt Compiler, bei denen man so einen Array-Bounds-Check
aktivieren kann - mit entsprechenden Performance-Einbußen, weil
natürlich bei jedem Zugriff dieser Check erfolgen muß.
> Sowohl im erlaubten Bereich (0..1), als auch darüber hinaus,> ist das ein Stackzugriff.
Es sei denn, die Variable liegt gar nicht im Stack, sondern nur in
Registern.
> Wie auch Peter bereits gesagt hat:> Bei manchen Mikroprozessoren sind sogar der Stack-Pointer und manchmal> der Programm-Counter ein normales Register.
Bei ARM ist das zum Beispiel so. Bei manchen ARM-Prozessoren kann man
einen Sprung durch einfaches Schreiben der Zieladresse nach r15
auslösen.
Marc Vesely schrieb:> Axel Schwenke schrieb:>> - man blockiert sich ein weiteres Indexregister als Stackpointer> Aha. Und was glaubst du, wie werden die übergebenen Argumente sonst> von der aufgerufenen Routine geholt ?
Über den Stackpointer?
Und was ist eigentlich unklar an
>> ein weiteres Indexregister
?
>> - die beiden Stacks können jetzt asynchron werden> Soll heissen ?
Bist du so dämlich oder stellst du dich nur so? Wenn da nur ein Stack
ist, dann ist da auch nur ein Stackpointer. Returnadressen und Variablen
(stack frames) liegen verschachtelt auf dem Stack und der Zusammenhang
zwischen beiden ergibt sich direkt aus dem Inhalt des Stacks.
Wenn es zwei Stacks sind - einer für Variablen und einer für Return-
adressen - dann sind da auch zwei Stackpointer. Und die müssen synchron
modifiziert werden. Wenn nicht, würden merkwürdige Dinge geschehen, wie
z.B. daß lokale Variablen auf einmal ganz andere Werte haben.
Mit nur einem Stack kann dieses Problem gar nicht auftreten. Und das
ist ein handfester Vorteil.
>> - man hat jetzt zwei Speicherbereiche (zusammen mit dem Heap: drei)>> deren Wachstum nicht vorhersagbar ist>> Speziell für µC mit dem notorisch knappen RAM ist der dritte Punkt>> wahrscheinlich der wichtigste.>> Und deshalb hat man weniger RAM ?
Habe ich das geschrieben? Hast du überhaupt ansatzweise verstanden, was
ich geschrieben habe?
> Ein richtiges Heap und uC gehen schon mal nicht zusammen.
Nun ist es raus. Du hast keine Ahnung.
> Aber wie ich das auch sehe und begründe ist ja unwichtig
Du hast es nach wie vor nicht begründet.
> ich arbeite nicht mit C, es riecht schon nach Streit
Das rieche ich. Du brichst vollkommen unbegründet einen Streit vom Zaun
indem du über Sachen redest du du weder verstehst noch verwendest. Du
bist ein Arschloch. EOD
(ohne Gruß)
Gaestchen schrieb:> Axel Schwenke schrieb:>> Kannst du mal erläutern, was da "viel besser" und "sicherer" sein soll?>> Bei der klassischen Methode, weist du genau wo die Rücksprungadresse> liegt, du kannst also gezielt dort eine falsche Rücksprungadresse> eintragen und gezielt springen. Also ein mögliches Angriffsszenario. Bei> einem getrennten Rücksprungstack, ist es ggf schwieriger herauszufinden> wo er ist.
Das ist kein Argument. Auch wenn da ein extra Variablenstack ist, gibt
es ja trotzdem noch den Callstack. Und auch die Position auf diesem ist
bekannt, die steht ja im Stackpointer. Also kann man den Inhalt des
Stacks auch manipulieren.
Das ist in dem Szenario mit separatem Variablenstack sogar einfacher.
Weil man ja weiß daß der Callstack genau an der aktuellen Position die
Rücksprungadresse enthält und da nicht noch ein stack frame von evtl.
unbekannter Länge davor liegt.
XL
Axel Schwenke schrieb:>> Aha. Und was glaubst du, wie werden die übergebenen Argumente sonst>> von der aufgerufenen Routine geholt ?>> Über den Stackpointer?
LOL.
Axel Schwenke schrieb:> Wenn es zwei Stacks sind - einer für Variablen und einer für Return-> adressen - dann sind da auch zwei Stackpointer. Und die müssen synchron> modifiziert werden.
Hahahaha.