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
intmain(void)
4
{
5
uint8_tvar_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
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
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.
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):
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.
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-
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