Vous êtes-vous déjà demandé ce qui se passe lorsque vous appuyez sur le bouton d'alimentation de votre ordinateur ? Derrière cette brève pause, avant que votre écran ne s'allume, une série complexe de processus se déroule. Cet article vous plongera dans le monde fascinant du firmware , en explorant la façon dont les différents composants interagissent pendant le processus de démarrage .
En comprenant ces connexions, vous obtiendrez une image plus claire des éléments fondamentaux qui donnent vie à votre système. Nous nous concentrerons principalement sur l' architecture Intel x86 , mais de nombreux principes s'appliquent également à d'autres architectures.
Si vous avez manqué la première partie de notre série, cliquez ici pour la rattraper. Découvrons maintenant les mystères qui se cachent derrière le firmware.
Pour comprendre comment les composants du firmware interagissent, nous allons explorer l'architecture entière avec toutes ses parties connectées. Le flux d'exécution, illustré dans le diagramme ci-dessous, démarre à partir du vecteur de réinitialisation , qui fait partie du chargeur de démarrage de première étape . À partir de là, il progresse à travers différentes étapes du firmware :
Le firmware ou BIOS peut généralement être divisé en deux parties principales, avec une interface généralement minimale entre elles :
La conception du micrologiciel de la plateforme peut être soit monolithique , combinant l'initialisation matérielle et la fonctionnalité de démarrage, soit suivre un flux de démarrage modulaire et par étapes. Le choix de la conception dépend des exigences du système et peut être préférable pour certains appareils.
Le diagramme suivant illustre comment les différents composants du micrologiciel interagissent et peuvent être utilisés ensemble pour prendre en charge le processus de démarrage (les flèches indiquent la séquence d'exécution) :
Si ces diagrammes vous semblent complexes maintenant, ne vous inquiétez pas. Relisez-les après avoir lu cet article et ils seront plus clairs.
Ce micrologiciel est conçu pour initialiser les ordinateurs et les systèmes embarqués en mettant l'accent sur l'initialisation matérielle minimale : faire uniquement ce qui est absolument nécessaire, puis passer le contrôle au chargeur de démarrage de deuxième étape afin de démarrer le système d'exploitation. Le FSBL ne charge pas les systèmes d'exploitation à partir de supports de stockage autres que la puce flash . Comme il initialise uniquement le matériel sous-jacent et ne gère pas les supports de démarrage tels que les disques durs, les SSD ou les clés USB, un autre logiciel est nécessaire pour démarrer réellement un système d'exploitation .
Principales responsabilités de la FSBL :
Aux débuts de l'informatique, les logiciels open source n'étaient pas très populaires et la plupart des implémentations du BIOS étaient propriétaires. Il n'existe que quelques solutions open source disponibles fournissant le code source POST du BIOS, telles que Super PC/Turbo XT BIOS et GLaBIOS . Ces projets ont été conçus pour fonctionner sur les systèmes IBM 5150/5155/5160 et la plupart des clones XT.
Cependant, les implémentations de BIOS open source les plus connues, comme OpenBIOS et SeaBIOS , n'effectuent pas d'initialisation matérielle car elles ne sont pas destinées à fonctionner sur du matériel nu. Mais elles sont largement utilisées comme chargeurs de démarrage de deuxième étape et fonctionnent nativement dans des environnements virtuels comme QEMU et Bochs.
Quoi qu'il en soit, il y a peu de chances que vous ayez besoin de travailler directement avec ces premiers BIOS ou de vous plonger dans leurs spécificités. Mais si vous souhaitez les explorer, les référentiels mentionnés sont un bon point de départ.
En ce qui concerne les tendances actuelles de développement, il ne semble pas y avoir de développement continu de solutions BIOS propriétaires, et de tels projets sont devenus obsolètes face aux alternatives modernes.
Le processus de démarrage suit un flux par étapes, commençant de la gauche vers la droite dans la figure suivante. La chronologie du processus de démarrage de la plateforme est divisée en plusieurs phases, comme indiqué par des cases jaunes :
ExitBootServices()
.
Ce processus et ses phases d'exécution sont couverts par la spécification UEFI Platform Initialization (PI) . Cependant, il existe également l' interface UEFI (indiquée par la ligne bleue en gras sur l'image), qui ne fait pas partie du document précédent et est décrite dans la spécification UEFI . Bien que les noms et l'utilisation fréquente de l'UEFI puissent prêter à confusion, ces deux documents ont des objectifs différents :
En fait, les deux spécifications concernent les interfaces, mais à des niveaux différents. Pour des informations détaillées, vous pouvez accéder aux deux spécifications sur le site Web du Forum UEFI .
L'UEFI PI a été initialement conçu comme une solution de micrologiciel unifiée, sans tenir compte de la distinction entre les chargeurs de démarrage de première et de deuxième étape. Cependant, lorsque nous faisons référence à l'UEFI comme chargeur de démarrage de première étape , cela inclut les phases SEC , PEI et DXE précoce . La raison pour laquelle nous divisons DXE en étapes précoces et tardives est due à leurs rôles différents dans le processus d'initialisation.
Au cours de la phase DXE initiale , les pilotes effectuent généralement l'initialisation essentielle du processeur/du PCH/de la carte et génèrent également des protocoles d'architecture DXE (AP) , qui aident à isoler la phase DXE du matériel spécifique à la plateforme. Les AP encapsulent les détails spécifiques à la plateforme, permettant à la phase DXE tardive de fonctionner indépendamment des spécificités matérielles.
Des articles détaillés sur le fonctionnement de Coreboot seront bientôt disponibles. Suivez-moi sur les réseaux sociaux – ils seront publiés très bientôt !
Une fois la configuration matérielle initiale terminée, la deuxième étape entre en jeu. Son rôle principal est de mettre en place une interface logicielle entre le système d'exploitation et le micrologiciel de la plateforme , garantissant que le système d'exploitation peut gérer les ressources système et interagir avec les composants matériels.
Le SSBL vise à masquer autant que possible les variations matérielles , en simplifiant le développement du système d'exploitation et des applications en gérant la plupart des interfaces au niveau matériel. Cette abstraction permet aux développeurs de se concentrer sur les fonctionnalités de niveau supérieur sans se soucier des différences matérielles sous-jacentes.
Principales responsabilités de SSBL :
Récupération des informations de la plate-forme : obtient des informations spécifiques à la plate-forme à partir du chargeur de démarrage de première étape , y compris le mappage de mémoire, le SMBIOS, les tables ACPI, le flash SPI, etc.
Exécutez des pilotes indépendants de la plate-forme : inclut des pilotes pour SMM, SPI, PCI, SCSI/ATA/IDE/DISK, USB, ACPI, interfaces réseau, etc.
Implémentation de services (également appelée interface) : fournit un ensemble de services qui facilitent la communication entre le système d'exploitation et les composants matériels.
Menu de configuration : propose un menu de configuration pour la configuration du système, permettant aux utilisateurs d'ajuster les paramètres liés à l'ordre de démarrage, aux préférences matérielles et à d'autres paramètres système.
Logique de démarrage : mécanisme permettant de localiser et de charger la charge utile (probablement le système d'exploitation) à partir du support de démarrage disponible.
L'interface du BIOS est connue sous le nom de services/fonctions/appels d'interruption du BIOS . Ces fonctions fournissent un ensemble de routines pour l'accès au matériel, mais les détails spécifiques de la manière dont elles sont exécutées sur le matériel particulier du système sont cachés à l'utilisateur.
En mode réel 16 bits, ils sont facilement accessibles en invoquant une interruption logicielle via l'instruction INT du langage assembleur x86. En mode protégé 32 bits, presque tous les services du BIOS ne sont pas disponibles en raison de la manière différente dont les valeurs de segment sont gérées.
Prenons par exemple Disk Services ( INT 13h
), qui fournit des services de lecture et d'écriture de disque dur et de disquette basés sur des secteurs en utilisant l'adressage CHS (Cylindres-têtes-secteurs) , comme exemple de la façon dont cette interface peut être utilisée. Supposons que nous souhaitons lire 2 secteurs (1024 octets) et les charger à l'adresse mémoire 0x9020 , le code suivant pourrait alors être exécuté :
mov $0x02, %ah # Set BIOS read sector routine mov $0x00, %ch # Select cylinder 0 mov $0x00, %dh # Select head 0 [has a base of 0] mov $0x02, %cl # Select sector 2 (next after the # boot sector) [has a base of 1] mov $0x02, %al # Read 2 sectors mov $0x00, %bx # Set BX general register to 0 mov %bx, %es # Set ES segment register to 0 mov $0x9020, %bx # Load sectors to ES:BX (0:0x9020) int $0x13 # Start reading from drive jmp $0x9020 # Jump to loaded code
Si vous êtes intéressé par la manière dont ce service est écrit dans SeaBios, jetez un œil à src/disk.c .
Commence à rechercher un périphérique de démarrage (il peut s'agir d'un disque dur, d'un CD-ROM, d'une disquette, etc.) en lisant le premier secteur de 512 octets (secteur zéro) des périphériques.
La séquence d'amorçage dans le BIOS charge le premier Master Boot Record (MBR) valide qu'il trouve dans la mémoire physique de l'ordinateur à l'adresse physique 0x7C00 (indice : 0x0000:0x7c00 et 0x7c0:0x0000 font référence à la même adresse physique).
Le BIOS transfère le contrôle aux 512 premiers octets de la charge utile. Ce secteur chargé , qui est trop petit pour contenir l'intégralité du code de la charge utile, sert à charger le reste de la charge utile à partir du périphérique de démarrage . À ce stade, la charge utile peut utiliser l'interface exposée par le BIOS.
Il est intéressant de noter que les spécifications du BIOS n'existaient pas à l'origine. Le BIOS est une norme de fait : il fonctionne de la même manière que sur les PC IBM réels, dans les années 1980. Les autres fabricants ont simplement procédé à une rétro-ingénierie et ont créé des BIOS compatibles IBM. Par conséquent, il n'y avait aucune réglementation pour empêcher les fabricants de BIOS d'inventer de nouvelles fonctions du BIOS ou d'avoir des fonctionnalités qui se chevauchent.
Comme mentionné précédemment, l'UEFI n'est qu'une spécification et possède de nombreuses implémentations. La plus utilisée est TianoCore EDK II , une implémentation de référence open source des spécifications UEFI et PI. Bien qu'EDKII seul ne soit pas suffisant pour créer un micrologiciel de démarrage entièrement fonctionnel, il fournit une base solide pour la plupart des solutions commerciales.
Pour prendre en charge différents chargeurs de démarrage de première étape et fournir une interface UEFI, le projet UEFI Payload est utilisé. Il s'appuie sur la configuration initiale effectuée et sur les informations de plate-forme fournies par le micrologiciel de démarrage pour préparer le système à l'environnement UEFI.
La charge utile UEFI utilise les phases DXE et BDS , qui sont conçues pour être indépendantes de la plateforme. Elle offre une charge utile générique qui peut s'adapter à différentes plateformes. Dans la plupart des cas, elle ne nécessite aucune personnalisation ni aucun ajustement spécifique à la plateforme et peut être utilisée telle quelle en consommant les informations de la plateforme à partir du chargeur de démarrage de première étape .
Variantes de la charge utile UEFI :
Charge utile UEFI héritée : nécessite une bibliothèque d'analyse pour extraire les informations de plateforme spécifiques à l'implémentation nécessaires. Si le chargeur de démarrage met à jour son API, la charge utile doit également être mise à jour.
Charge utile UEFI universelle : conforme à la spécification USF (Universal Scalable Firmware) , en utilisant le format ELF (Executable and Linkable Format) ou le format FIT (Flat Image Tree) comme format d'image commun. Au lieu de les analyser lui-même, il s'attend à recevoir les blocs de transfert (HOB) à l'entrée de la charge utile.
Bien que la charge utile UEFI Legacy fonctionne bien, la communauté EDK2 tente de faire évoluer l'industrie vers la charge utile UEFI universelle . Le choix entre les charges utiles dépend des composants de votre firmware. Par exemple, il n'est pas possible d'exécuter la charge utile Legacy avec prise en charge SMM sur Slim Bootloader sans mon patch . D'un autre côté, l'utilisation de la charge utile universelle avec coreboot nécessite une couche de calage pour traduire les tables coreboot en HOB , une fonctionnalité uniquement disponible dans le fork StarLabs EDK2 .
Chaque système compatible UEFI fournit une table système qui est transmise à chaque code exécuté dans l'environnement UEFI (pilotes, applications, chargeurs de système d'exploitation). Cette structure de données permet à un exécutable UEFI d'accéder aux tables de configuration système telles que ACPI , SMBIOS et un ensemble de services UEFI .
La structure de la table est décrite dans MdePkg/Include/Uefi/UefiSpec.h :
typedef struct { EFI_TABLE_HEADER Hdr; CHAR16 *FirmwareVendor; UINT32 FirmwareRevision; EFI_HANDLE ConsoleInHandle; EFI_SIMPLE_TEXT_INPUT_PROTOCOL *ConIn; EFI_HANDLE ConsoleOutHandle; EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL *ConOut; EFI_HANDLE StandardErrorHandle; EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL *StdErr; // // A pointer to the EFI Runtime Services Table. // EFI_RUNTIME_SERVICES *RuntimeServices; // // A pointer to the EFI Boot Services Table. // EFI_BOOT_SERVICES *BootServices; UINTN NumberOfTableEntries; EFI_CONFIGURATION_TABLE *ConfigurationTable; } EFI_SYSTEM_TABLE;
Les services incluent les types suivants : services de démarrage , services d’exécution et services fournis par des protocoles .
L'UEFI abstrait l'accès au périphérique en configurant des protocoles UEFI . Ces protocoles sont des structures de données contenant des pointeurs de fonction et sont identifiés par un identifiant unique global (GUID) qui permet aux autres modules de les localiser et de les utiliser. Ils peuvent être découverts via les services de démarrage.
Un pilote UEFI génère ces protocoles, et les fonctions réelles (pas les pointeurs !) sont contenues dans le pilote lui-même. Ce mécanisme permet aux différents composants de l'environnement UEFI de communiquer entre eux et garantit que le système d'exploitation peut interagir avec les périphériques avant de charger ses propres pilotes.
Alors que certains protocoles sont prédéfinis et décrits dans la spécification UEFI, les fournisseurs de micrologiciels peuvent également créer leurs propres protocoles personnalisés pour étendre les fonctionnalités d'une plate-forme.
Services de démarrage
Fournit des fonctions qui ne peuvent être utilisées qu'au démarrage. Ces services restent disponibles jusqu'à ce que la fonction EFI_BOOT_SERVICES.ExitBootServices()
soit appelée ( MdeModulePkg/Core/Dxe/DxeMain/DxeMain.c ).
Les pointeurs vers tous les services de démarrage sont stockés dans la table des services de démarrage ( MdePkg/Include/Uefi/UefiSpec.h ) :
typedef struct { EFI_TABLE_HEADER Hdr; ... EFI_GET_MEMORY_MAP GetMemoryMap; EFI_ALLOCATE_POOL AllocatePool; EFI_FREE_POOL FreePool; ... EFI_HANDLE_PROTOCOL HandleProtocol; ... EFI_EXIT_BOOT_SERVICES ExitBootServices; ... } EFI_BOOT_SERVICES;
Services d'exécution
Un ensemble minimal de services reste accessible pendant l'exécution du système d'exploitation. Contrairement aux services de démarrage, ces services restent valides après qu'une charge utile (par exemple, le chargeur de démarrage du système d'exploitation) a pris le contrôle de la plateforme via un appel à EFI_BOOT_SERVICES.ExitBootServices()
.
Les pointeurs vers tous les services d'exécution sont stockés dans la table des services d'exécution ( MdePkg/Include/Uefi/UefiSpec.h ) :
typedef struct { EFI_TABLE_HEADER Hdr; ... EFI_GET_TIME GetTime; EFI_SET_TIME SetTime; ... EFI_GET_VARIABLE GetVariable; EFI_GET_NEXT_VARIABLE_NAME GetNextVariableName; EFI_SET_VARIABLE SetVariable; ... EFI_GET_NEXT_HIGH_MONO_COUNT GetNextHighMonotonicCount; EFI_RESET_SYSTEM ResetSystem; ... } EFI_RUNTIME_SERVICES;
L'image ci-dessous montre la chronologie des services de démarrage et d'exécution, afin que vous puissiez voir exactement quand chacun d'eux est actif.
La spécification UEFI définit un moteur de stratégie de démarrage appelé gestionnaire de démarrage UEFI . Il tentera de charger les applications UEFI dans un ordre spécifique. Cet ordre et d'autres paramètres peuvent être configurés en modifiant les variables NVRAM (mémoire vive non volatile) globales. Examinons les plus importantes d'entre elles :
Boot####
( ####
est remplacé par une valeur hexadécimale unique) — une option de démarrage/chargement.BootCurrent
— l'option de démarrage utilisée pour démarrer le système en cours d'exécution.BootNext
— l'option de démarrage pour le prochain démarrage uniquement. Elle remplace BootOrder
pour un seul démarrage et est supprimée par le gestionnaire de démarrage après la première utilisation. Cela vous permet de modifier le comportement du prochain démarrage sans modifier BootOrder
.BootOrder
— la liste de chargement des options de démarrage ordonnées. Le gestionnaire de démarrage essaie de démarrer la première option active de cette liste. En cas d'échec, il essaie l'option suivante, et ainsi de suite.BootOptionSupport
— les types d’options de démarrage pris en charge par le gestionnaire de démarrage.Timeout
— le délai d'expiration des gestionnaires de démarrage du micrologiciel, en secondes, avant de choisir automatiquement la valeur de démarrage parmi BootNext
ou BootOrder
.
Ces variables peuvent être facilement obtenues à partir de Linux en utilisant efibootmgr(8) :
[root@localhost ~]# efibootmgr BootCurrent: 0000 Timeout: 5 seconds BootOrder: 0000,0001,2001,2002,2003 Boot0000* ARCHLINUX HD(5,GPT,d03ca3cf-1511-d94e-8400-c7a125866442,0x40164000,0x100000)/File(\EFI\ARCHLINUX\grubx64.efi) Boot0001* Windows Boot Manager HD(1,GPT,6f185443-09fc-4f15-afdf-01c523565e52,0x800,0x32000)/File(\EFI\Microsoft\Boot\bootmgfw.efi)57a94e544f5753000100000088900100780000004200430044039f0a42004a004500430054003d007b00390064006500610038003600320063002d1139006300640064002d0034006500370030102d0061006300630031002d006600330032006200330034003400640034003700390035007d00000033000300000710000000040000007fff0400 Boot0002* ARCHLINUX HD(5,GPT,d03ca3cf-1511-d94e-8400-c7a125866442,0x40164000,0x100000) Boot2001* EFI USB Device RC Boot2002* EFI DVD/CDROM RC Boot2003* EFI Network RC
Examinons le démarrage en nous appuyant sur l'extrait de code ci-dessus. UEFI va commencer à parcourir la liste BootOrder
. Pour chaque entrée de la liste, il recherche une variable Boot####
correspondante : Boot0000
pour 0000, Boot2003
pour 2003, etc. Si la variable n'existe pas, il passe à l'entrée suivante. Si la variable existe, il lit le contenu de la variable. Chaque variable d'option de démarrage contient un descripteur EFI_LOAD_OPTION
qui est un tampon rempli d'octets de champs de longueur variable (il s'agit simplement de la structure de données).
La structure des données est décrite dans [MdePkg/Include/Uefi/UefiSpec.h][ https://github.com/tianocore/edk2/blob/edk2-stable202405/MdePkg/Include/Uefi/UefiSpec.h#L2122 )
typedef struct _EFI_LOAD_OPTION { /// The attributes for this load option entry. UINT32 Attributes; /// Length in bytes of the FilePathList. UINT16 FilePathListLength; /// The user readable description for the load option. /// Example: 'ARCHLINUX' / 'Windows Boot Manager' / `EFI USB Device` // CHAR16 Description[]; /// A packed array of UEFI device paths. /// Example: 'HD(5,GPT,d03ca3cf-1511-d94e-8400-c7a125866442,0x40164000,0x100000)/File(\EFI\ARCHLINUX\grubx64.efi)' // EFI_DEVICE_PATH_PROTOCOL FilePathList[]; /// The remaining bytes in the load option descriptor are a binary data buffer that is passed to the loaded image. /// Example: '57a9...0400' in Boot0001 variable // UINT8 OptionalData[]; } EFI_LOAD_OPTION;
À ce stade, le micrologiciel examinera un chemin d'accès au périphérique ( EFI_DEVICE_PATH_PROTOCOL ). Dans la plupart des cas, notre ordinateur est démarré à partir d'un périphérique de stockage (disque dur/SSD/NVMe/etc.). Ainsi, le chemin d'accès au périphérique contiendrait le nœud HD(Partition Number, Type, Signature, Start sector, Size in sectors)
.
Remarque : si vous souhaitez savoir comment traduire d'autres chemins, lisez la spécification UEFI v2.10, 10.6.1.6 Référence du nœud de périphérique texte .
L'UEFI examinera le disque et verra s'il contient une partition correspondant au nœud. Si elle existe, elle doit être étiquetée avec un identifiant unique global (GUID) spécifique qui la marque comme partition système EFI (ESP) . Celle-ci est formatée avec un système de fichiers dont la spécification est basée sur la version spécifique du système de fichiers FAT et est nommée Système de fichiers EFI ; en fait, il s'agit simplement d'un FAT12/16/32 normal.
File(\Path\To\The\File.efi)
, alors UEFI recherchera ce fichier spécifique. Par exemple, l'option Boot0000
contient File(\EFI\ARCHLINUX\grubx64.efi)
.\EFI\BOOT\BOOT{arch}.EFI
( BOOTx64.EFI
pour amd64 ou BOOTia32.EFI
pour i386 / IA32 ). Ce mécanisme permet au support amovible de démarrage (par exemple, une clé USB) de fonctionner en UEFI ; ils utilisent simplement un chemin de démarrage de secours . Par exemple, l'option Boot0002
utilisera ce mécanisme.
Remarque : toutes les options Boot####
mentionnées ci-dessus font référence aux options de démarrage affichées dans l'exemple de sortie de efibootmgr .
Dans les deux cas, le gestionnaire de démarrage UEFI charge l' application UEFI (il peut s'agir du chargeur de démarrage du système d'exploitation , du shell UEFI, d'un logiciel utilitaire, de la configuration du système, etc.) en mémoire. À ce moment, le contrôle est alors transféré au point d'entrée de l' application UEFI . Contrairement au BIOS , l' application UEFI peut rendre le contrôle au micrologiciel (en dehors de la situation où l'application prend le contrôle du système). Si cela se produit ou si quelque chose ne va pas, le gestionnaire de démarrage passe à l'entrée Boot####
suivante et suit exactement le même processus.
La spécification mentionne que le gestionnaire de démarrage peut gérer automatiquement les variables de la base de données. Cela inclut la suppression des variables d'option de chargement qui ne sont pas référencées ou qui ne peuvent pas être analysées. De plus, il peut réécrire n'importe quelle liste ordonnée pour supprimer toutes les options de chargement sans variables d'option de chargement correspondantes.
Le texte ci-dessus décrit le démarrage UEFI . De plus, le micrologiciel UEFI peut s'exécuter en mode Compatibility Support Module (CSM) qui émule un BIOS.
Un logiciel démarré par le micrologiciel (généralement le chargeur de démarrage de deuxième étape ) et utilisant son interface pour charger le noyau du système d'exploitation . Il peut être aussi complexe qu'un système d'exploitation et offrir des fonctionnalités telles que :
Les conceptions courantes de ces programmes dépassent le cadre de cet article. Pour une comparaison détaillée des chargeurs de démarrage de systèmes d'exploitation les plus courants, vous pouvez vous référer au wiki ArchLinux et à l' article de Wikipédia .
Le système Windows utilise son propre chargeur de démarrage de système d'exploitation appelé Windows Boot Manager (BOOTMGR) .
Le micrologiciel n'est plus un petit morceau de code complexe . Il est devenu une énorme quantité de code complexe , et les tendances actuelles ne font qu'y contribuer. Nous pouvons exécuter Doom , Twitter et bien d'autres applications intéressantes dessus.
Comprendre l'architecture globale permet d'organiser ces composants dans votre esprit. En examinant la conception du micrologiciel existant, vous obtenez un aperçu du processus fascinant qui se déroule à chaque mise sous tension d'un ordinateur. Cette perspective descendante clarifie non seulement le rôle de chaque élément, mais met également en évidence la nature sophistiquée et évolutive des systèmes de micrologiciels modernes.