[Rubrica Programmazione]


coordinamento di Francesco Sileno





L'acchiappa scarafaggi!

L'idea originale era quella di un programma in grado di aiutare ad effettuare il debug low level dei nostri programmi (o di quelli di altri, per i più perfidi). Ma il DEBUG del DOS può essere utile anche per altri scopi...

di Francesco Sileno


In principio era il byte

Ad esempio possiamo usarlo per generare piccoli eseguibili con estensione .COM, programmando direttamente in assembly. I .COM hanno due principali limitazioni rispetto ai .EXE:
  • l'offset della prima istruzione deve essere obbligatoriamente 100h
  • il codice e i dati statici devono essere entrambi contenuti in un segmento, ossia in 64Kb
Ma per iniziare va benissimo anche così.

Come si usa il debug? Bene, avviatelo, e vi ritroverete al suo prompt ('-'). Dando il comando '?', avrete la lista di comandi disponibili e una breve spiegazione per ciascuno di essi (spiegazione assolutamente idiota, se avete il DOS tradotto in italiano).

I comandi che per ora ci interessano sono, pressapoco:

R [reg]
Register. Mostra lo stato attuale dei registri della CPU, e l'istruzione che sarà processata al prossimo passo. Se è specificato un registro particolare come parametro, ne permette la ridefinizione.

A [xxxx]
Assembla. Eventualmente a partire dall'offset xxxx, usando il CS attuale. Se non è specificato l'offset, viene preso il registro IP come riferimento.

U [xxxx] [yyyy]
Disassembla, come sopra. La prima volta che viene dato il comando, il registro IP o il parametro viene usato come offset di partenza. Le volte successive riprende da dove aveva interrotto prima, se non è usato nessun parametro. Il secondo parametro indica l'offset a cui fermarsi.

E [xxxx]
Enter, modifica il valore di un singolo byte, a partire dall'offset xxxx. I valori vengono immessi in esadecimale.

D [xxxx]
Display. Mostra l'hexdump della memoria.

G [xxxx]
Go, esegue il programma. Se viene specificato un indirizzo, questo avrà la funzione di breakpoint.

T [x]
Trace. Esegue un istruzione alla volta, oppure X istruzioni alla volta. In caso di chiamate a INT, o di CALL a procedure, con questa istruzione si entra nel corpo della procedura chiamata.

P [x]
Proceed. Idem come sopra, solo che in caso di INT o CALL, non si entra nel dettaglio, ma la si esegue come singola istruzione.

N file
Name. Assegna un nome di file, che poi potrà essere letto o scritto.

W [xxxx]
Write. Salva un area di memoria su file, a partire dall'offset xxxx. In CX si specifica la lunghezza della porzione di memoria da salvare.

L [xxxx]
Load. carica un area di memoria da file, a partire dall'offset xxxx.

Q
Quit. Avvia il processo di terminazione del segmento codice dove risiede il debugger, rilascia le aree di memoria ad esso collegate, e restituisce il controllo al kernel del DOS. Beh, credevate fosse semplice quittare un programma?

Salve, mondo!

Detto questo passiamo ad un semplice programmino che stampa la solita, banale, ripugnante frase: "Hello World!".

Diamo il comando 'A 100' (il debug lavora SOLO con numeri esadecimali).

-a100
24D6:0100 _
Il debug è in attesa dell'istruzione. Immettiamo 'PUSH CS'.
-a100
3678:0100 push cs
3678:0101 _
automaticamente l'offset viene incrementato all'indirizzo della prossima istruzione. Scrivete l'intero programma:
-a100
3678:0100 push cs
3678:0101 pop ds
3678:0102 mov ah,9
3678:0104 mov dx,10e
3678:0107 int 21
3678:0109 mov ax,4c00
3678:010C int 21
3678:010E _
Come vedete abbiamo già fatto uso di 2 chiamate ad un INT. L'INT 21, che raccoglie in sè la maggior parte delle funzioni messe a disposizione dal DOS. Il registro AH è usato per specificare quale funzione si intende usare. La funzione 09h è per stampare stringhe su schermo, vuole che DS:DX punti all' inizio della stringa, che deve terminare col carattere '$'. Sento che il solito curiosone chiede come facciamo a stampare il carattere '$'... beh, non me lo ricordo! Ma non c'è da preoccuparsi, quando s'inizierà a scrivere direttamente in memoria video questi problemi da DOS non si avranno più. La funzione 4Ch è per terminare il programma, ogni programma DOS DEVE avere questa chiamata alla fine. Il parametro AL serve per l'ERRORLEVEL, spesso usato nei batch.

