Forum: Mikrocontroller und Digitale Elektronik Stackinitialisierung durch den Compiler


von Jens E. (ppq)


Lesenswert?

Hallöchen!

momentan lerne ich die Zusammenhänge zwischen C-Code und AVR ASM. Ich 
nutze dazu das Atmel Studio 6.2 und simuliere damit einen Atmega2560, 
ohne Compileroptimierungen.

Folgender simpler C-Code:
1
#include <avr/io.h>
2
3
int main(void)
4
{
5
    uint8_t var_x,var_y;
6
    var_x=42;
7
    var_y=var_x;
8
    while(1);
9
}

ergibt im Tab "Disassembly" folgenden AVR ASM Code:
1
00000072  CLR R1     Clear Register 
2
00000073  OUT 0x3F,R1     Out to I/O location 
3
00000074  SER R28     Set Register 
4
00000075  LDI R29,0x21    Load immediate 
5
00000076  OUT 0x3E,R29     Out to I/O location 
6
00000077  OUT 0x3D,R28     Out to I/O location 
7
00000078  LDI R16,0x00    Load immediate 
8
00000079  OUT 0x3C,R16     Out to I/O location 
9
0000007A  RCALL PC+0x0003  Relative call subroutine 
10
0000007B  RJMP PC+0x000D   Relative jump 
11
0000007C  RJMP PC-0x007C   Relative jump 
12
0000007D  PUSH R28     Push register on stack 
13
0000007E  PUSH R29     Push register on stack 
14
0000007F  PUSH R1               Push register on stack 
15
00000080  PUSH R1     Push register on stack 
16
00000081  IN R28,0x3D     In from I/O location 
17
00000082  IN R29,0x3E     In from I/O location 
18
00000083  LDI R24,0x2A     Load immediate 
19
00000084  STD Y+1,R24     Store indirect with displacement 
20
00000085  LDD R24,Y+1     Load indirect with displacement 
21
00000086  STD Y+2,R24     Store indirect with displacement 
22
00000087  RJMP PC-0x0000   Relative jump

Zu Lernzwecken habe ich ihn mal nach bestem Wissen und Gewissen 
kommentiert (Korrekturen sind natürlich gern gesehen):
1
; Setze Statusregister SREG (0x3F, siehe doc2549.pdf S. 414) auf null
2
00000072  CLR R1     Clear Register 
3
00000073  OUT 0x3F,R1     Out to I/O location 
4
5
; stack pointer (0x3E, 0x3D) auf 0x21FF setzen - dort lebt jetzt der stack! Y (R28, R29) zeigt dahin.
6
00000074  SER R28     Set Register 
7
00000075  LDI R29,0x21     Load immediate 
8
00000076  OUT 0x3E,R29     Out to I/O location 
9
00000077  OUT 0x3D,R28     Out to I/O location 
10
11
; EIND (0x3C, S.17) wird auf null gesetzt. ist zusätzliches adressregister für EICALL/EIJMP befehle, 
12
; wird mit den beiden Z registern konkateniert → relative sprünge im kompletten program 
13
; memory möglich. ICALL/IJMP ignorieren es, nutzen nur die beiden Z register (nur 16 bit 
14
; adresse → nur untere 128 kbyte des program memory adressierbar).
15
00000078  LDI R16,0x00     Load immediate 
16
00000079  OUT 0x3C,R16     Out to I/O location 
17
18
; [...]
19
20
; total meta: stack adresse auf den stack packen (und zwei nullbytes hinterher), warum?
21
0000007D  PUSH R28    Push register on stack 
22
0000007E  PUSH R29     Push register on stack 
23
0000007F  PUSH R1    Push register on stack 
24
00000080  PUSH R1    Push register on stack 
25
26
; stack-adresse aus dem pointer in die Y register laden (sind aber vorher schon drin???)
27
00000081  IN R28,0x3D    In from I/O location 
28
00000082  IN R29,0x3E    In from I/O location 
29
30
; hier geht der eigentliche code los. schreibe eine 42 (2*16+10) in register R24
31
00000083  LDI R24,0x2A    Load immediate 
32
33
; schreibe den wert an die 16 bit große RAM adresse, die sich ergibt wenn man den adresswert 
34
; der im Y register steht um 1 erhöht(!), und lade ihn nochmal
35
00000084  STD Y+1,R24    Store indirect with displacement 
36
00000085  LDD R24,Y+1    Load indirect with displacement 
37
38
; schreibe den wert an die RAM adresse, die in Y+2 steht. er steht jetzt an zwei stellen!
39
00000086  STD Y+2,R24    Store indirect with displacement 
40
00000087  RJMP PC-0x0000  Relative jump, entspricht while(1);

Wie beschrieben sind mir die Zeilen 7D-82 unklar. Wir packen den Wert 
0x21FF aus R29 und R28 auf den Stack - das entspricht doch dann der 
Adresse in Y+0,richtig? Und indem wir zwei mal R1 (das 0x00 enthält) 
pushen, setzen wir den Wert in Y+1 auf 0x0000, richtig?

Meines Erachtens sind diese sechs Instruktionen unnötig, denn auf Y+0 
wird niemals zugegriffen und R29,R28 enthalten sowieso schon den 
gleichen Wert, den der Stack Pointer an 0x3D,0x3E hat.

Ich wäre glücklich, wenn einer von Euch Licht ins Dunkel bringen könnte: 
Sind das nur unnötige Compilerkapriolen oder habe ich das ganze noch 
nicht durchblickt?

Viele Grüße
ppq

von Bernd K. (prof7bit)


Lesenswert?

Kann es sein daß das der bereits der Prolog der funktion main() ist, 
also sowas wie einen stack frame für lokale Variablen einrichten?

Dann würd ich nämlich sagen er pusht R28/R29 nicht auf den Stack weil da 
zufällig grad der Stackpointer drin ist sondern er pusht sie auf den 
Stack um sie zu sichern (weil eine Funktion das immer tut wenn sie 
irgendwelche Register benutzt um sie beim Rücksprung aus der Funktion 
wieder herstellen zu können, ganz ungeachtet ihres Inhalts) und dann 
erst nachdem er die Register gesichert hat beschließt er diese Register 
als frame pointer zu benutzen für die lokalen Variablen und er kümmert 
sich nicht darum was da vorher drin war, das ist hier reiner Zufall, 
das geht eine Funktion ja nix an was vorher in den Registern war, er 
pusht sie ungeachtet ihres Inhalts und am Ende von main() würde er sie 
wieder poppen.

: Bearbeitet durch User
von Rolf M. (rmagnus)


Lesenswert?

Bernd K. schrieb:
> Kann es sein daß das der bereits der Prolog der funktion main() ist,
> also sowas wie einen stack frame für lokale Variablen einrichten?

Ja, so ist es. Hier mal der Assembler-Code, den gcc für das File 
generiert (also ohne Startup-Code):
1
        .file   "jens.c"
2
__SP_H__ = 0x3e
3
__SP_L__ = 0x3d
4
__SREG__ = 0x3f
5
__RAMPZ__ = 0x3b
6
__tmp_reg__ = 0
7
__zero_reg__ = 1
8
        .text
9
.global main
10
        .type   main, @function
11
main:
12
        push r28
13
        push r29
14
        push __zero_reg__
15
        push __zero_reg__
16
        in r28,__SP_L__
17
        in r29,__SP_H__
18
/* prologue: function */
19
/* frame size = 2 */
20
/* stack size = 4 */
21
.L__stack_usage = 4
22
        ldi r24,lo8(42)
23
        std Y+1,r24
24
        ldd r24,Y+1
25
        std Y+2,r24
26
.L2:
27
        rjmp .L2
28
        .size   main, .-main
