Traduction: Nicolas Provost
Relecture de la version française : Deny
Copyright © 2008 Aurelian Melinte
Copyright © 2008 Nicolas Provost
Copyright © 2008 Deny
Article paru dans le n°151 de la Gazette Linux de Juin 2008.
Article publié sous Open Publication License. La Linux Gazette n'est ni produite, ni sponsorisée, ni avalisée par notre hébergeur principal, SSC, Inc.
Table des matières
Il existe des outils pour surveiller les appels systèmes effectués par une application, mais comment écrire vos propres fonctions de surveillance à l'intérieur du programme lui-même ? Comment faire si nous voulons savoir quand une fonction est exécutée, avec quels arguments elle est invoquée, quand la fonction se termine et avec quelle valeur de retour ? Cet article présente un outil proof-of-concept pour réaliser ceci sans modifier le code de l'application.
Bien que le compilateur gcc se charge du code d'instrumentation pour nous, quelques points sont laissés au programmeur et dépendent à la fois de la version du compilateur et du CPU, principalement pour la récupération des arguments d'une fonction et des valeurs de retour.
Nous voulons aborder les points suivants :
quand entre-t-on ou sort-on d'une fonction/d'une méthode
que valaient les arguments à l'entrée de la fonction
quel a été le code retourné à la sortie de la fonction
éventuellement, depuis où la fonction a t-elle été appelée
Le premier point est simple ; à la demande, le compilateur va générer l'instrumentation de fonctions et méthodes, de telle façon que quand on entre ou on sort d'une fonction ou d'une méthode, un appel à une fonction d'instrumentation est effectué :
void __cyg_profile_func_enter(void *fonction, void *lieu_appel); void __cyg_profile_func_exit(void *fonction, void *lieu_appel);
Ceci est réalisé en compilant le code avec l'option
-finstrument-functions
. Les deux fonctions ci-dessus
peuvent être utilisées, par exemple, pour collecter des données à des fins
de journalisation ou de profilage. Nous les utiliserons pour suivre les
appels de fonctions. De plus, nous pouvons placer ces fonctions et le code
dans une librairie intermédiaire personnelle. Cette librairie peut être
chargée quand (et si) nécessaire, ce qui laisse le code de l'application
globalement inchangé.
Maintenant, quand on entre dans la fonction, nous obtenons les arguments d'appel :
void __cyg_profile_func_enter( void *fonction, void *lieu_appel ) { char buf_func[CTRACE_BUF_LEN+1] = {0}; char buf_file[CTRACE_BUF_LEN+1] = {0}; char buf_args[ARG_BUF_LEN + 1] = {0}; pthread_t self = (pthread_t)0; int *frame = NULL; int nargs = 0; self = pthread_self(); frame = (int *)__builtin_frame_address(1); /*de la 'fonction'*/ /*Quelle fonction*/ libtrace_resolve (fonction, buf_func, CTRACE_BUF_LEN, NULL, 0); /*Depuis où. KO si optimisations. */ libtrace_resolve (lieu_appel, NULL, 0, buf_file, CTRACE_BUF_LEN); nargs = nchr(buf_func, ',') + 1; /*le dernier argument n'a pas de virgule derrière*/ nargs += is_cpp(buf_func); /*'this'*/ if (nargs > MAX_ARG_SHOW) nargs = MAX_ARG_SHOW; printf("T%p: %p %s %s [depuis %s]\n", self, (int*)fonction, buf_func, args(buf_args, ARG_BUF_LEN, nargs, frame), buf_file); }
Et en sortie de fonction, nous obtenons la valeur de retour :
void __cyg_profile_func_exit( void *fonction, void *lieu_appel ) { long ret = 0L; char buf_func[CTRACE_BUF_LEN+1] = {0}; char buf_file[CTRACE_BUF_LEN+1] = {0}; pthread_t self = (pthread_t)0; GET_EBX(ret); self = pthread_self(); /*Quelle fonction*/ libtrace_resolve (fonction, buf_func, CTRACE_BUF_LEN, NULL, 0); printf("T%p: %p %s => %d\n", self, (int*)fonction, buf_func, ret); SET_EBX(ret); }
Comme ces deux fonctions d'instrumentation n'ont pas connaissance
des adresses, et que nous souhaitons que les traces soient compréhensibles
pour des humains, nous devons également convertir les adresses des
symboles en leurs noms, c'est ce que réalise
libtrace_resolve()
.
Tout d'abord, nous devons récupérer les informations symboliques
utiles. Pour cela, nous compilons notre application avec l'option
-g
. Nous pouvons alors associer les adresses aux noms
symboliques. Ceci aurait normalement imposé l'écriture de code non
compatible avec le format ELF.
Par chance, le paquet binutils existe, qui
provient d'une librairie qui réalise justement cela, libbfd
, avec un outil,
addr2line. Cet outil est un bon exemple d'utilisation
de la librairie libbfd
, et je
l'ai simplement repris pour exploiter libbfd
. Le résultat est la fonction
libtrace_resolve()
. Pour plus de détails,
référez-vous au fichier README
accompagnant le code
source joint à cet article.
Puisque les fonctions d'instrumentation sont isolées dans un module
indépendant, nous transmettons au module le nom de l'exécutable dont on
réalise l'instrumentation via une variable d'environnement
(CTRACE_PROGRAM
), que nous positionnons avant
l'exécution du programme. Ceci est requis afin d'initialiser proprement la
recherche des symboles par libbfd
.
libbfd
est en cours de
développement. J'ai utilisé la version 2.18. Elle réalise un travail
remarquable bien que l'utilisation de fonctions
"inline" affecte sa précision.
Pour régler le premier point, nous n'avons pas eu à tenir compte de
l'architecture sous-jacente (actuellement libbfd
ne dépend pas de l'architecture
choisie, mais les choses sont cachées derrière son
API). Néanmoins, pour obtenir les arguments et la
valeur de retour d'une fonction, nous avons dû examiner la pile, écrire un
peu de code spécifique à l'architecture, et exploiter certaines
excentricités de gcc. De nouveau, je signale
que les compilateurs utilisés sont gcc 4.1 et
4.2 ; les versions précédentes ou ultérieures peuvent fonctionner
différemment. Pour résumer :
l'architecture x86 fait grandir la pile vers le bas
gcc impose la manière d'utiliser la pile—une pile typique est détaillée ci-dessous
chaque fonction dispose d'un cadre de pile repéré par les
registres ebp
(pointeur de base) et
esp
(pointeur de pile)
normalement, il est attendu que le registre
eax
contienne la valeur de retour
\ +------------+ | | arg 2 | \ +------------+ >- cadre de pile de la fonction précédente | arg 1 | / +------------+ | | ret %eip | / +============+ | %ebp sauvé | \ %ebp-> +------------+ | | | | | variables | \ | locales, | >- cadre de pile de la fonction courante | etc. | / | | | | | | %esp-> +------------+ /
Dans un monde idéal, le code généré par le compilateur devrait assurer que lors de l'instrumentation à la sortie d'une fonction, la valeur de retour ait été fixée et que les registres CPU aient été mis sur la pile (pour être certain que l'instrumentation d'une fonction ne les affecte pas). Puis la fonction d'instrumentation devrait être appelée, et enfin les registres dépilés. Cet enchaînement permettrait de nous garantir la récupération de la valeur de retour par la fonction d'instrumentation. Le code généré par le compilateur est un peu différent ...
De même, en pratique, de nombreuses options de gcc affectent la structure de pile ou l'utilisation des registres. Les cas les plus évidents sont :
-fomit-frame-pointer
. Cette option modifie le
déplacement à utiliser pour trouver les arguments dans la pile.
les options d'optimisation (c'est-à-dire, -Ox
).
Chacune de ces options inclue d'autres optimisations. La pile n'est
pas affectée, et de façon plutôt surprenante, les arguments sont
toujours transmis aux fonctions indépendamment du niveau
d'optimisation demandé. On aurait pu attendre que certains arguments
soient transmis par les registres, auquel cas il aurait été difficile,
voire même impossible de récupérer ces arguments. Cependant ces
options compliquent la récupération du code de retour. Notez que sur
certaines architectures, ces options impliqueront l'optimisation de
type -fomit-frame-pointer
Dans tous les cas, soyez prudent : les options de compilation que vous utilisez peuvent vous réserver des surprises cachées.
Lors de mes tests avec les compilateurs, tous les arguments ont été
systématiquement transmis par la pile. Donc, c'est trivial, seulement
affectés dans une faible mesure par l'option
-fomit-frame-pointer
, cette option changeant le décalage
de début de chaque argument.
Combien d'arguments une fonction possède t-elle ? Combien
d'arguments sont passés sur la pile ? Une méthode pour inférer ce nombre
d'arguments est de considérer la signature de la fonction (en C++,
attention à l'argument caché « this »), c'est la technique utilisée par la
fonction __cyg_profile_func_enter()
.
Dés que nous connaissons le décalage indiquant où les arguments débutent dans la pile, et combien il y en a, il suffit de parcourir la pile pour récupérer les valeurs :
char *args(char *buf, int len, int nargs, int *frame) { int i; int offset; memset(buf, 0, len); snprintf(buf, len, "("); offset = 1; for (i=0; i<nargs && offset<len; i++) { offset += snprintf(buf+offset, len-offset, "%d%s", *(frame+ARG_OFFET+i), i==nargs-1 ? " ...)" : ", "); } return buf; }
Il apparaît que la récupération de la valeur de retour est possible
seulement lorsque l'on utilise l'option -O0
.
Regardons ce qui se passe avec la méthode suivante :
class B { ... virtual int m1(int i, int j) {printf("B::m1()\n"); f1(i); return 20;} ... };
lorsque l'instrumentation est réalisée avec -O0
:
080496a2 <_ZN1B2m1Eii>: 80496a2: 55 push %ebp 80496a3: 89 e5 mov %esp,%ebp 80496a5: 53 push %ebx 80496a6: 83 ec 24 sub $0x24,%esp 80496a9: 8b 45 04 mov 0x4(%ebp),%eax 80496ac: 89 44 24 04 mov %eax,0x4(%esp) 80496b0: c7 04 24 a2 96 04 08 movl $0x80496a2,(%esp) 80496b7: e8 b0 f4 ff ff call 8048b6c <__cyg_profile_func_enter@plt> 80496bc: c7 04 24 35 9c 04 08 movl $0x8049c35,(%esp) 80496c3: e8 b4 f4 ff ff call 8048b7c <puts@plt> 80496c8: 8b 45 0c mov 0xc(%ebp),%eax 80496cb: 89 04 24 mov %eax,(%esp) 80496ce: e8 9d f8 ff ff call 8048f70 <_Z2f1i> 80496d3: bb 14 00 00 00 mov $0x14,%ebx 80496d8: 8b 45 04 mov 0x4(%ebp),%eax 80496db: 89 44 24 04 mov %eax,0x4(%esp) 80496df: c7 04 24 a2 96 04 08 movl $0x80496a2,(%esp) 80496e6: e8 81 f5 ff ff call 8048c6c <__cyg_profile_func_exit@plt> 80496eb: 89 5d f8 mov %ebx,0xfffffff8(%ebp) 80496ee: eb 27 jmp 8049717 <_ZN1B2m1Eii+0x75> 80496f0: 89 45 f4 mov %eax,0xfffffff4(%ebp) 80496f3: 8b 5d f4 mov 0xfffffff4(%ebp),%ebx 80496f6: 8b 45 04 mov 0x4(%ebp),%eax 80496f9: 89 44 24 04 mov %eax,0x4(%esp) 80496fd: c7 04 24 a2 96 04 08 movl $0x80496a2,(%esp) 8049704: e8 63 f5 ff ff call 8048c6c <__cyg_profile_func_exit@plt> 8049709: 89 5d f4 mov %ebx,0xfffffff4(%ebp) 804970c: 8b 45 f4 mov 0xfffffff4(%ebp),%eax 804970f: 89 04 24 mov %eax,(%esp) 8049712: e8 15 f5 ff ff call 8048c2c <_Unwind_Resume@plt> 8049717: 8b 45 f8 mov 0xfffffff8(%ebp),%eax 804971a: 83 c4 24 add $0x24,%esp 804971d: 5b pop %ebx 804971e: 5d pop %ebp 804971f: c3 ret
Notez comment le code de retour est placé dans le registre
ebx
—ce qui est un peu inattendu ici, le
registre eax
étant utilisé traditionnellement
pour les valeurs de retour—et qu'après, la fonction d'instrumentation
est appelée. Parfait pour récupérer la valeur de retour, mais pour éviter
l'altération du registre ebx
dans la fonction
d'instrumentation, nous devons le sauver à l'entrée de la fonction et le
restaurer à la sortie.
Quand la compilation est effectuée avec d'autres niveaux
d'optimisation (-O1...3
; ici c'est le cas
-O2
), le code change :
080498c0 <_ZN1B2m1Eii>: 80498c0: 55 push %ebp 80498c1: 89 e5 mov %esp,%ebp 80498c3: 53 push %ebx 80498c4: 83 ec 14 sub $0x14,%esp 80498c7: 8b 45 04 mov 0x4(%ebp),%eax 80498ca: c7 04 24 c0 98 04 08 movl $0x80498c0,(%esp) 80498d1: 89 44 24 04 mov %eax,0x4(%esp) 80498d5: e8 12 f4 ff ff call 8048cec <__cyg_profile_func_enter@plt> 80498da: c7 04 24 2d 9c 04 08 movl $0x8049c2d,(%esp) 80498e1: e8 16 f4 ff ff call 8048cfc <puts@plt> 80498e6: 8b 45 0c mov 0xc(%ebp),%eax 80498e9: 89 04 24 mov %eax,(%esp) 80498ec: e8 af f7 ff ff call 80490a0 <_Z2f1i> 80498f1: 8b 45 04 mov 0x4(%ebp),%eax 80498f4: c7 04 24 c0 98 04 08 movl $0x80498c0,(%esp) 80498fb: 89 44 24 04 mov %eax,0x4(%esp) 80498ff: e8 88 f3 ff ff call 8048c8c <__cyg_profile_func_exit@plt> 8049904: 83 c4 14 add $0x14,%esp 8049907: b8 14 00 00 00 mov $0x14,%eax 804990c: 5b pop %ebx 804990d: 5d pop %ebp 804990e: c3 ret 804990f: 89 c3 mov %eax,%ebx 8049911: 8b 45 04 mov 0x4(%ebp),%eax 8049914: c7 04 24 c0 98 04 08 movl $0x80498c0,(%esp) 804991b: 89 44 24 04 mov %eax,0x4(%esp) 804991f: e8 68 f3 ff ff call 8048c8c <__cyg_profile_func_exit@plt> 8049924: 89 1c 24 mov %ebx,(%esp) 8049927: e8 f0 f3 ff ff call 8048d1c <_Unwind_Resume@plt> 804992c: 90 nop 804992d: 90 nop 804992e: 90 nop 804992f: 90 nop
Remarquez que la fonction d'instrumentation est appelée en premier,
et que seulement après le registre eax
est
positionné à la valeur de retour. Par conséquent, si nous voulons
absolument récupérer la valeur de retour, nous sommes obligés de compiler
avec l'option -O0
.
Voici finalement les résultats en sortie. Dans l'interpréteur de commandes, tapez :
$ export CTRACE_PROGRAM=./cpptraced $ LD_PRELOAD=./libctrace.so ./cpptraced
T0xb7c0f6c0: 0x8048d34 main (0 ...) [from ] ./cpptraced: main(argc=1) T0xb7c0ebb0: 0x80492d8 thread1(void*) (1 ...) [from ] T0xb7c0ebb0: 0x80498b2 D (134605416 ...) [from cpptraced.cpp:91] T0xb7c0ebb0: 0x8049630 B (134605416 ...) [from cpptraced.cpp:66] B::B() T0xb7c0ebb0: 0x8049630 B => -1209622540 [from ] D::D(int=-1210829552) T0xb7c0ebb0: 0x80498b2 D => -1209622540 [from ] Hello World! It's me, thread #1! ./cpptraced: done. T0xb7c0f6c0: 0x8048d34 main => -1212090144 [from ] T0xb740dbb0: 0x8049000 thread2(void*) (2 ...) [from ] T0xb740dbb0: 0x80498b2 D (134605432 ...) [from cpptraced.cpp:137] T0xb740dbb0: 0x8049630 B (134605432 ...) [from cpptraced.cpp:66] B::B() T0xb740dbb0: 0x8049630 B => -1209622540 [from ] D::D(int=-1210829568) T0xb740dbb0: 0x80498b2 D => -1209622540 [from ] Hello World! It's me, thread #2! T#2! T0xb6c0cbb0: 0x8049166 thread3(void*) (3 ...) [from ] T0xb6c0cbb0: 0x80498b2 D (134613288 ...) [from cpptraced.cpp:157] T0xb6c0cbb0: 0x8049630 B (134613288 ...) [from cpptraced.cpp:66] B::B() T0xb6c0cbb0: 0x8049630 B => -1209622540 [from ] D::D(int=0) T0xb6c0cbb0: 0x80498b2 D => -1209622540 [from ] Hello World! It's me, thread #3! T#1! T0xb7c0ebb0: 0x80490dc wrap_strerror_r (134525680 ...) [from cpptraced.cpp:105] T0xb7c0ebb0: 0x80490dc wrap_strerror_r => -1210887643 [from ] T#1+M2 (Success) T0xb740dbb0: 0x80495a0 D::m1(int, int) (134605432, 3, 4 ...) [from cpptraced.cpp:141] D::m1() T0xb740dbb0: 0x8049522 B::m2(int) (134605432, 14 ...) [from cpptraced.cpp:69] B::m2() T0xb740dbb0: 0x8048f70 f1 (14 ...) [from cpptraced.cpp:55] f1 14 T0xb740dbb0: 0x8048ee0 f2(int) (74 ...) [from cpptraced.cpp:44] f2 74 T0xb740dbb0: 0x8048e5e f3 (144 ...) [from cpptraced.cpp:36] f3 144 T0xb740dbb0: 0x8048e5e f3 => 80 [from ] T0xb740dbb0: 0x8048ee0 f2(int) => 70 [from ] T0xb740dbb0: 0x8048f70 f1 => 60 [from ] T0xb740dbb0: 0x8049522 B::m2(int) => 21 [from ] T0xb740dbb0: 0x80495a0 D::m1(int, int) => 30 [from ] T#2! T#3!
Notez comment libbfd
échoue
à résoudre certaines adresses quand la fonction est
inline.
Les codes sources de cet article -non traduits NdT- sont disponibles dans une archive unique à cette adresse.
Soyez certain d'utiliser binutils 2.18, ou
alors il vous manquera certains fichiers d'entêtes importants
(Debian
« Etch » ne contient que la version 2.17
actuellement). Vous pouvez tester le code sans installer
binutils 2.18, le fichier
Makefile
accédant au répertoire de construction de
binutils (changez le chemin pour pointer sur le
répertoire où vous avez décompressé les sources).
Veuillez noter que le code a été conçu pour l'architecture IA32 sur plate-forme Intel™ 32-bits. Nous avons essayé de l'exécuter sur un système x86_64 avec quelques modifications, mais finalement abandonné. Si vous portez les exemples sur plate-forme AMD™ x86_64, merci d'envoyer les patches à l'auteur --René.
Aurelian est un programmeur de métier. Il développe parfois sous Windows™, parfois sous
Linux
, et parfois sur des systèmes embarqués. Il a découvertLinux
en 1998 et apprécie de l'utiliser depuis. Il travaille couramment sousDebian
.
L'adaptation française de ce document a été réalisée dans le cadre du Projet de traduction de la Gazette Linux.
Vous pourrez lire d'autres articles traduits et en apprendre plus sur ce projet en visitant notre site : http://wiki.traduc.org/Gazette_Linux.
Si vous souhaitez apporter votre contribution, n'hésitez pas à nous rejoindre, nous serons heureux de vous accueillir.