Exemples exécutables
Techniquement, un programme qui fonctionne sans système d'exploitation est un système d'exploitation. Voyons maintenant comment créer et exécuter des systèmes d’exploitation minuscules Hello World.
Le code de tous les exemples ci-dessous est présent sur ce dépôt GitHub .
Secteur de démarrage
Sur x86, la chose la plus simple et la plus simple à faire est de créer un secteur de démarrage principal (MBR) , qui est un type de secteur de démarrage , puis de l’installer sur un disque.
Ici, nous en créons un avec un seul printf
appel:
printf '\364%509s\125\252' > main.img
sudo apt-get install qemu-system-x86
qemu-system-x86_64 -hda main.img
Résultat:
Testé sur Ubuntu 18.04, QEMU 2.11.1.
main.img
contient les éléments suivants:
\364
en octal == 0xf4
en hex: l'encodage d'une hlt
instruction, qui indique à la CPU de ne plus fonctionner.
Par conséquent, notre programme ne fera rien: démarrez et arrêtez seulement.
Nous utilisons octal car les \x
nombres hexadécimaux ne sont pas spécifiés par POSIX.
Nous pourrions obtenir cet encodage facilement avec:
echo hlt > a.asm
nasm -f bin a.asm
hd a
mais l' 0xf4
encodage est bien sûr également documenté dans le manuel d'Intel.
%509s
produire 509 espaces. Nécessaire pour remplir le fichier jusqu'à l'octet 510.
\125\252
en octal == 0x55
suivi de 0xaa
: octets magiques requis par le matériel. Ils doivent être les octets 511 et 512.
S'il n'est pas présent, le matériel ne le traitera pas comme un disque amorçable.
Notez que même sans rien faire, quelques caractères sont déjà imprimés à l'écran. Celles-ci sont imprimées par le firmware et servent à identifier le système.
Exécuter sur du vrai matériel
Les émulateurs sont amusants, mais le matériel est la vraie affaire.
Notez cependant que ceci est dangereux et que vous pourriez effacer votre disque par erreur: ne le faites que sur de vieilles machines qui ne contiennent pas de données critiques! Ou encore mieux, des cartes de développement telles que Raspberry Pi, voir l'exemple ARM ci-dessous.
Pour un ordinateur portable typique, vous devez faire quelque chose comme:
Gravez l'image sur une clé USB (détruira vos données!):
sudo dd if=main.img of=/dev/sdX
branchez la clé USB sur un ordinateur
allume ça
dites-lui de démarrer à partir de l'USB.
Cela signifie que le micrologiciel sélectionne USB avant le disque dur.
Si ce n'est pas le comportement par défaut de votre machine, continuez à appuyer sur Entrée, F12, ESC ou toute autre clé bizarre après la mise sous tension jusqu'à ce que vous obteniez un menu de démarrage dans lequel vous pouvez choisir de démarrer à partir de l'USB.
Il est souvent possible de configurer l'ordre de recherche dans ces menus.
Par exemple, sur mon ancien Lenovo Thinkpad T430, UEFI BIOS 1.16, je peux voir:
Bonjour le monde
Maintenant que nous avons créé un programme minimal, passons à un monde de salut.
La question évidente est: comment faire IO? Quelques options:
- demandez au firmware, par exemple BIOS ou UEFI, de le faire si pour nous
- VGA: région mémoire spéciale qui est imprimée à l'écran si elle est écrite. Peut être utilisé en mode protégé.
- écrivez un pilote et parlez directement au matériel d'affichage. C'est la "bonne" façon de le faire: plus puissant, mais plus complexe.
port série . C'est un protocole normalisé très simple qui envoie et récupère les caractères d'un terminal hôte.
Source .
Il n’est malheureusement pas exposé sur la plupart des ordinateurs portables modernes, mais c’est le moyen le plus courant d’utiliser des cartes de développement, voir les exemples ARM ci-dessous.
C'est vraiment dommage, car de telles interfaces sont vraiment utiles pour déboguer le noyau Linux par exemple .
utiliser les fonctionnalités de débogage des puces. ARM appelle leur semi-hôte par exemple. Sur du matériel réel, cela nécessite un support matériel et logiciel supplémentaire, mais sur les émulateurs, cela peut être une option pratique et gratuite. Exemple .
Ici, nous allons faire un exemple de BIOS car il est plus simple sous x86. Mais notez que ce n'est pas la méthode la plus robuste.
main.S
.code16
mov $msg, %si
mov $0x0e, %ah
loop:
lodsb
or %al, %al
jz halt
int $0x10
jmp loop
halt:
hlt
msg:
.asciz "hello world"
link.ld
SECTIONS
{
. = 0x7c00;
.text :
{
__start = .;
*(.text)
. = 0x1FE;
SHORT(0xAA55)
}
}
Assemblez et faites le lien avec:
gcc -c -g -o main.o main.S
ld --oformat binary -o main.img -T linker.ld main.o
Résultat:
Testé sur: Lenovo Thinkpad T430, UEFI BIOS 1.16. Disque généré sur un hôte Ubuntu 18.04.
Outre les instructions de montage standard de l’utilisateur, nous avons:
.code16
: indique à GAS de générer du code 16 bits
cli
: désactiver les interruptions logicielles. Ceux-ci pourraient faire redémarrer le processeur après lahlt
int $0x10
: fait un appel du BIOS. C'est ce qui imprime les caractères un par un.
Les drapeaux de liens importants sont:
--oformat binary
: sortie du code d'assemblage binaire brut, ne le faîtes pas dans un fichier ELF, comme c'est le cas pour les exécutables utilisateur standard.
Utilisez C au lieu d'assemblage
Puisque C est compilé en assemblage, l’utilisation de C sans la bibliothèque standard est assez simple, il vous faut en gros:
- un script d'éditeur de liens pour mettre les choses en mémoire au bon endroit
- drapeaux qui indiquent à GCC de ne pas utiliser la bibliothèque standard
- un point d'entrée d'assemblage minuscule qui définit l'état C requis pour
main
notamment:
TODO: lien donc quelques exemples x86 sur GitHub. Voici un bras que j'ai créé .
Cela devient plus amusant si vous souhaitez utiliser la bibliothèque standard, car nous n'avons pas le noyau Linux, qui implémente une grande partie des fonctionnalités de la bibliothèque standard C via POSIX .
Quelques possibilités, sans passer par un système d'exploitation complet tel que Linux, incluent:
Newlib
Exemple détaillé sur: https://electronics.stackexchange.com/questions/223929/c-standard-libraries-on-bare-metal/223931
Dans Newlib, vous devez implémenter vous-même les appels système, mais vous obtenez un système très minimal et il est très facile de les implémenter.
Par exemple, vous pouvez rediriger printf
vers les systèmes UART ou ARM, ou mettre exit()
en œuvre avec semi-hébergement .
systèmes d'exploitation embarqués tels que FreeRTOS et Zephyr .
De tels systèmes d'exploitation vous permettent généralement de désactiver la planification préemptive, vous donnant ainsi un contrôle total sur l'exécution du programme.
Ils peuvent être vus comme une sorte de Newlib pré-implémenté.
BRAS
Dans ARM, les idées générales sont les mêmes. J'ai téléchargé:
Pour le Raspberry Pi, https://github.com/dwelch67/raspberrypi ressemble au didacticiel le plus populaire disponible à ce jour.
Certaines différences par rapport à x86 incluent:
IO se fait en écrivant aux adresses magiques directement, il n'y a pas in
et out
instructions.
Ceci est appelé IO mappé en mémoire .
pour du matériel réel, comme le Raspberry Pi, vous pouvez ajouter le micrologiciel (BIOS) vous-même à l'image du disque.
C’est une bonne chose, car cela rend la mise à jour de ce micrologiciel plus transparente.
Micrologiciel
En réalité, votre secteur de démarrage n'est pas le premier logiciel qui s'exécute sur le processeur du système.
Ce qui fonctionne réellement en premier est le soi-disant micrologiciel , qui est un logiciel:
- fabriqué par les fabricants de matériel
- source généralement fermée mais probablement à base de C
- stocké dans la mémoire en lecture seule, et donc plus difficile / impossible à modifier sans le consentement du vendeur.
Les firmwares bien connus incluent:
- BIOS : ancien micrologiciel tout-présent x86. SeaBIOS est l'implémentation open source par défaut utilisée par QEMU.
- UEFI : successeur du BIOS, mieux standardisé, mais plus performant et incroyablement gonflé.
- Coreboot : la noble tentative d'open source cross arch arch
Le firmware fait des choses comme:
boucle sur chaque disque dur, clé USB, réseau, etc. jusqu'à ce que vous trouviez quelque chose de bootable.
Quand nous courons QEMU, -hda
dit que main.img
est un disque dur connecté au matériel, et
hda
est le premier à être essayé, et il est utilisé.
chargez les 512 premiers octets dans l’adresse de la mémoire RAM 0x7c00
, placez-y le RIP de la CPU et laissez-le fonctionner
afficher des éléments tels que le menu de démarrage ou les appels d'impression du BIOS sur l'écran
Le micrologiciel offre une fonctionnalité semblable à celle du système d'exploitation dont dépend la plupart des systèmes d'exploitation. Par exemple, un sous-ensemble Python a été porté pour fonctionner sur le BIOS / UEFI: https://www.youtube.com/watch?v=bYQ_lq5dcvM
On peut faire valoir que les firmwares sont indiscernables des systèmes d'exploitation et que les micrologiciels sont la seule "vraie" programmation en métal nu que l'on puisse faire.
Comme le dit ce développeur CoreOS :
La partie difficile
Lorsque vous mettez un PC sous tension, les puces composant le chipset (northbridge, southbridge et SuperIO) ne sont pas encore correctement initialisées. Même si la ROM du BIOS est aussi éloignée du processeur que possible, elle est accessible par le processeur, car elle doit l'être, sinon le processeur n'aurait aucune instruction à exécuter. Cela ne signifie pas que la ROM du BIOS est complètement mappée, généralement pas. Mais juste assez est mis en correspondance pour lancer le processus de démarrage. Tous les autres appareils, oubliez ça.
Lorsque vous exécutez Coreboot sous QEMU, vous pouvez expérimenter avec les couches supérieures de Coreboot et avec les charges utiles, mais QEMU offre peu d'occasions d'expérimenter le code de démarrage de bas niveau. D'une part, la RAM fonctionne bien dès le début.
Etat initial post-BIOS
Comme beaucoup de choses dans le matériel, la normalisation est faible et l’une des choses que vous ne devriez pas compter est l'état initial des registres lorsque votre code commence à s'exécuter après le BIOS.
Alors faites-vous une faveur et utilisez un code d'initialisation comme celui-ci: https://stackoverflow.com/a/32509555/895245
Les registres aiment %ds
et %es
ont des effets secondaires importants, vous devriez donc les mettre à zéro même si vous ne les utilisez pas explicitement.
Notez que certains émulateurs sont plus agréables qu'un matériel réel et vous offrent un bon état initial. Ensuite, lorsque vous utilisez du matériel réel, tout se brise.
GNU GRUB Multiboot
Les secteurs de démarrage sont simples, mais ils ne sont pas très pratiques:
- vous ne pouvez avoir qu'un seul système d'exploitation par disque
- le code de chargement doit être très petit et tenir dans 512 octets. Cela pourrait être résolu avec l' appel BIOS int 0x13 .
- vous devez faire vous-même beaucoup de démarrage, comme passer en mode protégé
C’est pour ces raisons que GNU GRUB a créé un format de fichier plus pratique appelé multiboot.
Exemple de travail minimal: https://github.com/cirosantilli/x86-bare-metal-examples/tree/d217b180be4220a0b4a453f31275d38e697a99e0/multiboot/hello-world
Je l’utilise également sur mon dépôt d’exemples GitHub pour pouvoir exécuter facilement tous les exemples sur du matériel réel sans graver la clé USB un million de fois. Sur QEMU cela ressemble à ceci:
Si vous préparez votre système d'exploitation en tant que fichier à démarrage multiple, GRUB est alors en mesure de le trouver dans un système de fichiers classique.
C’est ce que font la plupart des distributions, en mettant les images du SE sous /boot
.
Les fichiers à démarrage multiple sont essentiellement un fichier ELF avec un en-tête spécial. Ils sont spécifiés par GRUB à l’ adresse suivante : https://www.gnu.org/software/grub/manual/multiboot/multiboot.html
Vous pouvez transformer un fichier à démarrage multiple en un disque amorçable avec grub-mkrescue
.
El Torito
Format pouvant être gravé sur CD: https://en.wikipedia.org/wiki/El_Torito_%28CD-ROM_standard%29
Il est également possible de produire une image hybride qui fonctionne sur ISO ou USB. Ceci peut être fait avec grub-mkrescue
( exemple ), et est aussi fait par le noyau Linux en make isoimage
utilisant isohybrid
.
Ressources