Ora dobbiamo inserire la stringa, come vedete ho calcolato l'offset a 10Eh. Il comando E permette di inserire valori esadecimali, per cui dovremo convertire la nostra stringa in una sequenza di codici ASCII esadecimali.

H  e  l  l  o     w  o  r  l  d  !  $
48 65 6C 6C 6F 20 77 6F 72 6C 64 21 24

-e 10e
3678:010E  00.48   00.65
3678:0110  00.6c   00.6c   00.4f   00.20   00.77   00.6f   00.72   00.6c
3678:0118  00.64   00.21   00.24
- _
Notate che per modificare il byte successivo, dovete premere SPAZIO, mentre all'ultimo byte terminate l'inserimento con ENTER. Adesso salviamo, prima di inchiodare il PC.
-nhello.com
-rcx
CX 0000
:200
-w 100
Scrittura di 00200 byte in corso
- _
Per non tagliare fuori pezzi di codice o dati, mettiamo in CX un valore abbastanza alto. Oppure facciamo un paio di semplici calcoli per sapere il valore esatto (ultimobyte - 100h + 1, in questo caso dovrebbero essere 22h byte).

Uscite dal debug, e lanciate HELLO.COM. Il risultato dovrebbe essere, inchiodamenti a parte, qualcosa molto simile (uguale, oserei dire) a

C:\DEP>hello
Hello world!
C:\DEP> _
Bene, avete fatto il vostro primo programma in assembly! Facile no?

Modifichiamo.

Adesso supponiamo che vogliate modificare la stringa in modo che l'output su schermo sia
C:\DEP>prova
Hello world!

I'm Cthulhu, the beast.

C:\DEP> _
dove ovviamente al posto di 'Cthulhu' potete mettere il vostro nome, a seconda di quanto vi sentiate animaleschi in questo momento. Come fare? Semplice:
C:\DEP>debug prova.com
-d10e
3C01:0100                                            48 65                 He
3C01:0110  6C 6C 6F 20 77 6F 72 6C-64 21 24 36 34 00 C9 3B   llo world!$64..;
3C01:0120  AA 00 74 03 E9 62 FD E9-62 FD 8B 1E 5F AE B8 00   ..t..b..b..._...
3C01:0130  44 CD 21 80 E2 80 88 16-61 AE 74 0D 80 3E BD AE   D.!.....a.t..>..
3C01:0140  00 74 06 BA EB 9F E9 49-04 8B 1E 5F AE 8B 0E 88   .t.....I..._....
3C01:0150  AA 8B 16 8C AA 2B CA 75-0E E8 F4 02 80 3E C0 AE   .....+.u.....>..
3C01:0160  00 75 65 8B 0E 88 AA 1E-8E 1E 69 AA B4 3F CD 21   .ue.......i..?.!
3C01:0170  1F 72 8F 8B C8 E3 51 80-3E 61 AE 00 75 07 80 3E   .r....Q.>a..u..>
3C01:0180  C1 AE 00 74 1B 8B D1 8B-3E 8C AA B0 1A 06         ...t....>.....
- _
Sapendo che per andare a capo in DOS sono necessari 2 caratteri, CR e LF, rispettivamente 0Dh e 0Ah, quello che dobbiamo fare noi è aggiungere a partire dall'offset 11Ah la nuova parte di stringa.

NOTA: l'uso dei due caratteri per andare a capo è stato deciso per mantenere la compatibilità con le normali macchine da scrivere meccaniche, che hanno una leva per incrementare la riga (LF), e il carrello da spostare per tornarne all'inizio (CR). I primi prototipi di PC infatti non avevano il tasto ENTER, ma un meccanismo molto simile alla coppia carrello-leva delle macchine da scrivere. Poi Bill Gates ha suggerito l'uso di un singolo tasto, in uno dei suoi rari momenti di lucidità. Più tardi fu sempre lui a suggerire la creazione di un tasto di reset, per evitare di dover spegnere e riaccendere la macchina durante l'uso di Windows... ma questa è un altra storia.

Dopo aver effettuato le modifiche, il dump dovrà apparire così:

-d10e
3C01:0100                                            48 65                 He
3C01:0110  6C 6C 6F 20 77 6F 72 6C-64 21 0D 0A 0D 0A 49 27   llo world!....I'
3C01:0120  6D 20 43 74 68 75 6C 68-75 2C 20 74 68 65 20 62   m Cthulhu, the b
3C01:0130  65 61 73 74 2E 0D 0A 24-61 AE 74 0D 80 3E BD AE   east...$a.t..>..
3C01:0140  00 74 06 BA EB 9F E9 49-04 8B 1E 5F AE 8B 0E 88   .t.....I..._....
3C01:0150  AA 8B 16 8C AA 2B CA 75-0E E8 F4 02 80 3E C0 AE   .....+.u.....>..
3C01:0160  00 75 65 8B 0E 88 AA 1E-8E 1E 69 AA B4 3F CD 21   .ue.......i..?.!
3C01:0170  1F 72 8F 8B C8 E3 51 80-3E 61 AE 00 75 07 80 3E   .r....Q.>a..u..>
3C01:0180  C1 AE 00 74 1B 8B D1 8B-3E 8C AA B0 1A 06         ...t....>.....
- _
Salvate di nuovo, con lo stesso procedimento di poco fa, e lanciate.

In alternativa potete anche usare degli hex editor, come il DiskEdit o l'HackerView, per effettuare la modifica della stringa, facendo attenzione a non sovrascrivere il codice.


Per non impazzire

Per quanto questo metodo faccia sentire tanto dei pionieri, e ci aiuti a capire cosa succede ad un livello molto basso, ha dei grossi limiti. Anzitutto, gli offset per i riferimenti a dati o a locazioni di salto non possono essere determinati se non una volta scritto TUTTO il programma, a meno di non avere uno schema dei codici operativi delle istruzioni assembly e una gran DOSe di masochismo. A questo limite si ovvia lasciando tutti i riferimenti a valori casuali (magari FFFF per riconoscerli facilmente dopo), provvedendo poi ad aggiornarli al valore corretto una volta inserito tutto il codice.

Ma il fatto peggiore è la correzione dei bug: la modifica di una sola istruzione può portare alla riscrittura di TUTTE le istruzioni seguenti, e magari allo spostamento di aree dati. Es:

3BDA:0100 B409          MOV     AH,09
3BDA:0102 B200          MOV     DL,00
3BDA:0104 CD21          INT     21
3BDA:0106 B8004C        MOV     AX,4C00
3BDA:0109 CD21          INT     21
Supponiamo che all'offset 102h avremmo dovuto scrivere MOV DX,200. Correggendo, viene fuori
3BDA:0100 B409          MOV     AH,09
3BDA:0102 BA0002        MOV     DX,200
3BDA:0105 21B8004C      AND     [BX+SI+4C00],DI
3BDA:0109 CD21          INT     21
e quindi dobbiamo riscrivere tutto quello che segue.

Si può rimediare usando la redirezione del DOS e dei template batch, ovvero creando un file che contiene tutti i comandi per il debug, e mandarglielo in ingresso. Per il nostro esempio, dovremmo creare un file prova.dbg così fatto:

a100
push cs
pop ds
mov ax,9
mov dx,ffff
int 21
mov ax,4c00
int 21
%
n prova.com
rcx
200
w100
q
%
(con il carattere % ho indicato una linea vuota che DEVE esserci)

Dando il comando TYPE PROVA.DBG | DEBUG, otterremo il file PROVA.COM. Adesso dobbiamo ancora rimetterci mano per aggiornare il riferimento alla stringa e per inserire quest'ultima.


I compilatori

Il passo successivo consiste nel passare ad un compilatore assembly, magari lo avete già sperduto nei 40Mb del C++, oppure ripiegare su un compilatore share/freeware, e ce ne sono parecchi in giro. Con uno di questi compilatori potrete usare riferimenti simbolici sia per le locazioni di salto che per le variabili.

Quello che segue è lo scheletro che uso io per iniziare un nuovo programma:

    .MODEL SMALL
    .STACK 2048

    .DATA

    .CODE
    ASSUME CS:@CODE,DS:@DATA

    MOV     AX,@DATA
    MOV     DS,AX

;	[ ... ]	

FINE:
    MOV     AX,4C00H
    INT     21H

    END
Vediamo un po' cosa significano alcune di queste buffe parole:
MODEL SMALL
specifica il modello di memoria da utilizzare. I modelli sono sempre quelli che avete imparato a conoscere con il C.

STACK 2048
imposta la dimensione dello stack, inizializzando automaticamente SS:SP.

DATA
definisce un segmento come area riservata ai dati. In questo modo separiamo i dati dal codice.

CODE
definisce il segmento che conterrà il codice.

ASSUME CS:@CODE,DS:@DATA
fa capire al compilatore quali segmento deve usare per i dati e quale per il codice

