Wiki

Clone wiki

pymoult / Cymoult - Libama

Objectif du projet

Afin de réaliser une mise à jour, la DSU nécessite d'obtenir le contrôle sur l'exécution d'une application. Cela permet à la fois de remplacer les fonctions, changer les variables, mais aussi de réaliser l'opération sans risque. Ce contrôle est offert par un composant nommé "Manager".

Il s'agit donc de concevoir et implémenter un tel manager pour des programmes C. Dans un premier temps, il a fallu réfléchir aux moyens utilisables pour attacher un manager à un programme C et pour lui permettre de contrôler l'application. Dans un second temps, il a été question d'implémenter des premiers managers C grâce aux systèmes d'attachement préalablement trouvés. Dans un troisième temps, il s'est agit de rassembler ces différentes parties sous forme d'une bibliothèque.

L'intérêt à long terme est de pouvoir implémenter en langage C une bibliothèque similaire à l'API Pymoult développée pour Python.

I. Mécanismes d'attachement

Le premier cas sur lequel nous nous sommes penchés était celui d'un manager complètement extérieur au programme à mettre à jour.

Afin d'accéder à l'exécution du programme, et donc sa mémoire, nous utilisons l'appel système ptrace. Celui-ci nous permet donc de nous attacher au programme à partir de son PID. Nous pouvons alors vérifier les critères d'altérabilité du programme et les éventuels pré-requis de mise à jour. Nous pouvons ensuite appliquer la mise à jour en mettant en pause le programme au moment opportun et en remplaçant les variables et fonctions dans la mémoire.

Les droits attribués à ptrace par défaut ne permettant pas de modifier n'importe quel processus, il est nécessaire dans ce cas de les lui fournir :

#!bash

$ echo 0 > /proc/sys/kernel/yama/ptrace_scope
Nous avons ensuite imaginé que le programme à modifier soit lancé à travers le manager. Le manager est là encore extérieur, mais il possède par défaut les droits sur ptrace pour accéder au programme à mettre à jour. Il n'y a alors pas besoin de modifier le ptrace_scope. Le reste des mécanismes de mise à jour restent identique.

