URL: https://linuxfr.org/news/ecrire-son-os-partie-2-configurer-ses-outils Title: Écrire son OS - Partie 2 : configurer ses outils Authors: maxb Benoît Sibaud et palm123 Date: 2015年12月10日T20:46:33+01:00 License: CC By-SA Tags: embarqué, système_d'exploitation, arm, cortex, microcontrôleur et c Score: 47 Eh bien voilà, très (trop) longtemps après [le premier épisode](https://linuxfr.org/news/ecrire-son-systeme-d-exploitation-partie-1-preparer-le-terrain), me revoilà pour reprendre avec vous la série sur l'écriture d'un système d'exploitation pour un microcontrôleur STM32F103. Pour rappel, mon système d'exploitation [MOS](https://github.com/maxbernelas/MOS) est écrit dans un but d'apprentissage. Il vise à être simple à appréhender et à permettre à chacun de découvrir les entrailles d'un OS. Cela implique deux conséquences : * je réinvente la roue puisque je réécris tout de zéro * il y aura sûrement des bugs, n'essayez pas de le mettre en production (!) Voilà, tout ceci étant dit, on va pouvoir attaquer les choses sérieuses. Dans cet épisode, nous allons organiser notre projet et configurer nos outils. Bonne lecture ! ---- [Code source du projet MOS](https://github.com/maxbernelas/MOS) ---- # Organisation du projet # Pour commencer, on va organiser notre projet. L'arborescence que j'ai choisi d'adopter est la suivante : ``` . ├── COPYING ├── include │ ├── cpu │ ├── kernel │ └── soc ├── linker.lds ├── Makefile ├── README └── src ├── cpu ├── kernel └── soc ``` Les en-têtes (dans `include/`) et les sources (dans `src/`) sont organisées de manière symétrique, et séparées en plusieurs dossiers : `cpu/` pour ce qui est spécifique au Cortex-M3, `soc/` pour ce qui relève des particularités du STM32F103, et enfin `kernel/` pour tout ce qui est du ressort générique du noyau et non spécifique au matériel (gestion des tâches, de la mémoire, des événements, bibliothèque standard...). Notez également la présence d'un fichier `COPYING` qui contient la licence (BSD en l'occurrence) et d'un `README` qui contient une brève description du projet. Le _linker script_ et le Makefile seront décrits plus en détail dans les paragraphes suivants. # Configuration d'OpenOCD # Nous avons installé [OpenOCD](http://openocd.org), mais il nous reste à le configurer. Rassurez-vous, ça se résume à pas grand chose... une fois que l'on a trouvé quoi mettre dans les fichiers de configuration ! Je vous épargne donc les recherches sur le site d'OpenOCD et sur les diverses _mailing lists_, et je vous offre mon fichier de configuration, que j'ai stocké dans `~/.config/openocd/mos.cfg` : ``` source [find interface/olimex-arm-usb-tiny-h.cfg] source [find target/stm32f1x.cfg] ``` Dans la première ligne, on explique à OpenOCD comment s'interfacer avec la sonde JTAG, et dans la seconde, on lui indique quel microcontrôleur se trouve à l'autre bout. Si tout marche bien, on doit pouvoir le lancer comme cela `openocd -f ~/.config/openocd/mos.cfg`. Note : pour éviter les problèmes, vérifiez bien que vous avez le droit d'accéder au périphérique correspondant à votre sonde JTAG. Chez moi par exemple, le paquet OpenOCD installe des règles udev dans `/lib/udev/rules.d/40-openocd.rules` qui attribuent le périphérique de ma sonde au groupe `plugdev` lors du branchement. # Configuration de GDB # Notre ami [GDB](https://www.gnu.org/software/gdb) va nous servir à debugger le code tournant sur notre cible. Habituellement, lorsqu'on fait du _cross-debugging_, on exécute un `gdbserver` sur la cible afin que GDB sur l'hôte puisse savoir ce qui se passe sur la cible. Dans notre cas ce n'est évidemment pas possible, puisque nous n'avons pas d'OS sur la cible qui pourrait nous permettre d'exécuter `gdbserver`. D'où l'importance d'OpenOCD, qui va se faire passer pour un `gdbserver` et permettre, via le JTAG, d'aller étudier tout ce qui se passe sur la cible. Pour mieux comprendre comment tout cela s'articule, voici un résumé de la chaîne de debug : `gdb <---(socket)---> openocd <---(usb)---> sonde JTAG <------> STM32` On va donc se faire un fichier `.gdbinit` aux petits oignons pour faciliter notre debug. En ce qui me concerne, je l'ai placé à la racine de mon projet puisque c'est de là que je lance mon GDB. Voici à quoi il ressemble : ``` target remote :3333 monitor reset halt define mos_flash monitor flash probe 0 monitor stm32f1x mass_erase 0 monitor flash write_bank 0 mos.bin 0 monitor reset halt dont-repeat end ``` Les deux premières lignes seront exécutées systématiquement au lancement de GDB. La première indique à GDB que l'on souhaite contacter un `gdbserver` sur le port 3333 de `localhost`. C'est le port par défaut sur lequel OpenOCD attend une connexion de GDB. La seconde ligne est une commande `monitor`. Dans le langage de GDB, une commande `monitor` est une commande qui n'est pas interprétée par GDB lui-même, mais qui est envoyée telle quelle au `gdbserver` (c'est à dire à OpenOCD, vous suivez ?). Donc toutes les commandes `monitor` que vous verrez ici sont en fait écrites dans la syntaxe d'OpenOCD, et ne sauraient être comprises par GDB. Revenons à la signification de cette seconde ligne : on demande donc à OpenOCD de faire un _reset_ de la cible, puis d'arrêter le CPU juste après le _reset_, soit avant même l'exécution de la toute première instruction. Les lignes suivantes définissent une commande GDB du nom de `mos_flash`. Cette commande est simplement définie ici, elle n'est pas exécutée automatiquement au lancement de GDB. Son but est de nous permettre d'écrire le binaire de MOS dans la flash du microcontrôleur facilement. Et vous vous apercevez que toutes les commandes qui la composent sont des commandes `monitor`, ce qui est logique puisque GDB ne sait pas comment flasher un STM32... C'est bien OpenOCD qui « connaît » notre puce. Pour résumer ce que font ces commandes, dans l'ordre, on détecte la flash, puis on l'efface, on écrit le binaire `mos.bin` et enfin on effectue un _reset_ du CPU. Un mot sur la directive `dont-repeat` : par défaut, GDB relance la dernière commande exécutée lorsque l'on appuie sur `` sans avoir rien saisi au _prompt_ GDB. Cette directive inhibe ce comportement pour la commande `mos_flash`. On ne souhaite pas en effet écrire plusieurs fois de suite le même binaire dans la flash, car en plus d'être inutile, cela risque d'user la flash pour rien. Vous aurez peut-être noté que j'ai mis le chemin du fichier à écrire en flash `mos.bin` en relatif. Cela signifie que pour que la commande fonctionne correctement, OpenOCD doit être exécuté depuis le dossier où se trouve le binaire à écrire. J'aurais certainement pu faire quelque chose de plus intelligent, mais je suis une feignasse :-) Donc j'ai simplement pris l'habitude de lancer OpenOCD et GDB depuis le répertoire racine de mon projet. # Makefile # À ce stade, j'en vois au fond de la salle qui se disent que c'est bien beau d'avoir configuré tout ce bazar, mais qu'on a pas de code à debugger. Et ils ont raison. Dans cette partie, on va donc se concocter un beau Makefile pour compiler notre OS et pouvoir le charger sur notre cible. Sans plus attendre, sous vos yeux ébahis, le Makefile : ```Makefile ################################################################################ # Customizable variables ################################################################################ # Directories that need to be built MODULES := src/cpu src/kernel src/soc # Name of the output binary OUT := mos # Build options CFLAGS := -Iinclude -Wall -Wextra -Werror -mcpu=cortex-m3 -nostdinc -mthumb \ -ggdb -fomit-frame-pointer -DDEBUG LDFLAGS := -nostdlib CROSS := arm-none-eabi- ################################################################################ # Build instructions, nothing should be customized under this line ################################################################################ CC := $(CROSS)gcc LD := $(CROSS)ld AS := $(CROSS)as GDB := $(CROSS)gdb OBJCOPY := $(CROSS)objcopy SIZE := $(CROSS)size OBJ := # Include all subdirectries makefiles, they will add their object files to the # OBJ variable include $(foreach module, $(MODULES), $(wildcard $(module)/*.mk)) # Default target (build the OS !) all: $(OUT).bin # Flat binary output $(OUT).bin: $(OUT).elf $(OBJCOPY) -O binary $< $@ $(SIZE) $< # ELF binary output $(OUT).elf: $(OBJ) $(LD) $(LDFLAGS) -T linker.lds -o $@ $^ # Cleanup target, remove generated object files and binaries clean: rm -f $(OBJ) $(OUT).bin $(OUT).elf debug: $(OUT).elf $(OUT).bin $(GDB) $< ``` Je ne vais pas vous faire ici [un cours sur Make](https://www.gnu.org/software/make/manual), d'autant plus que j'ai mis des commentaires, mais on va discuter de quelques points. D'abord l'usage de la variable `CROSS`. C'est une méthode courante pour faire de la _cross-compilation_, qui permet d'appeler les outils de génération de code adaptés pour l'architecture de la cible. Ensuite, les options de compilation pour GCC, notamment une que vous n'avez pas forcément l'habitude de voir : `-notstdinc` indique à GCC que l'on ne veut pas utiliser les chemins d'en-têtes par défaut. Et ce pour la simple et bonne raison que nous n'avons pas de bibliothèque C standard sur notre cible. Cela signifie que si mon code contient un `#include `, je ne veux **pas** du fichier d'en-tête standard fourni par mon compilateur, et je devrai fournir le fichier `stdio.h` dans les répertoires d'en-têtes de mon projet. Bienvenue dans le monde du développement _bare-metal_. Pour le linker, même punition : `-nostdlib` demande de ne surtout pas lier les bibliothèques standards au binaire généré. Cela signifie que si mon code appelle une fonction standard, par exemple `strcpy()`, c'est à moi de fournir une implémentation de cette fonction dans les sources de mon projet. Enfin, après la génération de l'exécutable `mos.elf`, vous noterez que j'utilise `objcopy` pour le convertir en binaire brut. En effet, un fichier ELF est un fichier structuré, avec des en-têtes, il est destiné à être _parsé_ avant d'être executé. Ce qui est (entre autres) le travail d'un OS. Mais notre exécutable ne sera pas exécuté par un OS. Il faut donc en faire un binaire brut qui sera flashé dans la puce. Le fichier ELF par contre reste utile pour GDB, car il contient tous les symboles de debug. Le reste est du confort : la cible `debug` permet de compiler le binaire et de lancer GDB dans la foulée, tandis que l'appel à `size` après la compilation permet d'afficher dans le terminal les tailles de code et de données utilisées par notre noyau, à titre d'information. # Conclusion # Nous avons maintenant un environnement tout prêt pour commencer à coder et exécuter du code sur notre cible. Tous les outils sont là, il n'y a plus qu'à... Enfin presque. Les observateurs attentifs auront remarqué que je n'ai pas abordé le sujet du _linker script_ (le fichier `linker.lds`). On en parlera dans le prochain épisode, car cela nécessite d'abord un point sur les différentes sections d'un exécutable, ainsi que sur le processus de démarrage du STM32F103. Et c'est promis, dans le prochain épisode, on fera tourner du code sur notre cible ! Promis également, le prochain épisode ne sera pas dans un an :-)

AltStyle によって変換されたページ (->オリジナル) /