Questa domanda potrebbe essere un buon punto di partenza:come posso inserire un punto di interruzione su "qualcosa viene stampato sul terminale" in gdb?
Quindi potresti almeno interrompere ogni volta che qualcosa viene scritto su stdout. Il metodo prevede sostanzialmente l'impostazione di un punto di interruzione su write
syscall con una condizione che il primo argomento sia 1
(es. STDOUT). Nei commenti c'è anche un suggerimento su come ispezionare il parametro stringa del write
chiama pure.
modalità x86 a 32 bit
Ho trovato quanto segue e l'ho testato con gdb 7.0.1-debian. Sembra funzionare abbastanza bene. $esp + 8
contiene un puntatore alla locazione di memoria della stringa passata a write
, quindi prima lo trasformi in un integrale, quindi in un puntatore a char
. $esp + 4
contiene il descrittore di file su cui scrivere (1 per STDOUT).
$ gdb break write if 1 == *(int*)($esp + 4) && strcmp((char*)*(int*)($esp + 8), "your string") == 0
modalità x86 a 64 bit
Se il tuo processo è in esecuzione in modalità x86-64, i parametri vengono passati attraverso i registri di lavoro %rdi
e %rsi
$ gdb break write if 1 == $rdi && strcmp((char*)($rsi), "your string") == 0
Nota che un livello di riferimento indiretto è stato rimosso poiché stiamo utilizzando i registri di lavoro anziché le variabili nello stack.
Varianti
Funzioni diverse da strcmp
può essere utilizzato negli snippet precedenti:
strncmp
è utile se vuoi abbinare il primon
numero di caratteri della stringa da scriverestrstr
può essere utilizzato per trovare corrispondenze all'interno di una stringa, poiché non puoi sempre essere certo che la stringa che stai cercando sia all'inizio della stringa scritta attraverso ilwrite
funzione.
Modifica: Mi è piaciuta questa domanda e ho trovato la risposta successiva. Ho deciso di scrivere un post sul blog.
catch
+ strstr
condizione
La cosa bella di questo metodo è che non dipende da glibc write
in uso:traccia l'effettiva chiamata di sistema.
Inoltre, è più resistente a printf()
buffering, in quanto potrebbe persino rilevare stringhe stampate su più printf()
chiamate.
versione 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
Programma di prova:
#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;
}
Risultato:pause in:
qwer1
qwer2
fflush
. Il precedenteprintf
in realtà non hanno stampato nulla, sono stati bufferizzati! Ilwrite
syacall è avvenuto solo sulfflush
.
Note:
$bpnum
grazie a Tromey su:https://sourceware.org/bugzilla/show_bug.cgi?id=18727rdi
:registro che contiene il numero della chiamata di sistema Linux in x86_64,1
sta perwrite
rsi
:primo argomento della chiamata di sistema, perwrite
punta al bufferstrstr
:chiamata di funzione C standard, cerca submatch, restituisce NULL se non trovata
Testato in Ubuntu 17.10, gdb 8.0.1.
strace
Un'altra opzione se ti senti interattivo:
setarch "$(uname -m)" -R strace -i ./stdout.out |& grep '\] write'
Esempio di output:
[00007ffff7b00870] write(1, "a\nb\n", 4a
Ora copia quell'indirizzo e incollalo in:
setarch "$(uname -m)" -R strace -i ./stdout.out |& grep -E '\] write\(1, "a'
Il vantaggio di questo metodo è che puoi usare i soliti strumenti UNIX per manipolare strace
output e non richiede GDB-fu profondo.
Spiegazione:
-i
rende l'output di strace RIPsetarch -R
disabilita ASLR per un processo conpersonality
chiamata di sistema:come eseguire il debug con strace -i quando ogni volta che l'indirizzo è diverso GDB lo fa già per impostazione predefinita, quindi non è necessario farlo di nuovo.
La risposta di Antonio è fantastica. Dopo la sua risposta, ho provato un'altra soluzione su Windows (Windows x86-64 bit). So che questa domanda è per GDB su Linux, tuttavia, penso che questa soluzione sia un supplemento per questo tipo di domande. Potrebbe essere utile per gli altri.
Soluzione su Windows
In Linux una chiamata a printf
risulterebbe in una chiamata all'API write
. E poiché Linux è un sistema operativo open source, potremmo eseguire il debug all'interno dell'API. Tuttavia, l'API è diversa su Windows, ha fornito il proprio API WriteFile. Poiché Windows è un sistema operativo commerciale non open source, non è stato possibile aggiungere punti di interruzione nelle API.
Ma parte del codice sorgente di VC è pubblicato insieme a Visual Studio, quindi abbiamo potuto scoprire nel codice sorgente dove finalmente si chiamava WriteFile
API e impostare un punto di interruzione lì. Dopo aver eseguito il debug sul codice di esempio, ho trovato il printf
potrebbe risultare in una chiamata a _write_nolock
in cui WriteFile
è chiamato. La funzione si trova in:
your_VS_folder\VC\crt\src\write.c
Il prototipo è:
/* now define version that doesn't lock/unlock, validate fh */
int __cdecl _write_nolock (
int fh,
const void *buf,
unsigned cnt
)
Rispetto al write
API su Linux:
#include <unistd.h>
ssize_t write(int fd, const void *buf, size_t count);
Hanno totalmente gli stessi parametri. Quindi potremmo semplicemente impostare un condition breakpoint
in _write_nolock
basta fare riferimento alle soluzioni di cui sopra, con solo alcune differenze in dettaglio.
Soluzione portatile per Win32 e x64
È molto fortunato poter utilizzare direttamente il nome dei parametri in Visual Studio quando si imposta una condizione per i punti di interruzione sia in Win32 che in x64. Quindi diventa molto facile scrivere la condizione:
-
Aggiungi un punto di interruzione in
_write_nolock
AVVISO :Ci sono poche differenze su Win32 e x64. Potremmo semplicemente usare il nome della funzione per impostare la posizione dei punti di interruzione su Win32. Tuttavia, non funzionerà su x64 perché all'ingresso della funzione i parametri non vengono inizializzati. Pertanto, non è stato possibile utilizzare il nome del parametro per impostare la condizione dei punti di interruzione.
Ma fortunatamente abbiamo qualche soluzione:usa la posizione nella funzione piuttosto che il nome della funzione per impostare i punti di interruzione, ad esempio la prima riga della funzione. I parametri sono già inizializzati lì. (Intendo usare il
filename+line number
per impostare i punti di interruzione, oppure aprire direttamente il file e impostare un punto di interruzione nella funzione, non l'ingresso ma la prima riga. ) -
Limita la condizione:
fh == 1 && strstr((char *)buf, "Hello World") != 0
AVVISO :c'è ancora un problema qui, ho provato due modi diversi per scrivere qualcosa in stdout:printf
e std::cout
. printf
scriverebbe tutte le stringhe nel _write_nolock
funzione in una volta. Comunque std::cout
passerebbe solo carattere per carattere a _write_nolock
, il che significa che l'API si chiamerà strlen("your string")
volte. In questo caso, la condizione non potrebbe essere attivata per sempre.
Soluzione Win32
Ovviamente potremmo usare gli stessi metodi di Anthony
fornito:imposta la condizione dei punti di interruzione dei registri.
Per un programma Win32, la soluzione è quasi la stessa con GDB
su Linux. Potresti notare che c'è una decorazione __cdecl
nel prototipo di _write_nolock
. Questa convenzione di chiamata significa:
- L'ordine di passaggio degli argomenti è Da destra a sinistra.
- La funzione chiamante estrae gli argomenti dallo stack.
- Convenzione per la decorazione dei nomi:il carattere di sottolineatura (_) è preceduto dai nomi.
- Nessuna traduzione maiuscola eseguita.
C'è una descrizione qui. E c'è un esempio che viene utilizzato per mostrare i registri e gli stack sul sito Web di Microsoft. Il risultato potrebbe essere trovato qui.
Quindi è molto facile impostare la condizione dei punti di interruzione:
- Imposta un punto di interruzione in
_write_nolock
. -
Limita la condizione:
*(int *)($esp + 4) == 1 && strstr(*(char **)($esp + 8), "Hello") != 0
È lo stesso metodo di Linux. La prima condizione è assicurarsi che la stringa sia scritta in stdout
. Il secondo è quello di abbinare la stringa specificata.
Soluzione x64
Due importanti modifiche da x86 a x64 sono la capacità di indirizzamento a 64 bit e un set piatto di 16 registri a 64 bit per uso generale. Con l'aumentare dei registri, x64 usa solo __fastcall
come convenzione di chiamata. I primi quattro argomenti interi vengono passati nei registri. Gli argomenti dal quinto in su vengono passati in pila.
Puoi fare riferimento alla pagina Parameter Passing sul sito Web di Microsoft. I quattro registri (in ordine da sinistra a destra) sono RCX
, RDX
, R8
e R9
. Quindi è molto facile restringere la condizione:
-
Imposta un punto di interruzione in
_write_nolock
.AVVISO :è diverso dalla soluzione portatile sopra, potremmo semplicemente impostare la posizione del punto di interruzione sulla funzione piuttosto che sulla prima riga della funzione. Il motivo è che tutti i registri sono già inizializzati all'ingresso.
-
Condizione di restrizione:
$rcx == 1 && strstr((char *)$rdx, "Hello") != 0
Il motivo per cui abbiamo bisogno di cast e dereference su esp
è quel $esp
accede al ESP
register, ea tutti gli effetti è un void*
. Mentre i registri qui memorizzano direttamente i valori dei parametri. Quindi non è più necessario un altro livello di indiretto.
Pubblica
Anche questa domanda mi piace molto, quindi ho tradotto il post di Anthony in cinese e ho inserito la mia risposta come supplemento. Il post potrebbe essere trovato qui. Grazie per il permesso di @anthony-arnold.