Dans un troisième cas, nous avons considéré que le manager était déjà présent à l'intérieur du programme avant la mise à jour. Il n'y a alors pas à utiliser ptrace, ni pour s'attacher au programme puisque nous sommes dedans, ni pour en modifier la mémoire. Nous pouvons pensé à d'autres méthodes de vérification du critère d'altérabilité qui seraient par exemple des points d'arrêt sûrs définis par le programmeur à l'avance dans le programme. Lorsque ces points sont atteints, la mise à jour et sa redéfinition pourrait alors s'exécuter de manière sûre (en supposant que le programmeur sait ce qu'il fait). Ce troisième cas n'a pas encore d'implémentation.

II. Premières implémentations

Le projet est disponible dans le répertoire pymoult/cymoult/libama (actuellement sur la branche cymoult). Les premières implémentations manager.c et manager2.c correspondent aux deux premiers cas de la partie I. Afin de les compiler, deux choses sont nécessaires :

  • Avoir compilé le projet pymoult/cymoult/libuminati

  • Ajouter au LD_LIBRARY_PATH le chemin vers libuminati/lib/bin

Depuis le passage à la libama, ces implémentations ne sont plus tenues à jour et ne compilent donc plus si vous avez une version à jour de la libuminati. Cependant, elles possèdent un équivalent utilisant la libama (cf partie III).

En dehors du déclenchement de la mise à jour, le principe des deux managers reste le même : le programme possède un répertoire dynamic_updates/ qui attend les mises à jour. Lorsqu'une mise à jour est déclenchée, ce répertoire est vérifié. S'il possède un fichier, nous récupérons les informations de débug, puis nous passons à la vérification de l'altérabilité et au remplacement des fonctions (une par une). Dans cette première version naïve, si nous ne pouvons pas mettre à jour, nous laissons le programme continuer puis nous revenons 10 secondes plus tard. Cette attente est remplacée dans les nouvelles versions où nous sommes "prévenus" dès lors que la dernière occurrence de la fonction à mettre à jour sort de la pile.

Le déclenchement du manager2.c est automatique : toutes les 5 secondes, il vérifie l'état du répertoire dynamic_updates/. Le manager.c, quant à lui, attend de recevoir un signal SIGUSR1 de la part du développeur avant de lancer la mise à jour (la vérification du dossier).

Le choix de commencer par des managers extérieurs au programme a été motivé par la volonté de traiter un cas général où le programme n'a pas, à la base, été prévu pour réaliser une mise à jour dynamique grâce à notre plateforme. De plus, la littérature nous a permis rapidement d'identifier l'appel système ptrace comme une solution pour accéder à la mémoire du programme.

III. Passage à une bibliothèque

Les mécanismes étant identiques à partir du déclenchement de la mise à jour, nous avons donc regroupé et organisé les informations autour des managers sous forme d'une bibliothèque C nommée libama. Celle-ci possède les mêmes prérequis que les premières implémentations, puis :

#!bash

pymoult/cymoult/libama$ make libama
La bibliothèque met en place 2 structures de données:

  • ama_program_infos contenant les informations relatives au programme à mettre à jour : son nom, son dossier, et son PID.

  • ama_update_infos contenant les informations relatives à la mise à jour : le dossier de mise à jour (dynamic_updates/ par défaut), la liste des noms de fonctions à mettre à jour ainsi que celles qui les remplacent, et son état (mise à jour en cours ou non).

Une première partie des fonctions sert à accéder et initialiser ces données :

  • ama_init_program_infos_from_pid : remplit une structure ama_program_infos à partir du pid du programme à mettre à jour. Elle appelle les deux fonctions suivantes pour le faire.

  • ama_get_program_name_from_pid : remplit le nom du programme à partir du fichier /proc/pid/comm.

  • ama_get_program_directory_from_pid_name : remplit le chemin du répertoire contenant le programme à partir du pid du programme à mettre à jour. Ce répertoire est différent du répertoire d'exécution (par exemple si l'on exécute ../program_rep/prog). Il est nécessaire dans le cas où l'on souhaite écouter un répertoire de mise à jour fixe (plus fixe que le répertoire d'exécution) comme dans le cas de nos 2 managers.

  • ama_init_update_infos_from_program_infos : remplit une structure ama_update_infos à partir des informations du programme (ama_program_infos préalablement initialisée). Utilise la fonction suivante pour remplir le nom du répertoire de mise à jour (forcé à dynamic_updates/ ici mais prévoyant de laisser le choix libre au développeur à l'avenir).

  • ama_get_update_directory_from_program_directory_udirname : remplit le chemin du répertoire à écouter contenant les éventuels fichiers d'update à partir du répertoire du programme et du nom du répertoire d'update.

  • ama_set_update_functions_list : remplit la liste des noms de fonctions à mettre à jour ainsi que celles qui les remplacent à partir des listes passées en paramètre.

Une seconde partie des fonctions sert à lancer et exécuter la mise à jour :

  • ama_check_updates_from_repository : cherche s'il existe des fichiers de mise à jour dans le répertoire approprié et lance la mise à jour à partir du fichier trouvé.

  • ama_start_update_from_file : s'attache au programme (um_attach), récupère les informations de débug (um_init qui remplit une structure um_data), récupère l'état de la pile (um_unwind qui remplit une structure um_data), lance la modification des variables (non fonctionnelle sous Ubuntu donc non implémentée complètement ici, mais fonctionnelle dans le projet libuminati sous Arch) et la modification des fonctions fournies par le manager. Utilisation de la fonction suivante.

  • ama_update_function : la nouvelle version de cette fonction attend que la dernière occurrence de la fonction dans la pile soit retournée avant de mettre à jour la fonction par la nouvelle (um_wait_out_of_stack avant de um_redefine). A titre informatif, la redéfinition est simplement un jump vers la nouvelle fonction dans l'ancienne (cf projet libuminati).

Les managers tels qu'ils ont été développés ici remplissent un rôle bien plus grand que l'appellation "Manager" dans le sens où ils s'occupent à la fois de la partie Listener (quand détecter une mise à jour), de la partie Manager (comment mettre à jour) et de la partie Update (code contenant la mise à jour).

Les deux premières implémentations de la partie II ont été refaites en utilisant la libama et nous les retrouvons sous lamanager.c et lamanager2.c. Leur fonctionnement général est inchangé, mais le code appelé à travers la libama tient compte des mises à jours apportées dans la libuminati. De plus, ils permettent de pouvoir gérer quelles fonctions sont à mettre à jour.**

IV. Objectifs futurs

Un manager générique, issu des réflexions autours de Pymoult, a été ré-implémenté par Sébastien Martinez en C. Celui-ci a pour but de fournir un manager qui serait capable de s'adapter à la mise à jour à réaliser. Il permet également de définir ces managers dans le cas d'applications multi-threadées. Ce manager étant créé notamment dans l'optique d'être interne à l'application (3ème cas de la partie I), et il serait intéressant de réaliser une combinaison de celui-ci avec les managers externes précédemment développés pour concevoir un manager externe générique.

L'idée sous-jacente est d'arriver à réaliser une API similaire à Pymoult. Pour cela, il faut concevoir une architecture en C qui permettrait de séparer complètement les parties Listener, Manager et Update, tout en respectant des nouvelles problématiques soulevées par le langage C (la possibilité d'avoir des managers externes par exemple).

>> Démos

Updated