Programmare con l’assembler dell’8088 · 2015-03-18 · 1 1 Programmare con l’assembler...

24
1 1 Programmare con l’assembler dell’8088 2 Il linguaggio assembler Al livello di astrazione più basso, un programma scritto usando il linguaggio macchina (o ISA) di un certo processore è formato da una sequenza di istruzioni descritte ciascuna mediante una sequenza di bit. Per ogni istruzione, i bit di cui è composta indicano il tipo di operazione da compiere (somma, complemento, salto…) e gli operandi su cui eseguire l’operazione (registri della macchina o indirizzi delle celle di memoria) 3 Il linguaggio assembler Poiché scrivere programmi usando i numeri binari è estremamente scomodo, tutte le CPU permettono di usare una rappresentazione simbolica del linguaggio macchina, il linguaggio assembler Con l’assembler è possibile usare nomi simbolici per le istruzioni (ADD, SUB…) e per gli operandi (registri e indirizzi delle celle di memoria) Un programma assemblatore trasformerà poi le istruzioni assembler in corrispondenti istruzioni macchina

Transcript of Programmare con l’assembler dell’8088 · 2015-03-18 · 1 1 Programmare con l’assembler...

Page 1: Programmare con l’assembler dell’8088 · 2015-03-18 · 1 1 Programmare con l’assembler dell’8088 2 Il linguaggio assembler • Al livello di astrazione più basso, un programma

1

1

Programmare con l’assembler dell’8088

2

Il linguaggio assembler

•  Al livello di astrazione più basso, un programma scritto usando il linguaggio macchina (o ISA) di un certo processore è formato da una sequenza di istruzioni descritte ciascuna mediante una sequenza di bit.

•  Per ogni istruzione, i bit di cui è composta indicano il tipo di operazione da compiere (somma, complemento, salto…) e gli operandi su cui eseguire l’operazione (registri della macchina o indirizzi delle celle di memoria)

3

Il linguaggio assembler •  Poiché scrivere programmi usando i numeri binari è estremamente

scomodo, tutte le CPU permettono di usare una rappresentazione simbolica del linguaggio macchina, il linguaggio assembler

•  Con l’assembler è possibile usare nomi simbolici per le istruzioni (ADD, SUB…) e per gli operandi (registri e indirizzi delle celle di memoria)

•  Un programma assemblatore trasformerà poi le istruzioni assembler in corrispondenti istruzioni macchina

Page 2: Programmare con l’assembler dell’8088 · 2015-03-18 · 1 1 Programmare con l’assembler dell’8088 2 Il linguaggio assembler • Al livello di astrazione più basso, un programma

2

4

Il linguaggio assembler •  In un qualsiasi assembler, le istruzioni macchina sono indicate da

nomi mnemonici, ovviamente più facili da ricordare che non una sequenza di bit.

•  Nomi simbolici (o symbolic names) possono essere usati per indicare valori costanti

•  Labels (etichette) possono essere usate per fare riferimento ad una specifica istruzione (ad esempio la destinazione di un salto) e più in generale ad un indirizzo di memoria.

5

Il linguaggio assembler •  Di solito è anche possibile usare delle pseudoistruzioni, che

permettono di dare comandi all’assemblatore per guidare il processo di traduzione

•  Un particolare tipo di pseudoistruzioni sono le macro, che possono essere usate per specificare in modo abbreviato una sequenza di istruzioni assembler: ogni volta che l’assemblatore incontra in un programma una particolare macro, la sostituisce con la sequenza di istruzioni macchina associate a quella macro.

6

Il linguaggio assembler •  Per poter programmare in assembler è necessaria una conoscenza

approfondita non solo delle istruzioni di cui è formato l’assembler di un particolare processore: è necessario conoscere in dettaglio l’architettura del processore stesso

•  È la situazione opposta ai linguaggi ad alto livello, che (entro certi limiti) permettono di ignorare l’architettura interna del processore su cui verranno fatti girare i programmi scritti in quei linguaggi

Page 3: Programmare con l’assembler dell’8088 · 2015-03-18 · 1 1 Programmare con l’assembler dell’8088 2 Il linguaggio assembler • Al livello di astrazione più basso, un programma

3

7

Il linguaggio assembler •  Imparare a programmare in assembler sui processori usati nei

moderni computer è molto complicato, perché questi processori sono estremamente complessi e sofisticati.

•  Inoltre, quando si scrive un programma in assembler, lo si compila (si dice più comunemente: “lo si assembla”) per generare un programma binario, lo si esegue sulla CPU per cui è stato scritto, e il programma si comporta in maniera sbagliata perché contiene degli errori, è quasi sempre impossibile sapere cosa è andato storto…

8

Il linguaggio assembler •  Per questa ragione, è molto più comodo poter lanciare e testare il

programma non sul vero e proprio processore, ma su un simulatore, che permetta di:

–  Eseguire il programma una istruzione una alla volta

–  Fermare l’esecuzione del programma ad un punto specificato, e poi riprenderla da quel punto

–  Visualizzare lo stato interno del “processore”, ossia i valori contenuti nei vari registri e nella memoria primaria

–  (in alcuni casi) Modificare il contenuto di una qualsiasi registro o cella di RAM mentre l’esecuzione del programma è sospesa in un certo punto.

9

Il linguaggio assembler

•  Naturalmente, i programmi eseguiti su un simulatore girano molto più lentamente che se fossero eseguiti sul processore simulato, ma l’uso del simulatore rende estremamente più semplice l’apprendimento del linguaggio assembler e il debugging dei programmi.

•  Il simulatore è più comunemente chiamato interpreter o tracer, proprio perché interpreta i programmi e permette di tracciarne l’esecuzione.

•  Nota: tutte le figure di questi lucidi sono tratte dalla sezione C del Tanenbaum

Page 4: Programmare con l’assembler dell’8088 · 2015-03-18 · 1 1 Programmare con l’assembler dell’8088 2 Il linguaggio assembler • Al livello di astrazione più basso, un programma

4

10

Il linguaggio assembler •  Il tracer t88. (comando: t88 HlloWrld)

(a): il programma; (b): una schermata del tracer

11

Il processore 8088 •  L’8088 è uno degli antenati del moderni processori Intel: introdotto

nel 1979, era una macchina a 16 bit, clock a 5÷8 MHz, spazio di indirizzamento da 1 MB, era usato nei primi PC della IBM.

•  Molte delle istruzioni dell’8088 si ritrovano nell’ISA dei processori più moderni (ma a 64 o 32 bit anziché a 16 bit) e imparare a programmare l’8088 è una buona introduzione per la programmazione assembler dei processori Intel moderni

•  La semplicità delle istruzioni dell’8088 le rende simili a quelle di molte architetture RISC più moderne.

12

funzionamento dell’8088

•  L’8088 ha un datapath molto semplice, non pipelined (ricordate che nella famiglia 80x86 la pipeline è comparsa solo col 486) e senza cache. Ogni singola istruzione viene eseguita nei seguenti passi:

1.  prelievo dell’istruzione puntata dal PC dalla RAM 2.  incremento del PC 3.  decodifica dell’istruzione 4.  prelievo degli operandi dalla memoria o dai registri 5.  esecuzione dell’istruzione 6.  immagazzinamento del risultato in memoria o in un registro 7.  goto 1

•  (Le prossime figure: Tanenbaum, Fig. C-2.)

Page 5: Programmare con l’assembler dell’8088 · 2015-03-18 · 1 1 Programmare con l’assembler dell’8088 2 Il linguaggio assembler • Al livello di astrazione più basso, un programma

5

13

registri generali dell’8088 •  L’8088 ha 4 registri generali a 16 bit,

ciascuno suddiviso in 2 registri da 8 bit per le operazioni che lavorano su dati di tipo byte e caratteri. Sebbene i registri possano essere usati come registri generali, essi possono anche avere un uso “specifico”:

•  AX: accumulator. Comunemente usato per contenere il risultato di alcune istruzioni (ad esempio la MUL. E’ una forma di indirizzamento implicito).

–  MUL (addr) //AX = AX · “dato il cui indirizzo è addr”

•  BX: base register. Se usato tra parentesi specifica la word in RAM il cui indirizzo è contenuto in BX.

–  MOV AX, (BX) //AX = dato il cui indirizzo è in BX

14

registri generali dell’8088 •  CX: counter register. Usato per contenere

il contatore per gestire i loop. Viene automaticamente decrementato dalla istruzione LOOP, fino a che CX = 0.

MOV CX, 10 lb: ...

... LOOP lb // decrementa CX, salta se CX > 0

•  DX: data register. Usato insieme ad AX nelle operazioni a 32 bit.

15

registri generali dell’8088 •  DX: data register. Usato insieme ad AX

nelle operazioni a 32 bit.

•  In questo caso, DX conterrà i 16 bit più significativi, e AX i restanti 16 bit meno significativi.

•  DX ha quindi la funzione specifica di permettere di elaborare dati di tipo long

•  Ogni registro a 16 bit è diviso in due parti che possono essere manipolate separatamente. Ad esempio:

1.  ADDB AH, AL // AH = AH + AL

2.  MOV AX, 258 // AH e AL ora contengono i valori 1 e 2

•  Se eseguiamo la 1 dopo la 2 AX conterrà il valore 770 (030216)

Page 6: Programmare con l’assembler dell’8088 · 2015-03-18 · 1 1 Programmare con l’assembler dell’8088 2 Il linguaggio assembler • Al livello di astrazione più basso, un programma

6

16

Lo “stack segment” dell’8088 •  Nell’8088, ad un programma è sempre

associato una porzione di RAM da 64KB per contenere lo stack del programma

•  Quando il programma chiama una procedura (ad esempio A nella figura), nello stack vengono memorizzati gli argomenti della procedura, eventuali variabili locali, e l’indirizzo di ritorno quando la procedura termina. La porzione di stack che contiene le informazioni di A è detta stack frame

•  Se dentro A viene chiamata la procedura B, sulla cima dello stack viene allocato uno stack frame per B. Quando una procedura termina, il suo stack frame viene rimosso

65536

0

Stack frame della procedura A

Stack frame della procedura B

17

Lo “stack segment” dell’8088 •  Lo stack parte dall’indirizzo più alto del segmento da 64 KB, e

cresce “verso il basso”, ossia occupando man mano i byte di indirizzo più basso (questa è una scelta comune a molti processori).

•  Quando si parla della cima, o top dello stack, si fa quindi in realtà riferimento al dato nello stack memorizzato all’indirizzo più basso

•  La chiamata di una procedura (istruzione CALL) fa quindi sempre espandere lo stack, in quanto viene allocato un nuovo stack frame

•  Il ritorno da una procedura riduce invece la dimensione dello stack, perché viene deallocato lo stack frame della procedura terminata.

18

registri puntatori dell’8088 •  I registri puntatori contengono un indirizzo

di RAM:

•  SP: stack pointer. Punta sempre alla “cima” dello stack in RAM. Viene quindi decrementato dalla chiamata di una procedura e viene incrementato quando la procedura termina (perché il suo stack frame viene rimosso). Lo stack può essere anche usato per contenere risultati parziali del programma in esecuzione, mediante l’uso delle istruzioni push e pop:

PUSH AX // SP = SP-2; (SP) = AX POP AX // AX = (SP); SP = SP+2

