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.
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.))
gdb ejec
gdb
solo, y luego file ejec
gdb --args ejec temperature -i c facil.bmp
para llamar un binario con argumentos, por ejemplo acá llamo al binario ejec con varios argumentos.att
al imprimir lineas desensambladas.
Como usamos gdb-dashboard, que imprime el assembly de la línea actual arriba de todo, nos gustaría que ese assembly use sintaxis de intel.
Para configurar esto podemos usar el comando set disassembly-flavor intel
al comienzo de nuestra sesión.b asm.asm:20
// Agrega breakpoint en linea 20 de asm.asm (a veces anda medio raro)$b c.c:30
// Agrega breakpoint en linea 30 de c.cb holaMundo.debug
// Agrega breakpoint donde está .debug (el que me funciona mejor para asm)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…
p $RXX
(o print, imprimir valor de un registro, variable, expresión)
p (char*) $RXX
imprime “Susan” por ejemplo, si $RXX es el comienzo de una stringp *(struct_t*) $RXX
imprime todos los campos de un struct de manera ordenada, si $RXX es un puntero a dicho struct (vemos que desreferencié el puntero). Sumamente util para debuggear movimientos por estructuras.p var
imprime su valor. gdb ya sabe su tipo dado que c es fuertemente tipado.print $xmm#
permite imprimir registros XMM, pero hacerlo así tira todas las posibles interpretaciones (float, int, etc.) de los datos del registro (ver ejemplo de output del comando mas adelante). Si sabemos que los datos en xmm0 son 4 floats, por ejemplo, nos conviene llamar directamente print $xmm0.v4_float
(los nombres de los formatos aparecen al hacer print $xmm#). Listo los nombres de formatos:
x/1tb dir
(examinar 1B de memoria y expresar como entero en binario. El 1 representa cantidad de unidad, b indica byte y t indica binario. Me resulta útil para leer máscaras que declaré en .rodata o .data antes de bajarlas a registros, por ejemplo. referencia: ftp://ftp. gnu.org/old-gnu/Manuals/gdb/html_chapter/gdb_9.html#SEC56). Algunas formas útiles:
x/4wf dir
lee 4 floats a partir de la dir (w = words(DE 4 BYTES, nuestra doubleword), f = interpreta como floats)x/2gf dir
lee 2 doubles a partir de la dir (g = giant word (de 8 bytes, neustra quadword))x/1dw dir
lee 1 int de 32 bits CON SIGNO (d = signed decimal)x/1uw dir
lee 1 int de 32 bits SIN SIGNO (u = unsigned decimal)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
}
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):
info gdt
: muestra la GDT cargada actualmente
info gdt
– Muestra los descriptores presentesinfo gdt all
– Muestra todos los descriptoresinfo gdt [idx]
– Muestra el i-ésimo descriptorinfo idt
: Muestra la IDT cargada actualmente
info idt
– Muestra los descriptores presentesinfo idt all
– Muestra todos los descriptoresinfo idt [idx]
– Muestra el i-ésimo descriptorinfo page
: Muestra información de paginación
info page
– Muestra un resumen de la configuración de paginacióninfo page directory
– Muestra las entradas presentes en el page directoryinfo page table [idx]
– Muestra las entradas presentes en la page table dadainfo page [addr]
– Muestra información de paginación para la dirección virtual dadaCuando 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.
r
(run). También se puede hacer make
desde dentro de gdb y volver a correr el ejecutable con r
, y se cargará el binario nuevo. Útil para mantener los breakpoints preestablecidos luego de una corrección chica.Fuente: https://www.cse.unsw.edu.au/~learn/debugging/modules/gdb_conditional_breakpoints/
b
(o break
) por si solo solo crea un breakpoint en la línea actual.b asm.asm:27
crea un breakpoint en la línea 27 del archivo asm.asm (podría ser en un archivo de c también).b foo
crea un breakpoint al comienzo de la función foob foo.etiqueta_relativa
si usamos etiquetas con un . adelante en asm, son lo que llamo “etiquetas relativas” a la función actual (última etiqueta sin . adelante). Entonces al poner el breakpoint en gdb debemos especificar el nombre de la función y luego el nombre de la etiqueta relativa.b <lugar> if <expresion>
donde lugar se especifica de cualquiera de las maneras mencionadas arriba, y la expresión puede ser, por ejemplo $rdi > 5 o cualquier expresión booleana, en principio (se puede hacer un p <expresion>
antes de setear el breakpoint para revisar que ande bien).info breakpoints
para ver los breakpoints seteados y sus números correspondientes. Usamos los números de los breakpoints para borrarlos o ponerles condiciones:delete 5
borra el breakpoint con número 5delete
borra todos los breakpointscondition 7 $rdi > 5
agrega una condición al breakpoint número 7, que ahora frenará sólo si el valor de $rdi es mayor a 5condition 7
borra las condiciones del breakpoint 7, es decir frena siempre que el código pase por ahí.