Tout est dans les libs – Construire un système de plugin en utilisant le chargement dynamique | shoppingmaroc.net


Les bibliothèques partagées sont nos meilleurs amis pour étendre la fonctionnalité des programmes C sans réinventer la roue. Ils offrent une collection de fonctions exportées, de variables et d'autres symboles que nous pouvons utiliser dans notre propre programme comme si le contenu de la bibliothèque partagée faisait directement partie de notre code. La façon habituelle d'utiliser de telles bibliothèques est de les lier simplement au moment de la compilation, et de laisser l'éditeur de liens résoudre tous les symboles externes et s'assurer que tout est en place lors de la création de notre fichier exécutable. Chaque fois que nous exécutons notre exécutable, le chargeur, une partie du système d'exploitation, va essayer de résoudre à nouveau tous les symboles, et charger chaque bibliothèque requise en mémoire, avec notre exécutable lui-même.

Voulez-vous ajouter des bibliothèques au moment de la compilation, mais au lieu de les charger au besoin pendant l'exécution? Au lieu d'une dépendance prédéfinie sur une bibliothèque, nous pourrions rendre sa présence facultative et ajuster les fonctionnalités de notre programme en conséquence. Eh bien, nous pouvons le faire avec le concept de chargement dynamique . Dans cet article, nous examinerons le chargement dynamique, comment l'utiliser, et que faire avec, y compris la construction de notre propre système de plugin. Mais d'abord, nous regarderons de plus près les bibliothèques partagées et nous en créerons une nous-mêmes.

Notez que certains détails peuvent varier selon les architectures, et tous les exemples se concentrent sur x86_64 Linux, bien que les principes soient identiques systèmes, y compris Linux sur ARM (Raspberry Pi) et d'autres systèmes de type Unix.

Construction d'une bibliothèque partagée

La première étape vers des bibliothèques dynamiquement chargeables est la bibliothèque partagée normale. Les bibliothèques partagées sont juste une collection de code de programme et de données, et il n'y a rien de trop mystérieux à leur sujet. Ce sont des fichiers ELF comme un exécutable normal, sauf qu'ils n'ont généralement pas de fonction main () en guise de point d'entrée, et leurs symboles sont disposés de manière à ce que tout autre programme ou bibliothèque puisse les utiliser comme nécessaire dans leur propre contexte. Pour les organiser de cette façon, nous utilisons gcc avec l'option -fPIC pour générer un code indépendant de la position . Prenez le code suivant et placez-le dans un fichier libfunction.c .

 int double_me (valeur int)
{
    valeur de retour + valeur;
}

Oui, c'est tout ce qu'il va y avoir, une simple fonction double_me () qui va doubler une valeur donnée et la renvoyer. Pour transformer cela en notre propre bibliothèque partagée libmylib.so nous commençons par compiler le fichier C en tant que fichier objet indépendant de la position, puis le lions en tant que bibliothèque partagée:

 $ gcc -c -fPIC libfunction .c
$ gcc -shared -o libmylib.so libfunction.o

Bien sûr, nous pouvons le combiner en un seul appel à gcc et éviter les fichiers objets intermédiaires. Notez que vous voudrez peut-être ajouter un soname avec l'option -Wl, -soname, et ajouter du versioning au fichier de sortie, mais pour plus de simplicité, nous laissons cela de côté maintenant.

 $ gcc -shared -fPIC -o libmylib.so libfunction.c

De toute façon, nous avons maintenant notre propre bibliothèque partagée libmylib.so alors allons-y et utilisons-le.

 // fichier main.c
#include 

// déclare la fonction, idéalement la bibliothèque a un fichier .h pour cette
int double_me (int);

int main (vide)
{
    int i;
    pour (i = 1; i <= 10; i ++) {
        // appelle notre fonction de bibliothèque
        printf ("% d doublé est% d  n", i, double_me (i));
    }
    renvoie 0;
}

Maintenant, nous devons juste nous rappeler de lier notre bibliothèque quand nous compilons le fichier, et ajouter notre répertoire de travail actuel à la liste des chemins que gcc devrait chercher pour trouver les bibliothèques. Gardez à l'esprit que les noms de fichiers de bibliothèque doivent être sous la forme lib nom_bibliothèque .so et sont ensuite liés via -l nom_bibliothèque . [19659006] $ gcc -o Main main.c -L. -lmylib

