|
|
Guida al Linguaggio C
II
Parte
Gestione memoria in linguaggio C
Il linguaggio C, pur non possedendo di funzioni tipo quelle del Basic
(peek e poke), ci permette mediante alcuni accorgimenti di accedere alla memoria.
Il seguente esempio scrive la lettera A direttamente nella memoria video.
char far *var = 0xB8000000; /* Segmento + offset */
funzione()
{
*var = 'A';
..........
}
NOTA: 0xB800 e' il segmento della memoria video della scheda CGA.
Un esempio come quello precedente fa' emettere al compilatore un messaggio di warning ma in ogni caso e' efficace.
Come abbiamo gia' detto la scrittura nei registri di segmento in linguaggio C e possibile solo per quanto riguarda il registro DS ed ES.
Il discorso relativo alla segmentazione assume una notevole importanza
nel momento in cui si tenta di eseguire il link di due funzioni, una scritta in assembler e l'altra in linguaggio C.
In questo caso dovranno essere seguite alcune regole nella dichiarazione dei segmenti e delle funzioni al momento della scrittura del modulo in assembler.
Una tecnica del genere, ovvero il link di un modulo assembler e uno
C, puo' rivelarsi utile nel momento in cui si eseguono delle modifiche a vettori di interruzione.
Benche' sia possibile eseguire certe tecniche anche solo utilizzando il linguaggio C queste risultano piu' ostiche.
Vediamo uno schema che raffigura l'utilizzo fatto dal compilatore della memoria.
Memoria Alta +-------------------------------------------+
: Spazio per allocazione dinamica :
+-------------------------------------------+
: Stack :
+-------------------------------------------+
: _BSS e c_common :
+-------------------------------------------+
: CONST :
+-------------------------------------------+
: _DATA :
+-------------------------------------------+
: NULL :
+-------------------------------------------+
: Data Segments :
+-------------------------------------------+
: _TEXT :
Memoria bassa +-------------------------------------------+
La prima area consiste in una zona non allocata che il programma puo'utilizzare per le allocazioni dinamiche.
La descrizione dei segmenti e' la seguente.
STACK Il segmento dello stack viene utilizzato per tutte le variabili locali.
_BSS Questo segmento contiene tutte i dati statici non inizializzati eccetto quelli che utilizzano la key
far nella dichiarazione del sorgente.
c_common Il segmento c_common contiene tutti i dati globali non inizializzati per i modelli di memoria piccolo e medio. Nel modello largo di memoria questi tipi di dati vengono piazzati in un data segment con
classe FAR_BSS.
CONST Vengono contenute in questo segmento tutte le costanti che possono solo essere lette.
_DATA E' considerato il data segment di default. Dopo l'inizializzazione tutte le variabili globali e statiche vengono conservate qui eccetto quelle
utilizzanti nella dichiarazione la key far. Queste vengono poste in un segmento differente.
NULL E' un segmento particolare che viene utilizzato ad esempio per la memorizzazione del copyright del compilatore. Questo segmento e' testato prima e dopo l'esecuzione del programma. Se durante l'esecuzione il contenuto cambia allora il programma emettera' un messaggio di errore "Null pointer assignment".
Data Segment Le variabili statiche e globali far sono inserite in questo segmento con classe FAR_DATA.
_TEXT Questo e' il segmento di codice.
La scrittura di funzioni in assembler da linkare con quelle scritte in C ci obbliga a riferirci al segmento di codice _TEXT.
E' anche facile che venga utilizzato come segmento data _DATA.
Parlando nel capitolo riguardante l'hardware dei vari registri di segmento avevamo visto che questi erano 4.
Come e' possibile che il compilatore Microsoft tratti invece tutti i segmenti appena visti ?
Qui entra in ballo il concetto di gruppo.
Tutti i segmenti con lo stesso nome di gruppo possono trovarsi nello stesso segmento fisico.
Questo abilita tutti i segmenti di un gruppo ad essere acessibili attraverso
lo stesso registro di segmento.
I segmenti NULL, _DATA, CONST, _BSS, c-common e STACK sono
raggruppati insieme in un gruppo data chiamato DGROUP che viene
indirizzato utilizzando i registri DS o SS.
Questo abilita il compilatore a generare un codice per accedere ai dati in ogniuno di questi senza dover leggere di volta in volta il valore del segmento.
Guardatevi la seguente tabella riportata dal manuale del compilatore Microsoft in cui vengono indicati i segmenti, i gruppi e le classi per i modelli standard di memoria.
-----------------------------------------------------------------
Modello Nome Tipo di Classe di Nome Gruppo
memoria segmento allineamento combin. classe
-----------------------------------------------------------------
Small _TEXT byte public CODE
Data Seg.(a) para private FAR_DATA
Data Seg.(b) para public FAR_BSS
NULL para public BEGDATA DGROUP
_DATA word public DATA DGROUP
CONST word public CONST DGROUP
_BSS word public BSS DGROUP
STACK para stack STACK DGROUP
Medium mod_TEXT byte public CODE
.
.
Data Seg.(a) para private FAR_DATA
Data Seg.(b) para public FAR_BSS
NULL para public BEGDATA DGROUP
_DATA word public DATA DGROUP
CONST word public CONST DGROUP
_BSS word public BSS DGROUP
STACK para stack STACK DGROUP
Large mod_TEXT byte public CODE
.
.
Data Seg.(c) para private FAR_DATA
Data Seg.(d) para public FAR_BBS
NULL para public BEGDATA DGROUP
_DATA word public DATA DGROUP
CONST word public CONST DGROUP
_BSS word public BSS DGROUP
STACK para stack STACK DGROUP
(a) Segmento per i dati far inizializzati
(b) Segmento per i dati far non inizializzati
(c) Segmento per i dati globali e statici inizializzati
(d) Segmento per i dati globali e statici non inizializzati
Utilizzando con il linguaggio C delle routine scritte in assembler dobbiamo prestare attenzione al salvataggio in ingresso dei registri BP, SI e DI.
Dopo aver fatto questo si deve settare anche il registro BP con il contenuto del SP.
Lo stesso discorso vale per i registri SS, CS e DS.
Quando il modulo assembler, durante la sua esecuzione, modifica il contenuto di questi registri e necessario salvarli inizialmente e
ripristinarli in uscita dalla routine.
Non mi sembra il caso, vista la gia' consistente lunghezza di questo
testo, di estendere la trattazione al linguaggio assembler e quindi il significato di quanto riportato e' esclusivamente per coloro che possiedono gia' una conoscenza almeno concettuale di questo.
D'altra parte mi sembra inutile il discorso in quanto se non si possiede una conoscenza minima sull'argomento che permetta di scrivere almeno 4 righe in linguaggio assemblativo diventa impensabile scrivere programmi misti linguaggio C - assembler.
In ogni caso una routine classica di salvataggio dei registri da utilizzare in ingresso nel modulo assembler e' il seguente.
ingresso:
push bp
mov bp,sp
push di
push si
Nella fase di estrazione in uscita ricordatevi che questa deve essere contraria all'ordine seguito in ingresso.
uscita:
pop si
pop di
mov sp,bp
pop bp
ret
Come una funzione in linguaggio C anche un modulo in assembler,
mediante in registri AX e DX, puo' ritornare un valore alla funzione chiamante.
Per convenzione vale la seguente tabella :
Tipo valore ritornato Registri
-----------------------------------------------
char AX
short AX
int AX
unsigned char AX
unsigned short AX
unsigned int AX
long DX - parte alta
AX - parte bassa
unsigned long come long
struct o union AX - indirizzo
float o double AX - indirizzo
near pointer AX
far pointer DX - segmento
AX - offset
Le funzioni chiamate dal linguaggio assembler devono essere precedute dal carattere underscore (_).
Supponendo di voler chiamare dal modulo in assembler una funzione
della parte di programma scritto in C chiamata stampa(), dovremo fare riferimento, dopo opportuna dichiarazione extern nel seguente modo :
EXTRN _stampa
call _stampa
La stessa regola della dichiarazione come extern vale per le chiamate da linguaggio C a funzioni del modulo assembler.
La funzione chiamata dovra' essere
extern funz();
Anche il nome della funzione assembler deve cominciare con _ .
Vedremo degli esempi di utilizzo piu' avanti.
Interrupts hardware
Gli interrupts permettono alle varie periferiche di comunicare al processore il verificarsi di un determinato evento e quindi di avere la sua attenzione.
La battitura di un tasto sulla tastiera, ad esempio, fa' si che questa emetta una richiesta di interrupt sulla linea di controllo.
L'integrato 8259 riceverebbe la richiesta e dopo opportuna codifica la passerebbe al processore 8088/86.
Questo integrato accetta dal bus di controllo fino a 8 diverse richieste di interruzione provvenienti dai vari dispositivi che compongono il sistema.
8259 Codice Dispositivo
---------------------------------------------
IRQ0 08H Timer
IRQ1 09H Tastiera
IRQ2 0AH Scheda grafica
IRQ3 0BH RS_232 (COM1)
IRQ4 0CH RS_232 (COM2)
IRQ5 0DH Disco fisso
IRQ6 0EH Dischi
IRQ7 0FH Stampante
I servizi di interrupts di cui tra breve parleremo sono gia' relativi alle routine del sistema operativo ovvero alle funzioni
che servono a sodisfare le varie richieste di interrupt software.
Per ora ancora due parole relative al controllore di interruzioni.
Analizzando una delle strutture definita in dos.h, riportata precedentemente quando parlavamo delle funzioni per la gestione degli interrupts da linguaggio C, si puo' notare che uno dei membri di questa era sotto il nome di cflag.
Parlando del processore e dei vari registri presenti in questo avevo omesso la descrizione relativa al registro dei flags.
Il nome flag sta' per indicatore o bandierina.
Da questo risulta facile comprendere il compito svolto da questi.
Parlando della struttura dei processori della prima generazione avevamo detto a riguardo dello stack che una delle implementazioni fatte dai progettisti allo scopo di eliminare l'inconveniente della perdita' sul fondo dei dati era stata quella di inserire un flag di stack.
Questo, come dicevamo, indicava lo stato dello stack .
I flags infatti segnalano degli eventi collaterali frutto delle operazioni del processore.
Vediamo l'elenco dei flags nell'apposito registro.
11 10 9 8 7 6 4 2 0
+---+---+---+---+---+---+---+---+---+
:OF :DF :IF :TF :SF :ZF :AF :PF :CF :
+---+---+---+---+---+---+---+---+---+
Come noterete non ho riportato tutti i bit dei 16 presenti nel registro in quanto alcuni non sono utilizzati.
OF - Flag di overflow
Nel caso che si esegua un operazione aritmetica e che si abbia un riporto che invade o supera il bit di segno, quello piu' a sinistra, questo flag viene messo a 1.
DF - Flag di direzione
Quando in memoria si eseguono operazioni su stringhe e' possibile
utilizzare i registri DI e SI come indici.
Nel caso che questo flag sia a 0 i suddetti registri vengono incrementati mentre, al contrario, se a 1 vengono decrementati.
* IF - Flag di interruzione
* TF - Flag di trappola
Per questi due flags vedi la descrizione nella prossima pagina.
SF - Flag di segno
Dopo un operazione aritmetica il valore a 0 del flag indica un risultato positivo mentre uno a 1 lo indica negativo.
ZF - Flag di zero
Viene utilizzato nelle operazioni di confronto.
Un valore 0 indica un risultato diverso da 0 mentre 1 lo indica uguale a 0.
AF - Flag di riporto aux.
Viene settato ad 1 se un calcolo ha un riporto dal bit 3 di un operazione di un byte su di un registro.
PF - Flag di parita'
Viene settato a 0 se il controllo sugli 8 bit dei dati e' dispari mentre a 1 se e' pari.
Non ha nulla a che vedere con il controllo di parita'. Sinceramente non saprei indicarne un uso.
CF - Flag di riporto
Contine il riporto dal bit di ordine piu' alto (quello piu' a sinistra).
Ad esempio se un operazione ha avuto un risultato negativo il flag di segno lo indichera'.
Anche per quanto riguarda le interruzioni esistono dei flags che si interessano di queste.
Nell'elenco precedente le ho contrassegnate mediante un asterisco.
Il flag di trappola ha un significato particolare in quanto se settato permette l'esecuzione passo a passo del programma.
Il processore in questo caso, dopo l'esecuzione di ogni singola istruzione, segnalera' un interruzione di tipo 1.
Simile tecnica e' utilizzata dall'opzione trace del DEBUG.
Questa, come molti sapranno, permette di seguire lo svolgimento di un programma visualizzando ad ogni istruzione il contenuto dei vari registri, in pratica l'operazione definita di trace.
Il flag IF invece ha il compito di abilitare o di disabilitare gli interrupts.
Quando questo viene messo a 0 tutti gli interrupts sono disabilitati mentre se a 1 sono tutti abilitati.
Alcune interruzioni vengono definite di tipo mascherabile in quanto e' possibile disabilitarle mediante l'apposito settaggio di valore sulla porta
21H .
Questa operazione puo' essere eseguita su ogni singolo tipo di interruzione hardware visto precedentemente (non su l'interrupt non
mascherabile 02H).
Il registro di interruzione mascherabile ha il seguente schema :
+----+----+----+----+----+----+----+----+
Bit : 7 : 6 : 5 : 4 : 3 : 2 : 1 : 0 :
+----+----+----+----+----+----+----+----+
IRQ7 IRQ6 IRQ5 IRQ4 IRQ3 IRQ2 IRQ1 IRQ0
Ogni singolo bit se a 0 disabilita l'interrupt corrispondente mentre se a 1 lo abilita.
Il settaggio di un valore 0x20 all'interno della porta 0x20 indica che l'interrupt e' terminato.
Allo scopo di evitare la sovrapposizione di due chiamate di interrupt l'integrato 8259 assegna una priorita' a ciascuno di questi in modo che, nel caso che due giungano conteporaneamente, quella con priorita' maggiore viene eseguita per prima mentre la seconda viene accodata in attesa che la prima finisca.
Prima di proseguire voglio dire alcune parole sul chip di timer che risulta importantissimo per tutte le operazioni di temporizzazione del sistema.
8253 Timer Chip
In questo capitolo parleremo degli interrupts legati agli eventi del timer e della programmazione del chip 8253.
Iniziamo a vedere i primi anche se poi l'argomento sara' ripreso con i programmi residenti in memoria.
L' interrupt 08H (Clock Tick) e' molto importante per tutti i programmi che pretendono delle particolari temporizzazioni.
L'interrupt generalmente richiama a ogni tick di clock, dopo aver eseguito l'update del time of day, un altro interrupt e precisamente l'int 1CH.
Questo normalmente punta, se il programmatore non ha effettuato modifiche, a un istruzione di IRET.
Possiamo sfruttare questo per settare come routine di servizio delle nostre funzioni particolari che devono essere eseguite in continuazione in modo regolare.
Bisogna prestare molta attenzione in quanto, essendo l'interrupt richiamato a tempi fissi, si potrebbe verificare, nel caso di una routine di servizio troppo lunga, che la successiva call avvenga quando la prima non e' ancora completata.
L'interrupt 1CH costituisce un metodo per processi real-time.
L'interrupt viene richiamato 18.2 volte al secondo anche se con opportuna programmazione del chip del timer e' possibile modificare questo parametro.
Le porte di sistema da 40H a 43H si interessano della programmazione del chip di timer (8253).
Questo chip dispone internamente di tre timer indipendenti.
Il canale 0 del timer viene utilizzato per l'orologio di sistema tramite l'interrupt 08H di cui abbiamo appena parlato.
Il canale 1 temporizza i cicli di rinfresco della memoria mentre il canale 2 e' utilizzato per l'altoparlante.
Le porte che si interessano ai precedenti canali sono
40H Canale 0
41H Canale 1
42H Canale 2
Per poter programmare una delle precedenti porte si ha a disposizione la porta 43H che si interessa di indirizzare il canale, l'operazione da eseguire su questo e la modalita'.
Bit Descrizione
------------------------------------------
7-6 00 = Operazione su canale 0
01 = Operazione su canale 1
10 = Operazione su canale 2
5-4 00 = Blocca contatore
01 = Legge o scrive MSB
10 = Legge o scrive LSB
11 = Legge o scrive LSB e MSB
3-1 *000 = Decremento contatore con
inibizione opzionale
(uscita a livelli)
*001 = Decremento contatore con
ripartenza opzionale
(uscita a livelli)
010 = Usato per DMA su canale 1
011 = Genera un onda quadra
per canali 0 e 2
100 = Decremento contatore con
inibizione opzionale
(uscita a impulsi)
101 = Decremento contatore con
ripartenza opzionale
(uscita a impulsi)
0 Decremento contatore
0 = Binario
1 = BCD
(*) Inibizione e ripartenza mediante ricaricamento contatore.
I valori MSB e LSB sono la parte alta e la parte bassa del valore del divisore che deve essere settato in un canale che puo' variare da 0 a 65535.
Una schematizzazione e' la seguente
Canali 0 +-------+ +---------+
+------: Latch :-------: Counter :---> Timer Interrupt
: +-------+ +---------+
:
+----+---+ 1 +-------+ +---------+
:8253:----------: Latch :-------: Counter :---> Refresh RAM
+----+---+ +-------+ +---------+
:
: 2 +-------+ +---------+
+------: Latch :-------: Counter :---> Speaker Interface
+-------+ +---------+
Supponiamo, per esempio, di voler cambiare il valore di 18.2 a 1000.
La velocita' di input del clock 8253 e' di 1.193.180 Hz.
Per poter ottenere il divisore relativo a si divide questa frequenza per il numero di oscillazioni desiderate ovvero 1.193.180/1000 = 1193.
Il programma sara' il seguente.
mov al,00110110b ; valore di settaggio porta 43H
; 00 = canale 0
; 11 = scrive LSB e poi MSB
; 011 = genera onda quadra
; 0 = binario
out 43H,al ; setta la porta 43H
;
mov ax,1193 ; valore del divisore
; AH = parte alta e AL = parte bassa
out 40H,al ; LSB canale 0
out 40H,ah ; MSB canale 0
Spero che il discorso relativo alla programmazione del timer sia stato sufficentemente chiaro.
Ritornando all' interrupt 1CH si puo', allo scopo di chiarire le idee, riportare uno schema in cui risulta il "tragitto" fatto dallo stesso.
Vector Table
: +--------------+ Routine
: +----------->:- : INT 1CH
: / :- :
: / :- :
: / :- :
:/ :- :
:\ +---<:- IRET :
: \ : +--------------+
: \ :
: \ :
: \ : +--------------+ Time of Day
: +---\-----:--->:- : routine
: / \ : :- :
: / +--:---<:- INT 1CH :
:/ +--->:- MOV AL,20H :
:\ :- OUT 20H,AL :
: \ :- IRET :
: \ INT 08H +--------------+
Indietro - Continua...
Nuova pagina 1
|
|