Portail informatique 2019-2020

Systèmes d'exploitation

Avant de commencer, assurez-vous d'avoir terminé le TP précédent. Vous pouvez également vous baser sur le code disponible dans la branche tp3_base du dépôt git :

Le but de cet exercice est de créer une bibliothèque fournissant des primitives de synchronisation, et de l'utiliser dans le serveur facebook développé lors des TP précédents. Pour cela, nous utilisons un mécanisme permettant de "surcharger" des fonctions: LD_PRELOAD.

Fonctionnement d'une bibliothèque partagée

Lorsqu'il génère un exécutable, l'éditeur de liens stocke le programme sous le format ELF : un segment contient le code, un segment contient les variables globales, etc. L'éditeur de lien construit une table des symboles indiquant, pour chaque symbole (fonctions et variables), dans quel segment se trouve le symbole ainsi qu'à quel offset. Si le programme utilise un symbole sans le définir, on marque le symbole comme étant situé dans la section "U". L'appel à la fonction foo (située dans une bibliothèque partagée) est ainsi traduit en call <foo@plt><foo@plt> est une adresse dans la section plt (Procedure Linkage Table).

Vous pouvez observer le contenu de la table des symboles avec la commande nm :

$ nm server 000000000000298f T do_add_friend 0000000000002d36 T do_add_user 0000000000002480 t __do_global_dtors_aux 0000000000007d38 t __do_global_dtors_aux_fini_array_entry 00000000000028da T do_hello 00000000000028a0 T do_help 0000000000002bc1 T do_list_users 000000000000349c T do_post_message 0000000000002eae T do_view_user U pthread_mutex_init@@GLIBC_2.2.5 U pthread_mutex_lock@@GLIBC_2.2.5 U pthread_mutex_unlock@@GLIBC_2.2.5 00000000000024c5 T system_error 0000000000003f5c T thread_function 0000000000008280 b tids 0000000000008230 D __TMC_END__ 0000000000007dc8 d usage_template 00000000000082e0 B verbose U wait@@GLIBC_2.2.5 U write@@GLIBC_2.2.5 [...]

Lorsqu'on lance un programme, le système charge les différentes sections du fichier ELF, puis les différentes bibliothèques partagées requises. Les adresses des fonctions chargées sont insérées dans la table plt. Ainsi, lorsque le programme appelle la fonction foo, il saute à l'adresse indiquée et exécute donc le code de la fonction située dans la bibliothèque.

Fonctionnement de LD_PRELOAD

La variable d'environnement LD_PRELOAD permet de spécifier au système une liste de bibliothèques à charger avant de charger les bibliothèques requises par le programme.

En utilisant LD_PRELOAD, il est possible de modifier le comportement de la fonction foo. On défini une bibliothèque (libfoo.so) qui implémente une fonction foo ayant la même signature que la fonction foo utilisée par le programme:

int foo(int a) { printf("Interception de la fonction foo !\n"); }

En exécutant l'application avec LD_PRELOAD (LD_PRELOAD=./libfoo.so ./application), la "nouvelle" fonction foo est insérée dans la table plt en premier. Lorsque l'application appelle foo, c'est donc cette fonction qui est appelée:

$ LD_PRELOAD=./libfoo.so ./application Interception de la fonction foo !

LD_PRELOAD permet donc de modifier le comportement d'une bibliothèque.

Rappel: en bash, la syntaxe VARIABLE=value command exécute commande en positionnant la variable d'environnement VARIABLE à value. Sous Mac, au lieu d'utiliser LD_PRELOAD, il faut utiliser la variable DYLD_INSERT_LIBRARIES qui fonctionne de la même manière.

Création d'un wrapper

Lorsqu'on intercepte un appel à la fonction foo, il peut être utile de créer un "wrapper" qui effectue un traitement, puis appelle la fonction foo originelle, et ensuite effectue un autre traitement. Le système de wrapper est typiquement utiliser pour logguer les appels à une fonction :

int (*foo_original)(int a); int foo(int a) { printf("Entrée dans la fonction foo\n"); int ret = foo_original(a); printf("Sortie de la fonction foo\n"); return ret; }

Pour cela, il est nécessaire de connaître l'adresse de la fonction foo originelle. Cette adresse peut être déterminée grâce à la fonction dlsym :

static void __init(void) __attribute__((constructor)); static void __init(void) { foo_original = dlsym(RTLD_NEXT, "foo"); if(!foo_original) { fprintf(stderr, "Warning: cannot find 'foo': %s\n", dlerror()); abort(); } }

La fonction dlsym permet de trouver l'adresse d'un symbole. La constante RTLD_NEXT permet de trouver la définition suivante du symbole foo (c'est-à-dire la fonction foo originelle), et non la première entrée de la table plt.

La constante RTLD_NEXT est spécifique à Linux et nécessite de définir _GNU_SOURCE. Pour l'utiliser, il est donc nécessaire d'ajouter #define _GNU_SOURCE au tout début du fichier (avant les premiers #include) ou de le définir dans le Makefile (ajouter -D_GNU_SOURCE aux CFLAGS).

L'attribut __attribute__((constructor)) indique que la fonction doit être appelée au chargement de la bibliothèque. Cela permet d'initialiser la bibliothèque (ici, de récupérer l'adresse de la fonction foo originelle.

Pour invoquer une fonction au déchargement de la biblothèque, on peut utiliser l'attribut __attribute__((destructor)) d'une manière similaire.

Au travail !

Créez la bibliothèque liblock.so. Cette bibliothèque contient pour l'instant deux fonctions static void liblock_init() et static void liblock_finalize() appelées au chargement et à la terminaison d'un programme. Ces fonctions se contentent d'afficher un message.

Si vous ne vous souvenez pas de la manière de créer une bibliothèque, vous pouvez consulter le cours de CSC4103 correspondant.

Modifiez la bibliothèque afin qu'elle intercepte les appels à la fonction pthread_mutex_lock. À chaque interception, affichez un message à l'entrée et à la sortie de la fonctionn et appelez la fonction pthread_mutex_lock. Par exemple:

entering pthread_mutex_lock(mutex=0x559fbbd8e308) leaving pthread_mutex_lock(mutex=0x559fbbd8e308) -> 0 entering pthread_mutex_lock(mutex=0x559fbbd8e308) leaving pthread_mutex_lock(mutex=0x559fbbd8e308) -> 0 [...]

Complétez la bibliothèque liblock.so pour intercepter les appels aux autres fonctions pthread_mutex_* et pthread_cond_*. Par exemple :

entering pthread_mutex_init(mutex=0x559fbbd8e308, attr=(nil)) leaving pthread_mutex_init(mutex=0x559fbbd8e308, attr=(nil)) -> 0 entering pthread_cond_init (cond=0x559fbbd8e330, attr=(nil)) leaving pthread_cond_init (cond=0x559fbbd8e330, attr=(nil)). -> 0 entering pthread_mutex_lock(mutex=0x559fbbd8e308) leaving pthread_mutex_lock(mutex=0x559fbbd8e308) -> 0 entering pthread_cond_wait (cond=0x559fbbd8e330, mutex=0x559fbbd8e308) entering pthread_mutex_lock(mutex=0x559fbbd8e308) leaving pthread_mutex_lock(mutex=0x559fbbd8e308) -> 0

Vérifiez que la bibliothèque fonctionne en interceptant les appels de fonction du serveur facebook.

Voici la liste des fonctions qu'on souhaite intercepter: int pthread_mutex_lock(pthread_mutex_t *mutex); int pthread_mutex_trylock(pthread_mutex_t *mutex); int pthread_mutex_unlock(pthread_mutex_t *mutex); int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *attr); int pthread_mutex_destroy(pthread_mutex_t *mutex); int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex); int pthread_cond_timedwait(pthread_cond_t *cond, pthread_mutex_t *mutex, const struct timespec *abstime); int pthread_cond_signal(pthread_cond_t *cond); int pthread_cond_broadcast(pthread_cond_t *cond); int pthread_cond_init(pthread_cond_t *cond, const pthread_condattr_t *attr); int pthread_cond_destroy(pthread_cond_t *cond);

On souhaite maintenant fournir notre propre implémentation des mutex et des conditions se basant sur des futex. Pour cela, il est nécessaire d'utiliser un compteur pour chaque mutex ou condition. Puisque l'application alloue des pthread_mutex_t ou des pthread_cond_t, que nous n'utiliserons pas (puisqu'on n'appelle pas les fonctions de la libpthread), utilisons ces zones mémoires pour y stocker nos données ! Il suffit de définir une structure my_mutex_t, puis de caster un pthread_mutex_t en my_mutex_t pour y accéder.

Créez la bibliothèque libmylock.so qui intercepte également les fonctions pthread_mutex_* et pthread_cond_* mais qui implémente les fonctions en se basant sur des futex.

Vérifiez que la bibliothèque fonctionne en interceptant les appels de fonction du serveur facebook.

Le corrigé de cet exercice est disponible dans la branch tp3_corrige du dépôt git.