Cette question pourrait être un bon point de départ :comment puis-je mettre un point d'arrêt sur "quelque chose est imprimé sur le terminal" dans gdb ?
Ainsi, vous pourriez au moins casser chaque fois que quelque chose est écrit sur stdout. La méthode consiste essentiellement à définir un point d'arrêt sur le write
syscall avec une condition que le premier argument est 1
(c'est-à-dire STDOUT). Dans les commentaires, il y a aussi un indice sur la façon dont vous pourriez inspecter le paramètre de chaîne du write
appeler aussi.
Mode x86 32 bits
J'ai trouvé ce qui suit et je l'ai testé avec gdb 7.0.1-debian. Cela semble fonctionner assez bien. $esp + 8
contient un pointeur vers l'emplacement mémoire de la chaîne passée à write
, donc d'abord vous le convertissez en une intégrale, puis en un pointeur vers char
. $esp + 4
contient le descripteur de fichier dans lequel écrire (1 pour STDOUT).
$ gdb break write if 1 == *(int*)($esp + 4) && strcmp((char*)*(int*)($esp + 8), "your string") == 0
Mode x86 64 bits
Si votre processus s'exécute en mode x86-64, les paramètres sont transmis via les registres de travail %rdi
et %rsi
$ gdb break write if 1 == $rdi && strcmp((char*)($rsi), "your string") == 0
Notez qu'un niveau d'indirection est supprimé puisque nous utilisons des registres de travail plutôt que des variables sur la pile.
Variantes
Fonctions autres que strcmp
peut être utilisé dans les extraits ci-dessus :
strncmp
est utile si vous voulez faire correspondre le premiern
nombre de caractères de la chaîne en cours d'écriturestrstr
peut être utilisé pour trouver des correspondances dans une chaîne, car vous ne pouvez pas toujours être certain que la chaîne que vous recherchez est au début de chaîne en cours d'écriture via lewrite
fonction.
Modifier : J'ai apprécié cette question et trouver sa réponse ultérieure. J'ai décidé de faire un article de blog à ce sujet.
catch
+ strstr
état
L'avantage de cette méthode est qu'elle ne dépend pas de glibc write
en cours d'utilisation :il trace l'appel système réel.
De plus, il est plus résistant à printf()
mise en mémoire tampon, car il pourrait même intercepter des chaînes qui sont imprimées sur plusieurs printf()
appels.
Version 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
Programme d'essai :
#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;
}
Résultat :pauses à :
qwer1
qwer2
fflush
. Le précédentprintf
n'ont rien imprimé, ils ont été mis en mémoire tampon ! Lewrite
syacall ne s'est produit que sur lefflush
.
Remarques :
$bpnum
merci à Tromey sur :https://sourceware.org/bugzilla/show_bug.cgi?id=18727rdi
:registre qui contient le numéro de l'appel système Linux en x86_64,1
est pourwrite
rsi
:premier argument du syscall, pourwrite
il pointe vers le tamponstrstr
:appel de fonction C standard, recherche les sous-correspondances, renvoie NULL si non trouvé
Testé dans Ubuntu 17.10, gdb 8.0.1.
trace
Une autre option si vous vous sentez interactif :
setarch "$(uname -m)" -R strace -i ./stdout.out |& grep '\] write'
Exemple de sortie :
[00007ffff7b00870] write(1, "a\nb\n", 4a
Copiez maintenant cette adresse et collez-la dans :
setarch "$(uname -m)" -R strace -i ./stdout.out |& grep -E '\] write\(1, "a'
L'avantage de cette méthode est que vous pouvez utiliser les outils UNIX habituels pour manipuler strace
sortie, et il ne nécessite pas de profondeur GDB-fu.
Explication :
-i
rend la sortie strace RIPsetarch -R
désactive ASLR pour un processus avec unpersonality
appel système :comment déboguer avec strace -i lorsque chaque adresse est différente GDB le fait déjà par défaut, donc pas besoin de le refaire.
La réponse d'Anthony est géniale. Suite à sa réponse, j'ai essayé une autre solution sur Windows (Windows x86-64 bits). Je sais que cette question est pour GDB sous Linux, cependant, je pense que cette solution est un complément pour ce genre de question. Cela pourrait être utile pour d'autres.
Solution sous Windows
Sous Linux un appel à printf
entraînerait un appel à l'API write
. Et parce que Linux est un système d'exploitation open source, nous pourrions déboguer dans l'API. Cependant, l'API est différente sous Windows, elle a fourni sa propre API WriteFile. Étant donné que Windows est un système d'exploitation commercial non open source, les points d'arrêt n'ont pas pu être ajoutés dans les API.
Mais une partie du code source de VC est publiée avec Visual Studio, nous avons donc pu découvrir dans le code source où finalement appelé le WriteFile
API et définissez-y un point d'arrêt. Après avoir débogué sur l'exemple de code, j'ai trouvé le printf
méthode peut entraîner un appel à _write_nolock
dont WriteFile
est appelé. La fonction se trouve dans :
your_VS_folder\VC\crt\src\write.c
Le prototype est :
/* now define version that doesn't lock/unlock, validate fh */
int __cdecl _write_nolock (
int fh,
const void *buf,
unsigned cnt
)
Par rapport au write
API sous Linux :
#include <unistd.h>
ssize_t write(int fd, const void *buf, size_t count);
Ils ont totalement les mêmes paramètres. Nous pourrions donc simplement définir un condition breakpoint
en _write_nolock
il suffit de se référer aux solutions ci-dessus, avec seulement quelques différences de détail.
Solution portable pour Win32 et x64
C'est vraiment une chance que nous puissions utiliser le nom des paramètres directement sur Visual Studio lors de la définition d'une condition pour les points d'arrêt sur Win32 et x64. Il devient alors très simple d'écrire la condition :
-
Ajouter un point d'arrêt dans
_write_nolock
AVIS :Il y a peu de différence sur Win32 et x64. Nous pourrions simplement utiliser le nom de la fonction pour définir l'emplacement des points d'arrêt sur Win32. Cependant, cela ne fonctionnera pas sur x64 car à l'entrée de la fonction, les paramètres ne sont pas initialisés. Par conséquent, nous ne pouvions pas utiliser le nom du paramètre pour définir la condition des points d'arrêt.
Mais heureusement, nous avons une solution :utilisez l'emplacement dans la fonction plutôt que le nom de la fonction pour définir les points d'arrêt, par exemple, la 1ère ligne de la fonction. Les paramètres y sont déjà initialisés. (Je veux dire utiliser le
filename+line number
pour définir les points d'arrêt, ou ouvrez le fichier directement et définissez un point d'arrêt dans la fonction, pas l'entrée mais la première ligne. ) -
Restreindre la condition :
fh == 1 && strstr((char *)buf, "Hello World") != 0
AVIS :il y a toujours un problème ici, j'ai testé deux manières différentes d'écrire quelque chose dans stdout :printf
et std::cout
. printf
écrirait toutes les chaînes dans le _write_nolock
fonction à la fois. Cependant std::cout
ne passerait que caractère par caractère à _write_nolock
, ce qui signifie que l'API s'appellerait strlen("your string")
fois. Dans ce cas, la condition n'a pas pu être activée indéfiniment.
Solution Win32
Bien sûr, nous pourrions utiliser les mêmes méthodes que Anthony
fourni :définir la condition des points d'arrêt par registres.
Pour un programme Win32, la solution est presque la même avec GDB
sur Linux. Vous remarquerez peut-être qu'il y a une décoration __cdecl
dans le prototype de _write_nolock
. Cette convention d'appel signifie :
- L'ordre de passage des arguments est de droite à gauche.
- La fonction d'appel extrait les arguments de la pile.
- Convention de décoration des noms :le caractère de soulignement (_) est préfixé aux noms.
- Aucune traduction de cas effectuée.
Il y a une description ici. Et il y a un exemple qui est utilisé pour montrer les registres et les piles sur le site Web de Microsoft. Le résultat est à découvrir ici.
Ensuite, il est très facile de définir la condition des points d'arrêt :
- Définir un point d'arrêt dans
_write_nolock
. -
Restreindre la condition :
*(int *)($esp + 4) == 1 && strstr(*(char **)($esp + 8), "Hello") != 0
C'est la même méthode que sur Linux. La première condition est de s'assurer que la chaîne est écrite dans stdout
. La seconde consiste à faire correspondre la chaîne spécifiée.
Solution x64
Deux modifications importantes de x86 à x64 sont la capacité d'adressage 64 bits et un ensemble plat de 16 registres 64 bits à usage général. Au fur et à mesure de l'augmentation des registres, x64 n'utilise que __fastcall
comme convention d'appel. Les quatre premiers arguments entiers sont passés dans des registres. Les arguments cinq et plus sont passés sur la pile.
Vous pouvez vous référer à la page Parameter Passing sur le site Web de Microsoft. Les quatre registres (dans l'ordre de gauche à droite) sont RCX
, RDX
, R8
et R9
. Il est donc très facile de restreindre la condition :
-
Définir un point d'arrêt dans
_write_nolock
.AVIS :c'est différent de la solution portable ci-dessus, nous pourrions simplement définir l'emplacement du point d'arrêt sur la fonction plutôt que sur la 1ère ligne de la fonction. La raison en est que tous les registres sont déjà initialisés à l'entrée.
-
Condition de restriction :
$rcx == 1 && strstr((char *)$rdx, "Hello") != 0
La raison pour laquelle nous avons besoin de cast et de déréférencement sur esp
est-ce $esp
accède au ESP
enregistrer, et à toutes fins utiles est un void*
. Tandis que les registres ici stockent directement les valeurs des paramètres. Ainsi, un autre niveau d'indirection n'est plus nécessaire.
Publier
J'aime aussi beaucoup cette question, alors j'ai traduit le message d'Anthony en chinois et j'y ai mis ma réponse en complément. Le poste peut être trouvé ici. Merci pour la permission de @anthony-arnold.