Posiblemente sea el debug el depurador más rudimentario que existe; pero el hecho que desde el principio haya sido provisto con el sistema operativo, nos permite encontrarlo hoy en cualquier máquina DOS o Windows. Muchas tareas elementales pueden realizarse sin otra ayuda que el Debug y por eso vamos a ver algunos comandos básicos. Incluso es posible correr programas cargados en memoria utilizando breakpoints elementales, ejecutar paso a paso, saltar sobre procedimientos, editar programas en hexa y muchas más cosas. Ya hemos dicho cómo podemos arrancarlo desde una ventana DOS, y usando el comando R (mostrar registros) nos mostrará algo similar a esto:
AX=0000 BX=0000 CX=0000 DX=0000 SP=0000 BP=0000 SI=0000 DI=0000
DS=1332 ES=1332 SS=1332 CS=1332 IP=0100 NV UP EI PL NZ NA PO NC
1332:0100 C3 RET .
Esto muestra el contenido de los registros del procesador incluyendo varias banderas: en el ejemplo, y en el mismo orden tenemos: V=0, D=0, I=1, S=0, Z=0, AC=0, P=0 y C=0
Si ponemos después de la R el nombre de un registro, es posible modificar su contenido. Por ejemplo, para editar el contenido de CX, hay que poner el comando RCX. Debug nos presenta el contenido actual del registro y la posibilidad de ingresar un nuevo valor para sustituirlo.
Los comandos L y W se utilizan para leer y escribir en archivos de disco. La cantidad de bytes transferida en cada operación es el contenido de BX:CX. Previamente es necesario darle un nombre al archivo con el comando N. Se puede especificar la dirección a partir de la que se desea transferir datos o bien usar el vector por defecto DS:DX.
Los comandos más útiles y más usados en Debug son:
A |
dirección |
Ensamblar (ingresar código assembly) |
D |
dirección cantidad |
Mostrar en pantalla direcciones de memoria en presentación hexa |
E |
dirección |
Editar memoria desde dirección |
F |
direc1 direc2 valor |
Llenar memoria desde direc1 hasta direc2 con el dato valor |
G |
dirección |
Ir (durante la ejecución) a la dirección dirección |
H |
valor1 valor2 |
Muestra el resultado de la suma y resta hexadecimal entre valor1 valor2 |
I |
puerto |
Obtiene una entrada desde el puerto puerto |
M |
direc1 direc2 direc3 |
Mueve el bloque de memoria direc1- direc2 a partir de direc3 |
P |
cant |
Salta sobre procedimientos cant de veces o hasta dirección direc |
Q |
|
Sale de Debug |
S |
direc1 direc2 valores |
Busca en bloque de memoria desde direc1 hasta direc2 los bytes valores |
T |
cant |
Igual que P pero son instrucciones simples |
U |
direc cant |
Desensambla cant bytes a partir de la dirección direc |
XS |
|
Muestra estado de memoria expandida |
? |
|
Presenta pantalla de ayuda |
Nuestro primer programa
Usaremos el Debug para ensamblar un programa que realice algo tan útil (?) como dejar en alguna parte de la memoria el nombre de nuestra escuela ECCE. Para sacar algo a pantalla, debemos leer el tutorial de +gthorne, que será nuestro paso siguiente. Por ahora sólo queremos practicar de manera que abramos una ventana DOS y escribamos DEBUG (enter). Nos proponemos hacer que ECCE sea escrito en memoria, en el offset 200h de nuestro segmento de datos DS. Sabemos que los códigos ASCII son E=45h y C=43h, de manera que nuestro programa puede lucir así:
a 100
1322:0100 |
mov ax,4543 |
;cargamos el registro AX con el dato 4543 (EC en ASCII) |
1322:0103 |
mov bx,4345 |
;cargamos BX con «CE» en ASCII |
1322:0106 |
mov [200],ax |
;ponemos AX en la dirección de memoria 200 |
1322:0109 |
mov [202],bx |
;idem para BX, pero en la 202 (AX ocupó la 200 y 201) |
1322:010D |
int 20 |
;finalizar y salir a Debug |
1322:010F
Al apretar «enter» una vez más, Debug nos devuelve su prompt «-» y ya estamos listos para nuestro próximo comando. Podemos ver algunas curiosidades del listado anterior: 1) Debug asume que los números que le damos, sean direcciones o datos, son hexadecimales. 2) A medida que vamos ingresando el programa, nos va devolviendo la dirección de almacenamiento de la próxima instrucción que escribiremos. 3) Las tres primeras instrucciones MOV ocuparon de memoria de programa 3 bytes cada una, pero la cuarta ocupó 4 bytes y la INT 20 sólo ocupó 2 bytes. 4) Aunque nada se ha hablado de la INT 20, es lo que por el momento usaremos para terminar el programa . 5) Cuando hacemos referencia al contenido de una posición de memoria, encerramos la dirección entre corchetes []. Es muy importante saber distinguir entre la dirección y el valor almacenado en esa dirección de memoria.
Nuestra lógica es muy simple: cargamos el ASCII «EC» en AX y lo dejamos en la dirección 200. Luego cargamos «CE» en BX y lo dejamos en la 202. Tanto AX como BX han sido meros vehículos para cargar la memoria con datos y sólo a los efectos didácticos porque también está permitido :
MOV word ptr [200],4543 ; cargar la word de memoria 200 directamente con el dato 4543
Esta instrucción ocupa 6 bytes, de modo que no ganamos espacio poniéndola en lugar del más elíptico procedimiento de cargar AX y con éste escribir en 200. El prefijo «word ptr» es para que el procesador sepa que lo que moveremos a 200 es una word y no un byte o double-word.
Veamos cómo se ve nuestro programa usando el comando desensamblar:
-u 100 (desensamble a partir de la CS:100)
(Nótese que Debug listará usando sólo mayúsculas, sin importar cómo escribimos nuestro código)
1322:0100 |
B84345 |
MOV AX,4543 |
1322:0103 |
BB4543 |
MOV BX,4345 |
1322:0106 |
A30002 |
MOV [200],AX |
1322:0109 |
891E0202 |
MOV [202],BX |
1322:010D |
CD20 |
INT 20 |
NOTA: el valor de 1322 (el contenido del registro CS) es válido para la PC donde se escribió este ensayo. Por lo general los valores no coinciden de una a otra PC, salvo que las instalaciones de software sean idénticas y en ambas estén corriendo previamente al DEBUG los mismos programas. El listado es más largo, pero las líneas que siguen hacia abajo son alguna cosa que estaba en memoria, ya que Debug desensambla por defecto los 20h primeros bytes desde la dirección indicada (o desde la que esté apuntando), y en nuestro programa sólo hemos usado 0Fh bytes (15 en decimal). Echémosle un vistazo:
Ajá!!, Debug no deja de sorprendernos, en una columna entre la dirección y el listado en lenguaje assembly puso unos números hexadecimales. Son los códigos de operación (opcodes) que es lo que en definitiva se almacena en memoria y lo que nuestro Pentium debe interpretar y ejecutar. Debug compiló nuestro programa ingresado en assembly y produjo ese código binario con representación hexadecimal para que el Pentium lo interprete.
Antes de correr el fabuloso programa que hemos escrito, tenemos que ver qué hay en la posición de memoria 200. Para ello usamos el comando D 200, que nos muestra la basura que hay en nuestra RAM desde DS:0200 hasta DS:027F. Como deseamos leer claramente nuestro nombre ECCE, vamos a llenar este espacio con ceros usando el comando
– F 200 23F 00
con lo que le indicamos a Debug que debe llenar el bloque de memoria que comienza en 200 y termina en 23F con «00». Para estar seguros, escribamos nuevamente el comando D 200. Debemos ver las cuatro primeras filas del listado con los datos en 00. Estamos listos para correr nuestra maravilla.
Con el comando R nos aseguramos que CS:IP esté apuntando al inicio de nuestro programa (o sea a CS:0100). Para nuestro caso CS vale 1322, pero como ya se ha dicho, puede que en otra PC tenga otro valor. Corramos el programa con el comando G. Debug nos debe informar:
El programa ha finalizado con normalidad.
Bien! todo fue de maravillas. Veamos si nuestras siglas brillan en las posiciones 200 a 203 con el comando D 200
Esperábamos los hexa 45,43,43,45 a partir de la 200 (miremos además en la columna ASCII del Debug, en donde claramente nos dice CEEC) y están al revés. Qué habrá pasado? Será que hemos escrito BX en 200 y AX en 202?. Usemos al Debug para depurar , que para eso Bill Gates lo ha puesto donde está. Repitamos el comando F 200 23F 00 para dejar nuevamente en cero la memoria y ejecutemos nuestro programa paso a paso.
Primero el comando R. Nos debe decir que IP apunta a 0100:
1322:0100 B84345 MOV AX,4543
-T (comando para ejecutar una sola instrucción). Lo relevante es:
AX=4543 e IP=0103
1322:0103 BB4543 MOV BX,4345 es la próxima instrucción. Ejecutemos con T:
1322:0106 A30002 MOV [200],AX
Ejecutemos el comando D 200 para ver qué hay en la memoria: hasta ahora 00 de la dirección 200 a la 203. Todo ok, porque hasta aquí sólo hemos cargado los registros AX y BX. Hagamos otro T.
1322:0109 891E0202 MOV [0202],BX es la próxima instrucción
Hemos guardado AX en la dirección 200 y por lo tanto debería haber un 4543 («EC» en ASCII) en las direcciones 200 y 201. Verifiquemos con el comando D 200:
1322:0200 43 45 00 00 …….. CE…………….
QUE PASO???? Está al revés. Tengo «CE» en lugar de «EC». Mmmmm!! Mr Intel tiene algo que ver con esto: Resulta que lo que leemos en AX como «EC», en la realidad lo debemos asumir como : En AL tengo un 43 («C») y en AH un 45 («E»). Y el procesador hace algo sumamente lógico, a la porción más baja del registro (AL) la almacena en la dirección de memoria más baja (200) y a la porción más alta del registro (AH) la almacena en la dirección de memoria más alta (201). Todo parece bien pero no funciona?
Pero está bien tal como lo hizo Intel. Si leemos la memoria en sentido de direcciones ascendentes, debemos acostumbrarnos a leer los registro (y a cargarlos, ahí fue donde nos equivocamos!) desde la porción más baja hacia la más alta. Por lo tanto, debemos rescribir nuestro programa para que en AL se almacene la primera letra («E») y en AH la segunda («C»), y lo mismo para BX:
a 100
1322:0100 MOV AX,4345
1322:0103 MOV BX,4543
(enter) nuevamente para salir del comando A.
Ahora debemos modificar el registro IP, que nos quedó apuntando a la mitad del programa:
RIP (enter) nuestro comando
IP 0109 respuesta de Debug
:100 (enter) este valor lo ingresamos nosotros para decirle que queremos a IP=0100
Ejecutamos el programa nuevamente con G y examinamos la memoria con D 200 para ver nuestro hermosa sigla ECCE ya en su lugar y en el orden debido.