Effectuer des mises à jour en direct des appareils sur le terrain peut être une entreprise délicate. La fiabilité et la récupération sont bien sûr essentielles, mais même acheminer les bons bits vers les bons secteurs de stockage peut être un défi. Récemment, j’ai travaillé sur un projet qui appelait à la conception d’une nouvelle voie pour mettre à jour certains petits microcontrôleurs qui étaient décidément peu pratiques.

Un projet comme celui-ci comporte de nombreuses pièces; un chargeur de démarrage pour effectuer la mise à jour proprement dite, un protocole de communication robuste, des voies de récupération, un mécanisme de transfert de fichiers, etc. Ce qui rendait ces micros particulièrement gênants, c'est qu'ils n'étaient pas connectés eux-mêmes au réseau, mais nécessitaient un saut via un autre contrôleur intermédiaire, qui lui-même aussi pas connecté au réseau. Comme on pouvait s'y attendre, l'étape par ailleurs simple de «transfert de fichiers» s'est rapidement transformée en une multitude de tâches complexes à accomplir avant que le reste du projet puisse continuer. Comme on dit, ce sont des micros tout en bas.

Le système de jour

Le système en question n’est pas particulièrement exotique. La partie pertinente est composée d'un ordinateur Linux connecté au réseau câblé à un grand microcontrôleur, câblé à une variété de contrôleurs plus petits pour gérer diverses tâches. J'avais besoin de pouvoir mettre à jour cette distribution de contrôleurs plus petits. Pour économiser la complexité, j'ai décidé de confier au microcontrôleur intermédiaire la responsabilité du processus de mise à jour de ses enfants. Mais cela a présenté un nouveau problème; comment obtenir les images du firmware dans le contrôleur intermédiaire?

Le micro en question est assez puissant, avec une bonne cuillerée de flash externe (formaté avec littleFS, naturellement), mais le fait de placer les fichiers dans ce flash me forcerait à développer une interface de système de fichiers pour l'ordinateur Linux. Pas un problème, mais un gros travail et un détournement significatif de la tâche à accomplir: le chargement de ces fichus contrôleurs! Ensuite, quelqu'un a suggéré un excellent moyen de simplifier le processus à presque rien; Et si je regroupais les images de micrologiciel cible avec le micrologiciel du contrôleur intermédiaire lui-même? Ensuite, le flash de l'intermédiaire transférerait également les firmwares de charge utile gratuitement! Certainement une stratégie rapide, mais comment y parvenir? Cela s'est avéré être un problème plus intéressant que ce que j'avais prévu. Voyons comment insérer les images dans le micrologiciel.

Options pour les images

J'ai exploré quatre méthodes pour effectuer le regroupement d'images du micrologiciel: compiler le micrologiciel de la charge utile en tant qu'en-tête, le lier en tant que fichier objet ou modifier la sortie compilée pour l'injecter, et éditer directement le binaire final. Entre autres différences, chacune de ces stratégies se prête à une utilisation à différents stades du processus de développement. La liaison n'a de sens que lors de la compilation du micrologiciel, les astuces de l'éditeur de liens fonctionnent juste après la compilation et l'édition binaire peut se produire à tout moment une fois le binaire terminé.

Comme il s'agissait d'un élément d'un projet plus large axé sur la stabilité, le suivi de la version exacte de chaque image de micrologiciel distincte était extrêmement important pour la recherche de bogues et la traçabilité. Idéalement, chaque image serait quelque peu découplée, de sorte qu'elles puissent être modifiées individuellement sans tout recompiler. Pour ajouter encore plus de complexité au problème, il fallait trois images – chaque contrôleur exécute une application distincte – et deux chaînes d'outils, car les microcontrôleurs intermédiaires et cibles étaient d'architectures différentes. Même si l'une des quatre options correspondait le mieux à mes besoins, les quatre méritent d'être discutées.

Le classique: un fichier d'en-tête

Un exemple d'export de source GIMP C, de l'utilisateur Twitter (@whisperity)