•  Notate che la PUSH decrementa SP, mentre la POP incrementa SP.

Page 7: Programmare con l’assembler dell’8088 · 2015-03-18 · 1 1 Programmare con l’assembler dell’8088 2 Il linguaggio assembler • Al livello di astrazione più basso, un programma

7

19

registri puntatori dell’8088 •  BP: base pointer: può contenere un indirizzo qualsiasi dello stack,

ma di solito viene usato per puntare all’inizio dello stack frame sulla cima dello stack, ossia lo stack frame della procedura in esecuzione (questo stack frame è quindi delimitato da BP e SP). In questo caso, attraverso BP è possibile risalire alle variabili locali della procedura stessa (vedremo meglio più avanti)

•  SI e DI vengono usati insieme a BP per indirizzare dati nello stack, oppure insieme a BX per indirizzare dati nel segmento dati del programma in esecuzione. Ad esempio:

–  LODS (senza argomenti) usa il contenuto di SI per indirizzare il DATA SEGMENT e mette la word indirizzata in AX. SI viene automaticamente incrementato di 2 per puntare alla word successiva.

20

registri speciali dell’8088 •  IP: instruction pointer. Non è altro che

il Program Counter dell’8088 (ma ovviamente Intel deve cambiare i nomi standard...)

•  Flag Register. E’ la Program Status Word del processore, ossia un registro di bit (alcuni inutilizzati): ecco il significato di alcuni:

–  Z: il risultato dell’ultima operazione eseguita è zero (Zero bit)

–  S: il risultato dell’ultima operazione eseguita è negativo (Negative bit)

–  O: il risultato dell’ultima operazione ha generato overflow (Overflow bit)

–  C: il risultato dell’ultima operazione ha generato un riporto (Carry bit)

–  I: abilita/disabilita gli interrupt

21

I registri dei “segmenti” dell’8088 •  Un eseguibile 8088 è composto da 3

segmenti di RAM grandi al massimo 64KB: il codice, i dati e lo stack. L’indirizzo di partenza di ciascun segmento è contenuto nel corrispondente registro. E’ indirizzabile un ulteriore segmento extra attraverso il registro ES.

•  In realtà, di solito i segmento dei dati e dello stack coincidono. Però, lo stack viene allocato nella parte alta del segmento, e cresce verso il basso, mentre le strutture dati sono allocate a partire dalla prima locazione del segmento.

•  Notate che in assembler le variabili di un programma possono essere accedute solo attraverso il loro indirizzo (che però può essere associato ad un nome, una label, per comodità)

Page 8: Programmare con l’assembler dell’8088 · 2015-03-18 · 1 1 Programmare con l’assembler dell’8088 2 Il linguaggio assembler • Al livello di astrazione più basso, un programma

8

22

Organizzazione della memoria •  il PC punta sempre una locazione interna al code segment:

se PC = 0 viene puntata la prima locazione del code segment, non l’indirizzo di RAM 0.

•  l’8088 è in grado di gestire 1MB di RAM. Come è possibile con registri da 16 bit?

•  L’indirizzo di partenza di ogni segmento è sempre un multiplo di 64 KB, ed è un indirizzo su 20 bit costruito aggiungendo 4 bit a zero al fondo del corrispondente segment register (ossia quattro bit meno significativi vengono aggiungi all’indirizzo su 16 bit).

•  Quindi, un indirizzo assoluto è costruito moltiplicando il contenuto del corrispondente segment register per 16 e aggiungendo l’indirizzo specificato all’interno di quel segmento.

23

Organizzazione della memoria

•  Ad esempio, se per un programma DS = 7 e BX = 12 (in decimale), l’istruzione “MOV AX, (BX)” mette in AX il dato di 16 bit situato all’indirizzo in RAM: 7 · 16 + 12 = 124.

•  In altre parole, l’indirizzo binario su 20 bit implicato da “DS = 7” è 00000000000001110000. A questo va aggiunto BX = 12 = 1100 ottenendo 00000000000001111100 (124 decimale)

•  Per ogni riferimento in memoria uno dei segment register è usato nel modo visto per generare l’effettivo indirizzo in RAM: la trasformazione di un indirizzo da 16 bit a 20 bit viene ovviamente fatta tutta a livello hardware.

24

Indirizzamento della memoria •  Nell’assembler di un qualsiasi processore deve essere specificato in

quali modi le istruzioni possono fare riferimento ai dati in RAM, ossia, come possono indirizzare la memoria.

•  Nelle macchine RISC poi le diverse modalità possono essere usate solo nelle istruzioni LOAD e STORE, mentre nelle macchine CISC anche le altre istruzioni possono usare le diverse modalità per fare riferimento ai dati in RAM (eventualmente con alcune restrizioni, ad esempio: nelle istruzioni con almeno due operandi, uno dei due deve necessariamente essere un registro)

Page 9: Programmare con l’assembler dell’8088 · 2015-03-18 · 1 1 Programmare con l’assembler dell’8088 2 Il linguaggio assembler • Al livello di astrazione più basso, un programma

9

25

Indirizzamento della memoria •  Nel caso dell’8088, un indirizzo in RAM specifica sempre un byte

ben preciso, ma se si stanno usando argomenti a 16 bit (il caso più frequente) o a 32 bit (per i long), l’indirizzo specifica implicitamente anche il byte successivo o i tre byte successivi.

•  Ad esempio:

–  ADD CX (20)

•  Somma a CX il contenuto della word da 2 byte memorizzata agli indirizzi 20 e 21 della RAM, e mette il risultato in CX (poiché l’8088 adotta l’ordinamento little endian, il byte meno significativo di ogni word è memorizzato all’indirizzo più basso)

26

