Dalla C all'assemblea


16

Supponiamo di avere il seguente pezzo di codice C per un avr-8bit:

int v1=1;
int v2=2;
v2=v2+v1;

Mi aspettavo il seguente smontaggio

ldi r18, 1;
ldi r19, 2;
add r19, r18;

ma dopo che ho corso:

avr-gcc -mmcu=atmega2560 Test.c -o Test.elf

e

avr-objdump -S Test.elf > Test.lss

Ho avuto il seguente smontaggio

    ldi r24, 0x01   ; 1
    ldi r25, 0x00   ; 0
    std Y+2, r25    ; 0x02
    std Y+1, r24    ; 0x01
    ldi r24, 0x02   ; 2
    ldi r25, 0x00   ; 0
    std Y+4, r25    ; 0x04
    std Y+3, r24    ; 0x03
    ldd r18, Y+3    ; 0x03
    ldd r19, Y+4    ; 0x04
    ldd r24, Y+1    ; 0x01
    ldd r25, Y+2    ; 0x02
    add r24, r18
    adc r25, r19
    std Y+4, r25    ; 0x04
    std Y+3, r24    ; 0x03

c'è qualcuno che può aiutarmi a capire il risultato del disassemblatore?

Modifica: usando char l'assembly diventa:

ldi r24, 0x01
std Y+1, r24
ldi r24, 0x02
std Y+2, r24
ldd r25, Y+2
ldd r24, Y+1
add r24, r25
std Y+2, r24

Quando ci sono le istruzioni standard?

Risposte:


20

Risposta breve: i registri sono a 8 bit e i valori sono a 16 bit. Li sta quindi gestendo in due pezzi.

Risposta lunga:

    ldi r24, 0x01   ; 1
    ldi r25, 0x00   ; 0

Memorizzare il valore 1 a 16 bit nei registri a 8 bit r24, r25.

    std Y+2, r25    ; 0x02
    std Y+1, r24    ; 0x01

Conservalo nelle posizioni dello stack Y + 1, Y + 2.

    ldi r24, 0x02   ; 2
    ldi r25, 0x00   ; 0

Memorizzare il valore a 16 bit 2 nei registri a 8 bit r24, r25.

    std Y+4, r25    ; 0x04
    std Y+3, r24    ; 0x03

Conservalo nelle posizioni dello stack Y + 3, Y + 4.

    ldd r18, Y+3    ; 0x03
    ldd r19, Y+4    ; 0x04
    ldd r24, Y+1    ; 0x01
    ldd r25, Y+2    ; 0x02

Copiarli dallo stack a (r18, r19) e (r24, r25)

    add r24, r18
    adc r25, r19

Aggiungi (r18, r19) a (r24, r25), incluso il proseguimento della seconda aggiunta

    std Y+4, r25    ; 0x04
    std Y+3, r24    ; 0x03

Conservalo nuovamente in pila.

Per ottenere l'assemblaggio originale, provare due cose:

  • usa le variabili "char"
  • usa l'opzione del compilatore "-O2"

Modifica : il motivo per cui il compilatore memorizza le variabili nello stack anziché tenerle nei registri è perché sono archiviate con il tipo di archiviazione "auto" predefinito. Si può ottimizzarli in registri, ma non deve, anche se li si dichiara con la classe di memorizzazione "registrare".

Sebbene questo non sia un requisito rigoroso del linguaggio, è normale comportamento del compilatore. Se a un certo punto prendi l'indirizzo di v1, allora gli deve essere assegnato un percorso di memorizzazione e salvato di nuovo ogni volta che cambia il valore di "v1". Quindi, per salvare la contabilità dell'eventuale memorizzazione o meno di v1 in un registro o nello stack, lo mantiene nello stack e tratta ciascuna riga di codice separatamente.


Grazie! Ora è più chiaro! Trova la mia modifica nella domanda.
DarkCoffee,

1
Vedi la mia modifica. Prova anche -O2. Forse -O3, sebbene ciò possa produrre codice non funzionante.
pjc50,

3
Un sacco di codice incorporato con cui lavoro definisce tipi extra che sono specifici delle loro dimensioni, come "uint8, uint16, uint32" per gli integri senza segno, per esempio. In questo modo sai sempre esattamente con quale tipo di variabile hai a che fare. Soprattutto nei piccoli embedded, firmati, float, "int" di dimensioni / firma indefinite vi costerà al massimo i cicli della CPU e, nel peggiore dei casi, causerà gravi errori.
John U,

