Machete de GDB

Antes de llamar a GDB

Paso 0: se recomienda FUERTEMENTE utilizar gdb-dashboard. “Instalarlo” no es más que correr el comando

wget -P ~ https://git.io/.gdbinit

desde la terminal. No requiere sudo ni ningún permiso especial, así que se puede instalar en las compus de los labos sin problemas (lo único que hace es descargar un archivo llamado “.gdbinit” y ponerlo en el home).

Una vez que se compiló el programa USANDO LA FLAG -g en nasm (para incluir debug symbols), podemos debuggearlo.

Llamando a GDB

Supongamos que tengo los archivos asm.asm, c.c y el programa compilado (binario), llamemoslo ejec

(Mi archivo asm tiene la función (símbolo global) holaMundo:, Y creé dentro de ella la etiqueta .debug: para debuggear parte del código (util para ir rápido en el gdb.))

Configurando breakpoints

Iniciando la ejecución del código

1) run o r iniciará la ejecución del programa (o la reiniciará desde el principio, si ya se está ejecutando) y… 2) Nos llevará al primer breakpoint, donde podemos hacer * info registers // O cualquiera de sus variantes (reg, r,… ver help info) * continue o c * n (next, avanza de a una linea del fuente) * ni (next instruction, avanza una linea del assemble) o si (step instruction) * Imprimir información del contexto de ejecución: registros, memoria, el estado del programa…

Imprimiendo valores de registros/memoria

Ejemplo de salida de print $xmm

$1 = {
  v8_bfloat16 = {[0] = -nan(0x7f), [1] = -nan(0x7f), [2] = -nan(0x7f), [3] = -nan(0x7f), [4] = -nan(0x7f), [5] = -nan(0x7f), [6] = -nan(0x7f), [7] = -nan(0x7f)},
  v8_half = {[0] = -nan(0x3ff), [1] = -nan(0x3ff), [2] = -nan(0x3ff), [3] = -nan(0x3ff), [4] = -nan(0x3ff), [5] = -nan(0x3ff), [6] = -nan(0x3ff), [7] = -nan(0x3ff)},
  v4_float = {[0] = -nan(0x7fffff), [1] = -nan(0x7fffff), [2] = -nan(0x7fffff), [3] = -nan(0x7fffff)},
  v2_double = {[0] = -nan(0xfffffffffffff), [1] = -nan(0xfffffffffffff)},
  v16_int8 = {[0] = -1 <repeats 16 times>},
  v8_int16 = {[0] = -1, [1] = -1, [2] = -1, [3] = -1, [4] = -1, [5] = -1, [6] = -1, [7] = -1},
  v4_int32 = {[0] = -1, [1] = -1, [2] = -1, [3] = -1},
  v2_int64 = {[0] = -1, [1] = -1},
  uint128 = 340282366920938463463374607431768211455
}

Revisando el estado del programa

Proveemos varios comandos extra para imprimir el estado del programa en GDB durante los talleres de system programming (pueden verlos definidos en los archivos orga2.py de los talleres):

Casteos y prints con formato

Cuando tenemos una dirección de memoria (puntero) y conocemos su tipo, podemos castearlo a su tipo al hacer print para mejorar legibilidad. Cuando trabajamos en C las variables ya están tipadas, por lo que si me paro al final de este código:

lista_t* mi_lista = nueva_lista();
head_lista->arreglo = array;
head_lista->longitud = 12;
head_lista->next = NULL;
mi_lista->head = head_lista;
...

puedo hacer lo siguiente en gdb:

>>> p *mi_lista
$1 = {
  head = 0x4066b0
}
>>> p *(mi_lista->head)
$2 = {
  next = 0x0,
  longitud = 12,
  arreglo = 0x4068c0
}

Ahora, supongamos que hice lo mismo en assemblery tengo guardado en RAX el puntero a mi lista. Si no especificamos el tipo del puntero, vemos que asume algún tipo para el puntero (parece ser uint64_t*)

>>> p/x $rax
$3 = 0x406830
>>> p/x *$rax
$4 = 0x4066b0

Si queremos lograr la misma salida de antes, debemos especificar el tipo:

>>> p/x *(lista_t*)$rax
$5 = {
  head = 0x4066b0
}
>>> p/x *((lista_t*)$rax)->head
$6 = {
  next = 0x0,
  longitud = 0xc,
  arreglo = 0x4068c0
}

Si quisieramos imprimir el arreglo guardado en la lista, que se declara de tipo uint32_t, es interesante notar que ni siquiera en C se imprimirá todo el arreglo, ya que gdb no sabe en principio qué largo tiene (a diferencia de los strings (char), los arreglos no tienen carácter de límite al final)

>>> p *(mi_lista->head->arreglo)
$7 = 0

Pero gdb nos permite, a partir de un puntero tipado, especificar cuantos elementos consecutivos queremos imprimir usando la notación @NUM:

>>> p *(uint32_t*) mi_lista->head->arreglo@10
$6 = {[0] = 0, [1] = 1, [2] = 2, [3] = 3, [4] = 4, [5] = 5, [6] = 6, [7] = 7, [8] = 8, [9] = 9}

Esto es muy útil para, por ejemplo, debuggear la pila, que es un array de uint64_t (o de uint32_t en arquitectura de 32 bits). Supongamos el siguiente código y nos paramos al final:

int main (void){
  char* saludo = "Hola";
  return 0;
}

Con gdb podemos verlas últimas N posiciones apiladas del stack, por ejemplo si quisieramos ver las últimas 3 posiciones:

>>> p *((char**) $rsp)@3
$8 = {[0] = 0x1000 <error: Cannot access memory at address 0x1000>, [1] = 0x555555556008 "Hola", [2] = 0x1 <error: Cannot access memory at address 0x1>}

Como lo que se encuentra en el tope de la pila y en la tercer posición no son char* no funcionara desreferenciarlos, pero podemos ver su valor en hexadecimal de todos modos. En el caso de la segunda posición en pila, donde quedo guardada la variable local con la string “Hola”, si se imprimirá su valor.

Terminando o reiniciando la ejecución

Manejo de breakpoints

Fuente: https://www.cse.unsw.edu.au/~learn/debugging/modules/gdb_conditional_breakpoints/