29
        .ident  "GCC: (GNU) 4.8.1"

: Bearbeitet durch User
von Bernd K. (prof7bit)


Lesenswert?

Rolf Magnus schrieb:
> main:
>         push r28
>         push r29
>         push __zero_reg__
>         push __zero_reg__
>         in r28,__SP_L__
>         in r29,__SP_H__

Also aus Sicht der Funktion ist es vollkommen unbekannt was vorher in 
28/29 drin war. Er sichert sie und dann nimmt er das Registerpaar als 
Frame Pointer.

Das da in diesem einen Falle zufällig vorher schon der selbe Wert drin 
war ist reiner Zufall und darauf kann sich die Funktion nicht verlassen.

von jgdo (Gast)


Lesenswert?

Da keine Optimierung an ist, werden alle Variablen als volatile 
behandelt und auf dem Stack gespeichert. mit

push R1
push R1

wird Platz für var_x und var_y geschaffen. Dies geht schneller als den 
Stack-Pointer manuell zu dekrementieren. Zuvor werden die Y-Register 
gesichert, weil diese nach dem AVR-GCC-ABI call-saved sind, also nach 
Austritt der Funktion wieder den alten Wert wie vor Eintritt haben 
müssen.

Wie Bernd angemerkt hat, ist die Main aus Compilersicht eine ganz 
normale Funktion, der Compiler weiß also nicht, was davor war oder 
danach kommt. Deshalb erzeugt er auch für die main einen normalen 
Prolog, in dem benutzte call-saved Register auf dem Stack gesichert 
werden.

-jgdo-

von Jens E. (ppq)


Lesenswert?

Danke euch!

von Johann L. (gjlayde) Benutzerseite


Lesenswert?

Jens E. schrieb:
> Hallöchen!
>
> momentan lerne ich die Zusammenhänge zwischen C-Code und AVR ASM. Ich
> nutze dazu das Atmel Studio 6.2 und simuliere damit einen Atmega2560,
> ohne Compileroptimierungen.

Der erste Teil des Codes wir aus dem Startup-Code von crt*.o (in neueren 
Versionen der Tools aus einer statischen Bibliothek) hinzugelinkt, wird 
also nicht beeinflusst durch irgendwelche Optimierungsoptionen.  Derser 
Teil des Codes ist nur anhängig von dem µC für den übersetzt wird.

> ; Setze Statusregister SREG (0x3F, siehe doc2549.pdf S. 414) auf null
> 00000072  CLR R1     Clear Register
> 00000073  OUT 0x3F,R1     Out to I/O location

Nicht nur.  Dies setzt auch R1 auf 0.  avr-gcc geht davon aus, dass in 
diesem Register immer die 0 steht!

> ; EIND (0x3C, S.17) wird auf null gesetzt. ist zusätzliches
> adressregister für EICALL/EIJMP befehle,
> ; wird mit den beiden Z registern konkateniert → relative sprünge im
> kompletten program

Wobei Sprünge in den kompletten Adressbereich nicht dadurch geschehen, 
dass EIND auf einen anderen Wert als 0 gesetzt wird!  avr-gcc geht davon 
aus, dass sich EIND im Programm niemals ändert, und ändert auch nie 
den Wert von EIND.

Würde man nämlich EIND vor einem ICALL ändern, müsste man irgendwie an 
den Wert kommen, der in dieses SFR soll.  Da Zeiger nur 16 Bits groß 
sind, also nicht mehr als 64KiW Code adressieren können, ist dies nicht 
möglich.  Indirekte Sprünge in den ganzen Codebereich werden über 
Linker-Stubs realisiert.

> ; total meta: stack adresse auf den stack packen (und zwei nullbytes
> hinterher), warum?
> 0000007D  PUSH R28    Push register on stack
> 0000007E  PUSH R29     Push register on stack

Dies sichert Y.  Y ist callee-saved per avr-gcc ABI:

