Esta pregunta podría ser un buen punto de partida:¿cómo puedo poner un punto de interrupción en "algo está impreso en la terminal" en gdb?
Entonces, al menos podría interrumpir cada vez que se escribe algo en stdout. El método consiste básicamente en establecer un punto de interrupción en el write
syscall con la condición de que el primer argumento sea 1
(es decir, STDOUT). En los comentarios, también hay una pista sobre cómo podría inspeccionar el parámetro de cadena del write
llamar también.
modo x86 de 32 bits
Se me ocurrió lo siguiente y lo probé con gdb 7.0.1-debian. Parece que funciona bastante bien. $esp + 8
contiene un puntero a la ubicación de memoria de la cadena pasada a write
, así que primero lo conviertes en una integral, luego en un puntero a char
. $esp + 4
contiene el descriptor de archivo para escribir (1 para STDOUT).
$ gdb break write if 1 == *(int*)($esp + 4) && strcmp((char*)*(int*)($esp + 8), "your string") == 0
modo x86 de 64 bits
Si su proceso se ejecuta en modo x86-64, los parámetros se pasan a través de registros temporales %rdi
y %rsi
$ gdb break write if 1 == $rdi && strcmp((char*)($rsi), "your string") == 0
Tenga en cuenta que se elimina un nivel de direccionamiento indirecto ya que estamos usando registros temporales en lugar de variables en la pila.
Variantes
Funciones distintas a strcmp
se puede utilizar en los fragmentos anteriores:
strncmp
es útil si desea hacer coincidir el primern
número de caracteres de la cadena que se está escribiendostrstr
se puede usar para encontrar coincidencias dentro de una cadena, ya que no siempre puede estar seguro de que la cadena que está buscando está al comienzo de cadena que se escribe a través delwrite
función.
Editar: Disfruté esta pregunta y encontrar su respuesta posterior. Decidí hacer una publicación de blog al respecto.
catch
+ strstr
condición
Lo bueno de este método es que no depende de glibc write
siendo utilizado:rastrea la llamada al sistema real.
Además, es más resistente a printf()
almacenamiento en búfer, ya que incluso podría capturar cadenas que se imprimen en múltiples printf()
llamadas.
versión x86_64:
define stdout
catch syscall write
commands
printf "rsi = %s\n", $rsi
bt
end
condition $bpnum $rdi == 1 && strstr((char *)$rsi, "$arg0") != NULL
end
stdout qwer
Programa de prueba:
#define _XOPEN_SOURCE 700
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main() {
write(STDOUT_FILENO, "asdf1", 5);
write(STDOUT_FILENO, "qwer1", 5);
write(STDOUT_FILENO, "zxcv1", 5);
write(STDOUT_FILENO, "qwer2", 5);
printf("as");
printf("df");
printf("qw");
printf("er");
printf("zx");
printf("cv");
fflush(stdout);
return EXIT_SUCCESS;
}
Resultado:descansos en:
qwer1
qwer2
fflush
. El anteriorprintf
en realidad no imprimieron nada, ¡fueron almacenados en búfer! Elwrite
syacall solo sucedió en elfflush
.
Notas:
$bpnum
gracias a Tromey en:https://sourceware.org/bugzilla/show_bug.cgi?id=18727rdi
:registro que contiene el número de la llamada del sistema Linux en x86_64,1
es parawrite
rsi
:primer argumento de la llamada al sistema, parawrite
apunta al búferstrstr
:llamada de función C estándar, busca subcoincidencias, devuelve NULL si no se encuentra
Probado en Ubuntu 17.10, gdb 8.0.1.
straza
Otra opción si te sientes interactivo:
setarch "$(uname -m)" -R strace -i ./stdout.out |& grep '\] write'
Salida de muestra:
[00007ffff7b00870] write(1, "a\nb\n", 4a
Ahora copie esa dirección y péguela en:
setarch "$(uname -m)" -R strace -i ./stdout.out |& grep -E '\] write\(1, "a'
La ventaja de este método es que puede usar las herramientas habituales de UNIX para manipular strace
salida, y no requiere GDB-fu profundo.
Explicación:
-i
hace que la salida de strace RIPsetarch -R
deshabilita ASLR para un proceso con unpersonality
llamada al sistema:Cómo depurar con strace -i cuando cada vez que la dirección es diferente GDB ya lo hace de forma predeterminada, por lo que no es necesario volver a hacerlo.
La respuesta de Anthony es increíble. Siguiendo su respuesta, probé otra solución en Windows (Windows x86-64 bits). Sé que esta pregunta es para GDB en Linux, sin embargo, creo que esta solución es un complemento para este tipo de preguntas. Podría ser útil para otros.
Solución en Windows
En Linux una llamada a printf
daría como resultado una llamada a la API write
. Y debido a que Linux es un sistema operativo de código abierto, pudimos depurar dentro de la API. Sin embargo, la API es diferente en Windows, proporcionó su propia API WriteFile. Debido a que Windows es un sistema operativo comercial de código no abierto, no se pudieron agregar puntos de interrupción en las API.
Pero parte del código fuente de VC se publica junto con Visual Studio, por lo que pudimos averiguar en el código fuente dónde finalmente se llamó el WriteFile
API y establezca un punto de interrupción allí. Después de depurar el código de muestra, encontré el printf
El método podría resultar en una llamada a _write_nolock
en el que WriteFile
se llama. La función se encuentra en:
your_VS_folder\VC\crt\src\write.c
El prototipo es:
/* now define version that doesn't lock/unlock, validate fh */
int __cdecl _write_nolock (
int fh,
const void *buf,
unsigned cnt
)
Comparado con el write
API en Linux:
#include <unistd.h>
ssize_t write(int fd, const void *buf, size_t count);
Tienen totalmente los mismos parámetros. Así que podríamos establecer un condition breakpoint
en _write_nolock
solo consulte las soluciones anteriores, con solo algunas diferencias en los detalles.
Solución portátil para Win32 y x64
Es muy afortunado que podamos usar el nombre de los parámetros directamente en Visual Studio al establecer una condición para los puntos de interrupción en Win32 y x64. Entonces se vuelve muy fácil escribir la condición:
-
Agregue puntos de interrupción en
_write_nolock
AVISO :Hay poca diferencia en Win32 y x64. Podríamos usar el nombre de la función para establecer la ubicación de los puntos de interrupción en Win32. Sin embargo, no funcionará en x64 porque en la entrada de la función, los parámetros no se inicializan. Por lo tanto, no pudimos usar el nombre del parámetro para establecer la condición de los puntos de interrupción.
Pero, afortunadamente, tenemos algunas soluciones:use la ubicación en la función en lugar del nombre de la función para establecer los puntos de interrupción, por ejemplo, la primera línea de la función. Los parámetros ya están inicializados allí. (Me refiero a usar el
filename+line number
para establecer los puntos de interrupción, o abrir el archivo directamente y establecer un punto de interrupción en la función, no en la entrada sino en la primera línea. ) -
Restringir la condición:
fh == 1 && strstr((char *)buf, "Hello World") != 0
AVISO :todavía hay un problema aquí, probé dos formas diferentes de escribir algo en la salida estándar:printf
y std::cout
. printf
escribiría todas las cadenas en el _write_nolock
función a la vez. Sin embargo std::cout
solo pasaría carácter por carácter a _write_nolock
, lo que significa que la API se llamaría strlen("your string")
veces. En este caso, la condición no podría activarse para siempre.
Solución Win32
Por supuesto, podríamos usar los mismos métodos que Anthony
proporcionado:establece la condición de los puntos de interrupción por registros.
Para un programa Win32, la solución es casi la misma con GDB
en Linux. Puede notar que hay una decoración __cdecl
en el prototipo de _write_nolock
. Esta convención de llamadas significa:
- El orden de paso de argumentos es de derecha a izquierda.
- La función de llamada extrae los argumentos de la pila.
- Convención de decoración de nombres:el carácter de subrayado (_) se antepone a los nombres.
- No se realizó traducción de casos.
Hay una descripción aquí. Y hay un ejemplo que se usa para mostrar los registros y las pilas en el sitio web de Microsoft. El resultado se puede encontrar aquí.
Entonces es muy fácil establecer la condición de los puntos de interrupción:
- Establecer un punto de interrupción en
_write_nolock
. -
Restringir la condición:
*(int *)($esp + 4) == 1 && strstr(*(char **)($esp + 8), "Hello") != 0
Es el mismo método que en Linux. La primera condición es asegurarse de que la cadena se escriba en stdout
. El segundo es hacer coincidir la cadena especificada.
Solución x64
Dos modificaciones importantes de x86 a x64 son la capacidad de direccionamiento de 64 bits y un conjunto plano de 16 registros de 64 bits para uso general. A medida que aumentan los registros, x64 solo usa __fastcall
como la convención de llamada. Los primeros cuatro argumentos enteros se pasan en registros. Los argumentos cinco y superiores se pasan a la pila.
Puede consultar la página Paso de parámetros en el sitio web de Microsoft. Los cuatro registros (en orden de izquierda a derecha) son RCX
, RDX
, R8
y R9
. Entonces es muy fácil restringir la condición:
-
Establecer un punto de interrupción en
_write_nolock
.AVISO :es diferente de la solución portátil anterior, simplemente podríamos establecer la ubicación del punto de interrupción en la función en lugar de la primera línea de la función. La razón es que todos los registros ya están inicializados en la entrada.
-
Condición de restricción:
$rcx == 1 && strstr((char *)$rdx, "Hello") != 0
La razón por la que necesitamos convertir y desreferenciar en esp
es que $esp
accede al ESP
registrarse, y para todos los efectos es un void*
. Mientras que los registros aquí almacenan directamente los valores de los parámetros. Así que ya no se necesita otro nivel de direccionamiento indirecto.
Publicar
También disfruto mucho esta pregunta, así que traduje la publicación de Anthony al chino y puse mi respuesta como complemento. La publicación se puede encontrar aquí. Gracias por el permiso de @anthony-arnold.