Indirizzamento della memoria •  Gli assembler di tutti i processori adottano modalità per specificare

gli operandi più o meno simili fra loro, incluse le modalità dell’8088.

•  Ecco qui di seguito la specifica completa di tutte le modalità di indirizzamento dell’8088, che deve ovviamente specificare anche gli operandi che non sono variabili in RAM: i registri e i valori immediati (prossime 2 figure: Tanenbaum, Fig. C-3)

27

Indirizzamento della memoria •  Notate che quando parliamo di indirizzamento di un dato in RAM,

ci riferiamo evidentemente alle informazioni contenute nel segmento dati (il simbolo # indica un valore immediato: un numero o una label)

Page 10: Programmare con l’assembler dell’8088 · 2015-03-18 · 1 1 Programmare con l’assembler dell’8088 2 Il linguaggio assembler • Al livello di astrazione più basso, un programma

10

28

Indirizzamento della memoria •  Le principali modalità di indirizzamento della RAM sono:

•  Indirizzamento diretto, in cui l’istruzione contiene l’indirizzo dell’operando:

–  ADD CX, (20) // CX = CX + [20]

•  Indirizzamento a registro indiretto, in cui l’indirizzo dell’operando è contenuto in un registro (BX, SI o DI)

–  ADD CX, (BX) // CX = CX + [BX]

29

Indirizzamento della memoria •  Indirizzamento con spiazzamento, in cui il valore contenuto nel

registro viene sommato ad una costante numerica per formare l’indirizzo

–  ADD CX, 10(BX) // CX = CX + [10+BX]

•  Vi sono modalità di indirizzamento della RAM attraverso i registri più sofisticate, ad esempio: l’indirizzamento a registri con indice:

–  PUSH (BX) (DI)

•  che inserisce sulla cima dello stack il contenuto della cella di memoria il cui indirizzo è dato dalla somma dei valori in BX e DI

•  Tutte le modalità di indirizzamento che abbiamo visto (ma ve ne sono altre) permettono di specificare quelli che si chiamano: indirizzi effettivi

30

Istruzioni dell’8088 •  E’ impossibile scrivere programmi in assembler senza conoscere a

fondo e nei dettagli le istruzioni dell’ISA del processore su cui stiamo lavorando.

•  Per comodità le istruzioni vengono suddivise in un insieme di classi più o meno omogenee, perché hanno caratteristiche simili nella sintassi e nell’effetto che hanno sullo stato del processore.

•  Nei prossimi lucidi vediamo alcune delle istruzioni più significative dell’8088. In generale, per usare l’assembler di una qualsiasi CPU è necessario fare riferimento al manuale dell’assembler di quella CPU che descrive in dettaglio tutte le istruzioni riconosciute e il loro effetto sul processore (prossime 3 figure: Tanenbaum, Fig. C-4)

•  Potete trovare una descrizione completa degli instruction set x86 in rete

Page 11: Programmare con l’assembler dell’8088 · 2015-03-18 · 1 1 Programmare con l’assembler dell’8088 2 Il linguaggio assembler • Al livello di astrazione più basso, un programma

11

31

Istruzioni dell’8088 •  Trasferimento, copia e operazioni aritmetiche

–  MOV: è l’istruzione di gran lunga più usata, e permette di: •  Copiare quanto indicato da un indirizzo effettivo (e) in un registro (r) o

viceversa (r ! e, e ! r) •  Copiare un dato immediato (#) in un indirizzo effettivo (e ! #)

32

Istruzioni dell’8088 •  Trasferimento, copia e operazioni aritmetiche

–  La lettera (B) indica l’istruzione equivalente su operandi di tipo byte. Ad esempio: MOVB AH, (20)

–  Notate la colonna “status flag”: il gruppo di istruzioni da MOV a XLAT non modifica i flag del flag register

–  ADD e SUB: sono ovviamente le operazioni di somma e sottrazione. Possono avere effetto sugli status flag:

•  O (overflow) viene settato a 1 se il risultato dell’istruzione non può essere espresso con i 16 bit a disposizione. Viene resettato a 0 altrimenti

•  Gli altri flag vengono settati/resettati concordemente con quanto indicato nel lucido 20

–  ADC e SBB usano il Carry bit (il bit/flag di riporto) per poter lavorare con interi a 32 bit

33

Istruzioni dell’8088 •  Trasferimento, copia e operazioni aritmetiche

–  MUL, DIV: notate come queste istruzioni abbiano un solo operando esplicito:

•  MUL (BX) // DX:AX = AX · [BX]

•  L’Overflow ed il Carry bit vengono settati a 1 quando il risultato non può essere rappresentato in una word, i bit S e Z rimangono indefiniti

•  DIV (BX) // AX = quoziente di AX / [BX] // DX = resto di AX / [BX]

•  Tutti gli status bit rimangono indefiniti, ma un interrupt viene inviato in caso di divisione per zero.

–  IMUL, IDIV permettono di lavorare con valori signed

Page 12: Programmare con l’assembler dell’8088 · 2015-03-18 · 1 1 Programmare con l’assembler dell’8088 2 Il linguaggio assembler • Al livello di astrazione più basso, un programma

12

34

Istruzioni dell’8088 •  Operazioni logiche, di shift e sui bit

–  CBW e CWD operano sulla copia DX:AX –  Le RCL, RCR coinvolgono anche il Carry bit, e quindi di fatto

danno luogo ad operazioni di shift circolare a 17 bit. –  SHR sposta tutti i bit a destra di uno, aggiungendo uno zero sulla

sinistra.

35

Istruzioni dell’8088 •  Operazioni di test e manipolazione dei flag

–  Queste istruzioni vengono usate principalmente per produrre la condizione da usare nelle istruzioni di salto condizionato.

–  TEST: viene fatto l’AND bit a bit tra i due operandi. Se sono tutti uguali viene settato a 1 il flag Z (Zero bit)

–  CMP: viene fatta la differenza tra i due operandi, e i vari flag vengono settati corrispondentemente.

36

Istruzioni dell’8088 •  Cicli e operazioni su stringhe

–  LOOP: decrementa CX e salta alla label se CX > 0. Il salto deve essere nell’intervallo [–128 , +127] byte, perché è specificato su 8 bit con segno

–  Notate che non si può dire quante istruzioni si possono saltare al massimo perché le istruzioni x86 non hanno lunghezza fissa.

•  Le altre istruzioni LOOP testano anche il flag Zero

•  REP – CMPS eseguono operazioni ripetute su intere stringhe in RAM. Questo è un tipico esempio di istruzioni CISC

Errore: togliete “back”

errore: è “CX > 0”

Page 13: Programmare con l’assembler dell’8088 · 2015-03-18 · 1 1 Programmare con l’assembler dell’8088 2 Il linguaggio assembler • Al livello di astrazione più basso, un programma

13

37

Istruzioni dell’8088 •  Istruzioni di salto e salto condizionato

–  JMP xxx: dove xxx può essere una label o un effective address, e il salto deve rimanere nel range [– 32768, +32767] byte

–  I salti condizionati (spesso usati subito dopo una TEST o una CMP), ammettono uno spiazzamento massimo di 128 byte

–  Per saltare più lontano si usa la tecnica “jump over jump”. Ad esempio, il branch:

JE far_label

diventa:

JNE skip JMP far_label

skip: …

38

Istruzioni dell’8088 •  Chiamate di Subroutine (procedure)

–  All’interno di un programma assembler è possibile definire delle subroutine, che possono essere chiamate con l’istruzione CALL

–  Le subroutine possono avere degli argomenti, che il codice della subroutine si aspetta di trovare sullo stack.

–  Prima di chiamare la subroutine quindi, il programma chiamante deve mettere gli argomenti sullo stack (in ordine inverso, se si fa riferimento alla sintassi del C) usando una PUSH per ciascun argomento.

–  Quando viene eseguita la CALL, per prima cosa il valore corrente del PC viene automaticamente caricato sullo stack, in modo da salvare l’indirizzo di ritorno dalla subroutine (return address): ossia da quale istruzione prosegue l’esecuzione dopo la terminazione della subroutine

39

Istruzioni dell’8088 •  Chiamate di Subroutine

–  Dopo il salvataggio dell’indirizzo di ritorno sullo stack, nel PC viene scritto l’indirizzo della prima istruzione della subroutine, che corrisponde all’argomento (label o indirizzo effettivo) dell’istruzione CALL

–  L’ultima istruzione della subroutine deve essere RET, che semplicemente preleva l’indirizzo di ritorno sulla cima dello stack e lo scrive nel PC

–  RET può avere un argomento numerico: il numero di byte occupati dagli argomenti passati alla subroutine. Viene automaticamente aggiunto a SP per pulire lo stack alla fine della subroutine.

Page 14: Programmare con l’assembler dell’8088 · 2015-03-18 · 1 1 Programmare con l’assembler dell’8088 2 Il linguaggio assembler • Al livello di astrazione più basso, un programma

14

40

Istruzioni dell’8088 •  Codice delle Subroutine

–  Per semplificare il ritorno da una subroutine, le prime istruzioni della subroutine sono di solito le seguenti (vedremo poi perché):

•  PUSH BP // salva BP sullo stack.

•  MOV BP, SP // ora BP punta alla cima dello stack, dove // è memorizzato OLD BP. BP = SP

–  Return address si trova ora al’indirizzo BP+2, e Argument 1 e Argument 2 si trovano a BP+4 e BP+6

–  Ecco esempi di come possono essere acceduti gli argomenti all’interno della subroutine:

•  MUL 4(BP)

•  ADD AX, 6(BP)

41

Istruzioni dell’8088 •  Codice delle Subroutine

–  Il codice della subroutine può usare lo stack nel modo appena visto, per passare argomenti e chiamare un’altra subroutine.

–  Ma lo stack si può anche usare come spazio per allocare variabili locali alla procedura, che sono riferite attraverso BP. BP e SP delimitano lo stack frame corrente: SP cambia col variare della dimensione dello stack frame corrente, mentre BP punta alla base dello stack frame, e quindi rimane costante finché quel frame è attivo)

–  Così, ad esempio:

ADD -2(BP), AX

–  usa i primi due byte dello stack frame per salvare un risultato parziale

42

Istruzioni dell’8088 •  Ritorno dalla Subroutine

–  Il codice della subroutine può allocare un numero imprecisato di variabili locali sullo stack, e soprattutto un numero che può variare da una chiamata all’altra.

–  Senza dover calcolare ogni volta la quantità di stack usato dalla subroutine, quando questa termina il suo stack frame può essere rimosso, e si può riattivare lo stack frame della procedura chiamante così:

•  MOV SP, BP

•  POP BP // BP " OLD BP

•  RET

–  Notate che questo non è altro che il complemento di quanto fatto all’inizio

Page 15: Programmare con l’assembler dell’8088 · 2015-03-18 · 1 1 Programmare con l’assembler dell’8088 2 Il linguaggio assembler • Al livello di astrazione più basso, un programma

15

43

Istruzioni dell’8088 •  Precauzioni nella chiamata di una Subroutine

–  La subroutine può ovviamente utilizzare i registri generali della macchina.

–  Se il codice chiamante ha messo dei valori significativi in questi registri, prima di chiamare la subroutine dovrebbe salvarne il contenuto, ad esempio sullo stack. Con quattro registri generali in tutto, è molto probabile che ciò si renda necessario

–  Ecco un esempio di come, avere molti registri a disposizione, riduce la probabilità di dover ricorrere all’uso di spazio temporaneo in memoria principale

–  Se si scrive codice in un linguaggio ad alto livello, tutti questi dettagli devono essere gestiti in modo automatico dal compilatore, una cosa assolutamente non banale…

44

Istruzioni dell’8088 •  Subroutine di Sistema

–  Un qualsiasi processore possiede delle istruzioni per comunicare con l’esterno, usando opportune porte di comunicazione.

–  Ad esempio, nell’8088 l’istruzione:

•  OUT portnumber, AX

–  Scrive il contenuto di AX sulla porta di uscita specificata da portnumber

–  Queste istruzioni sono ovviamente necessarie per operare sui file, e in generale per implementare le operazioni di input/output del processore, ma per permettere ad un interprete di funzionare su piattaforme e sistemi operativi diversi, vengono sostituite da un insieme opportuno di “subroutine di sistema”

45

Istruzioni dell’8088 •  Subroutine di Sistema

–  Essendo delle subroutine, sono chiamate come abbiamo visto: 1) gli argomenti vengono inseriti sullo stack in ordine inverso 2) viene inserita sullo stack la label/nome della subroutine 3) viene chiamata la system trap SYS

Page 16: Programmare con l’assembler dell’8088 · 2015-03-18 · 1 1 Programmare con l’assembler dell’8088 2 Il linguaggio assembler • Al livello di astrazione più basso, un programma

16

46

Istruzioni dell’8088 •  Subroutine di Sistema

–  Eventuali valori di ritorno vengono messi in AX, o in DX:AX

–  Le subroutine di sistema garantiscono che tutti i registri (a parte AX e DX) non vengono modificati dalla subroutine stessa.

–  Quando la subroutine di sistema termina, lascia sullo stack gli argomenti con cui è stata chiamata, che possono essere cancellati (basta modificare opportunamente SP) o riutilizzati per un’altra chiamata.

47

Programmare in Assembler •  Come abbiamo già osservato, sviluppare programmi in assembler

per un processore, è un buon modo per conoscere a fondo e “vedere all’opera” l’architettura di quel processore.

•  Un programma scritto in assembler sarà assemblato da un assemblatore, e potrà poi essere eseguito su un processore che adotti quel linguaggio macchina.

•  Ovviamente, il programma può contenere errori, che però sono difficili da trovare semplicemente eseguendo il programma.

•  Per questa ragione, lo sviluppo di programmi assembler è di solito portato avanti attraverso un simulatore della macchina che permette di simulare appunto l’esecuzione dei programmi sul processore per cui sono stati sviluppati.

•  Il simulatore della macchina interpreta le istruzioni di un programma e permette di tracciarne l’esecuzione: viene quindi di solito chiamato interpreter o tracer

48

Programmare in Assembler •  Ma un simulatore svolge di solito anche funzioni di debugger:

permette in fatti di eseguire il programma passo passo, una sola istruzione per volta, tracciando il contenuto dei registri della macchina che simula mano a mano che le istruzioni li usano e li modificano.

•  Naturalmente, il simulatore permette di fare molte altre cose, ad esempio di eseguire n istruzioni, di inserire dei breakpoint e di eseguire il programma fino al raggiungimento di uno specifico breakpoint, di visualizzare il contenuto di un segmento di memoria, e così via.

•  Nel caso di processori embedded, è comodo usare il simulatore di un processore per sviluppare applicazioni che dovranno girare su quel processore, lavorando però in un ambiente più user-friendly, (ossia usando il simulatore in ambiente Windows o Unix)

Page 17: Programmare con l’assembler dell’8088 · 2015-03-18 · 1 1 Programmare con l’assembler dell’8088 2 Il linguaggio assembler • Al livello di astrazione più basso, un programma

17

49

Programmare in Assembler •  Sebbene vi sia una corrispondenza 1 a 1 tra istruzioni assembler e

istruzioni macchina, l’assembler permette di programmare usando nomi simbolici per le istruzioni e i registri: l’assemblatore genererà poi il codice macchina vero e proprio, formato da sequenze di bit.

•  L’assembler permette inoltre di usare etichette (in inglese label), per indicare specifiche posizioni in memoria: la posizione di una variabile in RAM o una istruzione a cui deve saltare un branch

•  Durante la generazione del codice macchina l’assemblatore mappa le label del programma ai valori binari che rappresentano: indirizzi della RAM

•  Un assembler permette anche di definire costanti: ossia associazioni tra valori numerici e nomi simbolici.

50

Una nota sugli assemblatori •  Il compilatore di un programma scritto in assembler viene

comunemente detto assemblatore

•  L’output di un assemblatore è un file oggetto

•  Spesso, un programma usa subroutines contenute in file oggetto separati. Il linker mette insieme tutti i file oggetto e produce un file eseguibile binario, che può essere caricato in memoria ed eseguito.

•  Mentre l’assemblatore produce il codice oggetto, scrive in una Symbol table i nomi di tutte le costanti e le label che incontra, insieme con il loro valore numerico.

•  Però, mentre le costanti definite all’inizio del programma in fase di compilazione possono essere immediatamente associate al loro valore numerico nella symbol table, per le label non è così ovvio.

51

Una nota sugli assemblatori •  Infatti una label può fare riferimento ad una locazione che ha un

indirizzo maggiore di quello del punto in cui compare la label. Ad esempio come in questo caso:

JB mylabel

MUL (BX)

mylabel: ADD AX, BX

•  Quando il compilatore incontra la prima occorrenza di mylabel, non sa ancora a quale indirizzo fa riferimento.

