Lo scopo di questo post è di capire il funzionamento dei registri del processore senza entrare in calcoli matematici o schemi logici complessi.

Per salvare i nostri dati abbiamo a disposizione diversi tipi di memoria: la RAM, i velocissimi SSD, i vecchi hard disk… Ma esattamente come loro, anche il processore ha delle sue memorie interne, microscopiche e incredibilmente veloci, su cui appoggia i dati mentre lavora: i registri.

Per arrivare a capire come funzionano i registri dobbiamo capire cosa succede quando apriamo un programma sul nostro desktop come ad esempio paint.exe.

Doppio click su paint:

Il sistema operativo crea un nuovo processo in memoria RAM, il codice eseguibile di paint viene copiato dal disco alla RAM.

Qui entra in gioco il loader (un componente del sistema operativo). Il suo compito è analizzare l’intestazione del file per calcolare quanta memoria RAM serve, mappare le librerie necessarie (le dipendenze) e individuare l’Entry Point. Una volta preparato il terreno, il loader passa il controllo del processore al nostro programma. Come? Molto semplicemente: dice alla CPU di impostare l’Instruction Pointer (RIP) sull’indirizzo dell’Entry Point. Da quel millesimo di secondo, la CPU inizia a eseguire il codice di Paint

RIP: Instruction Pointer Register

Abbiamo detto che l’indirizzo della prima istruzione del programma viene caricato nel registro RIP (Instruction Pointer).

Questo registro non esegue direttamente i calcoli, ma ha un compito fondamentale: tenere traccia di quale riga di codice deve essere eseguita momento per momento.

Immagina di leggere un libro e di usare il dito per seguire le parole riga per riga, in modo da non perdere mai il filo. L’Instruction Pointer fa esattamente questo: è il “dito” della CPU puntato sulla RAM, che si sposta costantemente in avanti man mano che le istruzioni vengono lette ed eseguite.

Vediamolo nel dettaglio con questo semplice programma.

Per riprodurlo compilare aggiungendo i simboli di debug.

gcc -g ciao.c -o ciao
#include <stdio.h>

int main(){

int a=10,b=2; //Entry Point

int somma = a+b;

printf("Somma:%d\n",somma);


	return 0;
}

La prima istruzione del programma è quella dove sono presenti le variabili a e b, andiamo a vedere più in dettaglio come funziona.

Avvio il debugger gdb con un’estensione “gef” scaricabile da qui: https://github.com/hugsy/gef

lancio gdb:

gdb -q ./ciao

Inserisco un breakpoint nel main:

break main

Eseguo il programma:

run     

Il registro RIP in azione

Il registro RIP contiene l’indirizzo esatto a cui punta la CPU per sapere dove si trova la prossima istruzione da eseguire. In pratica, RIP dice all’unità di Instruction Fetch del processore: “Vai a questo indirizzo di memoria, prendi l’istruzione che trovi lì e passala al decoder per essere eseguita”.

Quindi l’assegnazione del valore 10 alla variabile a non è ancora avvenuta ma sarà la prossima istruzione da eseguire.

infatti se provo a guardare il contenuto di a.

print a 

ecco il risultato:

$1 = 0x0

la variabile a contiene 0, oppure può contenere spazzatura (valore casuale).

mandando avanti il programma con:

nexti

e proviamo a stampare il valore di a e b:

print a 

Il registro RIP in azione

Se guardiamo in alto, nel rettangolo rosso c’è l’istruzione appena eseguita, mentre nel rettangolo verde c’è la prossima istruzione puntata da RIP

La variabile a contiene 10 in esadecimale mentre la variabile b contiene spazzatura, questo perché l’assegnazione di 2 alla variabile b non è ancora avvenuta, infatti quando in C dichiariamo una variabile senza inizializzarla il sistema operativo si limita a riservarle dello spazio nello stack ma senza pulirlo.

Eseguo nuovamente nexti per andare avanti e finalmente assegnare il valore 2 a b:

nexti

Il registro RIP in azione

$rbp : 0x00007fffffffe3a0 → 0x00007fffffffe4c8 → 0x00007fffffffe865 → “/home/fra/c/ciao” $rbp : 0x00007fffffffe380 → 0x00007fffffffe3a0 → 0x00007fffffffe4c8 → 0x00007fffffffe865 → “/home/fra/c/ciao”