[successivo] [precedente] [inizio] [fine] [indice generale] [indice ridotto] [indice analitico] [home] [volume] [parte]


Capitolo 569.   Programmi completamente autonomi

A volte esiste la necessità di realizzare un programma funzionante in modo autonomo, ovvero stand alone, senza il sostegno del sistema operativo. Un lavoro di questo tipo richiede lo studio delle stesse problematiche che riguardano inizialmente la costruzione di un nuovo sistema operativo, ma di norma è meglio fermarsi alla produzione di un programma singolo.(1)

C'è da osservare che l'avvio di un programma «autonomo» in un elaboratore x86 può essere di una complessità mostruosa, a causa di problematiche ereditate dall'architettura originale del microprocessore 8086. La prima difficoltà che si incontra a tale proposito sta nel far sì che il microprocessore si metta a lavorare in «modalità protetta», ovvero in una condizione che consenta di utilizzare in modo ragionevole la memoria centrale. Nel tempo, questa e altre questioni sono diventate di competenza dei programmi che si occupano di avviare un sistema operativo, come è il caso di GRUB 1 e di LILO, che così predispongono un contesto più confortevole al programma o al kernel da avviare successivamente.

In questo capitolo si mostrano esempi che utilizzano anche codice in linguaggio C, linguaggio che viene descritto nella parte lxxxix.

569.1   Le specifiche «multiboot»

Le specifiche multiboot sono definite dal documento Multiboot specification, disponibile presso <http://www.gnu.org/software/grub/manual/multiboot/>. Si tratta della definizione di un'interfaccia tra sistema di avvio e sistema operativo (o programma autonomo), inizialmente per un'architettura x86. Il documento citato contiene sia le specifiche, sia un esempio completo di programma che interagisce con il sistema di avvio secondo le specifiche stesse. Qui si riassumono i concetti principali.

569.1.1   Formato del file che deve essere avviato

Il file-immagine che contiene il programma da avviare (programma che potrebbe essere il kernel di un sistema operativo), deve contenere un'intestazione particolare, definita multiboot header, costituita in pratica da un'impronta di riconoscimento e da una serie di dati. Attraverso questa intestazione, il sistema di avvio è almeno in grado di riconoscere il file-immagine come qualcosa che deve essere avviato effettivamente e di recepirne le caratteristiche.

Questa intestazione deve trovarsi nella parte iniziale del file-immagine da caricare ed eseguire, ma non è necessario che sia esattamente all'inizio dello stesso, essendo sufficiente che sia contenuta completamente entro i primi 8 Kibyte.

Figura 569.1. La prima parte obbligatoria dell'intestazione.

multiboot header

Il primo campo da 32 bit, definito magic, contiene un'impronta di riconoscimento, costituita precisamente dal numero 1BADB00216. Questa serve al sistema di avvio a individuare la presenza e l'inizio di una tale intestazione. Il secondo campo da 32 bit, definito flags, contiene degli indicatori con i quali si richiede un certo comportamento al sistema di avvio. Il terzo campo da 32 bit, definito checksum, contiene un numero calcolato in modo tale che la somma tra i numeri contenuti nei tre campi da 32 bit porti a ottenere zero, senza considerare i riporti.

I nomi indicati sono quelli definiti dallo standard e, come si vede, il campo checksum si ottiene calcolando -(magic + flags), dove si deve intendere che i calcoli avvengono con valori interi senza segno e si ignorano i riporti.

Se il file-immagine da avviare è informato ELF, le informazioni che il sistema di avvio necessita per piazzarlo correttamente in memoria e per passare il controllo allo stesso, sono già disponibili e non c'è la necessità di occuparsi di altri campi facoltativi che possono seguire i tre già descritti. Stante questa semplificazione, per quanto riguarda il campo flags sono importanti i primi due bit, mentre gli altri vanno lasciati a zero.

Figura 569.2. Il campo flags e il suo utilizzo fondamentale.

multiboot header: flags

Il bit meno significativo del campo flags, se impostato a uno, serve a richiedere il caricamento in memoria dei moduli eventuali (assieme al file-immagine principale) in modo che risultino allineati all'inizio di una «pagina» (ovvero all'inizio di un blocco da 4 Kibyte). Alcuni sistemi operativi hanno la necessità di trovare i moduli allineati in questo modo e in generale l'attivazione di tale bit non può creare danno.

Il secondo bit del campo flags serve a richiedere al sistema di avvio di passare le informazioni disponibili sulla memoria. Queste informazioni vengono rese disponibili a partire da un'area a cui punta inizialmente il registro EBX. In generale si tratta di un'informazione utile (che al massimo può essere ignorata), pertanto conviene attivare anche questo bit.

Figura 569.3. Calcolo del campo checksum.

multiboot header: checksum

569.1.2   Situazione dopo l'avvio del file-immagine

Quando, dopo il trasferimento in memoria del programma, il sistema di avvio passa il controllo allo stesso, la situazione che questo programma si trova è sostanzialmente quella seguente, dove però sono stati omessi molti dettagli importanti:

Di norma, la prima cosa che fa il programma che è stato avviato in questo modo è di azzerare il registro EFLAGS e di predisporre uno spazio per la pila dei dati, posizionando il registro ESP di conseguenza.

569.1.3   Informazioni passate dal sistema di avvio al programma

Il sistema di avvio conforme alle specifiche multiboot offre una serie di informazioni, collocate in una struttura che parte dall'indirizzo indicato nel registro EBX. Questa struttura ha un certo grado di complessità, in quanto può fare riferimento ad altre strutture. Qui viene descritta brevemente solo una prima porzione di questa struttura; per l'approfondimento occorre consultare le specifiche, pubblicate presso <http://www.gnu.org/software/grub/manual/multiboot/>.

Figura 569.4. Inizio della struttura informativa offerta da un sistema di avvio aderente alle specifiche multiboot.

multiboot information

Tabella 569.5. Descrizione dei primi campi della struttura informativa fornita dal sistema di avvio multiboot.

Nome mnemonico del campo bit del campo flags da cui dipende Descrizione
flags
Il primo campo definisce una serie di indicatori, con i quali si dichiara se una certa informazione, successiva, viene fornita ed è valida.
mem_lower
mem_upper
0 Se è attivo il bit meno significativo del campo flags, i campi mem_lower e mem_upper contengono la dimensione della memoria bassa (da zero a un massimo di 640 Kibyte) e della memoria alta (quella che si trova a partire da un mebibyte). La dimensione è da intendersi in kibibyte (simbolo Kibyte) e, per quanto riguarda la memoria alta, viene indicata solo la dimensione continua fino al primo «buco».
boot_device
1 Se è attivo il secondo bit, partendo dal lato meno significativo, il campo boot_device dà informazioni sull'unità di avvio. L'informazione è divisa in quattro byte, come descritto nelle specifiche multiboot.
cmdline
2 Se è attivo il terzo bit, partendo dal lato meno significativo, il campo cmdline contiene l'indirizzo iniziale di una stringa che riproduce la riga di comando passata al kernel.

Come si può intuire leggendo la tabella che descrive i primi cinque campi, il significato dei bit del campo flags viene attribuito, mano a mano che l'aggiornamento delle specifiche prevedono l'espansione della struttura informativa. Per esempio, un campo flags con il valore 1002 sta a significare che esistono i campi fino a cmdline e il contenuto di quelli precedenti non è valido, ma i campi successivi, non esistono affatto. La comprensione di questo concetto dovrebbe rendere un po' più semplice la lettura delle specifiche.

569.2   Esempio di programma da avviare secondo le specifiche «multiboot»

I listati seguenti mostrano il contenuto dei file necessari a produrre un programma da avviare secondo le specifiche multiboot. Per la precisione, il programma non fa alcunché e serve solo come base di partenza per lo sviluppo di qualcosa di più complesso, con l'ausilio del linguaggio C. I file mostrati hanno, nell'ordine di apparizione, i nomi: loader.s, kernel.c, linker.ld e Makefile.

Listato 569.6. File loader.s usato per la prima parte del codice, contenente l'intestazione multiboot e la preparazione dell'ambiente minimo di funzionamento, compresa la collocazione della pila dei dati. Il programma chiama la funzione _kernel presente nel file kernel.c, passando come parametri il codice di riconoscimento del sistema di avvio e il puntatore alle altre informazioni che questo può fornire. Al ritorno dalla chiamata della funzione, il programma tenta di arrestare il microprocessore, ma se non ci riesce si mette in un ciclo senza fine che produce apparentemente lo stesso risultato.

.globl  _loader
.extern _kernel
#
# Dimensione della pila interna al kernel. Qui vengono previsti
# 16384 elementi (0x4000) da 32 bit, pari a 65536 byte.
#
.equ STACK_SIZE,  0x4000
#
# Si inizia subito con il codice che si mescola con i dati.
#
_loader:
    jmp boot    # Salta all'inizio del codice.
    .align 4    # Fa in modo di riempire lo spazio mancante
                # al completamento di un blocco di 4 byte.
