Il debug e la lista comandi

[Lezione 1] - Il debug e lista comandi (sistemi a 16 e 32 bit)

17/03/2017


Il debug è un software messo a disposizione dalle prime versioni del sistema operativo MS-DOS. In nome deriva dalla parola BUG che in inglese significa insetto ma che in informatica rappresenta un errore del software (provate a leggere la storia del Mark I di Hardvard). Questa utility ci da la possibilità di eseguire un programma una istruzione alla volta in modo da arrivare a capire quale istruzione provoca l'errore. La tecnica in questione è detta anche debugging (collaudo).

Avviare e uscire dal debug è molto semplice. Per avviarlo basterà entrare in una shell del DOS, digitare il nome del software da avviare ("debug") e premere ENTER. Per uscire basterà invece premere il comando -Q e premere ENTER (il trattino rappresenta il prompt del debug). Oltre che utilizzzare il debug per visualizzare i registri e fare piccoli esperimenti con il linguaggio assembly è possibile agganciare un eseguibile per poterlo eseguire istruzione per istruzione oppure eseguirlo fino ad un break-point (solitamente rappresentato dall'istruzione assembly INT 03H). Per poter far questo basterà digitare "debug ".

Il debug si occupa del caricamento in memoria dell'eseguibile (come il LOADER del sistema operativo) e dopo aver preparato i puntatori (registri CS:IP) sulla prima istruzione e (registri SS:SP) sulla prima locazione dello stack cede il controllo all'utente. Sarà quindi mostrato all'utente il codice dissassemblato e risulterà quindi facile osservare il contenuto, per cercare di capire i trucchi utilizzati dal programmatore.
Il primo comando che andiamo ad esplorare è l'help che visualizzerà tutti i comandi che il debug mette a disposizione.



Comando ? - Help
Il comando ? mostra l'elenco dei comandi messi a disposizione.
-?
Assembla     A [indirizzo]
Confronta    C intervallo indirizzo
Dump         D [intervallo]
Immetti      E indirizzo [elenco]
Riempi       F intervallo elenco
Vai          G [=indirizzo] [indirizzi]
Esadecimale  H valore1 valore2
Input        I porta
Carica       L [indirizzo] [unità] [primo settore] [numero]
Muovi        M intervallo indirizzo
Nome         N [nome percorso] [elenco argomenti]
Output       O porta byte
Procedi      P [=indirizzo] [numero]
Esci         Q
Registro     R [registro]
Cerca        S intervallo elenco
Traccia      T [=indirizzo] [valore]
Disassembla  U [intervallo]
Scrivi       W [indirizzo] [unità] [primo settore] [numero]
Alloca memoria espansa              XA [n. pagine]
Rilascia memoria espansa            XD [handle]
Mapping pagine di memoria espansa   XM [pagLog] [pagFis] [handle]
Visualizza stato memoria espansa    XS



Comando Q - Quit - sintassi: Q
Il comando per uscire da debug è Q (Quit, uscita). Il controllo viene ceduto al sistema operativo DOS.



Comando H - Hexarithmetic - sintassi: H
Il comando H (Hexarithmetic, aritmetica esadecimale) calcola la somma e la differenza dei due numeri esadecimali forniti; entrambi non possono essere più grandi di 4 cifre. I risultati negativi sono espressi in complemento a 2.
-H 3 2
0005 0001



Comando R - Register - sintassi: R [registro]
Il comando R (Register, registri) mostra il contenuto dei registri (lunghezza 16-bit) del processore ma se viene seguito dal nome di un registro allora sarà possibile modificarlo.
-r
AX=0000 BX=0000 CX=0000 DX=0000 SP=FFEE BP=0000 SI=0000 DI=0000
DS=3570 ES=3570 SS=3570 CS=3570 IP=0100 NV UP EI PL NZ NA PO NC
3570:0100 E483      IN AL,83
-r AX
AX 0000
:3AA
-r
AX=03AA BX=0000 CX=0000 DX=0000 SP=FFEE BP=0000 SI=0000 DI=0000
DS=3570 ES=3570 SS=3570 CS=3570 IP=0100 NV UP EI PL NZ NA PO NC
3570:0100 E483      IN AL,83



Comando E - Enter - sintassi: E [[indirizzo]:offset]
Il comando E (Enter, inserimento) è utile per esaminare e modificare il contenuto di una o più locazioni di memoria a partire dall'indirizzo indicato (segmento:offset); dopo di esso possono essere scritti uno o più valori esadecimali, separati da spazi o da virgole.
L'esempio seguente mostra come inserire un'istruzione in codice macchina (01D8 che corrisponde a ADD AX,BX in assembly) in una locazione della memoria:
-e 100
3756:0100    EE.01 D2.D8
L'esempio seguente mostra come modificare una locazione della memoria video:
-d B800:A0
B800:00A0 43 07 3A 07 5C 07 3E 07-64 07 65 07 62 07 75 07 C.:.\.>.d.e.b.u
B800:00B0 67 07 20 07 20 07 20 07-20 07 20 07 20 07 20 07 g . . . . . . .
B800:00C0 20 07 20 07 20 07 20 07-20 07 20 07 20 07 20 07 . . . . . . . .
B800:00D0 20 07 20 07 20 07 20 07-20 07 20 07 20 07 20 07 . . . . . . . .
B800:00E0 20 07 20 07 20 07 20 07-20 07 20 07 20 07 20 07 . . . . . . . .
B800:00F0 20 07 20 07 20 07 20 07-20 07 20 07 20 07 20 07 . . . . . . . .
B800:0100 20 07 20 07 20 07 20 07-20 07 20 07 20 07 20 07 . . . . . . . .
B800:0110 20 07 20 07 20 07 20 07-20 07 20 07 20 07 20 07 . . . . . . . .
-e B800:BE 'A' 0E 42 0E 43 0E 44 0E
Il comando E può essere dato anche senza digitare di seguito la sequenza di bytes. Questo metodo consente di modificare il contenuto della ram in modo più sicuro del precedente. Infatti digitando E e confermando con ENTER viene mostrato l'indirizzo specificato seguito dal suo contento originale in esadecimale e da un punto. Se si digita un valore esadecimale esso andrà a sostituire quello visualizzato a sinistra. A questo punto possiamo seguire una delle due modalità possibili:
- se si preme il tasto Invio il processo di modifica viene confermato e riappare il prompt del debug (il trattino)
- se invece si preme lo spazio appare il contenuto della locazione successiva seguito ancora da un punto e il processo può continuare con le stesse modalità; i nuovi valori digitati andranno a sostituire i vecchi non appena si deciderà di dare la conferma finale (sempre con la pressione del tasto ENTER)



Comando T - Trace - sintassi: T [=indirizzo-da-eseguire o numero-istruzioni]
Il comando T (Trace, traccia) esegue una (esecuzione passo-passo) o più istruzioni; di solito viene usato senza parametri, al fine di eseguire l'istruzione corrente (cioè quella puntata da CS:IP); subito dopo visualizza il contenuto dei registri e le informazioni relative all'istruzione che verrà eseguita al prossimo step (indirizzo logico, il codice macchina e la corrispondente istruzione assembly).
Questo comando consente l'uso di 2 parametri:
- il primo consente di specificare l'indirizzo della prima istruzione da eseguire (esempio: T = 0100). IN questo modo non dovranno essere prevalorizzati i registri CS (se sullo stesso segmento) e IP che puntano all'istruzione che sarà eseguita.
- il secondo parametro consente di eseguire di seguito più istruzioni, semplicemente indicandone il numero dopo il comando T (esempio: T 8). Il rischio è di entrare dentro le procedure e perdere il filo del programma, specialmente se si entra in una INT di sistema.



Comando G - Go - sintassi: G [indirizzo-di-destinazione]
Il comando G (Go till, vai fino) esegue tutto il programma se non viene seguito da nessun paramentro a partire dall'indirizzo CS:IP, altrimenti, se seguito da un indirizzo ([segmento:]offset), esegue il programma fino all'indirizzo specificato.
Di solito il comando G viene utilizzato in questo modo: si annota l'indirizzo fino al quale l'esecuzione del programma deve arrivare (come fosse un breakpoint) e poi si continua il debugging con il comando T (esecuzione passo-passo) o P.



Comando P - Proceed - sintassi: P [=indirizzo-da-eseguire o numero-istruzioni]
Il comando P (Proceed, procedi) esegue un'intera procedura, di solito rappresentata dall'istruzione CALL ??? o INT xyH: a) ??? è un sottoprogramma sviluppato con il codice eseguibile sotto test, normalmente allocato nel medesimo segmento di codice dell'istruzione CALL; b) INT rappresenta la chiamata ad una procedura di sistema, sempre esterna al segmento del nostro codice.



