Il existe de nombreuses façons de mettre à jour un système embarqué sur le terrain. Les images peuvent voler dans les airs une fois, voyager en baskets ou faire du stop sur d’autres données qui passent. OK, c’est peut-être exagéré, mais il existe certainement une pléthore de façons d’obtenir ces octets de mise à jour dans un système cible. Comment ces octets sont-ils assemblés et quels sont les outils qui font l’assemblage? C’est le problème que je devais résoudre.
Rappelez-vous, mon système n’était pas particulièrement nouveau (voir le diagramme ci-dessous). Juste quelques ordinateurs qui se demandent une mise à jour sur certains bus série. J’avais choisi de regrouper les images du firmware de la charge utile dans le binaire pour le microcontrôleur intermédiaire qui devait effectuer le processus de mise à jour. La contrainte supplémentaire était que le mélange des trois images de micrologiciel (un support et deux données utiles) devait se produire longtemps après la compilation, sur un système différent avec une chaîne d’outils séparée. Il y avait finalement deux options qui cadraient avec le projet de loi.
Il y a plus d’une façon d’utiliser l’éditeur de liens pour coller les binaires ensemble – c’est son travail après tout. Une chaîne d’outils de compilation typique utilise ld
pour enchaîner les fichiers objets ensemble, mais il existe d’autres outils adjacents à l’éditeur de liens qui sont utiles pour faire des ravages avec le bon type de fichier binaire. Cette méthode de regroupement d’images de firmware se concentrera sur un nouvel outil dans le GCC, objcopy
.
Pour abuser de la métaphore, objcopy
est un peu un couteau utilitaire pour la manipulation de fichiers objets. L’étendue de son fonctionnement est large, mais la chose spécifique dont nous avons besoin ici est la --update-section
paramètre, qui nous permet de remplacer une section nommée dans un fichier objet par un autre fichier objet. Alors, comment pouvons-nous obtenir cette section? C’est là qu’intervient un fichier de configuration appelé «script de l’éditeur de liens».
Une discussion détaillée de l’utilisation du script de l’éditeur de liens est hors de portée ici, mais pour l’instant, résumons-la comme un script pour l’éditeur de liens! Un exécutable complet est composé de nombreuses sections différentes, et le script de l’éditeur de liens est ce qui décrit où ils doivent aller et comment s’y référer. Par exemple, dans un microcontrôleur, la section de texte où le code à exécuter est stocké est souvent placée en flash à une adresse où le micro commencera l’exécution. Le script de l’éditeur de liens est ce qui décrit la région où ces données doivent aller et le fait qu’elles doivent être localisées en flash. Nous pouvons utiliser le même mécanisme pour injecter une charge utile de firmware dans notre image.
Ma première étape a été de décrire deux nouvelles sections, une pour chaque nouvelle image de firmware. Le segment suivant montre la section de données pour le microcontrôleur intermédiaire et les deux sections de charge utile que j’ai ajoutées en dessous.
/* Initialized data sections into "RAM" Ram type memory */ .data : { . = ALIGN(4); _sdata = .; /* create a global symbol at data start */ *(.data) /* .data sections */ *(.data*) /* .data* sections */ . = ALIGN(4); _edata = .; /* define a global symbol at data end */ } >RAM AT> FLASH /* Below this point are the sections I added */ .payload1 : { . = ALIGN(4); BYTE(0x0); payload1End = .; } >FLASH .payload2 : { . = ALIGN(4); BYTE(0x0); payload2End = .; } >FLASH
C’est un peu énigmatique, mais il devrait être clair que payload1
et payload2
sont deux sections spécialement délimitées qui doivent être placées en flash. Une fois ceux-ci définis, l’éditeur de liens veillera à ce que les symboles soient définis, que les sections soient suffisamment grandes et qu’elles ne chevauchent rien d’autre. Ils flotteront par rapport à leurs voisins dans le même ordre, en conservant ces deux sections à la fin du flash et *end
symboles à la fin de leurs sections respectives.
Pour accéder à ces sections au moment de l’exécution, nous pouvons nous référer aux symboles nommés payload1/payload1End
et payload2/payload2End
comme extern
symboles dans notre source C, mais ils sont également visibles dans le binaire compilé. Pour cela, nous devons utiliser le fichier lié exécutable (ELF) que l’éditeur de liens produit comme sortie finale. Dans mon cas, c’était la sortie par défaut et a été convertie d’un ELF en un binaire pour flasher sur le microcontrôleur. En fonction de votre plate-forme, cela peut ou non être le cas.
Le fichier ELF contient tout ce qui est nécessaire pour exécuter votre programme, y compris toutes les métadonnées et symboles que l’éditeur de liens possède au moment de la liaison, alors que le binaire peut les avoir supprimés. S’ils sont présents que des outils comme objcopy
peut s’y référer. À ce stade, l’injection du micrologiciel n’est plus qu’une simple commande bien formée. Convertissez la charge utile bin en question en fichier objet puis utilisez objcopy
pour l’injecter dans le binaire final comme ceci
borgel$ objcopy --update-section .payload1=payload1.bin combined-firmware.elf
objcopy
utilise les mêmes astuces que l’éditeur de liens pour que tout rentre et déplace nos symboles selon les besoins pour marquer les sections en question. Le micrologiciel en cours d’exécution peut maintenant obtenir la taille en soustrayant l’adresse des symboles de début et de fin, et se référer à la mémoire de charge utile comme commençant à l’adresse de début. Facile!
Alors pourquoi n’était-ce pas ma dernière méthode pour injecter des firmwares de charge utile? Il nécessite une copie de objcopy
qui sait gérer les ELF de l’architecture cible. Dans mon cas d’utilisation, je n’avais pas une telle chaîne d’outils disponible au bon endroit pour l’utiliser, alors je suis passé à la méthode suivante.
Si rien de plus intelligent ne fonctionne, un binaire n’est qu’un fichier à éditer. Ce n’est pas particulièrement élégant, mais modifier directement le binaire n’est rien sinon universellement applicable, tant que vous connaissez les détails du fichier sous le couteau.
Commençons par attacher plusieurs fichiers binaires ensemble. Cela s’avère extrêmement simple; il suffit de les concaténer! La commande ressemble à
borgel$ cat payload1.bin payload2.bin >> combined-firmware.bin
C’est si facile! J’avais besoin d’obtenir le fichier combiné dans le microcontrôleur intermédiaire, ce qui s’est avéré être tout aussi simple. L’outil clignotant du microcontrôleur écrira volontiers le micrologiciel combiné sur l’appareil, plaçant les fichiers de charge utile à la bonne adresse. Mais comment le micrologiciel en cours d’exécution sait-il où chercher pour extraire les charges utiles? Et comment sait-il quelle est leur taille? Pour cela, nous devons retourner à notre ami l’éditeur de liens.
Gardez à l’esprit que les fichiers avec lesquels nous devons travailler sont les images binaires finales sans aucune des métadonnées incorporées dans la méthode précédente. Et même s’ils l’étaient, ce processus devait fonctionner sans la chaîne d’outils spécifique au processeur à portée de main. Alors que faire? Lors de l’édition de l’ELF, nous avons utilisé le script de l’éditeur de liens pour décrire une nouvelle section en flash. Nous pouvons utiliser la même astuce ici pour créer une section et un symbole spéciaux «fin de firmware». Tant que le script de l’éditeur de liens l’oriente comme le dernier élément de l’image à écrire sur la mémoire flash, il sera garanti de flotter et de rester toujours à la fin pendant que le reste du micrologiciel grandit et rétrécit pendant le développement.
Maintenant que nous avons un indicateur de fin de firmware, le puzzle peut être résolu. Pour plus de simplicité, dans cette section j’ai choisi de placer 8 octets magiques (comme deux uint32_t
‘s) décrivant la taille des images à suivre. À l’avenir, si j’ai besoin de plus de flexibilité, je pourrais placer un fichier avec un objet JSON, un msgpack sérialisé ou autre chose à la place. L’image finale ressemble à
[main firmware][8 byte size file][payload 1][payload 2]
Mais que se passe-t-il s’il n’y a pas de charge utile ajoutée? Le microcontrôleur intermédiaire a besoin d’un moyen de savoir s’il y a quelque chose dans la nature du flash à rechercher. Le flash effacé peut fonctionner (il doit toujours se lire comme 0xFF), mais si l’image flashée se rétrécit et que j’étais malheureusement sur une limite d’effacement, il pourrait y avoir des données de déchets valides au lieu d’un nouveau champ de 0xFF. Pour éviter cela, je me suis à nouveau tourné vers l’éditeur de liens.
Le script de l’éditeur de liens permet la définition d’octets codés en dur ou de modèles d’octets pour remplir une région. J’ai donc ajouté deux valeurs de contrôle de 4 octets dans une région après la fin du drapeau du firmware. Lors de l’exécution, le micrologiciel charge les tailles de micrologiciel de la charge utile à partir de la fin du décalage du micrologiciel et les compare aux valeurs de contrôle. S’ils correspondent, cela suppose qu’aucune charge utile n’est présente. La section définie dans le script de l’éditeur de liens ressemble à ceci
.endmatter : { . = ALIGN(4); endmatterStart = .; LONG(0xDEADC0DE); /* Payload 1 size */ LONG(0xDEADBEEF); /* Payload 2 size */ } >FLASH
Lorsque le micrologiciel est regroupé, je tronque le fichier principal du micrologiciel et supprime les 8 octets finaux pour supprimer cette section. Ensuite, je crée un fichier contenant les deux tailles de charge utile et je le concatène. L’éditeur de liens garantit que les 8 derniers octets sont toujours les valeurs de contrôle (qui peuvent être confirmées avec un hexdump
). À ce stade, la commande ressemble à
borgel$ truncate -s -8 combined-firmware.bin borgel$ cat size.bin payload1.bin payload2.bin >> combined-firmware.bin
La dernière pièce du puzzle est la construction de ce fichier de 8 octets. Je voulais intégrer la taille sous forme de valeurs d’octets bruts (pas ASCII) pour les forcer à une taille constante. Pour cela, nous devons atteindre le début de ce voyage pour retrouver xxd
. Auparavant, j’utilisais xxd
pour convertir un fichier binaire en fichier hexadécimal (sous la forme d’une source C), mais l’outil fonctionne également en sens inverse, en convertissant un fichier hexadécimal en fichier binaire. Ce n’est pas trop difficile, donc tout ce que nous devons lui donner est l’adresse de départ du «fichier hexadécimal» à convertir, puis la série d’octets qu’il devrait consommer pour produire le fichier de sortie binaire. Tous ensemble qui ressemble à
borgel$ printf '0: %08x %08x' `stat -c %s payload1.bin` `stat -c %s payload2.bin` | xxd -r -p - >lenfile.bin
et donne un fichier avec exactement le contenu attendu:
borgel$ stat -c %s payload1.bin 922 borgel$ stat -c %s payload2.bin 176 borgel$ hexdump lenfile.bin 0000000 00 00 03 9a 00 00 00 b0 0000008
Ça le fait! Presque de manière anticlimatique, la séquence finale de commandes se révèle être juste trois commandes shell
borgel$ truncate -s -8 combined-firmware.bin borgel$ printf '0: %08x %08x' `stat -c %s payload1.bin` `stat -c %s payload2.bin` | xxd -r -p - >lenfile.bin cat size.bin payload1.bin payload2.bin >> combined-firmware.bin
On y va! Deux autres méthodes pour éditer les fichiers binaires. Entre celles-ci et les deux premières options décrites, la plupart des cas d’utilisation devraient être couverts. La plupart de ces techniques devraient bien servir pour tous les actifs qui doivent être combinés; pensez à ajouter des effets sonores ou des images à un système sans stockage externe pour les contenir.
J’espère que vous vous sentez maintenant habilité à trancher et à dés votre chemin vers la victoire de la charge utile binaire, quelle que soit la géométrie en question.
Nous remercions cet excellent article «Intégration de données binaires dans des exécutables» par [Christian Stigen Larsen] en 2016 pour les noyaux de certaines de ces idées. Si vous êtes intéressé par un ou deux autres choix, il fournit un excellent résumé de certaines autres options.