•  Per gestire queste situazioni, l’assemblatore deve scandire due volte il codice del programma

Page 18: Programmare con l’assembler dell’8088 · 2015-03-18 · 1 1 Programmare con l’assembler dell’8088 2 Il linguaggio assembler • Al livello di astrazione più basso, un programma

18

52

Una nota sugli assemblatori •  Nella prima passata, ogni riga di codice viene analizzata e un

location counter viene incrementato del numero di byte necessari per contenere quella riga. Ad esempio, nel caso:

JB mylabel //occupa 2 byte

MUL (BX) //occupa 3 byte

mylabel: ADD AX, BX

•  Quando l’assemblatore incontra “JB mylabel” inserisce una nuova entry mylabel nella symbol table. Quando nel programma incontra “mylabel:” associa alla entry mylabel della symbol table il valore corrente del location counter

•  Nella seconda passata il valore numerico di ogni label è noto, e può iniziare la generazione del codice binario (ad esempio, “JB mylabel” può essere trasformato in “JB 5”, e poi in una sequenza di cifre binarie)

53

l’ACK-Based Assembler as88 •  Per un dato processore, possono essere disponibili diversi

assemblatori/linker. Ovviamente tutti producono codice binario in grado di girare sul processore per cui sono stati concepiti.

•  Ogni assemblatore/linker è poi normalmente accompagnato da un interprete/tracer/debugger per poter operare con una certa comodità sul codice sviluppato.

•  Diversi kit di sviluppo possono quindi essere disponibili in letteratura e sul mercato. Quelli professionali possono avere costi molto alti, dato che sono usati per lo sviluppo di software che verrà poi venduto.

54

l’ACK-Based Assembler as88 •  Pseudoistruzioni: ogni assemblatore prevede un insieme specifico

di pseudoistruzioni: direttive che influenzano il processo di generazione del codice ma che non vengono tradotte in codice binario.

•  Nell’Amsterdam Compiler Kit (ACK) as88, le pseudoistruzioni più importanti sono quelle che permettono di definire le sezioni del codice (TEXT) e dei dati (DATA) del programma, che andranno poi memorizzate nei corrispondenti segmenti di memoria

•  All’interno della sezione dati è anche possibile definire una sottosezione di dati (variabili) non inizializzati detta BSS (Block Started by Symbol)

Page 19: Programmare con l’assembler dell’8088 · 2015-03-18 · 1 1 Programmare con l’assembler dell’8088 2 Il linguaggio assembler • Al livello di astrazione più basso, un programma

19

55

l’ACK-Based Assembler as88 •  Vi sono altre pseudoistruzioni riconosciute dall’ACK.

Ad esempio “.WORD” per definire un vettore di dati, o “.ASCII” per definire una stringa.

•  Vedremo l’uso di alcune di queste pseudoistruzioni negli esempi.

56

l’ACK-Based Assembler as88 •  Labels: Nell’ACK qualsiasi istruzione o dato può essere preceduto

da una label. Ne esistono di due tipi:

•  Global labels: stringhe alfanumeriche seguite dai due punti (ad esempio: “mylabel5:”)

–  Devono essere uniche in tutto il programma, e non devono essere uguali a nomi di istruzioni, pseudoistruzioni, eccetera.

•  Local labels: una cifra da 0 a 9 seguita dai due punti. (es: “5:”)

–  Sono limitate alla sola sezione TEXT, e possono occorrere più volte. Una istruzione come:

JE 2f significa: Jump if Equal forward alla prima label 2 che incontri

JE 4b significa: Jump if Equal back alla prima label 4 che incontri

57

l’ACK-Based tracer t88 •  Il tracer-debugger che useremo assume di interfacciarsi con un

terminale standard 24 x 80 linee. Il suo layout, formato da 7 “finestre”, è il seguente (Tanenbaum, Fig. C-10)

1 2 3

4 5

6

7

Page 20: Programmare con l’assembler dell’8088 · 2015-03-18 · 1 1 Programmare con l’assembler dell’8088 2 Il linguaggio assembler • Al livello di astrazione più basso, un programma

20

58

l’ACK-Based tracer t88 •  Processor with registers (1): mostra i registri generali in decimale

e gli altri registri (inclusi i byte register, come AL) in esadecimale.

•  Al di sotto dell’IP (Program Counter) viene indicata l’istruzione correntemente puntata dall’IP rispetto all’ultima label globale

•  Sopra l’IP compaiono alcuni flag della PSW, e al di sotto il loro valore:

•  O (overflow): indicato da una v

•  D (direction): > o <

•  S (sign): p o n

•  Z (zero): indicato da una z

•  C (carry): indicato da una c

•  “-” indica che il flag è a 0

59

l’ACK-Based tracer t88 •  Stack (2): visualizza il contenuto dello stack e la locazione

correntemente puntata da SP

•  Un numero a fianco di un indirizzo indica che quello è l’indirizzo di ritorno di una subroutine. Il valore del numero indica di quale livello di subroutine si tratta rispetto al programma principale

•  Program text (3): visualizza la porzione di codice correntemente in esecuzione, insieme con la posizione precisa del program counter

2 3

60

l’ACK-Based tracer t88 •  Subroutine call stack (4): le righe di codice relative alle chiamate

di subroutine più recenti (nell’esempio, la chiamata di subroutine più recente è avvenuta alla riga di codice “inpstart + 7”)

•  Interpreters Command (6): ultimi comandi dell’interprete dati, e cursore dei comandi

•  Error/Input/Output (5): E: eventuali errori del tracer I: eventuali valori dati in input righe restanti: output

•  Global variables (7): mostra una porzione del data segment:

4

6

5

7