#
# Intestazione «multiboot», poco dopo l'inizio del file-immagine.
#
multiboot_header:
    .int 0x1BADB002                     # magic
    .int 0x00000003                     # flags
    .int -(0x1BADB002 + 0x00000003)     # checksum
#
# Inizia il codice di avvio.
#
boot:
    #
    # Regola ESP alla base della pila.
    #
    movl $(stack_max + STACK_SIZE), %esp
    #
    # Azzera gli indicatori (e per questo usa la pila appena sistemata).
    #
    pushl $0
    popf
    #
    # Chiama il kernel scritto in C, passandogli le informazioni
    # ottenute dal sistema di avvio.
    #
    # void _kernel (unsigned int magic, void *multiboot_info)
    #
    pushl %ebx          # Puntatore alla struttura contenente le
                        # informazioni passate dal sistema di avvio.
    pushl %eax          # Codice di riconoscimento del sistema di avvio.
    #
    call _kernel        # Chiama la funzione _kernel()
    #
halt:
    hlt                 # Se il kernel termina, ferma il microprocessore.
    jmp halt            # Se non si è fermato, crea un ciclo senza fine.
#
# Alla fine del programma, viene collocato lo spazio per la pila
# dei dati, senza inizializzarlo. Per scrupolo si allinea ai
# 4 byte (32 bit).
#
.align 4
.comm stack_max, STACK_SIZE
#

Listato 569.7. File kernel.c che potrebbe contenere idealmente il kernel di un piccolo sistema operativo. In questo caso il programma non fa alcunché e ignora anche la presenza di parametri nella chiamata.

void _kernel(void)
{
     ;
}

Listato 569.8. File linker.ld, da usare come script per GNU ld. Si può osservare che la sezione .data viene distanziata da .text e .rodata, in quanto si deve collocare in una pagina di memoria differente, per poter limitare i permessi di accesso in scrittura ai soli dati variabili.

ENTRY (_loader)
SECTIONS {
    . = 0x00100000;
    .text : { *(.text) }
    .rodata : { *(.rodata) }
    .data ALIGN (0x1000) : { *(.data) }
    .bss : {
        _sbss = .;
        *(.bss)
        *(COMMON)
        _ebss = .;
    }
}

Listato 569.9. File Makefile da usare per la compilazione.

all: loader kernel link
#
clean:
        rm *.o
        rm kernel
#
loader:
        as -o loader.o loader.s
#
kernel:
        gcc -Wall -Werror -o kernel.o -c kernel.c \
            -nostdlib -nostartfiles -nodefaultlibs
#
link:
        ld --script=linker.ld -o kernel loader.o kernel.o

Per avviare il programma che si ottiene, si può usare GRUB 1, utilizzando le direttive seguenti nel suo file di configurazione:

...
title  mio kernel
kernel (fd0)/kernel
...

In questo caso si suppone di utilizzare un dischetto per l'avvio e che il file da avviare sia kernel, contenuto proprio nella radice.

569.3   Visualizzazione di messaggi

Quando si scrive un programma autonomo, come descritto sinteticamente nella sezione precedente, occorre considerare che il linguaggio C non può essere usato sfruttando le librerie consuete, pertanto occorre produrre tutto internamente, anche le funzioni per la visualizzazione dei messaggi. I listati seguenti vanno a sostituire il file kernel.c della sezione precedente, allo scopo di visualizzare qualcosa sullo schermo (il contenuto dei primi campi della struttura informativa creata dal sistema di avvio), attraverso delle funzioni elementari definite internamente.

Per visualizzare un messaggio sullo schermo di un elaboratore x86 è necessario scrivere in una porzione di memoria che parte dall'indirizzo B800016 (a partire da 736 Kibyte), utilizzando coppie di byte, dove il primo byte è un codice che descrive i colori da usare per il carattere e il suo sfondo, mentre il secondo contiene il carattere da visualizzare. Nel programma il codice in questione è 0716 che mostra un carattere bianco su sfondo nero. Ciò che si deve osservare è che il programma tratta la coppia di byte come un numero a 16 bit, nel quale il carattere e il suo codice di visualizzazione sembrano invertiti, a causa del fatto che l'architettura è di tipo little endian.

Listato 569.11. File kernel.c che include automaticamente i file display.c e multiboot.c.