Cela devrait garder le lien heureux et nous sortir notre principal exécutable. Mais qu'en est-il du chargeur? Trouvera-t-il automatiquement notre bibliothèque?

 $ ./main
./main: erreur lors du chargement des bibliothèques partagées: libmylib.so: impossible d'ouvrir le fichier objet partagé: aucun fichier ou répertoire de ce type

Eh bien c'est un gros problème, et ça montre que dire à l'éditeur de liens (partie de la suite du compilateur) à propos de notre bibliothèque ne va pas rendre le chargeur (une partie de l'OS) magique par rapport à son emplacement. Pour savoir quelles bibliothèques sont requises, ainsi que la situation du chargeur pour résoudre ces dépendances, nous pouvons utiliser la commande ldd . Pour obtenir plus de sorties de débogage du chargeur, nous pouvons définir la variable d'environnement LD_DEBUG = all lors de l'appel de notre exécutable.

Donc, pour que le chargeur trouve notre bibliothèque, nous devons lui dire où rechercher, soit en ajoutant le répertoire correct à la variable d'environnement LD_LIBRARY_PATH soit en l'ajoutant aux chemins ldconfig dans /etc/ld.so.conf dans le répertoire /etc/ld.so.conf.d/ . Essayons avec la variable d'environnement pour le moment.

 $ LD_LIBRARY_PATH =.: $ LD_LIBRARY_PATH ./main
1 doublé est 2
2 doublé est 4
...
10 doublé est 20
$

Oui, le chargeur va maintenant trouver notre bibliothèque et exécuter avec succès l'exécutable

Chargement dynamique d'une bibliothèque partagée

Pour notre prochain tour, nous utiliserons le chargement dynamique pour lire la bibliothèque dans notre code lors de l'exécution. Une fois chargés, nous pouvons y rechercher des symboles et les extraire dans des pointeurs, puis les utiliser comme si la bibliothèque était liée en premier lieu. Les systèmes Unix et Unix-like fournissent libdl pour cela. Voyons comment nous pouvons appeler notre fonction double_up () de cette façon.

 // dynload.c
#include 
#include 

int main (vide) {
    // gère les fonctions de chargement dynamiques
    void * handle;

    // pointeur de fonction pour la fonction double_me () de la bibliothèque
    int (* double_me) (int);

    // juste un compteur
    int i;

    // ouvre notre bibliothèque
    if ((handle = dlopen ("./ libmylib.so", RTLD_LAZY)) == NULL) {
        retour 1;
    }

    // essaie d'extraire le symbole "double_me" de la bibliothèque
    double_me = dlsym (handle, "double_me");
    if (dlerror ()! = NULL) {
        dlclose (handle);
        retour 2;
    }

    // utilise double_me () comme avec une bibliothèque régulièrement liée
    pour (i = 1; i <= 10; i ++) {
        printf ("% d doublé est% d  n", i, double_me (i));
    }

    dlclose (handle);
    renvoie 0;
}

Nous essayons de charger notre bibliothèque en utilisant la fonction dlopen () qui retourne un handle de pointeur générique en cas de succès. Nous pouvons ensuite trouver et extraire le symbole double_me de notre bibliothèque avec dlsym () en lui passant le handle précédemment retourné. Si le symbole est trouvé, dlsym () renvoie son adresse en tant que void * qui peut être affectée à un type de pointeur (de préférence correspondant) représentant le symbole. Dans notre cas, un pointeur de fonction prend un int comme paramètre et renvoie int tout comme notre fonction double_me () . Si tout a réussi, nous pouvons appeler la fonction nouvellement extraite comme si elle était là depuis le début, et la sortie sera exactement la même. N'oubliez pas de lier libdl lors de la compilation.

 $ gcc -o dynload dynload.c -ldl
$ ./dynload
1 doublé est 2
2 doublé est 4
...
10 doublé est 20
$

Voilà, au lieu de créer des liens au moment de la compilation, nous avons chargé notre bibliothèque à l'exécution, et après avoir extrait nos symboles, nous pouvons l'utiliser comme auparavant. Certes, l'utilisation de chargement dynamique uniquement en remplacement de l'éditeur de liens n'est pas très utile en soi. Une utilisation plus courante du chargement dynamique consiste à étendre les fonctionnalités principales d'un programme en intégrant un système de plug-in permettant aux utilisateurs d'ajouter des composants externes en fonction de leurs besoins. Un excellent exemple est le serveur web Apache qui a une longue liste de modules à ajouter individuellement à votre guise. Bien sûr, nous allons nous concentrer sur une approche beaucoup plus simple ici.

Construire son propre système de plugin

Prenez le bon vieux jeu des enfants Téléphone (ou murmure chinois, courrier chuchoté, téléphone cassé, etc. ). Quelqu'un commence par un message et il se fait chuchoter, et le dernier enfant dit ce que le message initial était supposé être. Eh bien, cela ressemble à quelque chose qu'un groupe de plugins pourrait faire en passant un message de l'un à l'autre, ce qui gâcherait légèrement les données au fur et à mesure. Nous écrirons le code pour lancer le système téléphonique, et n'importe qui peut contribuer un enfant / plugin.

En tant qu'API, disons que le plugin prend un pointeur sur le message et une longueur comme paramètres et modifie le message directement dans Mémoire. Appelons-le simplement process donc la fonction ressemblerait à void process (char **, int) . C'est ce qu'un plugin avec une fonction process () qui met chaque caractère en majuscule pourrait ressembler à:

 // fichier plugin-uppercase.c
inclure 

processus vide (char ** message, int len)
{
    int i;
    char * msg = * message;

    pour (i = 1; i <len; i + = 2) {
        msg [i] = toupper (msg [i]);
    }
}

Essayons maintenant de le transformer en un fichier majuscule.plugin et supposons que nous avons deux autres plugins, increase.plugin qui augmente chaque chiffre trouvé, et leet .plugin qui fait justement notre message: l337

 $ gcc -shared -fPIC -o majuscule.plugin plugin-majuscule.c
$ gcc -shared -fPIC -o augmentation.plugin plugin-increase.c
$ gcc -shared -fPIC -o leet.plugin leet-replace.c
$

Notre programme principal prendrait alors un message comme premier argument, et un nombre arbitraire de fichiers plugin comme le reste de la liste d'arguments. Il va charger les plugins un par un, passer le message d'un plugin à l'autre via leurs fonctions process () puis imprimer le résultat. (Pour se concentrer, nous prétendons que nous vivons dans un petit monde parfait où les erreurs ne se produisent pas.)

 // file telephone.c
#include 
#include 
#include 

int main (int argc, char ** argv) {
    void * handle;
    void (* processus) (char **, int);
    indice int;

    if (argc <3) {
        printf ("usage:% s    [...]  n", argv [0]);
        retour 1;
    }

    // argv [1] est le message, commence à partir de l'index 2 pour la liste des plugins
    pour (index = 2; index <argc; index ++) {
        // ouvre le prochain plugin
        handle = dlopen (argv [index]RTLD_NOW);
        // extrait la fonction process ()
        process = dlsym (gérer, "traiter");
        // appelle la fonction process en modifiant argv [1] directement
        processus (& argv [1]strlen (argv [1]));
        // ferme le plugin
        dlclose (handle);
    }

    // imprime le message résultant
    printf ("% s  n", argv [1]);
    renvoie 0;
}

Tout comme avant, nous chargeons le fichier plugin (une librairie partagée dynamiquement chargée), extrayons la fonction dont nous avons besoin et l'exécutons - seulement cette fois en boucle. Alors, compilons et testons-le.

 $ gcc -o telephone telephone.c -ldl
$ ./telephone "bonjour hackaday" ./uppercase.plugin
hELoHaKoDaY
$ ./telephone "bonjour hackaday" ./uppercase.plugin ./leet.plugin
h3lL0 h4cK4D4Y
$ ./telephone "bonjour hackaday" ./uppercase.plugin ./leet.plugin ./increase.plugin
h4lL1 h5cK5D5Y
$

Comme prévu, chaque plugin modifiant le message d'entrée à sa manière, la quantité et l'ordre des plugins donnés en paramètre à notre programme principal affecteront le message final. Maintenant, cela peut ne pas compter beaucoup comme exemple de traitement de données, mais le même concept peut bien sûr être utilisé pour des scénarios plus utiles. Si vous êtes intéressé par l'implémentation complète, vous pouvez le trouver sur GitHub. Notez également que notre programme principal n'a jamais changé, et si nous décidons de faire des ajustements à l'un des plugins, nous n'avons qu'à recompiler ce plugin. Nous pourrions même ajouter des mécanismes au programme principal pour recharger les plugins, et nous n'aurions même pas à redémarrer le programme principal lui-même

RIPberry Pi GPIO Monitor

Un de ces scénarios plus utiles qui suivraient les mêmes principes pourrait être un programme qui surveille les broches GPIO sur un Raspberry Pi. Nous aurions différents plugins qui peuvent tous gérer toutes les informations que notre programme principal lit à partir des GPIO. Chaque plugin aurait un ensemble de fonctions de base qu'il pourrait implémenter: une fonction pour la phase de configuration du plugin, une pour gérer chaque changement d'état des GPIOs, et une pour supprimer le plugin quand il n'est plus utilisé. Un plugin peut gérer le changement d'entrée sur une broche pour changer l'état d'une broche de sortie, un autre peut effectuer certaines tâches lorsqu'une broche d'entrée spécifique devient haute, et une troisième peut écrire toutes les modifications d'état dans un fichier journal.

En fin de compte, la partie de chargement dynamique ne sera pas très différente de celle de l'exemple précédent, et entrer dans les détails d'un tel moniteur GPIO irait au-delà de la portée de cet article. Cependant, nous ne le mentionnerions pas si nous ne l'avions pas implémenté, donc un moniteur GPIO basique peut également être trouvé sur GitHub.

Où aller à partir d'ici

Avec le chargement dynamique, nous avons vu une approche alternative à liaison à la compilation qui facilite l'extension de la fonctionnalité principale de notre programme avec des bibliothèques externes. Bien qu'il ajoute un peu de complexité pour extraire les symboles d'une bibliothèque, les principes principaux sont simples et directs: vous ouvrez une bibliothèque, vous en extrayez des symboles et vous la fermez.
Cependant, cette simplicité a aussi son défaut: pour étendre les fonctionnalités d'un programme grâce à un chargement dynamique, nous devons savoir à l'avance ce que nous pouvons trouver dans la bibliothèque ou le plugin chargé. Nous ne pouvons pas simplement ajouter une fonction complètement nouvelle et espérer que notre programme en saura par magie à la volée. Mais si vous concevez votre programme principal avec ces limitations à l'esprit, le chargement dynamique vous donnera un moyen flexible d'étendre les fonctionnalités si nécessaire.

Notez que nous avons ouvert une boîte de Pandore contenant des problèmes de sécurité. Si des fonctions externes arbitraires peuvent s'exécuter dans notre code principal, elles sont aussi sûres que les bibliothèques auxquelles elles sont liées dynamiquement. Abuser de cette confiance est à la base des attaques par injection de DLL ou du détournement de DLL. Si un attaquant peut tromper le système d'exploitation en chargeant le programme appelant de sa bibliothèque dynamiquement chargeable, ils l'ont emporté.

Puisque le chargement dynamique aura besoin du support du système d'exploitation, ce n'est pas vraiment un 8 bits environnement de microcontrôleur. Vous aurez toujours des pointeurs de fonction.

Quelques mots sur les symboles recherchés

Vous pourriez penser que si dlsym () peut résoudre des symboles dans un fichier chargé dynamiquement, il doit y avoir un moyen de trouver également les symboles disponibles en premier lieu, peut-être obtenir une liste complète d'entre eux. Eh bien, oui, les outils communs binutils tels que lire ou nm font exactement cela, avec l'aide de la bibliothèque des descripteurs de fichiers binaires libbfd . En outre, l'extension GNU de notre bibliothèque de chargement dynamique libdl offre la fonction dlinfo () pour obtenir plus d'informations sur le fichier chargé. Quelques lectures supplémentaires sur le format de fichier ELF sont recommandées avant de descendre dans ce trou de lapin.


raspberry pi 3 maroc
Acheter raspberry pi 3 ICI

Source

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *