Analyse dynamique de programmesL'analyse dynamique de programme (dynamic program analysis ou DPA), est une forme d'analyse de programme qui nécessite leur exécution. Elle permet d'étudier le comportement d'un programme informatique et les effets de son exécution sur son environnement. Appliquée dans un environnement physique ou virtuel, elle est souvent utilisée pour profiler des programmes. Que ce soit pour retirer des informations sur le temps d'utilisation du processeur, l'utilisation de la mémoire ou encore l'énergie dépensée par le programme. Elle permet également de trouver des problèmes dans les programmes. Elle peut par exemple détecter si le programme accède ou non à des zones mémoires interdites, ou encore, de révéler des bogues dans un programme à l'aide de fuzzers. Elle peut aussi permettre de déboguer un programme en temps réel, en donnant la possibilité de regarder ce qu'il se passe dans la mémoire et dans le processeur à n'importe quel moment de son exécution. ProblématiquePour analyser un programme trois sources d'informations sont disponibles : le code source, l'exécution du programme et les spécifications du programme. L'analyse du code source est plutôt réservée à l'analyse statique de programmes tandis que l'exécution du programme est plutôt utilisé pour l'analyse dynamique de programme[1]. De manière générale l'analyse statique de programme doit analyser l'entièreté des branches d'un code source tandis que l'analyse dynamique ne se concentre que sur l'exécution du programme avec un jeu de données spécifique[2]. Le profilage par exemple s'inscrit dans l'analyse dynamique puisqu'il retrace les parties du code exécuté. Ces traces permettent de déduire l'impact du programme sur les différents composants matériels sur lequel il s'exécute (le processeur, la mémoire, le disque dur, etc.). Étant donné qu'en général 80 % du temps d'exécution d'un programme est occupé par 20 % du code source[3],[4]. L'utilisation d'un profileur permet de localiser précisément où sont situés ces 20 %[4],[5]. Dans le cas de programme qui s'exécutent en parallèle sur plusieurs processeurs l'utilisation d'un profileur permet de donner une indication sur le degré de parallélisation du programme[3]. Tester et déboguer du code est essentiel pour tout langage. Les tests peuvent être considérés comme une forme d'analyse dynamique d'un programme dont le but est de détecter les erreurs. Tandis que le débogueur analyse le code pour localiser et corriger une erreur[3],[6]. Analyse pour la vitesse d'exécutionProfileur statistiquePour améliorer la vitesse d'exécution d'un programme, l'un des outils que le développeur peut utiliser est le profilage statistique (Sampling profiler). Il aide à détecter les points chauds et les goulots d’étranglement liés aux performances, en guidant le développeur vers les parties d'un programme à optimiser[4],[5],[7]. Ci-dessous un exemple de profileur minimaliste écrit en Ruby par Aaron Patterson pour expliquer simplement le fonctionnement d'un Sampling Profiler[8] : def fast; end
def slow; sleep(0.1); end
def benchmark_me
1000.times { fast }
10.times { slow }
end
def sample_profiler
target = Thread.current # récupère l'environnement d'exécution courant
samples = Hash.new(0) # initialise un dictionnaire qui va contenir les noms des fonctions appelées
t = Thread.new do # initialise un nouveau thread
loop do # boucle infinie
sleep 0.0001 # vitesse d'échantillonnage
function_name = target.backtrace_locations.first.label # récupère le nom de la fonction en cours d'exécution
samples[function_name] += 1 # incrémente le nombre de fois que la fonction a été vue
end
end
yield # execute le code passé dans le bloc
t.kill # arrête le thread
samples.dup
end
result = sample_profiler { benchmark_me }
p result # => {"sleep"=>6497, "slow"=>1}
Comme vu ci-dessus, lorsque le profileur arrête le programme pour regarder quelle fonction est en cours d'exécution, la fonction Bien que le profileur par échantillonnage ralentisse le temps d'exécution du programme (puisqu'il faut stopper la tache en cours afin de regarder quelle fonction est en train de s'exécuter) , il ne nécessite pas de modifier directement le code à la volée ou en amont[9]. Profileur de latenceDans le cas d'application interactive les profileurs traditionnels ne remontent que peu de données utiles sur les latences ressenties par l'utilisateur. Le profileur renvoie des données sur le code pendant toute la durée d'exécution du programme. Or dans le cas d'un programme interactif, (par exemple un éditeur de texte), les régions intéressantes sont celles où l'utilisateur essaie d'interagir avec le programme (mouvement de souris, utilisation du clavier, etc.). Ces interactions représentent un temps et un coût CPU très faible comparé à la totalité du temps d'exécution de l'éditeur. Elles n'apparaissent donc pas comme des endroits à optimiser lorsqu'en profilant le programme. C'est pourquoi il existe des Latency profiler qui se concentrent sur ces ralentissements[10],[11],[12]. Analyse de la consommation de mémoireIl est important de profiler l'utilisation de la mémoire lors de exécution d'un programme. En particulier si le programme est destiné à fonctionner sur une longue durée (comme sur un serveur) et éviter les dénis de service[13]. Pour ce type d'analyse ce sont des types de « profileurs exact » qui sont utilisés. C'est-à-dire un profileur qui regarde exactement le nombre de fois que chaque fonction a été appelée. Ci-dessous un exemple de profileur minimaliste écrit en Ruby par Aaron Patterson pour expliquer simplement le fonctionnement d'un Exact Profiler[8] : def fast; end # return immediatly
def slow; sleep(0.1); end # return after 0.1 second
def benchmark_me
1000.times { fast } # 1000 appels a la fonction fast
10.times { slow } # 10 appels a la fonction slow
end
def exact_profiler
counter = Hash.new(0) # dictionnaire vide
tp = TracePoint.new(:call) do |event| # lorsqu'une fonction est appelée
counter[event.method_id] += 1 # on l'ajoute dans le dictionnaire
end
tp.enable # on active le code ci dessus
yield # on appelle le code passé dans le bloc
tp.disable # on arrête de compter les appels de fonction
return counter
end
result = exact_profiler { benchmark_me }
p result # {:benchmark_me=>1, :fast=>1000, :slow=>10}
Quel que soit le langage utilisé, ce type de profileur va nécessiter d'instrumentaliser le code. Il faut ajouter des instructions en plus dans le programme qui aurait dû s'exécuter, à chaque fois qu'une fonction est appelée ou terminée[14]. Cela peut être dramatique pour le temps d'exécution du programme. Il peut se retrouver 10 à 4 000 fois plus lent[15]. En java par exemple, le Java Profiler (JP) va insérer du bytecode en plus pour incrémenter un compteur à chaque fois qu'une méthode est appelée[16]. Cela va altérer le temps réel d'exécution du code et souvent poser des problèmes de synchronisation (l'ordre d'exécution des threads risque d'être changé) lorsqu'il sera lancé sur des applications multi-threadé[15]. Dans le cas du profileur de mémoire il n'y a que les fonctions ayant un rapport avec la mémoire qu'il faut instrumentaliser[17]. Par exemple dans le cas de Valgrind les fonctions malloc et free vont être remplacées par une fonction qui notifie Valgrind qu'elles ont été appelées avant de les exécuter. Une partie des fonctions qui écrivent dans la mémoire et qui sont couramment utilisé ou qui posent problème à Valgrind comme memcpy ou strcpy sont également instrumentalisée[18]. Analyse énergétiqueAvec l’apparition des smartphones et avec le développement de l'internet des objets , la consommation énergétique est devenue un enjeu très important depuis ces dernières années. Bien qu'il soit possible de réduire la fréquence des processeurs ou encore de mutualiser les ressources matérielles pour réduire la consommation d'un service, réduire directement la consommation énergétique des programmes reste une solution utilisable en parallèle. Le profilage énergétique, est une des solutions qui peut permettre de se rendre compte de la consommation d'un programme. Cela consiste à profiler le code d'une application tout en évaluant la consommation énergétique, afin d'associer une consommation à des instructions. Plusieurs solutions peuvent permettre de mesurer la consommation énergétique lorsqu'un programme fonctionne. Par exemple il est possible de mesurer directement la consommation avec des composants matériels[19], qui viennent directement se placer devant chacun des composants utilisés par le programme. Néanmoins, ce n'est pas toujours possible d'avoir accès à de tels composants de mesure[19]. Une autre stratégie consiste donc à utiliser des modèles de consommation construits préalablement[20]. Ces modèles doivent prendre en compte les éléments matériels utilisés par le programme, mais aussi les différents états de ces composants[21]. L'analyse énergétique est particulière, puisqu'elle demande non seulement de faire fonctionner le programme, mais elle demande aussi de connaître le profil énergétique de tous les composants de la machine sur lequel il fonctionne, afin de calculer précisément l'impact énergétique. L'analyse énergétique des programmes reste très compliquée à évaluer précisément. En effet même si certains modèles permettent de se rapprocher de 10 % de marge d'erreur par rapport à la consommation réelle[22], plusieurs facteurs rendent la tâche compliquée. Tout d'abord le fait que certains appareils possèdent beaucoup de composants(e.g. les smartphones), est problématique pour tout prendre en compte[23]. Ensuite, les différents états que peut prendre un processeur (tel que l'état d'économie d'énergie) ou encore sa fréquence, font varier les résultats[24]. Le fait que certains composants tels que les GPS soient actifs ou non, ou que les antennes WIFI soient en transmission ou réception, sont aussi des éléments à prendre en compte dans l'équation. Ils peuvent de plus varier indépendamment du programme qui tourne[25], ce qui complique encore plus la tâche. En fonction des techniques utilisées, les profilages énergétiques peuvent être vus avec plus ou moins de granularité[26]. Certaines technologies essaient même de fournir une estimation de la consommation énergétique pour une ligne de code donnée[27]. Analyse de teinteL'analyse de teinte (ou Taint analysis en anglais) consiste à teinter des données afin de pouvoir suivre leur progression dans un système informatique[28]. Ces données peuvent être des fichiers stockés dans un disque dur, dans des variables stockées dans la mémoire vive, ou toute autre information circulant dans n'importe quel registre matériel[29]. Tous les programmes qui sont dépendants de données teintées, sont alors à leur tour teintés[28]. Elle peut être mise en place soit par l'intermédiaire de composants matériels (physique ou alors en émulation) directement implémentés sur une machine[30], ou alors grâce à un logiciel[31]. En mettant en place un système de teinte directement sur le matériel il est beaucoup plus simple de contrôler les données qui passent dans chaque composant. «Par exemple, teinter une entrée au niveau du clavier est préférable que de teinter une entrée au niveau d'un formulaire web. Sinon les logiciels malveillants (par exemple) peuvent essayer d'éviter la détection en créant des hooks (qui altèrent le fonctionnement normal d'un programme) qui sont appelés avant que l'entrée clavier arrive au navigateur web»[29]. Néanmoins, ajouter des composants physiques peut-être fastidieux. Et si ces composants sont émulés, le fonctionnement du programme observé risque d'être ralenti[30]. Il peut y avoir plusieurs cas d'utilisation possibles pour l'analyse de teinte :
Les analyses de teinte sont régies par des politiques qui dictent leurs comportements, en fonction des besoins de l'utilisateur. Chaque politique possède 3 propriétés : la façon dont la teinte est introduite dans un programme; la façon dont elle se propage à un autre programme pendant l’exécution; la façon dont elle est vérifiée pendant l'exécution[28]. Il peut coexister plusieurs politiques de teinture à la fois. À noter que l'analyse de teinte, peut parfois être sujette à des faux positifs[34],[33]. DébogageTrouver les boguesUne manière de pousser les bogues à apparaître dans un programme est de faire appel à un fuzzer (AFL par exemple)[35],[36],[37]. Ce type de programme ne demande en général aucune modification du code source et va générer des données d'entrée de manière aléatoire en pour faire planter le code[35],[36]. L'efficacité d'un fuzzer est souvent mesurée en fonction de la couverture de code qu'il fournit (au plus le code a été exécuté au plus le fuzzer est bon)[38]. Ce type de programme qui génère des entrées aléatoires sans avoir d'autre information que les temps de réponse du programme en face et s'il a planté s'appelle un black box fuzzer[39]. De nombreux fuzzer souvent appelés white box fuzzer font appel à un mix d'exécution symbolique (analyse statique) et d'analyse dynamique. Ils utilisent l'exécution symbolique pour déterminer quels paramètres permettent d'explorer les différentes branches du code. Puis à l'aide de fuzzer plus traditionnel la recherche de bogues s'effectue normalement[40]. Par exemple, le code suivant a une chance sur 232 de déclencher un bug : void foo(int c) // la variable que le fuzzer va pouvoir faire varier
{
if ((c + 2) == 12)
abort(); // une erreur
}
En entrant dans le DébogueurUne manière courante pour déboguer du code est de l'instrumentaliser à la main. C'est-à-dire observer son comportement pendant de l'exécution et ajouter des prints pour savoir ce qui a été exécuté ou non et à quel moment se trouve le bogue. L'exécution dans un débogueur d'un programme permet de trouver rapidement la source du bogue sans avoir à modifier son code[6],[41]. Généralement, les débogueurs permettent quelques opérations alors que le programme est en train de s'exécuter :
Mais ces opérations ne sont pas évidentes étant donné que le code risque d'être optimisé par le compilateur. Et que parmi les optimisations le compilateur peut supprimer des variables ou réarranger les lignes et perdre les numéros de lignes[48]. ApplicationOn pourra noter que Microsoft utilise énormément les fuzzer comme méthode pour tester leur code. À tel point que c'en est devenu une procédure avant de pouvoir mettre du code en production[40]. Notes et références
Voir aussiArticles connexesLiens externesBibliographie
|