Una comprensione di come i computer sono organizzati, di come sembrano funzionare a un livello molto basso, è necessaria per capire come funziona un programma di linguaggio assembly. Al livello più semplicistico, i computer hanno tre parti principali:
- memoria principale o RAM che contiene dati e istruzioni,
- un processore, che elabora i dati eseguendo le istruzioni, e
- ingresso e uscita (a volte abbreviati in I/O), che permettono al computer di comunicare con il mondo esterno e di memorizzare i dati al di fuori della memoria principale in modo da poterli recuperare in seguito.
Memoria principale
Nella maggior parte dei computer, la memoria è suddivisa in byte. Ogni byte contiene 8 bit. Ogni byte in memoria ha anche un indirizzo che è un numero che dice dove si trova il byte in memoria. Il primo byte in memoria ha un indirizzo di 0, il successivo ha un indirizzo di 1, e così via. Dividere la memoria in byte la rende indirizzabile perché ogni byte ottiene un indirizzo univoco. Gli indirizzi delle memorie di byte non possono essere usati per riferirsi ad un singolo bit di un byte. Un byte è il più piccolo pezzo di memoria che può essere indirizzato.
Anche se un indirizzo si riferisce ad un particolare byte in memoria, i processori consentono di utilizzare più byte di memoria in fila. L'uso più comune di questa funzione è quello di utilizzare 2 o 4 byte di fila per rappresentare un numero, di solito un numero intero. I singoli byte sono talvolta utilizzati anche per rappresentare gli interi, ma poiché sono lunghi solo 8 bit, possono contenere solo 28 o 256 diversi valori possibili. L'uso di 2 o 4 byte in una riga aumenta il numero dei diversi valori possibili rispettivamente a 216, 65536 o 232, 4294967296.
Quando un programma usa un byte o un numero di byte in una riga per rappresentare qualcosa come una lettera, un numero o qualsiasi altra cosa, quei byte sono chiamati oggetti perché fanno tutti parte della stessa cosa. Anche se gli oggetti sono tutti memorizzati in byte di memoria identici, sono trattati come se avessero un 'tipo', che dice come i byte dovrebbero essere intesi: o come un intero o un carattere o qualche altro tipo (come un valore non intero). Anche il codice macchina può essere pensato come un tipo che viene interpretato come istruzioni. La nozione di tipo è molto, molto importante perché definisce ciò che si può e non si può fare all'oggetto e come interpretare i byte dell'oggetto. Per esempio, non è valido per memorizzare un numero negativo in un oggetto con numero positivo e non è valido per memorizzare una frazione in un numero intero.
Un indirizzo che punta a (è l'indirizzo di) un oggetto a più byte è l'indirizzo del primo byte di quell'oggetto - il byte che ha l'indirizzo più basso. A parte questo, una cosa importante da notare è che non si può dire quale sia il tipo di oggetto - o anche la sua dimensione - in base al suo indirizzo. Infatti, non si può nemmeno dire che tipo di oggetto sia un oggetto guardandolo. Un programma di linguaggio assembly deve tenere traccia di quali indirizzi della memoria contengono quali oggetti e quanto sono grandi. Un programma che lo fa è sicuro del tipo, perché fa cose sicure solo agli oggetti che sono sicuri di fare sul loro tipo. Un programma che non lo fa probabilmente non funzionerà correttamente. Si noti che la maggior parte dei programmi non memorizza esplicitamente il tipo di oggetto, ma si limita ad accedere agli oggetti in modo coerente - lo stesso oggetto viene sempre trattato come se fosse dello stesso tipo.
Il processore
Il processore esegue (esegue) le istruzioni, che vengono memorizzate come codice macchina nella memoria principale. Oltre ad essere in grado di accedere alla memoria per la memorizzazione, la maggior parte dei processori dispone di alcuni piccoli, veloci e fissi spazi per contenere gli oggetti con cui si sta lavorando. Questi spazi sono chiamati registri. I processori di solito eseguono tre tipi di istruzioni, anche se alcune istruzioni possono essere una combinazione di questi tipi. Di seguito sono riportati alcuni esempi di ogni tipo nel linguaggio di assemblaggio x86.
Istruzioni che leggono o scrivono la memoria
La seguente istruzione del linguaggio di assemblaggio x86 legge (carica) un oggetto a 2 byte dal byte all'indirizzo 4096 (0x1000 in esadecimale) in un registro a 16 bit chiamato 'ax':
mov ax, [1000h]
In questo linguaggio di montaggio, le parentesi quadre attorno ad un numero (o ad un nome di registro) indicano che il numero deve essere utilizzato come indirizzo dei dati che devono essere utilizzati. L'uso di un indirizzo per indicare i dati si chiama indirezione. In questo esempio successivo, senza le parentesi quadre, un altro registro, bx, ottiene in realtà il valore 20 caricato in esso.
mov bx, 20
Poiché non è stata utilizzata alcuna indirezione, il valore effettivo è stato messo nel registro.
Se gli operandi (le cose che vengono dopo il mnemonico), appaiono in ordine inverso, un'istruzione che carica qualcosa dalla memoria invece lo scrive in memoria:
mov [1000h], asse
Qui, la memoria all'indirizzo 1000h ottiene il valore di ax. Se questo esempio viene eseguito subito dopo il precedente, i 2 byte a 1000h e 1001h saranno un intero di 2 byte con il valore di 20.
Istruzioni che eseguono operazioni matematiche o logiche
Alcune istruzioni fanno cose come la sottrazione o operazioni logiche come non fare:
L'esempio di codice macchina precedente in questo articolo sarebbe questo nel linguaggio assembly:
aggiungere l'ascia, 42
Qui si sommano 42 e ax e il risultato viene memorizzato nuovamente in ax. Nel montaggio x86 è anche possibile combinare un accesso alla memoria e un'operazione matematica come questa:
aggiungere l'asse, [1000h]
Questa istruzione aggiunge il valore dei 2 byte interi memorizzati a 1000h all'asse e memorizza la risposta in asse.
o ascia, bx
Questa istruzione calcola il o del contenuto dei registri ax e bx e memorizza il risultato in ax.
Istruzioni che decidono quale sarà la prossima istruzione
Di solito le istruzioni vengono eseguite nell'ordine in cui appaiono in memoria, che è l'ordine in cui vengono digitate nel codice di assemblaggio. Il processore le esegue solo una dopo l'altra. Tuttavia, affinché i processori possano fare cose complicate, devono eseguire istruzioni diverse in base a quali sono i dati che gli sono stati dati. La capacità dei processori di eseguire istruzioni diverse a seconda del risultato di qualcosa si chiama ramificazione. Le istruzioni che decidono quale deve essere la prossima istruzione sono chiamate istruzioni di diramazione.
In questo esempio, supponiamo che qualcuno voglia calcolare la quantità di vernice di cui avrà bisogno per dipingere un quadrato con una certa lunghezza di lato. Tuttavia, a causa dell'economia di scala, il negozio di vernici non venderà meno della quantità di vernice necessaria per dipingere un quadrato di 100 x 100.
Per capire la quantità di vernice che dovranno ottenere in base alla lunghezza del quadrato che vogliono dipingere, escogitano questa serie di passi:
- sottrarre 100 dalla lunghezza del lato
- se la risposta è inferiore a zero, impostare la lunghezza del lato a 100
- moltiplicare la lunghezza del lato per se stesso
Tale algoritmo può essere espresso nel seguente codice dove l'asse è la lunghezza del lato.
mov bx, ax sub bx, 100 jge continua mov ax, 100 continua: mul ax
Questo esempio introduce diverse novità, ma le prime due istruzioni sono familiari. Copiano il valore dell'asse in bx e poi sottraggono 100 da bx.
Una delle novità di questo esempio si chiama etichetta, un concetto che si trova nei linguaggi di assemblaggio in generale. Le etichette possono essere qualsiasi cosa il programmatore voglia (a meno che non sia il nome di un'istruzione, che confonderebbe l'assemblatore). In questo esempio, l'etichetta è "continua". Viene interpretata dall'assemblatore come l'indirizzo di un'istruzione. In questo caso, è l'indirizzo di mult ax.
Un altro nuovo concetto è quello delle bandiere. Sui processori x86, molte istruzioni impostano dei 'flag' nel processore che possono essere usati dalle istruzioni successive per decidere cosa fare. In questo caso, se bx era inferiore a 100, sub imposterà un flag che dice che il risultato era inferiore a zero.
L'istruzione successiva è jge che è l'abbreviazione di 'Salta se maggiore o uguale a'. Si tratta di un'istruzione di ramo. Se i flag nel processore specificano che il risultato è stato maggiore o uguale a zero, invece di andare semplicemente all'istruzione successiva il processore salterà all'istruzione all'etichetta continua, che è mul ax.
Questo esempio funziona bene, ma non è quello che la maggior parte dei programmatori scriverebbe. L'istruzione sottrarre imposta correttamente il flag, ma modifica anche il valore su cui opera, il che richiede che l'asse sia copiato in bx. La maggior parte dei linguaggi di assemblaggio consentono istruzioni di confronto che non modificano nessuno degli argomenti che vengono passati, ma impostano comunque i flag correttamente e l'assemblaggio x86 non fa eccezione.
asse cmp, 100 jge continua mov ax, 100 continua: mul ax
Ora, invece di sottrarre 100 dall'asse, vedere se quel numero è inferiore a zero, ed assegnarlo di nuovo all'asse, l'asse viene lasciato invariato. Le bandiere sono ancora impostate allo stesso modo, e il salto è ancora preso nelle stesse situazioni.
Ingresso e uscita
Mentre l'input e l'output sono una parte fondamentale del calcolo, non c'è un solo modo in cui vengono fatti nel linguaggio assembly. Questo perché il modo in cui l'I/O funziona dipende dall'impostazione del computer e dal sistema operativo in funzione, non solo dal tipo di processore che ha. Nella sezione di esempio l'esempio di Hello World usa le chiamate al sistema operativo MS-DOS e l'esempio dopo le chiamate al BIOS.
E' possibile effettuare I/O nel linguaggio di assemblaggio. In effetti, il linguaggio assembly può generalmente esprimere tutto ciò che un computer è in grado di fare. Tuttavia, anche se ci sono istruzioni da aggiungere e diramare nel linguaggio assembly che faranno sempre la stessa cosa, non ci sono istruzioni nel linguaggio assembly che fanno sempre I/O.
La cosa importante da notare è che il modo in cui funziona l'I/O non fa parte di alcun linguaggio di assemblaggio perché non fa parte di come funziona il processore.