Si vous avez lu le titre de cet article et que vous vous êtes dit "c'est facile, compilez-le simplement dans un en-tête!" tu n'es pas seul. C’est certainement une approche classique du problème, et si les ressources sont toutes disponibles au moment de la compilation, cela peut être la meilleure option.

Il n’y a pas d’astuce pour cette méthode, c’est exactement aussi simple qu’il y paraît. Vous utilisez un outil pour «imprimer» efficacement le binaire de la charge utile sous la forme d'une série de constantes d'octets individuelles, puis les vider dans un fichier source sous forme de tableau et les compiler. L'accès est un jeu d'enfant, c'est juste un tableau de taille fixe! Vous pouvez même utiliser un sizeof() pour déterminer la quantité de données disponibles.

Il existe de nombreux outils qui facilitent ce flux de travail, le plus surprenant étant ce parangon d'utilitaires GIMP, qui permet d'exporter directement vers la source C. Une option plus conviviale pour la CLI (et qui reviendra) est xxd. xxd est nominalement utilisé pour la conversion entre le binaire brut et une variété de formats de fichiers hexadécimaux, mais peut également être configuré pour exporter un fichier source C directement avec le -i drapeau. La sortie devrait sembler assez familière si vous avez déjà vu un tableau C constant:

borgel$ xxd -i somefile.bin
unsigned char somefile_bin() = {
0x22, 0x22, 0x22, 0x47, 0x75, 0x69, 0x64, 0x65, 0x20, 0x74, 0x68, 0x65,
...
0x69, 0x6f, 0x6e, 0x22, 0x5d, 0x0a, 0x29, 0x0a
};
unsigned int somefile_bin_len = 10568;

Assez pratique, non? Dirigez cette sortie vers un fichier source et compilez-la. Malheureusement, cela ne correspondait pas à ma candidature. Je ne pouvais pas garantir que le micrologiciel de la charge utile serait toujours disponible au moment de la compilation, et même s'il l'était, il serait un peu plus difficile de retracer la version exacte à partir de laquelle il avait été construit sans le lire à partir du binaire final lui-même ou en créant un fichier séparé. fichier de version. Donc, un en-tête C était sorti.

Magie des liens lisses

Pour un langage de programmation compilé comme C, le compilateur produit probablement des fichiers objets intermédiaires. Vous les avez vus comme .oTraîne dans votre arborescence de répertoires. Ceux-ci contiennent des segments compilés du programme en question, ainsi que des métadonnées sur l'endroit où ce code finira par atterrir dans l'exécutable final ainsi que d'autres informations utilisées par un éditeur de liens ou un débogueur. Eh bien, il s'avère que tout peut être transformé en fichier objet en l'enveloppant dans les bons octets, y compris un autre binaire.

