Comment faire des plugins en utilisant la librairie lidl . Ces fonctions sont à la base de la notion des "plug-in" en C et C++, leur utilisation me semble encore trop restreinte. Voyons à travers quelques exemples ce que nous pouvons en faire.
Voir la suite ...
Notice
Le présent document est publié sous la licence
« Verbatim Copying »
telle que définie par la Free Software Fondation dans le cadre du projet
GNU.
Toute copie ou diffusion de ce document dans son intégralité est permise (et même encouragée),
quel qu'en soit le support à la seule condition que cette notice, incluant les copyrights, soit préservée.
Copyright (C) 2003, 2004 Yann LANGLAIS, ilay, Intrasys.
La plus récente version en date de ce document est sise à l'url http://ilay.org/yann/articles/dlfcn/.
Toute remarque, suggestion ou correction est bienvenue et peut être adressée à Yann LANGLAIS, ilay.org.
Introduction
En 1992, alors que je découvrais les joies de l'édition de texte avec emacs, une question m'est venue à
l'esprit. Pourquoi diable les extensions d'emacs ont-elles été écrites en elisp (emacs-lisp) et non en C?
La réponse était simple. Il n'existait pas alors de possibilité standard en C de charger et décharger
des bibliothèques en cours d'exécution.
Quelques années plus tard, et bien qu'emacs ait été remplacé par vi dans mes habitudes dactylographiques,
je découvris avec plaisir un outil qui rendait caduque mon explication. Je venais de tomber par hasard sur dlfcn.h.
Ce fichier d'entète défini les prototypes de points d'entrée fort appréciable et permettant de charger et
décharcher des bibliothèques et de récupérer des points d'entrée ou des symboles (variables) et de les
utiliser a volonté.
Aujourd'hui, et bien que ces fonctions soient à la base de la notion des "plug-in" en C et C++, leur utilisation
me semble encore trop restreinte. Voyons à travers quelques exemples ce que nous pouvons en faire.
Présentation de la libdl.so
Présentation générale
Description des points d'entrée
dlopen
extern void * dlopen(const char *, int);
Cette fonction charge en mémoire la bibliothèque désignée par la chaîne de caractère passée en
premier paramètre.
Le second paramètre est un drapeau qui fixe le mode de fonctionnement de l'éditeur de lien à la volée.
Ce second peut prendre les valeurs (documentées) suivantes :
- RTLD_LAZY, pour ne résoudre les symboles de la bibliothèque à ouvrir que lorsque ceux-ci sont explicitement demandés,
- RTLD_NOW, pour resoudre tous les symboles lors du chargement de la bibliothèque.
En complément de ces valeurs, il existe une co-valeur RTLD_GLOBAL augmentant la portée des
symboles en les rendant disponibles externes (i.e. not préfixes en C par static) pour les
bibliothèques chargées par la suite.
Elle s'empoie en l'accolant par un ou binaire « | »
à l'une des valeur possible du drapeau :
RTLD_LAZY | RTLD_GLOBAL ou RTLD_NOW | RTLD_GLOBAL
dlopen retourne un pointeur vers une poignée (handle) caractéristique de la bibliothèque sous
forme d'un pointeur générique (void *). Celui-ci sera nul en cas d'échec.
Notons que si dlopen renvoie NULL, la variable globale errno ne devrait pas avoir été
modifiée et que strerr() ne pourra donc fournir d'explication correcte.
Il faudra pour cela appeler dlerror().
dlsym
extern void * dlsym(void *, const char *);
dlsym se charge de retrouver (résoudre) l'adresse d'un symbole dont le nom est passé en
paramètre.
Le premier paramètre passé est un pointeur vers la « poignée » retournée par
dlopen.
Il existe deux poignées paticulières définies lorsque _GNU_SOURCE est définie.
Ces poignées sont définie par les symboles RTLD_DEFAULT et RTLD_NEXT.
RTLD_DEFAULT précise à dlsym qu'il faut rechercher la première occurence du
symbole dans les bibliothèques déjà chargées en mémoire, dans l'ordre de leur chargement.
RTLD_NEXT stipule à dlsym qu'il lui faut chercher l'occurence suivante du symbole
dans les bibliothèques suivant la bibliothèque en cours. Ce pseudo-pointeur ne fonctionne que pour les
bibliothèques partagées.
Le second paramètre est le symbole à rechercher.
dlsym retourne le pointeur sur le symbole demandé, ou, si celui-ci n'est pas retrouvé, un poineur NULL.
dlclose
extern int dlclose(void *);
dlerror
extern char * dlerror(void);
dladdr
extern int dladdr(void *, Dl_info *);
Principe des "plug-in" ou greffons
Le principe des greffons se base sur 2 points. Le premier est constitué par les fonctions dl* (ou des fonctions équivalentes) et le second
par la définition d'un « protocole » représenté par un nombre prédédini de points d'entrée
aux prototypes fixés, voire par la définitions d'une API particulière.
Les implémentations de ce principe sont nombreuses. Les modules du noyau linux souscrit à ce principe.
Vers l'auto-programmation
Le mythe de l'auto-programmation n'est pas récent. Le concept lisp » le programme est une
liste et la liste est [potentiellement] un programme » en témoigne.
En règle générale, les langages de script sont des instruments de choix pour l'autoprogrammation ou la
génération de code spécifique conditionnel et son evaluation à la volée :
#!/bin/zsh
cmd="ls -alrt $1"
eval $cmd
Le BASIC des années 80 avait sa commande « merge » qui permettait de créer du code dans
un fichier et de le reinjecter par la suite.
De nombreux programmeur, dont je fais partie, ont un peu été déroutés par l'absence d'équivalent en C.
Mais, à l'étape de compilation près, les fonctions dl* sont maintenant disponibles pour mimer ce
fonctionnement :
#include
#include
#include
#include
#include
/* Compilation : gcc -o dltest -g dltest.c -ldl */
int main(void) {
int fd;
void *lib;
void (*funct)();
char code[] = "#include
void hello() { printf("hello, worldn"); }
";
/* Create hello.c */
if ((fd = open("./hello.c", O_WRONLY | O_CREAT, 0640)) /* compile hello.c */
if (system("gcc -shared -fPIC -G hello.c -o hello.so") /* read hello.so */
if (!((lib = dlopen("./hello.so", RTLD_LAZY))))
return 3;
/* retrieve "hello" symbol pointer */
if (!(funct = dlsym(lib, "hello"))) {
dlclose(lib);
return 4;
}
/* call funct() */
funct();
dlclose(lib);
return 0;
}
Chaînage de fonctions
Détournements
admettons que nous voulions afficher un message à chaque malloc() et a chaque free(). Il nous suffit de réécrire nos deux fonctions et de les faire charger en lieux et place des points d'entrée système par un petit tour de passe-passe. Cependant, il nous faut aussi appeler les fonctions du système afin d'effectuer les opérations d'allocation ou de désallocation.
Le prémier tour de passe-passe (le chargement de notre code au lieu du code système) peut se faire de deux façons :
- en recompilant le programme cible et en forcant l'éditeur de lien à utiliser notre bibliothèque,
- en demandant le préchargement de notre bibliothèque (nettement plus élégant, et presque toujours possible.
Concentrons nous sur la seconde possibilité. Il nous suffit, avant lancement du programme cible, de renseigner la variable LD_PRELOAD :
# en sh/ksh/bash/zsh ...:
LD_PRELOAD=malib.so programme_cible
# en csh/tcsh:
(setenv LD_PRELOAD malib.so programme_cible)
Supposons que notre programme cible tst_alloc soit le suivant :
#include
int main() {
char *str;
if (!(str = (char *) malloc(32))) return 1;
free(str);
return 0;
}
que nous compilons de la manière suivante :
gcc -o tst_alloc tst_alloc.c
Supposons maintenant le code mymalloc.c suivant que nous voulons executer :
#include
void * malloc(size_t size) {
printf("malloc(%d)
", size);
return NULL;
}
void free(void *p) {
printf("free(%x)
", p);
}
que nous compilerons de la façon suivante afin d'en faire une bibliothèque :
gcc -shared -o libmymalloc.so mymalloc.c
Le lancement de tst_malloc seul s'effectue (normalement) sans erreur ni message et retournera 0 (la variable $? vaudra 0). Inserrons maintenant notre bibliothèque:
LD_PRELOAD=libmymalloc.so tst_malloc
affichera
malloc(32)
de plus, la commande echo $? retournera 1, ce qui est tout à fait normal, puisque notre fonction malloc retournera le pointeur 0 et n'effectuera pas l'allocation.
Voyons maintenant comment appeler le VRAI malloc au sein de notre malloc. Pour cela, penchons nous sur l'aide de dlsym() (man dlsym). Nous voyons que certains defines peuvent remplacer le pointeur vers la bibliothèque (handle). En particuliers RTLD_NEXT. L'appel à dlsym(RTLD_NEXT, symbol) va rechercher le point d'entrée "symbol" dans la bibliothèque suivante (par rapport à celle en cours).
Notre bibliothèque libmymalloc.so va donc se compliquer de la sorte :
#include
#include
void * malloc(size_t size) {
static void *(*sys_malloc)(size_t) = NULL;
if (!sys_malloc) {
if (!(sys_malloc = (void *(*)(size_t)) dlsym(RTLD_NEXT, "malloc"))) {
perror("cannot fetch system malloc
");
exit(1);
}
}
printf("malloc(%d)
", size);
return sys_malloc(size);
}
void free(void *p) {
static void (*sys_free)(void *) = NULL;
if (!sys_free) {
if (!(sys_free = (void (*)(void *)) dlsym(RTLD_NEXT, "free"))) {
perror("cannot fetch system free
");
exit(2);
}
}
printf("free(%x)
", p);
sys_free(p);
}
L'étape de compilation de la bibliothèque se voit aussi légèrement modifiée :
gcc -shared -o libmymalloc.so mymalloc.c -ldl -D_GNU_SOURCE
La commande
LD_PRELOAD=libmymalloc.so tst_malloc
affichera
malloc(32)
free(20888)
(l'adresse en paramêtre de free est variable en fonction de l'architecture, entre autre) et la variable $? devrait maintenant être à 0
De nombreuses applications de cette technique sont possibles. Elles peuvent aller du débugger mémoire (voir http://ilay.org/yann/articles/memdbg/ pour un débugger plus "complet") à l'écriture de chevaux de Troie ou portes dérobées... Certaines restrictions sont cependant tout naturellement imposées pour des questions de sécurité : LD_PRELOAD est ignoré par les programmes en suid :).
Espionnage de greffons
Lorsqu'on programme des greffons, il arrive que les greffons ne se chargent pas correctements, que certains symboles ne soient pas définis, ou encore que le freffon chargé ne soit pas celui attendu. Il arrive de plus que le programme greffé ne nous transmette pas les messages d'erreur de la libdl.so. Dans ces cas, il est assez difficile de trouver les causes des dysfonctionements.
Pour contourner le problème, on peut détourner les fonctions dlsym, dlopen, dlerror et dlclose. Oui mais voilà, en détournant dlsym, comment récupérer le point d'entrée dlsym du système?
Certains systèmes nous facilitent la tache. Un nm de /usr/lib/libdl.so.1 sur Solaris 8 nous montre que les symboles dlsym, dlopen, dlclose et dlerror ont des contreparties en _dlsym, _dlopen, _dlclose et _dlerror :
nm /usr/lib/libdl.so.1 | egrep "dlsym|dlopen|dlclose|dlerror"
[37] | 2236| 8|FUNC |GLOB |0 |7 |_dlclose
[35] | 2244| 8|FUNC |GLOB |0 |7 |_dlerror
[27] | 2220| 8|FUNC |GLOB |0 |7 |_dlopen
[55] | 2228| 8|FUNC |GLOB |0 |7 |_dlsym
[26] | 2236| 8|FUNC |WEAK |0 |7 |dlclose
[53] | 2244| 8|FUNC |WEAK |0 |7 |dlerror
[47] | 2220| 8|FUNC |WEAK |0 |7 |dlopen
[44] | 2228| 8|FUNC |WEAK |0 |7 |dlsym
Les points d'entrée standard semblent bien être des alias de plus faible priorité que les originaux préfixés par un souligné. Dans ce cas, il est possible d'appeler directement _dlsym dans à l'intérieur de notre fonction détournée.
Sous Linux, par contre, ces alias n'existent pas. un nm de la libdl.so
ne nous apprendra en fait rien dans les distributions où les bibliothèques systèmes sont débarassées de leurs symboles (strip). L'utilitaire string nous donnera les points d'entrée. Las , aucun de ces points d'entrée, après consulatation des sources de la glibc et tests ne nous sera d'un grand secours.
Il faut alors se tourner vers la fonction dlvsym(void *handle, const char *name, const char *version_str) dont la documentation laisse encore à désirer, quelqu'en soit la source. Cette fonction nécessite en plus du nom du point d'entrée une chaîne de caractères représentant la version. Pour retrouver cette chaîne, il suffit d'écrire un petit bout de code faisant appel à dlsym, de le compiler et le lier avec la libdl.so, puis faire un nm sur le binaire.
nm dltest
...
08049840 W data_start
U dlclose@@GLIBC_2.0
U dlopen@@GLIBC_2.1
U dlsym@@GLIBC_2.0
...
Cette chaîne est accolée au symbole dlsym sous la forme "dlsym@@version_st" :
nm dltest | grep dlsym | cut -d@ -f3
GLIBC_2.0
Une fois cette chaîne trouvée, nous retrouvons le point d'entré, et nous pouvons réécrire nos fonctions :
#include
#include
#ifdef _LINUX_
#ifndef _GNU_SOURCE
#define _GNU_SOURCE
#endif
#endif
#include
typedef struct {
void *handle;
void *(*open)(const char *, int);
void *(*sym)(void *, const char *);
char *(*error)(void);
int (*close)(void *);
} dlspy_t;
dlspy_t __dlspy__ = { NULL, NULL, NULL, NULL, NULL };
#ifdef LINUX
#include "config.h"
#ifndef DLSPY_GLIBC_VERSION
#define DLSPY_GLIBC_VERSION "GLIBC_2.0"
#endif
#endif
void
dlspy_init() {
/* 1st step: save standard dl* functions */
#if defined(SUN)
__dlspy__.sym = (void *(*)(void *, const char *)) _dlsym;
__dlspy__.open = (void *(*)(const char *, int)) _dlopen;
__dlspy__.error = (char *(*)(void)) _dlerror;
__dlspy__.close = (int (*)(void *)) _dlclose;
#elif defined(LINUX)
if (!(__dlspy__.sym = dlvsym(RTLD_NEXT, "dlsym", DLSPY_GLIBC_VERSION))) {
perror("dlspy fatal: cannot fetch system's dlsym entry point
");
exit(1);
}
__dlspy__.open = (void *(*)(const char *, int)) __dlspy__.sym(RTLD_NEXT, "dlopen");
__dlspy__.error = (char *(*)(void)) __dlspy__.sym(RTLD_NEXT, "dlerror");
__dlspy__.close = (int (*)(void *)) __dlspy__.sym(RTLD_NEXT, "dlclose");
#else
#error ERROR: cannot get dl* original entry points.;
#endif
}
void
dlspy_echo(char *fmt, ...) {
va_list args;
char buf[1024], buf2[1024];
va_start(args, fmt);
sprintf(buf2, "dlspy: %s", fmt);
vsprintf(buf, buf2, args);
fprintf(stderr, "%s
", buf);
va_end(args);
}
char *
dlerror(void) {
static char *errstr = NULL;
char *t;
if (!__dlspy__.open) dlspy_init();
t = __dlspy__.error();
if (!t && errstr) return errstr;
if (t) {
errstr = t;
return t;
}
return (char *) strdup(" ");
}
void *
dlopen(const char *name, int mode) {
void *t = NULL;
if (!__dlspy__.open) dlspy_init();
dlspy_echo("try to open "%s" shared object with mode %d...", name, mode);
if (!(t = __dlspy__.open(name, mode))) dlspy_echo("failed (%s)
", dlerror());
else dlspy_echo("ok
");
return t;
}
void *
dlsym(void *h, const char *name) {
void *t = NULL;
if (!__dlspy__.open) dlspy_init();
dlspy_echo("try to bind symbol "%s" as new entry point...", name);
if (!(t = __dlspy__.sym(h, name))) dlspy_echo("failed (%s)
", dlerror());
else dlspy_echo("ok at %x
", t);
return t;
}
int
dlclose(void *h) {
if (!__dlspy__.open) dlspy_init();
dlspy_echo("dlclose(%p)", h);
return __dlspy__.close(h);
}
Peut-on aller plus loin?
Un de mes objectifs est de pouvoir tracer chaque appel aux fonctions
résolues par dlsym(). Pour ce faire, il semble nécessaire de définir un pool de fonctions qui
devront, lors du premier appel, prendre l'addresse du point d'entrée fraichement résolu, et dans,
un second temps, appeler le point d'entrée avec les paramètres qui lui ont été
passés, sans pour autant connaître leur nombre ni leur taille. Je n'ai pour le moment aucune
solution précise en tête, mais il me semble, a priori, qu'il faille pour ce faire jongler en
(assembleur?) avec la pile. En tout état de cause, je suis preneur de toute solution.
La tour de babel
Conclusions
Références
linux standard base v1.3, chapitre 13 Libraries
man dlopen; man dlclose; man dlsym; man dlvsym, man dladdr; man dlerror
L'éditeur de lien et ses variables d'environnement, Samuel Dralet, Linux Magazine France numéro 63, Juillet/Août 2004, page 76
Gestion de la mémoire en C, http://ilay.org/yann/articles/mem/
Structures avancées de gestion de la mémoire (listes doublement chaînées, btrees, hash coding, ...), http://ilay.org/yann/articles/toolbox/
D'autres articles techniques : http://ilay.org/yann/articles/
|