PROCEDE DE CHARGEMENT D'UN CODE LOGICIEL EN LANGAGE INTERMEDIAIRE ORIENTE OBJET DANS UN APPAREIL PORTATIF
La présente invention concerne les logiciels et environnements d'exécution embarqués dans un appareil portatif et en particulier les procédés de conversion de tels logiciels lors de leur chargement dans l'appareil portatif pour exécution.
Un appareil portatif désignera par la suite tout appareil numérique portatif doté d'un microcontrôleur réunissant un processeur et des mémoires, tel qu'une carte à puce par exemple. La volonté de créer des applications inter opérables a conduit au développement des langages intermédiaires de programmation orientée objet. Le but principal de ces langages est ainsi de rendre leurs logiciels indépendants du matériel sur lequel ils doivent être exécutés. Les logiciels sont alors prévus pour s'exécuter sous la forme d'un code intermédiaire indépendant de l'architecture sous-jacente.
Les programmeurs sont ainsi globalement dégagés des contraintes liées à des matériels spécifiques. La diffusion d'une même version de logiciel pour différents matériels est également possible. Les langages intermédiaires orientés objet tels que le pseudo-code Java (désigné par bytecode en langue anglaise) , obtenu après compilation du langage source Java, ont ainsi connu un essor conséquent. On peut également citer le langage intermédiaire MSIL (acronyme anglo-saxon pour « Microsoft intermediate langage »)
déployé dans le cadre de l'environnement .net ou DotNet (marque déposée) , obtenu après compilation de divers langages sources possibles tels que C++ ou C#.
Le code intermédiaire correspond donc classiquement à une forme compilée du logiciel. Ces codes intermédiaires ne peuvent pas être exécutés tel quels par le processeur de l'appareil sur lequel on désire lancer un programme compilé sous forme de code intermédiaire. Il est nécessaire d'introduire une couche logicielle ayant pour principale fonction d' interpréter le code intermédiaire en instructions exécutables par le processeur de l'appareil hôte. Cette couche logicielle est appelée « machine virtuelle ». Par exemple, la machine virtuelle JAVA permet l'exécution d'un logiciel Java sur une plate-forme donnée sur laquelle elle est implémentée. Pour ce faire, la machine virtuelle dispose d'un jeu d'instructions propre, chaque instruction étant codée sur un ou plusieurs octets. Le jeu d'instructions de la machine virtuelle comporte par exemple un certain nombre d'instructions classiques comme les opérations arithmétiques, logiques et les sauts. Par ailleurs, la machine virtuelle dispose d'une pile d'exécution. En effet, elle ne comprend aucun registre utilisateur, que le programmeur du logiciel pourrait utiliser directement. A la place, on utilise des variables locales numérotées à partir de zéro.
La figure 1 illustre un exemple de moyens mis en œuvre pour charger un logiciel compilé 10 sous forme d'un code intermédiaire source dans une carte à puce 20, qui constitue en l'occurrence l'appareil portatif.
La carte à puce est munie d'une machine virtuelle pour l'exécution de tels logiciels. Une partie de la machine virtuelle, plus particulièrement nommée chargeur 30, est prévue pour charger en mémoire de travail de la carte à puce 20, à partir d'un appareil source 40, par exemple un serveur auquel la carte est connectée par des moyens quelconques non représentés, le code logiciel 10 compilé en langage intermédiaire orienté objet. Le chargeur 30 est une application qui est par exemple stockée dans une mémoire non volatile de l'appareil portatif.
Lors du chargement d'une méthode par exemple du code intermédiaire source 10 sur la carte par la machine virtuelle, celle-ci est convertie en un code interne 50, dit code destination par la suite. Typiquement, les informations du code intermédiaire 10 sont converties en des instructions en code machine 50 directement exécutables par le processeur du système considéré, en l'occurrence la carte à puce. Le code destination peut également être sous forme d'un code intermédiaire converti.
En effet, dans certains environnements tels que l'environnement .net, le code intermédiaire MSIL est destiné à être traduit en code machine par un compilateur « Juste à temps » (« Jit compiler » en terminologie anglo-saxonne) . Pendant le chargement d'une méthode, une conversion du code intermédiaire source 10 en un code intermédiaire interne visant à rendre l'exécution de la méthode plus efficace peut être mis en oeuvre.
Par exemple, dans le cas d'un logiciel compilé en langage MSIL, le code d'instruction « Add » pour l'addition est susceptible d'agir sur des nombres entiers, flottants ou autres. Ainsi, si le code intermédiaire source n'est pas modifié, il sera nécessaire de conserver des informations de typage pour pouvoir déterminer lors de l'exécution quel type de nombre le logiciel est en train de manipuler lorsqu'une instruction « Add » est rencontrée. La transformation du code d' instruction « Add » dans le code intermédiaire source en un code d' instruction dans le code destination indiquant directement l'information de typage associée à l'opération d'addition, par exemple « Add Float », permettra alors de faire l'exécution sans qu'il soit nécessaire de conserver l'information de typage.
Toutefois, du fait de la conversion opérée lors du chargement sur la carte des instructions d'une méthode du code intermédiaire source, les instructions correspondantes du code destination, constitué soit par le code machine exécutable par le processeur de la carte, soit par un code intermédiaire converti, ne vont plus nécessairement conserver la même taille que les instructions dans le code source. En effet, un code d' instruction peut par exemple être converti en plusieurs séquences d'instructions.
Un problème se pose alors dans le cas de la présence d' instructions de saut dans une méthode du code intermédiaire source destiné à être converti. En effet, dans le code intermédiaire, les adresses de destination des instructions de saut sont exprimées en
déplacements positifs ou négatifs en termes d'octets par rapport au début de la méthode, pour indiquer l'instruction à l'adresse cible visée. Par exemple, une instruction de saut peut consister à indiquer au programme qu'il faut sauter à l'instruction qui se trouve 5 octets en avant par rapport au début de la méthode dans la structure de données en mémoire de travail dans laquelle est chargée la méthode du code intermédiaire source. Aussi, comme la taille des instructions est amenée à changer dans le code destination lors de l'étape de chargement (et de conversion) du code intermédiaire source, il est nécessaire dans ce cas de réajuster les paramètres des instructions de saut décrivant les adresses de destination de saut. Il faut ainsi pouvoir recalculer les déplacements prévus en octets dans le code destination compte-tenu de la taille des nouvelles instructions converties, de façon à indiquer dans le code destination les bonnes adresses de destination de saut correspondant aux paramètres des instructions de saut.
Une solution de l'art antérieur consiste typiquement, durant le chargement du code, à élaborer et stocker en mémoire des tables de conversion, permettant d'associer, pour chaque méthode chargée, les anciennes adresses de destination cibles de saut dans le code intermédiaire source aux nouvelles adresses de destination de saut correspondantes dans le code destination. Il faut alors prévoir une entrée dans la table pour chaque adresse de destination de saut. Cependant, l'utilisation de cette solution présente des
inconvénients. Elle implique en effet de monopoliser une certaine quantité de mémoire vive, utilisée comme mémoire de travail, pour stocker les tables de conversion et la quantité de mémoire requise est de plus proportionnelle au nombre d'adresse de destination de saut différentes dans le code.
Aussi, si cette étape de chargement (et de conversion) de logiciels sous forme de code intermédiaire, basée sur l'utilisation de telles tables de conversion, est réalisable sur la plupart des ordinateurs de bureau présentant des ressources importantes de mémoire vive et de processeur, permettant ainsi de stocker les tables sans pénaliser pour autant l'exécution du logiciel en mémoire vive, certains systèmes devant embarquer des logiciels en code intermédiaire ont des capacités beaucoup plus limitées, qui empêchent l'utilisation des tables.
Ainsi, du fait des contraintes de miniaturisation, une carte à puce comprend typiquement entre 1 et 4 KiloOctets de mémoire vive (utilisée comme mémoire de travail) , entre 32 et 64 KiloOctets de mémoire non volatile réinscriptible (utilisée comme mémoire de travail et comme mémoire de masse) et environ 256 KiloOctets de mémoire morte (utilisée pour stocker le code de l'environnement d'exécution) . Une telle carte à puce est donc inadaptée à un fonctionnement qui impliquerait de stocker des tables de conversion pour recalculer les adresses de destination de saut dans le code destination. En effet, pour des méthodes comprenant un grand nombre de sauts, la taille de la
table deviendrait un problème susceptible d'empêcher le chargement du programme en mémoire.
Une autre solution pour de tels système consiste alors à utiliser un format de code destination qui ne modifie pas les déplacements objets des instructions de saut par rapport au code intermédiaire source qui est chargé. On utilise typiquement un format de code interne converti où les instructions du code destination ont la même taille que celles du code intermédiaire source, de façon à ne pas avoir à recalculer les déplacements objets des sauts par l'intermédiaire de tables de conversion. C'est la solution généralement adoptée par la machine virtuelle Java dans le cadre de la spécification JavaCard, permettant de faire tourner la technologie Java sur les cartes à puce et autres appareils à mémoire limitée. Cependant, cette solution n'est pas toujours envisageable.
La présente invention vise à résoudre les inconvénients précités. L'invention a ainsi pour objet un procédé de chargement d'un code logiciel source compilé en langage intermédiaire orienté objet dans un appareil numérique portatif muni d'une machine virtuelle d'exécution, comprenant une étape de conversion de la suite d'instructions dudit code source pour générer un code destination formé de ladite suite d'instructions converties, et une étape de mise à jour du code destination comprenant la détermination, pour chaque instruction de saut convertie, de la nouvelle adresse de destination de saut dans le code destination correspondant à l'adresse de destination de saut de
l'instruction de saut correspondante dans le code source, caractérisé en ce que la détermination de la nouvelle adresse de destination de saut dans le code destination comprend une analyse simultanée des codes source et destination prenant en compte la taille des instructions du code source et celle des instructions converties correspondantes du code destination.
Selon un mode de réalisation, l'analyse simultanée des codes source et destination pour la détermination de la nouvelle adresse de destination de saut dans le code destination comprend les étapes suivantes :
- lire la première instruction du code source et ajouter la taille de ladite instruction à un index de source préalablement initialisé à zéro ; - ajouter la taille de l'instruction convertie correspondante dans le code destination à un index de destination préalablement initialisé à zéro ;
- pour chaque instruction suivante du code source, lire ladite instruction et mettre à jour l'index de source en ajoutant la taille de ladite instruction et l'index de destination en ajoutant la taille de l'instruction convertie correspondante dans le code destination ;
- réitérer l'étape précédente jusqu'à ce que la valeur de l'index de source soit égale à l'adresse de destination de saut de l'instruction de saut correspondante dans le code source, et
- mettre à jour la nouvelle adresse de destination de saut dans le code destination avec la valeur de l'index de destination.
Selon un mode de réalisation particulier, l'ajout de la taille de l'instruction convertie correspondante dans le code destination à l'index de destination comprend une étape préalable de lecture de ladite instruction dans le code destination.
De préférence, le code destination est un code en langage intermédiaire.
Dans une variante, le code destination est un code machine exécutable par l'appareil portatif. L'invention concerne également un chargeur d'un code logiciel source compilé en langage intermédiaire orienté objet destiné à être chargé dans un appareil numérique portatif, caractérisé en ce qu'il est susceptible d'être stocké dans une mémoire non volatile de l'appareil pour la mise en œuvre du procédé selon 1' invention.
L'invention concerne encore un appareil numérique portatif comprenant une mémoire non volatile mémorisant le chargeur tel qu'il vient d'être décrit. De préférence, cet appareil est une carte à puce.
D'autres caractéristiques et avantages de la présente invention apparaîtront plus clairement à la lecture de la description suivante donnée à titre d'exemple illustratif et non limitatif et faite en référence aux figures annexées dans lesquelles :
-la figure 1 illustre un exemple de moyens mis en œuvre pour charger un code logiciel compilé sous forme d'un code intermédiaire dit code source, dans une carte à puce et a déjà été décrite ;
-la figure 2 est un organigramme résumant les principales étapes du procédé de l'invention selon un mode de réalisation préféré.
Par la suite, on détaillera plus particulièrement un exemple de réalisation pratique, dans le cas d'un logiciel compilé sous la forme d'un code intermédiaire
MSIL pour son chargement sur une carte à puce, bien que l'invention s'applique également pour des logiciels compilés dans d'autres langages intermédiaires orientés objet, tel que Java.
Ainsi, l'exemple suivant correspond à une méthode écrite en langage C#, prévue pour exécuter une boucle et appeler de manière répétée la fonction f() ou g() selon que la variable i est paire ou impaire : public static void example () { int i; for(i=0; i<100; ++i) { if( (i % 2) == 0 ) { f 0;
} else { g();
} } }
Tableau 1 : méthode en C#
Une fois ce programme compilé, on obtient le code source correspondant en langage intermédiaire MSIL. Le code suivant est plus précisément une représentation textuelle du code binaire qui sera chargé dans la carte :
.method public hidebysig static void example () cil managed {
.entrypoint
// Code size 31 (OxIf)
.maxstack 2
.locals init (int32 V 0) ldc.i4.0 stloc.O br.s 0019
0004 ldloc.O ldc.i4.2 rem brtrue.s 0010 call void Example.ExI::f (] br.s 0015
0010 call void Example.ExI: :g(]
0015 ldloc.O
\ \ Idc.i4.1
\ \ add stloc.O
0019 ldloc.O ldc.i4.s 100 bit.s 0004 ret
}
Tableau 2 : code intermédiaire MSIL
Les nombres sur la gauche en écriture hexadécimale correspondent à l'adresse de l'instruction correspondante. Chaque adresse d'instruction est plus particulièrement définie comme un déplacement ou une position (« offset » selon la terminologie anglo- saxonne) par rapport au début de la méthode. La première instruction de la méthode est ainsi à l'adresse 0000, puis suit une liste d'instructions, parmi lesquelles des instructions de saut, avec leur adresse, ou position par rapport au début de la méthode, associée. Les adresses d'instruction apparaissant en gras et en italique correspondent à des adresses de destination d'instructions de saut.
Ces adresses sont donc les adresses qui posent problème et qui vont devoir être mises à jour car il y a des instructions de saut dans le code intermédiaire qui les prennent en adresse de destination. Ainsi, à l'adresse 0002, on trouve l'instruction « br.s », qui
correspond à un saut et qui prend « 0019 » en paramètre indiquant l'adresse de destination de saut correspondante. A l'adresse 0007, on trouve l'instruction « brtrue.s », qui correspond à un saut et qui prend « 0010 » en paramètre indiquant l'adresse de destination de saut correspondante. A l'adresse 000e, on trouve l'instruction « br.s », qui prend « 0015 » en paramètre indiquant l'adresse de destination de saut correspondante. Enfin, à l'adresse 001c, on trouve l'instruction « bit. s », qui correspond à un saut et qui prend « 0004 » en paramètre indiquant l'adresse de destination de saut correspondante.
Ces adresses de destination de saut devront donc être mises à jour après conversion du code intermédiaire vers un code destination par le chargeur.
Selon l'exemple, la méthode est convertie dans le code destination suivant : example .nParams 0 .nLocals 1 .maxStack 2
LDC_1 0
STORE_4 0
BR 0019
LOAD_VAL 0
LDC_1 2
REM
BRNZ 0010
CALL 0 Method: Example.ExI.f
BR 0015
CALL 1 Method: Example.ExI.g
LOAD_VAL 0
LDC_1 1
ADD
STORE_4 0
LOAD_VAL 0
LDC_1 100
BLT 0004
RET_0 Tableau 3: code converti avant mise à jour des adresses de destination de saut
Tout d'abord, on s'aperçoit, en comparant les adresses des instructions entre le code source et le code destination, que la taille des instructions converties est différente. Cependant, dans un premier temps, les adresses de destination de saut, apparaissant en gras et en italique ci-dessus, correspondant aux instructions de saut converties dans le code destination sont inchangées et restent identiques aux adresses de destination de saut correspondant aux instructions de saut correspondante dans le code source. Ces paramètres sont donc maintenant invalides car les instructions converties n'ont plus la même taille que les instructions correspondantes dans le code source. Par exemple, l'instruction de saut convertie « BRNZ » à l'adresse 000c, qui est une conversion de l'instruction de saut correspondante « brtrue.s » du code source, ne doit pas cibler l'adresse de destination de saut 0010, mais bien plutôt l'adresse de destination de saut 0015, où se trouve dorénavant codée l'instruction « CALL », qui est l'instruction devant être ciblée par cette instruction de saut.
L'étape suivante consiste alors pour le chargeur à réécrire les adresses de destination de saut dans le code destination, de sorte qu'elles pointent vers l'instruction appropriée dans le code destination.
Pour déterminer les adresses de destinations devant être mises à jour dans le code destination, le chargeur peut parcourir le code source ou le code destination jusqu'à ce qu'une instruction de saut nécessitant d'être mise à jour soit identifiée. Dans ce
cas, le chargeur doit convertir l'adresse de destination de saut de l'instruction de saut dans le code source en une nouvelle adresse de destination de saut appropriée dans le code destination. Ainsi, dans l'exemple, l'instruction de saut du code destination à l'adresse 0004 nécessite d'être mise à jour, de même que les instructions aux adresses 000c, 0012 et 0023.
La détermination des nouvelles adresses de destination de saut dans le code destination pour ces instructions de saut va être effectuée à la volée, en ce sens qu'elle est effectuée dynamiquement et qu'aucun intermédiaire telle qu'une table de conversion n'est nécessaire. Elle va plus particulièrement être basée sur une analyse simultanée des codes source et destination, prenant en compte la taille des instructions du code source et celle des instructions converties correspondantes du code destination.
Plus précisément, le chargeur est prévu pour examiner chaque instruction dans le code source en maintenant un index de source et un index de destination préalablement initialisés à zéro. Ainsi, pour chaque instruction lue dans le code source, la taille de l'instruction de source est ajoutée à l'index de source et la taille de l'instruction convertie correspondante est ajoutée à l'index de destination. De cette façon, à chaque itération, la valeur de l'index de source indique l'adresse de l'instruction courante dans le code source tandis que la valeur de l'index de destination indique l'adresse de l'instruction correspondante dans le code destination. Lorsque
l'index de source est égal à l'adresse de destination de saut recherchée que l'on veut mettre à jour dans le code destination, alors l'index de destination indique une adresse d'instruction correspondante dans le code destination, correspondant à la nouvelle adresse de destination de saut qu'il convient de mettre à jour.
Prenons un exemple se rapportant à la mise à jour de l'instruction de saut à l'adresse 000c dans le code destination. Le problème consiste à déterminer la nouvelle adresse de destination de saut dans le code destination correspondant à l'adresse de destination de saut 0010 de l'instruction de saut correspondante dans le code source.
Dans une première étape, les index source et destination étant initialisés à zéro, on lit la première instruction « ldc.i4.0 » dans le code source et on met à jour la valeur de l'index de source avec la taille de cette instruction, en l'occurrence 1 octet. Une telle instruction est toujours convertie en une instruction sur 2 octets, en l'occurrence « LDC 1 param ». La taille de cette instruction convertie correspondante est alors ajoutée à l'index de destination pour sa mise à jour, qui prend alors la valeur 2. Puis, on passe à l'instruction suivante du code source à l'adresse 0001. Grâce aux index source et destination qui sont maintenus par le chargeur, on sait que l'instruction courante dans le code source à l'adresse 0001 a une instruction convertie correspondante à l'adresse 0002 dans le code destination.
On passe alors à la deuxième instruction dans le code source. En ce qui concerne la deuxième instruction lue dans le code source « stloc.O », sa taille est de 1 octet et l'index de source est alors mis à jour en ajoutant la taille de l'instruction et prend la valeur 2.
L'instruction « stloc.O » peut être convertie en instructions de taille différente suivant le type de variable qui est accédée. La taille de l'instruction convertie peut ainsi varier de 2 à 3 octets. Dans cette situation, il est nécessaire d'aller lire l'instruction convertie correspondante dans le code destination, en l'occurrence « STORE 4 », indiquant que c'est la variante sur 2 octets qui a été utilisée. Aussi, l'index de destination est mis à jour en ajoutant la taille de cette instruction et prend la valeur 4. De cette manière, on sait que la prochaine instruction courante à l'adresse 0002 du code source a une instruction convertie correspondante à l'adresse 0004 dans le code destination.
Ce processus est réitéré jusqu'à ce que valeur de l'index de source soit égale à l'adresse de destination de saut 0010. Lorsque c'est le cas, l'index de destination contient la valeur indiquant la nouvelle adresse de destination de saut correspondante dans le code destination, en l'occurrence 0015, qui peut alors être mise à jour dans l'instruction de saut à l'adresse 000c dans le code destination.
On opère de la même façon pour la mise à jour des instructions de saut converties du code destination à l'adresse 0004, 0012 et 0023, consistant à déterminer
les nouvelles adresses de destination de saut correspondantes dans le code destination.
On obtient pour finir le code destination final avec la mise à jour effectuée des adresses de destination de saut apparaissant en gras et en italique ci-dessous : example
.nParams 0
.nLocals 1
.maxStack 2
LDC 1 0
STORE 4 0
BR 24 (001f)
>v> BRNZ 6 (0015)
>v> V : CALL 0 Method: Example.ExI.f
BR 3 (0018)
0015 CALL 1 Method: Example.ExI.g
STORE 4 0
00 If LOAD VAL 0
>V' LDC 1 100
BLT - 31 (0007)
%, RET 0
La figure 2 décrit un organigramme résumant les principales étapes du procédé de l'invention tel qu'il vient d'être décrit en référence à l'exemple. Ainsi, lors de l'itération sur chaque instruction dans le code source, tant que la valeur de l'index de source n'est pas égale à l'adresse de destination de saut recherchée que l'on veut mettre à jour, l'instruction courante dans le code source est lue et l'index de source est mis à jour en ajoutant la taille de l'instruction courante.
La figure 2 illustre les deux alternatives qui se présentent pour la mise à jour de l'index de
destination. En effet, comme on l'a vu en référence à l'exemple ci-dessus, une instruction dans le code source peut être convertie en une autre instruction dans le code destination d'une taille prédéfinie et connue. Dans ce cas, on a immédiatement la taille de l'instruction convertie correspondante, qui est ajoutée à l'index de destination. Cependant, il peut y avoir d'autres cas où il est nécessaire, pour mettre à jour l'index de destination, de lire l'instruction convertie correspondante dans le code destination pour connaître la taille de l'instruction convertie, voir l'exemple avec l'instruction « stloc.O » ci-dessus.
Puis, lorsque la valeur de l'index de source devient égale à l'adresse de destination de saut recherchée, la mise à jour de la nouvelle adresse de destination de saut est réalisée avec la valeur de l'index de destination.
Bien que plus coûteux en termes de ressources de calcul que les méthodes classiques pour mettre à jour les instructions de saut converties, l'invention permet d'effectuer une telle mise à jour sans qu'il soit nécessaire d'élaborer et de stocker en mémoire des tables de conversion d'adresses, souvent consommatrices d'espace mémoire. Cette solution permet donc de libérer de la place en mémoire RAM lors du chargement d'un programme en langage intermédiaire et est donc particulièrement avantageuse pour les appareils portatifs contraints en mémoire.