Comando N - Name - sintassi:N [nome-percorso-file]
E' possibile caricare un file anche dopo aver effettuato l'accesso al debug digitando i comandi (N [nome file] e L):



Comando L - Load - sintassi: L [[CS]:offset] [id-disco numero-primo-settore numero-settori]
Il comando L (Load, carica file) carica in memoria un file di qualunque tipo; di solito viene dato senza parametri, dopo aver specificato il nome (con percorso su disco) mediante il comando N, già visto in precedenza. Il file viene sistemato in memoria a partire dall'indirizzo CS:0100H (ad esclusione dei file EXE che vengono rilocati all'indirizzo indicato nel suo header).
-n prova.com
-l
-r
AX=0000 BX=0000 CX=0407 DX=0000 SP=FFFE BP=0000 SI=0000 DI=0000
DS=168C ES=168C SS=168C CS=168C IP=0100 NV UP EI PL NZ NA PO NC
168C:0100 E92301                JMP 0226



Comando D - Dump - sintassi: D [[segm:]offSet]
Cominciamo con il comando -D (dump). Scriviamo quindi -debug e poi il comando -d

-d
166C:0100 4D 00 00 0E 00 FE BF 81-00 8B 36 92 DE 8B 44 FE M.........6...D.
166C:0110 54 4D 50 3D 43 3A 5C 57-49 4E 44 4F 34 00 5B 16 TMP=C:\WINDO4.[.
166C:0120 45 4D 50 00 54 45 4D 50-3D 43 3A 5C 57 49 4E 44 EMP.TEMP=C:\WIND
166C:0130 4F 57 53 5C 54 45 4D 50-00 50 52 4F 4D 50 54 3D OWS\TEMP.PROMPT=
166C:0140 24 70 24 67 00 77 69 6E-62 6F 6F 74 64 69 72 3D $p$g.winbootdir=
166C:0150 43 3A 5C 57 49 4E 44 4F-57 53 00 50 41 54 48 3D C:\WINDOWS.PATH=
166C:0160 43 3A 5C 57 49 4E 44 4F-57 53 3B 43 3A 5C 57 49 C:\WINDOWS;C:\WI
166C:0170 4E 44 4F 57 53 5C 43 4F-4D 4D 41 4E 44 00 43 4F NDOWS\COMMAND.CO
La ripetizione del comando D visualizza la successiva sequenza di 128 bytes.

Analizziamo la videata ottenuta: sono 8 righe così strutturate: - parte sinistra - mostra l'indirizzo segmentato e offset (registri CS:IP) della prima locazione di memoria che andremo ad ispezionare (espresso in esadecimale). - parte centrale - elenca il contenuto di 16 locazioni (byte) consecutive (espresse in esadecimale), a partire da quella indirizzata nella parte a sinistra. - parte destra - traduce in formato ASCII il contenuto delle stesse locazioni visualizzate nella parte centrale. I caratteri ASCII stampabili sono mostrati in chiaro per quello che sono, mentre tutti gli altri sono visualizzati con un punto (da 00H a 1FH: caratteri di controllo - da 80H a FFH: caratteri ascii estesi).

Se volessimo visualizzare il contenuto della RAM Video per vedere da quali valori è costituita basterà puntare all'indirizzo B800:0000. Prima di fare questo, usciamo da debug con il comando Q, digitiamo il comando DOS CLS (pulisce lo schermo), riavviamo debug ed infine digitiamo il comando D B800:0000 e ridiamo il solo comando D.
-d B800:0000
B800:0000 20 07 20 07 20 07 20 07-20 07 20 07 20 07 20 07  . . . . . . . .
B800:0010 20 07 20 07 20 07 20 07-20 07 20 07 20 07 20 07  . . . . . . . .
B800:0020 20 07 20 07 20 07 20 07-20 07 20 07 20 07 20 07  . . . . . . . .
B800:0030 20 07 20 07 20 07 20 07-20 07 20 07 20 07 20 07  . . . . . . . .
B800:0040 20 07 20 07 20 07 20 07-20 07 20 07 20 07 20 07  . . . . . . . .
B800:0050 20 07 20 07 20 07 20 07-20 07 20 07 20 07 20 07  . . . . . . . .
B800:0060 20 07 20 07 20 07 20 07-20 07 20 07 20 07 20 07  . . . . . . . .
B800:0070 20 07 20 07 20 07 20 07-20 07 20 07 20 07 20 07  . . . . . . . .
-d
B800:0080 20 07 20 07 20 07 20 07-20 07 20 07 20 07 20 07  . . . . . . . .
B800:0090 20 07 20 07 20 07 20 07-20 07 20 07 20 07 20 07  . . . . . . . .
B800:00A0 43 07 3A 07 5C 07 3E 07-64 07 65 07 62 07 75 07 C.:.\.>.d.e.b.u.
B800:00B0 67 07 20 07 20 07 20 07-20 07 20 07 20 07 20 07 g. . . . . . . .
B800:00C0 20 07 20 07 20 07 20 07-20 07 20 07 20 07 20 07  . . . . . . . .
B800:00D0 20 07 20 07 20 07 20 07-20 07 20 07 20 07 20 07  . . . . . . . .
B800:00E0 20 07 20 07 20 07 20 07-20 07 20 07 20 07 20 07  . . . . . . . .
B800:00F0 20 07 20 07 20 07 20 07-20 07 20 07 20 07 20 07  . . . . . . . .
Dal risultato costatiamo che la memoria porzione visualizzata di memoria video è effettivamente costituita da bytes alternati di carattere (ASCII) e colore (attributo).

La prima sequenza di 8 righe (da B800:0000 a B800:0070) ci dice che le prime 128 locazioni contengono 64 coppie consecutive di bytes 20.07 (notazione esadecimale dove 20h in ASCII corrisponde al carattere spazio mentre 07h rappresenta il colore bianco su sfondo nero).
All'indirizzo B800:00A0 inizia la sequenza di caratteri che rappresenta quanto visualizzato "c:\>debug".

Se volessimo invece visualizzare la parte di RAM dedicata alla data del BIOS dovremmo posizionarci alla locazione di memoria FFFF:0000.



Comando F - Fill - sintassi: F
Il comando F (Fill, riempi) nella prima sintassi si aspetta l'intervallo di indirizzi iniziale e finale (espresso come segmento:offset - 100h e 17Fh) e la sequenza di valori da inserire (in questo caso il valore '\0'). Quest'ultimo valore può essere valorizzato come singoli byte, sequenze di byte (es. "-f 100 10F 41,42,43,44,45,46"), caratteri ASCII (es. "-f 110 11F '*'") o stringhe ASCII di qualunque lunghezza (es. "-f 120 12F 'Ciao'").
-d
170C:0100 BA 42 86 E9 65 FE BF 81-00 8B 36 92 DE 8B 44 FE .B..e.....6...D.
170C:0110 BE C6 DB 8B 74 09 03 C6-50 E8 0D FA 34 00 5B 16 ....t...P...4.[.
170C:0120 03 F1 2B C6 8B C8 E8 7B-F4 83 F9 7F 72 0B B9 7E ..+....{....r..~
170C:0130 00 F3 A4 B0 0D AA 47 EB-08 AC AA 3C 0D 74 02 EB ......G....<.t..
170C:0140 F8 8B CF 81 E9 82 00 26-88 0E 80 00 C3 8B 1E 92 .......&........
170C:0150 DE BE 1A D4 BA FF FF B8-00 AE CD 2F 3C 00 C3 A0 .........../<...
170C:0160 DB E2 0A C0 74 09 56 57-E8 2A 21 5F 5E 73 0A B9 ....t.VW.*!_^s..
170C:0170 04 01 FC 56 57 F3 A4 5F-5E C3 50 56 33 C9 33 DB ...VW.._^.PV3.3.
-f 100 17F 00
-d 100
170C:0100 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................
170C:0110 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................
170C:0120 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................
170C:0130 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................
170C:0140 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................
170C:0150 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................
170C:0160 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................
170C:0170 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................
Il range degli indirizzi da coinvolgere può essere passato anche sotto forma di indirizzo iniziale e lunghezza (espressa con una "L" (lenght) seguita dal valore esadecimale corrispondente al numero di locazioni da riempire; scrivere "lC" se si desiderano sovrascrivere 12 byte - es. "-f 120 lC 'Hello Nimius'")



Comando C - Compare - sintassi: C
Il comando è utile per confrontare tra di loro due aree di memoria della stessa grandezza. I parametri da passare al comando sono i seguenti: indirizzo iniziale e finale relativo alla prima area di memoria (segmento:offset) e l'indirizzo iniziale della seconda area di memoria (segmento:offset). Se i byte contenuti in queste due aree di memoria corrispondono non verrà mostrato alcun valore.
Confrontiamo le seguenti aree di memoria: area-memoria-1: 170C:0100 a 170C:010A - area-memoria-2: 170C:0200 a 170C:020A.
-c 170C:0100 170C:010A 170C:0110
170C:0100 41 2A 170C:0110
170C:0101 42 2A 170C:0111
170C:0102 43 2A 170C:0112
170C:0103 44 2A 170C:0113
170C:0104 45 2A 170C:0114
170C:0105 46 2A 170C:0115
170C:0106 41 2A 170C:0116
170C:0107 42 2A 170C:0117
170C:0108 43 2A 170C:0118
170C:0109 44 2A 170C:0119
170C:010A 45 2A 170C:011A



Comando M - Move - sintassi: M
Il comando M (Move, muovi) è utile per copiare una area di memoria in un'altra area. I parametri da passare al comando sono i seguenti: indirizzo iniziale e finale (segmento:offset) dell'area in cui sono contenuti i bytes da copiare e quello iniziale dell'area in cui si desidera copiarli.
Quindi più che di uno spostamento si tratta di una copia, dato che i bytes coinvolti saranno ancora presenti nelle locazioni sorgente. Vediamo un esempio:
-m 170C:0110 11F 170C:0160



Comando S - Search- sintassi: S
Il comando S (Search, ricerca) è utile per cercare una sequenza di bytes in memoria. Questo comando viene spesso utilizzato per cercare tutti i punti di un programma in cui viene, per esempio, chiamata una certa procedura di sistema (ricordiamo che le istruzioni sono codificate con uno o più bytes).
Così, per esempio, può essere utile sapere in quali punti di un dato programma abbiamo l'istruzione INT 21H (interrupt del DOS) corrispondente alla coppia di byte: CD21 (linguaggio macchina corrispondente). Basta quindi utilizzare questo comando seguito dall'indirizzo iniziale e finale (espressi come segmento:offset) e dalla sequenza di bytes da cercare.
L'immagine seguente mostra il risultato della ricerca in una zona di memoria in cui è stato caricato il programma nome-programma.exe che usa questa istruzione 4 volte:
debug nome-programma.exe
-u 100 l13
170E:0100 B405 MOV AH,05
170E:0102 B250 MOV DL,50
170E:0104 CD21 INT 21
170E:0106 B255 MOV DL,55
170E:0108 CD21 INT 21
170E:010A B250 MOV DL,50
170E:010C CD21 INT 21
170E:010E B25F MOV DL,5F
170E:0110 CD21 INT 21
170E:0112 C3 RET
-s 170E:0100 170E:0112 cd,21
170E:0104
170E:0108
170E:010C
170E:0110
Per cercare una string scriveremo: -s 0:0 LFFFF 'Hello'



Comando W - Write - sintassi: W [[CS]:offset] [id-disco numero-primo-settore numero-settori]
Il comando W (Write, scrivi) permette di scrivere/sovrascrivere un file su disco; di solito viene dato dopo aver specificato il nome (con percorso) del file con il comando N, già visto in precedenza. La sintassi in questo caso è la seguente: W [[CS:]offset] - se viene specificato solo l'offset, i dati da scrivere si ritengono appartenenti al segmento CS.
Prima di utilizzare il comando W bisogna impostare i registri BX e CX che, insieme, conterranno la lunghezza in byte del programma da salvare. BX conterrà la parte alta del valore e CX la parte bassa.
-a
1679:0100 mov ah,02
1679:0102 mov dl,2A
1679:0105 int 21
1679:0107 int 20
-r bx
BX 0000
:0
-r cx
CX 0000
:8
-n prova.com
-w
Scrittura di 00008 byte in corso



Comando U - Unassemble - sintassi: U
Il comando U (Unassemble, disassembla) ha senso solo dopo aver specificato il nome di un file eseguibile (con il comando N) e averlo caricato in memoria (con il comando L), magari dopo aver pulito la memoria chiamata ad ospitarlo (con il comando F). Se viene dato senza parametri inizia a tradurre i bytes presenti in memoria a partire dall'indirizzo CS:0100H; il segmento di default è ovviamente quello del codice: CS.
E' facile capire la potenza di questo comando: con esso si possono analizzare le istruzioni di qualunque file eseguibile.
Il comando U prova comunque a tradurre in assembly i bytes che trova; se per qualche ragione il programma contiene tabelle, dati o messaggi il risultato sarà del tutto illogico, cioè verranno mostrate sequenze di istruzioni assurde e scorrelate. Bisogna quindi saper localizzare le aree dati di un programma, escludendole dall'indagine con U a beneficio di quella effettuata con il comando D.



Comando A - Assemble - Crea un programma eseguibile - sintassi: A [[CS]:offset]
Il comando A (Assemble, assembla) permette la creazione di programmi eseguibili riconoscendo e segnalando eventuali gli errori. Il vantaggio nell'utilizzare questo comando si ha dal fatto che invece di scrivere il codice macchina scriveremo attraverso il linguaggio assembly:
-A 100
3970:0100 INT 21
3970:0102 INT 20
3970:0104
-
Una volta finito l'inserimento basterà premere il tasto Enter per tornare al prompt del debug.



Comando I - Input da una porta - sintassi: I
Il comando I (INPUT, ingresso) esegue la lettura della porta specificata dal parametro e mostra, sulla riga successiva, il valore esadecimale letto.
Lettura registro di stato della porta parallela LPT1 e LPT2:
-i 379
DE
-i 279
00



Comando O - Ouput - sintassi: O
Il comando O (Ouput, uscita) spedisce un byte alla porta specificata dal primo parametro; il byte da trasmettere è passato come secondo parametro.
Vediamo un esempio di scrittura nel registro di uscita della porta parallela LPT1:
-o 378 ff