#include "display.c"
#include "multiboot.c"
//
//
//
void _kernel(unsigned long magic, type_multiboot_info *info)
{
    clear_screen ();
    //
    print_string ("Salve!\n\0");
    //
    if (magic == 0x2BADB002)
      {
        print_string ("Sono stato avviato attraverso un sistema \0");
        print_string ("di avvio aderente alle specifiche \n\0");
        print_string ("\"multiboot\". Ecco solo alcune informazioni:\n\0");
        multiboot_information (info);
      }
    else
      {
        print_string ("Sono stato avviato attraverso un sistema \0");
        print_string ("di avvio che non e` conforme alle specifiche\n\0");
        print_string ("\"multiboot\".\n\0");
      }
}

Listato 569.12. File display.c, contenente le funzioni necessarie a visualizzare stringhe e numeri in forma di stringa.

static unsigned short *Screen = (unsigned short *) 0xB8000;
//
static const unsigned int Rows = 25, Columns = 80;
static       unsigned int Row = 0, Column = 0;
static       unsigned char Attrib = 0x07;
//
//
//
static unsigned short screen_cell (unsigned char c, unsigned char attrib)
{
    //
    // Assembla i due caratteri in un numero a 16 bit.
    //
    return (short) c | (((short) attrib) * 0x100);
}
//
//
//
static void clear_screen (void)
{
    unsigned int i;
    //
    for (i = 0; i < (Rows * Columns) ; i++)
      {
        //
        // Scrive uno spazio nella posizione.
        //
        *(Screen + i) = screen_cell (0x20, Attrib);
      }
}
//
//
//
static void new_line (void)
{
    int i, j;
    //
    Column = 0;
    Row++;
    //
    if (Row >= Rows)
      {
        //
        // Copia il testo in su.
        //
        for (i = 0; i < (Rows - 1) * Columns ; i++)
          {
            j = i + Columns;
            //
            // Trascrive la cella della riga successiva.
            //
            *(Screen + i) = *(Screen + j);
          }         
        //
        // Mette l'indice di riga nell'ultima posizione.
        //
        Row = Rows - 1;
        //
        // Pulisce la riga alla base dello schermo.
        //
        for (i = ((Rows - 1) * Columns); i < (Rows * Columns) ; i++)
          {
            //
            // Cancella la cella dello schermo.
            //
            *(Screen + i) = screen_cell (0x20, Attrib);
          }         
      }
}
//
//
//
static void print_char (unsigned char c)
{
    //
    // Put the character.
    //
    if (c == '\n' || c == '\r')
      {
        new_line ();
      }
    else
      {
        *(Screen + (Row * Columns + Column)) = screen_cell (c, Attrib);
        //
        // Move cursor.
        //
        Column++;
        if (Column >= Columns)
          {
            new_line ();
          }
      }
}
//
//
//
static void print_string (char *string)
{
    unsigned int i;
    //
    for (i = 0; i < 100000 ; i++)
      {
        if (string[i] != 0)
          {
            print_char (string[i]);
          }
        else
          {
            break;
          }
      }
}
//
//
//
static void reverse_string (char *string)
{
    unsigned int i, j;
    unsigned char c;
    //
    // Scandisce la stringa alla ricerca del valore a zero.
    //
    for (i = 0; string[i] != 0; i++)
      {
        ;
      }
    //
    // L'indice "i" punta alla cella a zero.
    // Viene rimesso l'indice "i" in modo da puntare
    // all'ultimo carattere.
    //
    i--;
    //
    // Si inverte l'ordine delle cifre.
    //
    for (j = 0; j < i; j++, i--)
      {
        c = string[i];
        string[i] = string[j];
        string[j] = c;
      }
    //
}
//
//
//
static void num_to_string (unsigned long num, unsigned int base, char *string)
{
    unsigned int i;
    unsigned char remainder;
    //
    if (num == 0)
      {
        string[0] = '0';
        string[1] = 0;
        return;
      }
    //
    for (i = 0; num != 0; i++)
      {
        remainder = num % base;
        num = num / base;
        //
        if (remainder <= 9)
          {
            string[i] = '0' + remainder;
          }
        else
          {
            string[i] = 'A' + remainder - 10;
          }
      }
    //
    // Aggiunge la terminazione, tenendo conto che l'indice "i"
    // è già posizionato dopo l'ultima cifra inserita.
    //
    string[i] = 0;
    //
    reverse_string (string);
}
//
//
//
static void print_num (unsigned long num, char base)
{
    char string[100];
    //
    if (base == 'x')
      {
        num_to_string (num, 16, string);
        print_string ("0x\0");
        print_string (string);
      }
    else if (base == 'o')
      {
        num_to_string (num, 8, string);
        print_string ("0o\0");
        print_string (string);
      }
    else if (base == 'b')
      {
        num_to_string (num, 2, string);
        print_string ("0b\0");
        print_string (string);
      }
    else
      {
        num_to_string (num, 10, string);
        print_string (string);
      }
}

