¿Cómo funciona un depurador de C/C++? (Parte I)

Todos aquellos programadores que hayan programado en C o C++ conocen los típicos depuradores de código disponibles para el código máquina generado por el compilador de dichos lenguajes.

Dependiendo del sistema operativo estaremos acostumbrados a gdb y sus amigos (ddd, kgdb, etc, etc), LLDB, WinDbg, OllyDbg, etc. No vamos a entrar en cual de estos depuradores es mejor sino que vamos a hacer un pequeño recorrido por su funcionamiento interno.

Para entender el funcionamiento de un depurador, primero debemos entender el funcionamiento del procesador y de la memoria.

La CPU y los registros de propósito general


Un registro es, de forma simplificada, un pequeño almacén en la CPU y es, evidentemente, la forma más rápida que tiene la CPU de acceder a datos. En la arquitectura x86 de Intel existen ocho registros de propósito general: EAX, EDX, ECX, ESI, EDI, EBP, ESP y EBX (cambiar E por R en arquitecturas de 64 bits).

Con la aparición de x86_64 (o amd64) se extendió su número hasta dieciséis registros, añadiéndose a los ya citados la siguiente lista: R8, R9, R10, R11, R12, R13, R14, R15. También se añadieron ocho registros de 128 bits de tipo XMM (SEE) pero eso va más allá de las pretensiones de este artículo.

Vamos a suponer que tenemos una arquitectura de 32 bits para simplificar un poco el asunto, a todos los efectos, y salvando algunas diferencias, para 64 bits es exactamente igual. Cada uno de los ocho registros de propósito general está diseñado para un uso específico.

El registro EAX (RAX)


El registro EAX (RAX en arquitectura de 64 bits) o también conocido como acumulador cumple el propósito de realizar cálculos así como almacenar los valores devueltos por las llamadas a funciones.

Muchas instrucciones optimizadas en el juego de instrucciones de x86 han sido diseñadas para mover información hacia y desde este registro y realizar cálculos con esa información. Operaciones como la adición, reducción, y la comparación están optimizadas para utilizar el registro EAX.

Algunas operaciones más especializadas como la multiplicación o la división solo pueden tener lugar en el registro EAX. Como este registro es también utilizado para devolver el valor de retorno de una función, es fácil saber si una función ha fallado o no y cual es su valor, esto es de gran importancia a la hora de depurar una aplicación.

El registro EDX (RDX)


El registro EDX (RDX) es el registro de datos y es básicamente una extensión del registro EAX al que ayuda a almacenar información extra para cálculos más complejos. Puede ser usado también para almacenamiento general pero es más comúnmente utilizado en conjunción con EAX.

El registro ECX (RCX)


El registro ECX (RCX) es conocido como el registro de conteo y es comúnmente usado para realizar operaciones de bucle. Es importante entender que el conteo dentro del registro ECX es hacia abajo y no hacia arriba. Por ejemplo este típico bucle en C:
while(i < 100)
{
     ...
}

Trasladado a código ensamblador, el registro ECX contendría un valor inicial de 100 en la primera iteración e iría reduciéndose en uno en cada iteración por lo que en la segunda iteración su valor sería de 99 y no al contrario.

Los registros ESI (XSI) y EDI (RDI)


Los bucles que procesan datos se apoyan en los registros ESI y EDI para una manipulación eficiente de los mismos. El registro ESI es también conocido como índice de origen para las operaciones de datos y almacena la dirección inicial del flujo de entrada (comúnmente cadenas).

El registro EDI apunta a la dirección donde se almacena el resultado de una operación de datos o al indice de destino. Una forma sencilla de recordar esto es que ESI se usa para leer y EDI para escribir.

Al usar los registros de índice de origen y destino se mejora el rendimiento de la aplicación en ejecución.

Los registros ESP (RSP) y EBP (RBP)


Estos dos registros son el puntero de pila y el puntero base respectivamente. Básicamente son utilizados para administrar llamadas a funciones y operaciones en la pila. Cuando una función es invocada, los argumentos de la misma se introducen en la pila seguidos por la dirección de retorno.

El registro ESP apunta a la parte superior de la pila y por lo tanto a la dirección de retorno. El registro EBP apunta a la parte inferior de la pila. Algunos compiladores pueden usar optimizaciones para eliminar el registro EBP como puntero de marco de pila y poder ser usado como cualquier otro registro de propósito general.

El registro EBX (RBX)


Este es el único registro que no ha sido diseñado para nada en específico. Puede ser usado como almacenamiento extra.

Registro especial EIP (RIP)


Este registro es el puntero de instrucción y apunta a la dirección de memoria de la instrucción que está siendo ejecutada por el procesador. EIP es actualizado para reflejar la dirección donde la ejecución de la aplicación está teniendo lugar.

Conclusión


Un depurador debe ser capaz de leer y modificar el contenido de estos registros. El kernel de cada sistema operativo provee de una interfaz para que el depurador interactúe con el procesador y lea o escriba de sus registros (y de la memoria) para conducir la ejecución de la aplicación.

En el siguiente artículo hablaremos sobre la pila.

Más información | Listado de instrucciones del procesador x86

Portada de Genbeta