Page 21: Programmare con l’assembler dell’8088 · 2015-03-18 · 1 1 Programmare con l’assembler dell’8088 2 Il linguaggio assembler • Al livello di astrazione più basso, un programma

21

61

Comandi dell’ACK tracer t88 •  Ogni comando va seguito da return, e dando il solo return viene

eseguita solo una istruzione

•  # (dove # è un numero > 0) esegue # istruzioni di seguito

•  #g esegue le istruzioni fino alla linea di codice #

•  Label+#b mette un breakpoint alla riga label+# (con c al posto di b rimuove il breakpoint)

•  r esegue fino al prossimo breakpoint o fino alla fine, se non ci sono breakpoint

62

Comandi dell’ACK tracer t88 •  - esegue fino all’uscita dalla subroutine correntemente chiamata.

•  + esegue fino alla chiamata di una subroutine

•  = esegue fino che non si incontra lo stesso livello di subroutine corrente. Comodo per eseguire una chiamata di subroutine se ci si trova alla sua istruzione CALL

63

Primo esempio: Helloworld.s •  Questo programma è praticamente fatto solo di istruzioni per

produrre l’output •  Definizione delle pseudoistruzioni necessarie •  Inizio della sezione di codice (.TEXT) •  Calcola la lunghezza di “Hello world\n” come

differenza di due label •  Esecuzione della system call _WRITE,

che ha tre argomenti: file descriptor, indirizzo della stringa da stampare, lunghezza della stringa.

•  Pulisce lo stack (|PUSH X| = 2) •  Termina restituendo l’esito della _WRITE

(la _WRITE scrive in AX il numero di byte scritti)

•  Inizio della sezione .DATA: le label hw e de servono per delimitare la stringa in stampa

Page 22: Programmare con l’assembler dell’8088 · 2015-03-18 · 1 1 Programmare con l’assembler dell’8088 2 Il linguaggio assembler • Al livello di astrazione più basso, un programma

22

64

Primo esempio: Helloworld.s •  comando “t88 HlloWrld”

ecco la situazione quando il PC sta per eseguire l’istruzione numero 12, per pulire lo stack prima della terminazione

65

Secondo esempio: vecprod.s •  Questo programma moltiplica gli elementi di due vettori a coppie, e

produce in output la somma dei singoli prodotti.

•  Il programma inizia preparandosi a chiamare la subroutine vecmul(count, vec1, vec2)

•  count contiene il numero di elementi di uno due vettori. Riuscite a vedere come viene calcolato?

•  Nella sezione DATA, la pseudoistruzione .ALIGN sposta il location counter alla prima locazione pari disponibile, in modo che vec1 possa essere allocato a partire da una locazione di indirizzo pari.

•  .SPACE viene usato per riservare uno spazio di due byte per la variabile inprod, che sarà usata dal programma pricipale

66

Secondo esempio: vecprod.s •  Il codice della subroutine vecmul inizia, come abbiamo visto in

precedenza, predisponendo i registri per trovare in BP+2 l’indirizzo di ritorno, e in BP+4, BP+6 e BP+8 gli argomenti della chiamata,che vengono caricati nei registri: CX = count; SI = vec1; DI = vec2

•  vecmul ha bisogno di una variabile locale inizializzata a zero (chiamiamola “varloc”)

•  AX = vec1[i] · vec2[i]

•  varloc = varloc + AX

•  Spostati alla word successiva di vec2

•  CX = CX – 1; Jump if CX > 0

•  AX = varloc

Page 23: Programmare con l’assembler dell’8088 · 2015-03-18 · 1 1 Programmare con l’assembler dell’8088 2 Il linguaggio assembler • Al livello di astrazione più basso, un programma

23

67

Secondo esempio: vecprod.s •  Al ritorno da vecmul il risultato viene salvato nella variabile locale

inprod, e poi vengono preparati gli argomenti per la stampa del risultato.

•  In particolare, la pseudoistruzione _PRINTF usa la sintassi della printf del C: _PRINTF(“Inner product is: %d\n”, %d), dove:

pfmt = “Inner product is: %d\n” AX = %d

•  Infine, lo stack viene ripulito

•  E il programma termina con una EXIT (0)

68

Secondo esempio: vecprod.s •  In questa figura vediamo la configurazione del tracer quando il

programma deve eseguire per la prima volta la LODS. Notate sullo stack gli argomenti della vecmul e l’indirizzo di ritorno, il vecchio valore di BP e la “varloc”.

69

Un semplice esercizio: Power.s •  Scrivete un programma assembler per l’elevazione

a potenza di due numeri interi in cui la base viene moltiplicata per se stessa un numero di volte pari all’esponente.

•  Base ed esponente sono due numeri interi dichiarati come WORD nel DATA SEGMENT (in altre parole, sono già in memoria e non devono essere richiesti in input, un po’ come abbiamo visto per i due vettori di vecprod)

•  Il programma deve stampare in output il risultato dell’elevazione a potenza (usate lo schema visto in vecprod)

Page 24: Programmare con l’assembler dell’8088 · 2015-03-18 · 1 1 Programmare con l’assembler dell’8088 2 Il linguaggio assembler • Al livello di astrazione più basso, un programma

24

70

L’assembler dell’8088 •  Trovate una descrizione dettagliata delle cose viste in questi lucidi

nell’appendice C del Tanenbaum.

•  trovate l’Amsterdam Compiler Kit sul CD che accompagna il testo o agli indirizzi: www.cs.vu.nl/ack www.prenhall.com/tanenbaum

•  Il kit contiene i due programmi fondamentali:

•  as88 // l’assemblatore (i file devono avere // l’estensione .”s.” Ad esempio: “as88 power.s”)

•  t88 // il tracer (ad esempio, “t88 power”)