http://gcc.gnu.org/wiki/avr-gcc#Call-Saved_Registers

> 0000007F  PUSH R1    Push register on stack
> 00000080  PUSH R1    Push register on stack

Dies mach ein SP -= 2.

> ; stack-adresse aus dem pointer in die Y register laden (sind aber
> vorher schon drin???)

NEIN. In Y ist zu diesem Zeitpunkt nicht der gleiche Wert wie in SP, 
denn SP wurde durch das "call main" in

> 0000007A  RCALL PC+0x0003  Relative call subroutine

verändert!

> ; schreibe den wert an die 16 bit große RAM adresse, die sich ergibt
> wenn man den adresswert
> ; der im Y register steht um 1 erhöht(!), und lade ihn nochmal
> 00000084  STD Y+1,R24    Store indirect with displacement

In diesem Fall enthält Y den Frame-Pointer, und dieser zeigt bei avr-gcc 
ein Byte unter die unterste Frame-Adresse.  Der Frame ist wie im Prolog 
ersichtlich 2 Bytes groß: var_x lebt in Y+1 und var_y in Y+2.  Dass es 
nicht umgekehrt ist sieht man übrigens am Dump unten -- allein aus dem 
erzeugten Code kann man dies nämlich nicht folgern.

> Und indem wir zwei mal R1 (das 0x00 enthält) pushen,
> setzen wir den Wert in Y+1 auf 0x0000, richtig?

Das stimmt zwar, ist aber nicht der Zweck der Aktion, siehe oben.

> Meines Erachtens sind diese sechs Instruktionen unnötig, denn auf Y+0
> wird niemals zugegriffen und R29,R28 enthalten sowieso schon den
> gleichen Wert, den der Stack Pointer an 0x3D,0x3E hat.

Was erwartest du mit deaktivierter Optimierung?

> Ich wäre glücklich, wenn einer von Euch Licht ins Dunkel bringen könnte:
> Sind das nur unnötige Compilerkapriolen oder habe ich das ganze noch
> nicht durchblickt?

Manchmal ist optimierter Code leichter nachvollziehbar, da er näher an 
dem dran ist, was ein Asembler-Programmierer machen würde.


jgdo schrieb:
> Da keine Optimierung an ist, werden alle Variablen als volatile
> behandelt [...]

NEIN! Diese Variablen sind nicht volatile!  Auch ohne Optimierung gibt 
es Stellen, wo gcc Volatiles und nicht-Volatiles unterschiedlich 
behandelt!

> Wie Bernd angemerkt hat, ist die Main aus Compilersicht eine ganz
> normale Funktion,

Schlüsselwort "main" hat in C eine besondere Rolle, auch wenn der Code 
nicht optimiert wird, denn Semantik ist unabhängig von Optimierungen.

> der Compiler weiß also nicht, was davor war oder danach kommt.

Ohne Optimierung (bzw. mit -O0, was auf Host-Resourcen optimiert) kommt 
das hin.  Mit Optimierung wird der Code jedoch von
1
main:
2
  push r28   ; 
3
  push r29   ; 
4
   ; SP -= 2   ; 
5
  push __zero_reg__
6
  push __zero_reg__
7
  in r28,__SP_L__   ; 
8
  in r29,__SP_H__   ; 
9
  ldi r24,lo8(42)   ;  tmp43,
10
  std Y+1,r24   ;  var_x, tmp43
11
  ldd r24,Y+1   ;  tmp44, var_x
12
  std Y+2,r24   ;  var_y, tmp44
13
.L2:
14
  rjmp .L2   ;
zu
1
main:
2
.L2:
3
  rjmp .L2   ;

Bitte melde dich an um einen Beitrag zu schreiben. Anmeldung ist kostenlos und dauert nur eine Minute.
Bestehender Account
Schon ein Account bei Google/GoogleMail? Keine Anmeldung erforderlich!
Mit Google-Account einloggen
Noch kein Account? Hier anmelden.