Listato 569.13. File multiboot.c, contenente la definizione parziale della struttura delle informazioni multiboot e la funzione necessaria a visualizzarne il contenuto.

//
// The multiboot information.
//
typedef struct multiboot_info
{
    unsigned long flags;
    unsigned long mem_lower;
    unsigned long mem_upper;
    unsigned long boot_device;
             char *cmdline;
} type_multiboot_info;
//
//
//
static void multiboot_information (type_multiboot_info *info)
{
    print_string ("flags:       \0");
    print_num (info->flags, 'b');
    print_string ("\n\0");
    //
    if ((info->flags & 1) > 0)
      {
        print_string ("mem_lower:   \0");
        print_num (info->mem_lower, 'x');
        print_string (" \0");
        print_num (info->mem_lower, 'd');
        print_string (" Kibyte\0");
        print_string ("\n\0");
        //
        print_string ("mem_upper:   \0");
        print_num (info->mem_upper, 'x');
        print_string (" \0");
        print_num (info->mem_upper, 'd');
        print_string (" Kibyte\0");
        print_string ("\n\0");
      }
    if ((info->flags & 2) > 0)
      {
        print_string ("boot_device: \0");
        print_num (info->boot_device, 'x');
        print_string ("\n\0");
      }
    if ((info->flags & 4) > 0)
      {
        print_string ("cmdline:     \0");
        print_string (info->cmdline);
        print_string ("\n\0");
      }
}

Utilizzando questo programma si potrebbe visualizzare una schermata simile a quella seguente:

Salve!
Sono stato avviato attraverso un sistema di avvio aderente alle specifiche
"multiboot". Ecco solo alcune informazioni:
flags:       0b11111100111
mem_lower:   0x27F 639 Kibyte
mem_upper:   0x7C00 31744 Kibyte
boot_device: 0xFFFFFF
cmdline:     (fd0)/kernel

569.4   Colori dello schermo

Nella sezione precedente si accenna al fatto che a partire dall'indirizzo di memoria B800016, ciò che si scrive serve a ottenere una rappresentazione sullo schermo. Ogni carattere utilizza due byte, in quanto uno dei due contiene il carattere vero e proprio e l'altro l'attributo che ne definisce il colore. Il byte del colore va usato suddividendo i bit nel modo seguente:

colori

Come si vede, se è attivo il bit più significativo si ottiene un carattere lampeggiante, quindi i tre bit successivi descrivono lo sfondo e i quattro bit meno significativi descrivono invece il colore del carattere (in primo piano). Pertanto, i colori dello sfondo sono in quantità minore rispetto a quelli utilizzabili per il primo piano.

Tabella 569.16. Colore associato al primo piano o allo sfondo.

Codice Sfondo Primo piano
016 nero nero
116 blu blu
216 verde verde
316 ciano (azzurro) ciano (azzurro)
416 rosso rosso
516 magenta (violetto) magenta (violetto)
616 marrone marrone
716 bianco bianco
816 nero con lampeggio grigio scuro
916 blu con lampeggio blu chiaro
A16 verde con lampeggio verde chiaro
B16 ciano con lampeggio ciano chiaro
C16 rosso con lampeggio rosa
D16 magenta con lampeggio magenta chiaro
E16 marrone con lampeggio giallo
F16 bianco con lampeggio bianco luminoso

Per esempio, un colore indicato come 2816 genera un testo di colore grigio scuro su sfondo verde, mentre A016 genera un testo lampeggiante nero su sfondo verde.

569.5   Riferimenti


1) Sono pochi i sistemi operativi affermati, mentre esistono una miriade di progetti più o meno abbandonati; quindi: prima di pensare di scrivere il proprio sistema converrebbe dare un'occhiata a quanti lavori del genere sono stati catalogati.


Appunti di informatica libera 2008 --- Copyright © 2000-2008 Daniele Giacomini -- <appunti2 (ad) gmail·com>


Dovrebbe essere possibile fare riferimento a questa pagina anche con il nome programmi_completamente_autonomi.htm

[successivo] [precedente] [inizio] [fine] [indice generale] [indice ridotto] [indice analitico] [home]

Valid ISO-HTML!

CSS validator!

Gjlg Metamotore e Web Directory