I veri compilatori hanno smesso di comportarsi così circa 10-15 anni fa. Il problema di allocazione del registro è per lo più risolto e i compilatori sono dannatamente bravi. Sanno esattamente quando una variabile deve essere nello stack e quando può essere in un registro, se vale la pena spostarla e quando farlo. La contabilità viene eseguita al momento della compilazione e gli stessi compilatori dispongono di gigabyte di memoria. La grande eccezione è la modalità di debug, per ovvi motivi, ma poi tutto è nello stack.
MSalters il

@ pjc50 -O3può produrre codice non funzionante? [citazione necessaria] (e no, il codice C che invoca un comportamento indefinito e quindi si interrompe con alcune impostazioni di ottimizzazione non conta)
marcelm

4

Come ho trovato un codice di esempio, farò una risposta al mio commento - altri hanno già spiegato il problema.

Un sacco di codice incorporato con cui lavoro definisce tipi extra che sono specifici delle loro dimensioni, come "uint8, uint16, uint32" per gli integri senza segno, per esempio. In questo modo sai sempre esattamente con quale tipo di variabile hai a che fare. Soprattutto nei piccoli embedded, firmati, float, "int" di dimensioni / firma indefinite vi costerà al massimo i cicli della CPU e, nel peggiore dei casi, causerà gravi errori.

Ecco i nostri #define attuali:

/*
 * Example - the basic data types from our embedded code
 */
typedef unsigned char       uint8;  /*  8 bits */
typedef unsigned short int  uint16; /* 16 bits */
typedef unsigned long int   uint32; /* 32 bits */

typedef char                int8;   /*  8 bits */
typedef short int           int16;  /* 16 bits */
typedef int                 int32;  /* 32 bits */

typedef volatile int8       vint8;  /*  8 bits */
typedef volatile int16      vint16; /* 16 bits */
typedef volatile int32      vint32; /* 32 bits */

typedef volatile uint8      vuint8;  /*  8 bits */
typedef volatile uint16     vuint16; /* 16 bits */
typedef volatile uint32     vuint32; /* 32 bits */

3
Buona idea; uint8_t e gli amici sono ormai parte dello standard: stackoverflow.com/questions/16937459/...
pjc50

Comodo! Abbiamo ereditato quelli con un progetto che era C89, quindi è bello sapere che esiste una versione ufficiale.
John U,

2

Il tuo codice C utilizza variabili intere a 16 bit (int). Il compilatore non può leggere la tua mente, quindi compila esattamente ciò che è nel file di origine. Quindi, se vuoi variabili a 8 bit, devi usare il rispettivo tipo.

Di conseguenza continuerai a memorizzare i valori in memoria (anche se più semplici). Non sono così bravo in C, ma IMHO, ci sono alcune opzioni per assegnare la variabile ad un registro, se si desidera che alcune variabili siano nei registri anziché nella RAM. Qualcosa di simile a:

register unsigned char VARNAME asm("r3");

Si noti che non tutti i registri sono disponibili per tali trucchi.

Quindi, la conclusione? Scrivi i tuoi programmi in assemblea. Saranno sempre più piccoli, più veloci e facili da leggere / supportare.


L'assemblaggio è più facile da leggere rispetto a C?
dext0rb,

@ dext0rb - Sì. Ovviamente se conosci entrambi abbastanza bene. Se conosci solo C, allora sarà difficile leggere assembly e altre lingue.
johnfound,

Non sono d'accordo con l'ultimo punto, i programmi scritti in assembler sono molto più difficili da leggere. Basta confrontare il codice sorgente indicato sopra. Il codice C è molto più chiaro e più breve, così come la sua intenzione. Questa differenza aumenta solo quando vengono utilizzate le strutture.
soandos,

@soandos - Il codice C è più corto, sì. Più chiaro? Non sono sicuro. Se così fosse, la domanda sopra non dovrebbe essere posta affatto. In realtà il prezzo della "mancanza" è la "sfocatura" dei dettagli.
johnfound,

Naturalmente, il ragazzo che dice "Non sono così bravo in C" proclamerà le virtù della pura assemblea. : D
dext0rb,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.