Avec gcc et gcc-outils compatibles avec ce wrapping de fichier objet ld (l'éditeur de liens) lui-même avec le -r et -b drapeaux. -r demande ld pour produire un fichier objet comme sortie, et -b nous permet de lui indiquer le format d'entrée (dans ce cas binaire). Remarque sur certaines plateformes -b semble être obsolète et peut ne pas être nécessaire. La commande complète ressemble à ceci:

borgel$ ld -r -b binary somefile.bin -o somefile.o

C’est essentiellement cela. Ce somefile.o peut être lié au reste des fichiers objets pour constituer un programme complet.

L'utilisation du binaire intégré avec l'exécutable en cours d'exécution est plus complexe que la lecture d'un tableau constant, mais seulement. L'éditeur de liens ajoute automatiquement des symboles magiques au fichier objet décrivant l'adresse de début, l'adresse de fin et la taille de la charge utile (dans ce cas, un binaire). Sur la machine de développement, ceux-ci peuvent être vérifiés de plusieurs manières, la plus ergonomique étant nm (pour lister les symboles dans un fichier objet) et objdump (le videur de fichier objet) – tous deux faisant partie d'une installation GNU binutils normale. Chacun est très puissant mais utilisons objdump comme notre échantillon:

borgel$ objdump -x somefile.o

somefile.o: file format ELF32-arm-little

Sections:
Idx Name Size Address Type
0 00000000 0000000000000000
1 .data 00002948 0000000000000000 DATA
2 .symtab 00000050 0000000000000000
3 .strtab 0000004f 0000000000000000
4 .shstrtab 00000021 0000000000000000

SYMBOL TABLE:
00000000 *UND* 00000000
00000000 l d .data 00000000 .data
00000000 .data 00000000 _binary_somefile_bin_start
00002948 .data 00000000 _binary_somefile_bin_end
00002948 *ABS* 00000000 _binary_somefile_bin_size

La plupart de ces éléments ne nous intéressent pas vraiment, même s'il est intéressant de fouiller dans les binaires compilés pour voir ce qu'ils contiennent (pour cela, essayez également le strings outil). Pour nos besoins, nous voulons les trois __binary_somefile_bin_* symboles. Notez leur absence dans le ld une ligne ci-dessus, ils sont automatiquement nommés et placés par l'éditeur de liens. La colonne de nombres à gauche du SYMBOL TABLE section sont les décalages dans le fichier objet où se trouve chaque symbole, en hexadécimal. Nous pouvons obtenir la taille de notre binaire (dans ce cas avec un simple ls)

borgel$ ls -l somefile.bin somefile.bin
-rw-r--r--@ 1 borgel staff 10568 Aug 15 14:22 somefile.bin

pour voir que les symboles _binary_somefile_bin_size et _binary_somefile_bin_end sont correctement placés après un bloc de la même taille que notre fichier d'entrée (10568 = 0x2948). Pour accéder à ces symboles en C, nous les ajoutons comme extern à n'importe quel fichier qui en a besoin, comme ceci:

extern const char _binary_somefile_bin_start;
extern const char _binary_somefile_bin_end;
extern const int _binary_somefile_bin_size;

Ensuite, ils peuvent être référencés dans le code comme n'importe quelle variable. N'oubliez pas qu'ils sont conçus comme des pointeurs vers les données sur le disque. Autrement dit, leur adresse est significatif, et les données stockées à cet emplacement sont les données sur le disque à cette adresse.

Cette méthode fonctionne très bien et se sent mieux encapsulée que de tout convertir directement en fichier source. Mais il souffre de problèmes similaires à l'approche précédente; que le suivi de la provenance du binaire résultant peut être complexe sans intégrer de symboles supplémentaires.

Dans mon cas, le plus gros problème était que pour travailler avec les fichiers objets, vous devez disposer d'outils prenant en charge cette architecture de processeur particulière. Pas de problème lors du regroupement de logiciels pour un ordinateur de bureau, mais dans mon cas, cela signifiait que j'avais besoin d'une copie du arm-none-eabi-gcc outillage disponible à un moment du processus de construction où il n’était pas déjà présent. Cela était possible à résoudre, mais il y avait de meilleures options.

Mais attendez, il y en a plus?

Avons-nous juste couvert toutes les manières possibles de construire un binaire? À peine! Mais ces deux options sont toutes deux les mieux adaptées au moment de la compilation et couvrent les besoins les plus courants pour l'incorporation de binaires. Où êtes-vous le plus susceptible de les rencontrer dans ces pages? Probablement lors de l'incorporation d'images dans le micrologiciel du microcontrôleur. La compilation d'une image bitmap jusqu'à un en-tête est peut-être le moyen le plus simple de passer d'une image sur un bureau à un écran attaché à un micro.

Si toutes ces discussions sur les fichiers objets vous amènent à réfléchir à d'autres moyens d'explorer les avancées du compilateur, consultez l'excellent article de (Sven Gregori) sur la création de systèmes de plugins avec des bibliothèques partagées; un sujet étroitement lié. Et si l'idée de travailler avec des binaires remplit votre esprit de possibilités, (Al Williams) a votre numéro alors qu'il travaille à travers le processus de création de fichiers binaires à l'aide de xxd et d'autres outils Linux courants.

LAISSER UN COMMENTAIRE

Rédigez votre commentaire !
Entrez votre nom ici