Funzionamento concettuale subroutine assembly

« Older   Newer »
 
  Share  
.
  1. Dodiz
         
     
    .

    User deleted


    Ciao per l'ennesima volta , nel corso di informatica è presente una piccola parte in assembly (nessuna versione in particolare, solo per dimostrare una probabile e superficiale traduzione dal c..) nella quale ho problemi a capire concettualmente come vengono gestite le funzioni.

    premetto che ho letto questa discussione, capendoci ancora meno :asd:

    Innanzitutto uno pseudo codice è così formato:
    CODICE
    X: RES 1


    dove X è il nome della variabile a cui viene allocata 1 porzione di memoria

    CODICE
    START: READ X


    start è l'etichetta per l'inizio del programma, come una funzione main, mentre read sarebbero funzioni di input

    CODICE
    MOV #1, X


    MOV indica un assegnamento, si assegna il valore 1 (se non ci fosse stato l'asterisco sarebbe stato il valore contenuto nella cella di indirizzo 1), in X

    CODICE
    CICLO: BREQ X, FINE
    ADD #-1, X
    BR CICLO


    BREQ è il salto condizionato, se X = 0, allora salta all'etichetta con segnato FINE
    ADD ovviamente aggiunge un valore ad un altro, e BR è il salto incondizionato, ovvero ritorna all'etichetta CICLO in ogni caso..

    CODICE
    FINE: WRITE X
    EXIT
    END START


    Codice per terminare (termina la prima l'etichetta START)

    Oltre a questi operatori, si ha
    CODICE
    JTS Etichetta

    Jump to subroutine per saltare a una funzione
    CODICE
    RTS
    per il ritorno dalla funzione

    E gli operandi:
    CODICE
    Ri e (Ri)
    Ri sono registri (con i numero del registro)
    la differenza è che se viene utilizzato Ri, l'operando è in Ri, mentre se viene utilizzato (Ri), l'operando è nella cella di indirizzo Ri
    CODICE
    #X e X
    , se viene utilizzato il primo, allora si utilizza il valore contenuto in X, per il secondo invece, l'operando è nella cella di indirizzo X

    Il mio problema è capire la logica che viene utilizzata nelle subroutine..per esempio:

    CODICE
    int A, B, C;
    int sum(int p1, int p2)
    {int temp;
    temp = p1+ p2;
    return(temp);
    }
    void main ()
    {
    A=2; B=3;
    C = sum(A,B);
    }


    Nello pseudo assembly con le istruzioni qui sopra, sarebbe:
    CODICE
    A: RES 1
    B: RES 1
    C: RES 1
    STACK: RES1000 //riservo 1000 "celle" di memoria per lo stack
    MOV #STACK, SP //SP è lo stack pointer, punta al primo elemento dello stack
    ADD #999, SP //tratto lo STACK come pila, facendo puntare SP all'ultimo elemento dello stack
    ADD #-1, SP //riservo l'ultimo elemento dello stack per il valore di ritorno
    MOV A, (SP) //passaggio per parametri della funzione, "l'indirizzo" dello stack 998 contiene l'indirizzo di A
    ADD #-1, SP //alloco un altro spazio di memoria per B
    MOV B, (SP)
    ADD #-1, SP
    JTS SUM //passa ad eseguire la sub


    l'istruzione JTS, inoltre, carica l'indirizzo della prossima istruzione da eseguire (quella dopo la fine della funzione che avrà etichetta RET:)
    in pratica si avrà MOV #RET (SP);
    SP scalerà ancora di 1 ADD #-1 SP;
    e nel program counter (PC) la prossima istruzione da eseguire sarà SUM: MOV #SUM, PC

    In questo momento lo STACK e' così formato:
    |_______________| <-SP
    |_____#RET____|
    |_______B______|
    |_______A______|
    |_______________| (valore di ritorno, verrà inserito durante la funzione)

    fin qui non ho avuto problemi..
    Quando viene eseguita la funzione SUM, la prima cosa da fare è allocare uno spazio dello STACK per un registro R0, che verrà utilizzato come indirizzo fisso per le variabili locali..
    CODICE
    SUM: MOV R0, (SP) //R0 &#232; nell'indirizzo prima del valore di ritorno
    ADD #-1 SP //prossimo valore
    MOV SP, R0 //Il testo dice che viene caricato il valore di SP in R0, vuol dire che ora R0 punta all'elemento dove puntava SP? Non l'ho capito..
    ADD #-1, SP //altro spazio di memoria per SP (la variabile TEMP)
    MOV R0, R1 //R1 &#232; ora l'indirizzo di riferimento, R1 dovrebbe puntare all'elemento prima di R0
    ADD #4 R1 //R1 contiene l'indirizzo di A, come se R1 fosse temp e temp = A + B fosse scomposta in temp = A, e temp += B
    MOV (R1) (R0) //Il valore di A viene inserito nella casella di indirizzo R0, che sarebbe quella dove R1 puntava?


    vediamo la situazione dello stack a questo punto (se ho capito bene..)
    |________________| <- SP
    |_______A_______|
    |_______R0______|
    |_____#RET_____|
    |_______B_______|
    |________A______| <- R1
    |________________|
    dove R0 conteva l'indirizzo di R1, punta ad A (l'ultima A), sempre se ho capito bene
    Ora la somma del secondo elemento:
    CODICE
    MOV R0, R1 //R1 ora punta alla prima cella di A
    MOV #3, R1 //R1 ora punta a B
    ADD (R1), (R0)  //Il valore di B viene sommato alla cella di indirizzo contenuto in R0, cio&#232; alla cella di A, giusto....?
    MOV R0, R1 //R1 torna a puntare ad A + B
    ADD #5 R1 //R1 ora punta al valore di ritorno
    MOV (R0), (R1) //Il valore di R0 viene copiato nell'indirizzo di ritorno
    ADD #2 SP //SP punta a R0 e i valori precedenti vengono "deallocati"
    MOV (SP), R0 //che vuol dire? Che R0 punta a se stesso?
    RTS //Aggiunge 1 A SP, facendolo puntare a #RET, e poi sposta l'indirizzo di RET nel Program Counter come prossima istruzione


    Adesso ciò che succede (sempre nello start, dopo il ritorno)
    CODICE
    RET: ADD #3 SP //"Deallocazione" di tutti i valori, ora SP punta al valore di ritorno
    MOV (SP), C
    EXIT
    END START


    Alcune istruzioni mi mandano in confusione..e a voi?
    Le mie domande sono presenti nei commenti stessi..vorrei avere la conferma di aver capito bene la logica..

    Edited by Dodiz - 16/2/2016, 17:49
     
    .
  2.     +1    
     
    .
    Avatar

    Senior Member

    Group
    Manager
    Posts
    10,796
    Reputazione
    +266

    Status
    Stacco ora da lavoro, verso sera leggo bene e ti rispondo. ;)
     
    .
  3.     +1    
     
    .
    Avatar

    Senior Member

    Group
    Manager
    Posts
    10,796
    Reputazione
    +266

    Status
    Riguardo allo Stack, dipende come lo stai rappresentando. Teoricamente non lo staresti rappresentando bene, in quanto lo stack cresce verso il basso e non verso l'alto (credo sia così in quasi tutte le architetture). Sulla base di questo credo cambi il modo in cui hai interpretato alcune parti del "codice" (in effetti è un autentico casino visto in questo modo).




    ATTENZIONE: la sintassi è mov destinazione, sorgente, quindi banalmente se vedi mov bp, sp, significa bp = sp.


    Perdonami, però mi esce più semplice spiegartelo con situazioni reali. Inizio con una breve spiegazione, così da chiarire i concetti fondamentali.
    In moltissime architetture lo stack cresce verso il basso. Ciò significa che come nel tuo caso ci sarà un registro che punta in cima, su un indirizzo più basso.

    stack1



    Nell'architettura x86 di Intel ci sono pochi registri da ricordare:
    SS rappresenta la base dello stack, o per meglio dire, il segmento dello stack (negli esempi non lo utilizzerò, è implicita la sua presenza)
    SP rappresenta il puntatore nello stack; per intenderci, è quello che nella figura si chiama "Top of stack" (corrisponde in pratica al tuo SP);
    BP questo è l'indirizzo base, viene utilizzato dal programmatore per accedere allo stack (al posto di manipolare SP);

    IP non ha nulla ache fare con lo stack, ma è un puntatore anch'esso (Instruction Pointer); come dice il nome, punta all'istruzione da eseguire (nel tuo caso è il program counter, PC)

    Queste sono le informazioni base.
    Nell'architettura di Intel, il programmatore può aggiungere o rimuovere dati dallo stack utilizzando le istruzioni PUSH e POP.

    Detto ciò, considera questa semplicissima situazione in linguaggio C:

    CODICE
    int   somma() {
     return 0;
    }

    int main() {
     int n =  somma();

     return 0;
    }


    L'aspetto importante, è cosa accade quando avviene la chiamata a somma()?

    In asm, la chiamata avviene nel seguente modo:

    CODICE
    call  funzione


    Quando avviene una chiamata come in questo caso, la CPU effettua il push del registro IP sullo stack, e come seconda cosa assegna al registro IP un nuovo valore: quello della funzione (il suo indirizzo).
    Fatto questo, l'esecuzione inizia da quel punto (e la subroutine viene eseguita).

    Supponiamo quindi che l'inizio dello stack (l'indirizzo più alto) sia 100 (decimale, è più comodo). Quindi SP = 100.
    Nel momento della CALL avviene il PUSH (implicito, nel senso che non è controllato dal programmatore) del valore di IP per non perdere l'istruzione successiva da eseguire.

    CODICE
    [SP] = IP  // 100


    ora avremo SP = 99. La PUSH fa di fatto questo, che è ciò che nel tuo codice continua a comparire: esegue una sottrazione in pratica, decrementando SP. Nella pratica rispecchierà la dimensione del dato (se l'architettura è a 32bit, il decremento avverrà di 32bit, o se preferisci 4byte).

    Sino a qui spero sia chiaro.
    Quando passi dei parametri, i valori devono essere inseriti sullo stack prima della chiamata alla funzione. In C la situazione ora è la seguente:

    CODICE
    int   somma(int a, int b) {
     return 0;
    }

    int main() {
     int a = 1, b = 1;
     int n =  somma(a, b);

     return 0;
    }


    In asm possiamo scrivere la parte del main() come:
    CODICE
    push   b
    push   a
    call      somma


    In questo caso ci sono 2 PUSH subito. Quindi se iniziamo da SP=100, dopo alla prima PUSH avremo SP=99 e dopo la seconda SP=98. La CALL provoca un altro decremento del valore, e quindi SP=97 (perchè viene inserito il registro IP).
    Questa è la parte facile.

    Ho parlato inizialmente del registro BP. Questo registro è quello che tecnicamente controlla lo stack frame. Quando chiami una procedura hai un frame; BP punta praticamente li, e viene utilizzato per accedere allo stack (in maniera sicura).

    Se hai letto l'altro topic, avrai notato che la procedura inizia con quello che viene definito prologo:

    CODICE
    push     bp
    mov      bp, sp


    Il push avviene in quanto (solitamente, come dice anche Intel lol) BP punta all'indirizzo di ritorno. Quindi quella PUSH preserva il valore. La situazione dello stack, eseguita quindi anche quella PUSH di BP è la seguente:

    CODICE
    100: b
    099: a
    098: [valore di IP]
    097: [valore di BP]

    SP => 0097


    Giunti a questo punto, il codice prosegue con mov bp, sp; ora possiamo accedere allo stack tramite BP. Visto che lo stack cresce verso il basso ed inizia da indirizzi più alti, per "andare sopra" e prendere i parametri "pushati", dobbiamo incrementare di un certo numero BP.

    Il certo numero in questo caso è [BP+2] per prelevare il valore di 'a' e [BP+3] per prelevare il valore di 'b'.

    NOTA:
    In una situazione reale su 32bit i valori non saranno esattametne questi, in quanto i dati sono di 4byte e non di 1 (come nell'esempio sopra). Quindi avremo:
    CODICE
    100: b
    096: a
    092: [valore di IP]
    088: [valore di BP]


    e di conseguenza, il primo parametro della funzione sarà a [BP+8] ed il secondo a [BP+12].
    Nel caso del topic che hai letto inizialmente, i dati sono a 16bit, quindi solo 2 byte... e di conseguenza, l'accesso sarà diverso rispetto al caso mostrato ora: avrai [BP+4] e [BP+6].

    Spero che sino a qui sia tutto chiaro ora - perdona le continue disgressioni, però preferisco essere preciso per non fornirti informazioni fuorvianti.

    Bene, a questo punto, c'è un altro aspetto che non sapevo se toccare... i parametri locali, o meglio, quelle che si chiamano semplicemente "variabili locali della funzione" (o con nomi analoghi).
    Ragiona sullo stack che vedi nel penultimo CODE: se lo stack cresce verso il basso, vuol dire per logica che la parte sopra è piena, e quella sotto è vuota... ed in effetti contiene i parametri della funzione; ergo non rimane che andare nella direzione opposta. Andare nella direzione opposta implica dover sottrarre qualcosa a BP.
    In questo caso la situazione si complica un pochino:

    CODICE
    push   bp
    mov    bp, sp
    sub     sp,1


    decrementando il valore di sp, riserviamo spazio per la variabile locale. L'accesso alla variabile locale avviene con [bp-1].

    Ricorda sempre che con dati reali, si tratta di 16/32bit e non 1byte come in questo caso (ergo, se salvi un dato a 32bit, dovrai sottrarre 4 ad SP).




    A questo punto, è il momento dell'epilogo:
    CODICE
    mov  sp, bp
    pop   bp
    ret


    Preciso una cosa molto importante: arrivato a questo punto, è importante che lo stack sia stato manipolato correttamente e che sia coerente: questo significa banalmente che se nella funzione per qualche motivo hai fatto N push, prima del termine della funzione, dovrai fare N pop. In caso contrario la funzione non terminerà correttamente ed il risultato è imprevedibile... al 99.9% si verificherà un crash in quanto il dato non è un indirizzo valido, oppure non puoi accedervi.

    Solitamente all'istruzione ret fa seguito il numero di byte da sottrarre per liberare lo stack. Una volta liberato tramite la ret, viene fatto il push dell'indirizzo IP inserito inizialmente... e l'esecuzione riprende in pratica dalla riga successiva alla chiamata.


    Spero perdonerai la mia spiegazione al posto di un commento passo-passo del tuo codice... e spero ti sia stata utile.
    In caso di domande chiedi pure!
     
    .
  4.      
     
    .
    Avatar

    Senior Member

    Group
    Manager
    Posts
    10,796
    Reputazione
    +266

    Status
    Dodiz se ti è stata utile la mia risposta ti chiederei di segnarla, così setto poi il topic come "Risolto". Se invece non è ancora risolto, ma hai altre domande, allora poni pure!
     
    .
3 replies since 16/2/2016, 10:42   402 views
  Share  
.
Top