MOV AX,@DATA
MOV DS,AX
inizializza il segmento DS con il segmento effettivo dei dati. La direttiva ASSUME da sola non basta.

FINE:
esempio di etichetta di salto. In tutto il codice sarà possibile inserire istruzioni del tipo JMP FINE
Bene, quindi il nostro programma di saluto al mondo, riscritto in questa nuova veste, sarà così:
    .MODEL SMALL
    .STACK 2048

    .DATA
frase   db 'Hallo World!',0Dh,0Ah,0Dh,0Ah,'I'm Cthulhu, the beast.'0Dh,'$'

    .CODE
    ASSUME CS:@CODE,DS:@DATA

    MOV     AX,@DATA
    MOV     DS,AX

    mov     ah,9
    mov     dx,offset frase
    int     21h

FINE:
    MOV     AX,4C00H
    INT     21H

    END
DB sta per Define Byte, inizializza una sequenza di byte e fa si che l'etichetta FRASE contenga l'offset del primo di essi.
Vedete che ho usato la direttiva OFFSET ad un certo punto? Ricordate che quando usavate il debug dovevate inserirlo a mano DOPO aver scritto tutto? Ora è il compilatore che se lo calcola! Notate anche che ogni numero espresso in esadecimale deve essere seguito da un H, in quanto il compilatore assume per default la notazione decimale.

Per creare il programma, dovrete prima compilare (*ASM), poi eseguire il link (*LINK), ed avrete il vostro .EXE.


Evolution...

Adesso, per rimanere nella scia dell'incredibile fantasia che ci precede, facciamo un programma che ci chiede un nome e poi ci ricama qualche considerazione sopra. Per leggere una stringa da tastiera, usiamo la funzione DOS 0Ah, che prevede in ingresso un buffer particolare. Il primo byte di questo buffer indica la lunghezza massima di se stesso (quindi max 255 caratteri, in pratica 254), il secondo è usato per dirci quanti caratteri sono stati effettivamente letti, escludendo il carattere ENTER (che però è incluso nel buffer).

Guardate un po' se vi piace:

    .MODEL SMALL
    .STACK 2048

    .DATA
domanda db 'Ciao, quale sarebbe il tuo nome?',0Dh,0Ah,'> $'
nome    db 255, 0, 254 DUP (00)
ricamo1 db 0Dh,0Ah,'Così ti chiami $'
ricamo2 db '... bah, pensavo meglio.',0Dh,0Ah,'$'

    .CODE
    ASSUME CS:@CODE,DS:@DATA

    MOV     AX,@DATA
    MOV     DS,AX

    mov     ah,9
    mov     dx,offset domanda
    int     21h

    mov     ax,0A00h
    mov     dx,offset nome
    int     21h

    mov     bx,0			; nota1
    mov     bl,[nome+1]
    mov     [nome+bx+2],'$'

    mov     ah,9
    mov     dx,offset ricamo1
    int     21h

    mov     ah,9			; nota2
    mov     dx,offset nome
    inc     dx
    inc     dx
    int     21h

    mov     ah,9
    mov     dx,offset ricamo2
    int     21h

FINE:
    MOV     AX,4C00H
    INT     21H

    END
Nota 1: per stampare il nostro nome, usando la funzione DOS, dobbiamo inserire il carattere '$' alla fine del medesimo. Allora ci andiamo a prendere il secondo byte del buffer, che ci dice di quanti caratteri è composto il nostro nome, lo incrementiamo di 2 per tenere in conto i primi 2 byte usati per altri scopi, lo sommiamo all'offset di partenza della stringa, e sbattiamo lì il nostro '$'. Ah, quelle strane mov si spiegano col fatto che in realtà l'etichetta NOME è un puntatore, e le parentesi quadre hanno lo stesso effetto di '*' in C, o del '^' in PASCAL.

Nota 2: Per stampare il nome, dobbiamo partire DOPO i 2 byte che servono ad altro, quindi incrementiamo un paio di volte l'offset di partenza.

Il risultato è questo (mi auguro):

C:\DEP>prova.exe
Ciao, quale sarebbe il tuo nome?
> GianEzechiele
Così ti chiami GianEzechiele... bah, pensavo meglio.

C:\DEP> _
Beh, anche per questa volta è tutto, vi lascio col vostro stupefacente programma, immaginando già le cose sconce che gli farete dire tra breve...
                                                    assemblatamente,
                                                          Cthulhu

Copyright © 1996 Beta Working Group. Tutti i diritti riservati.