URL: https://linuxfr.org/users/pied/journaux/aujourd-hui-je-euggubed-un-programme-dans-gdb Title: Aujourd'hui, je euggubed un programme dans GDB... Authors: Pinaraf Date: 2019年07月07日T10:26:56+02:00 License: CC By-SA Tags: gdb et assembleur Score: 76 Bonjour bonjour ! En ce moment, pour beaucoup de fun, je tente de bidouiller une grammaire générée avec flex et bison, en mode un peu "boite noire" (interdiction de modifier la grammaire d'origine, et à vrai dire je sais même pas quelle est la tronche exacte du fichier source, je joue avec libpg_query pour ceux que ça intéresse). Mais quand on tombe sur une erreur, la backtrace est fort peu instructive : ``` #0 base_yyerror (base_yylloc=0x7fffffffc124, msg=0x5555555f5104 "syntax error", yyscanner=0x0) at src/postgres/src_backend_parser_gram.c:44051 #1 base_yyparse (yyscanner=yyscanner@entry=0x55555584b3a8) at src/postgres/src_backend_parser_gram.c:43856 #2 0x00005555555727bc in raw_parser (str=str@entry=0x5555555a6008 "SELECT 1, ") at src/postgres/src_backend_parser_parser.c:61 ``` Toutes les erreurs sont émises depuis la même ligne de base_yyparse, et il n'y a pas de sous-fonctions : cette fonction fait 18000 lignes... En effet, bison génère du goto en masse, et qui dit goto dit pas de frame dans la stack. Qui dit pas de frame dans la stack dit pas d'entrée dans la backtrace. Qui dit pas d'entrée dans la backtrace dit pas content. Alors pour plus de fun, on va sortir une petite perle introduite il y a deux ans de cela dans GDB : le reverse-debug. # 0) le reverse-debug, mais kesako ? # Comme montré dans le cas d'une grammaire générée par notre ami m. bison, qui n'est pas si méchant que ça, poser des breakpoints et réaliser du debug à base de backtrace a des limites. On aimerait bien disposer de plus dans notre debuggueur que d'un bouton pause et de boutons ligne/function/instruction suivante. On a donc vu apparaitre ces dernières années le reverse debugging, mis en avant notamment par mozilla et son projet rr. Le problème, c'est que le matériel ne le permet pas. Il est impossible pour le processeur d'enregistrer toutes ses variations d'état, ainsi que toutes les variations d'état de la RAM. La solution mise en place dans GDB est donc simple : si le processeur ne sait pas faire, faisons-le à sa place. Donc gdb émule un processeur dans un tel cas, ce qui évidemment se ressent sur les performances... Assez parlé, petit exemple: ``` #include void boo() { printf("PERDU\n"); } int main (int argc, char ** argv) { if (argc != 2) return -1; int v = atoi(argv[1]); if (v < 42) goto lose; if (v> 42) goto lose; return 0; lose: boo(); return -1; } ``` On compile et on va debugger ça... ``` gcc -g test-rev.c -o test-rev gdb ./test-rev ``` On s'intéresse à la fonction boo et on veut savoir pourquoi elle a été appelée... ```> b boo> r 73> bt #0 boo () at test-rev.c:4 #1 0x00005555555551b0 in main (argc=2, argv=0x7fffffffe088) at test-rev.c:19 ``` Comme dans mon cas avec bison, ce n'est pas très très pratique : impossible de savoir quelle branche de if a bien pu déclencher le goto, à moins bien sûr de poser des breakpoints partout (ou de réfléchir, vu la taille du programme c'est pas bien dur, mais c'est pas le but de l'exercice). ``` (gdb) b main Breakpoint 1 at 0x1167: file test-rev.c, line 8. (gdb) b boo Breakpoint 2 at 0x1149: file test-rev.c, line 4. (gdb) r 73 Starting program: /tmp/test-rev 73 Breakpoint 1, main (argc=2, argv=0x7fffffffe088) at test-rev.c:8 8 if (argc != 2) (gdb) target record-full (gdb) c Continuing. Breakpoint 2, boo () at test-rev.c:4 4 printf("PERDU\n"); (gdb) rs main (argc=2, argv=0x7fffffffe088) at test-rev.c:19 19 boo(); (gdb) 15 goto lose; ``` Et voilà ! Nous avons pris le goto de la ligne 15, à savoir donc v> 42. Nous avons pour cela activé le mode reverse (target record-full) et effecturé un reverse-step pour remonter dans le temps. Simple, non ? Du coup, on peut retourner à notre grammaire... # 1) La joie du dentiste : l'impact entre le mur de la réalité et tes dents # Alors, faisons simple... ``` (gdb) b base_yyparse Breakpoint 1 at 0x27d30: file src/postgres/src_backend_parser_gram.c, line 26257. (gdb) b base_yyerror Breakpoint 2 at 0x3f6e0: base_yyerror. (2 locations) (gdb) r Starting program: /home/pierre/projects/pglast/libpg_query/examples/simple Breakpoint 1, base_yyparse (yyscanner=yyscanner@entry=0x5555556863a8) at src/postgres/src_backend_parser_gram.c:26257 26257 yytype_int16 *yyss = yyssa; (gdb) target record-full (gdb) set variable base_yydebug = 1 (gdb) c Continuing. Starting parse Process record does not support instruction 0xc5 at address 0x7ffff7f26db2. ``` Ha. C'est pas vraiment ce à quoi je m'attendais... Que s'est-il passé ici ? Pourquoi notre exemple simple marchait et que dans la vraie vie on se prend un mur ? Pour pouvoir implémenter un enregistrement complet des traces d'exécution pour pouvoir remonter dans le temps, il est nécessaire d'avoir au sein de GDB un émulateur pour le processeur. Mais ce dernier n'est pas complet : il ne gère pas notamment les instructions AVX des processeurs modernes. Et c'est bien ce qu'il a rencontré ici : ``` 0x00007ffff7f26db2 <+2>: vmovd %esi,%xmm0 ``` Il s'agit d'une instruction de l'implémentation en AVX2 de la fonction strhrnul. Comme de nombreuses fonctions de manipulation de chaines de caractères, elles ont été écrites en plusieurs versions, optimisée selon les processeurs. # 2) Le choix dynamique d'implémentation # Vous souvenez-vous de l'époque où nous avions des paquets libc optimisés selon le type de processeur ? Ce n'est plus le cas sur nos distributions, comment se fait-il qu'un paquet générique soit optimisé ainsi ? Au démarrage d'un binaire au format ELF, un interpréteur est lancé qui va récupérer les différents binaires, les mettre en place en RAM, et mettre les bons appels de fonctions aux bons endroits. C'est cet interpréteur qui va appeler la méthode cpuid pour obtenir du processeur les différentes fonctionnalités qu'il supporte et donc choisir les versions les plus optimales des fonctions dont il dispose. Il faut donc que nous fassions comprendre à ce trublion que non, même si notre processeur dispose des derniers raffinements en vigueur, il ne faut pas les activer. Pour cela... quoi de plus simple que de le patcher, directement ? (ouais, c'est bourrin mais j'ai vraiment pas plus simple) L'interpréteur ELF par défaut sur amd64 est /lib64/ld-linux-x86-64.so.2: ``` $ file /bin/bash /bin/bash: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 3.2.0, BuildID[sha1]=ffe165dc81a64aea2b05beda07aeda8ad71f1e7c, stripped ``` Il est "préférable" de ne pas toucher directement à ce fichier (sans blague), aussi allons-nous procéder sur une copie du fichier. Le code à patcher initial est: ``` __cpuid (0, cpu_features->max_cpuid, ebx, ecx, edx); ``` (libc, sysdeps/x86/cpu-features.c) Cela correspond à mettre 0 dans le registre EAX, puis appeler la fonction cpuid. Nous devons donc trouver dans ld-linux.so l'assembleur suivant: ``` 31 c0 xor eax, eax // Le moyen le plus court pour mettre 0 dans EAX 0f a2 cpuid ``` Si on cherche dans le binaire, on ne trouve pas cette séquence. Il est en effet possible d'avoir quelques instructions intermédiaires ou de padding, selon le compilateur et le sens du vent... Du coup, il faut chercher avec éventuellement quelques octets entre ces deux instructions. Faisons simple : le one-liner Perl suivant devrait résoudre le problème :) ``` perl -pe 's/\x{31}\x{c0}.{0,32}\K\x{0f}\x{a2}/\x{66}\x{90}/' < /lib64/ld-linux-x86-64.so.2> ld-linux-x86-64.so.2.nocpu ``` On génère donc un nouveau fichier ld-linux.so qui ne va pas aller méchamment lire les infos du CPU pour tenter d'être intelligent. non mais... Il suffit maintenant d'appeler patchelf pour que notre exécutable utilise notre interpréteur patché. ``` patchelf --set-interpreter `readlink -f ld-linux-x86-64.so.2.nocpu` ./examples/simple ``` Et voilà ! # 3) Jouer... # Je peux maintenant faire du reverse-debug dans ma grammaire, et donc retracer "aisément" les variations dans la machine à états. ``` 44051 parser_yyerror(msg); (gdb) rs 26461 if (yyn == 0) (gdb) rs 26460 yyn = yydefact[yystate]; (gdb) rs 26424 if (yyn < 0 || YYLAST < yyn || yycheck[yyn] != yytoken) (gdb) p yyn 1ドル = 28679 ``` Le reste sera entre moi et mon psy. Bonne fin de week-end à tous !

AltStyle によって変換されたページ (->オリジナル) /