- SerenityOS fonctionnait surtout bien sous QEMU, mais sur un vrai ordinateur portable, les problèmes sont apparus un par un dès le démarrage, le débogage et l’accès au stockage, révélant les lacunes de la prise en charge matérielle
- La machine de test est un Dell 3100 Chromebook équipé d’un Intel Celeron N4020, de 4 Go de DDR4, de 32 Go d’eMMC et d’un écran TN 1366×768 ; le débogage boîtier fermé basé sur Cr50, attendu, a échoué sur cette carte
- La voie Cr50 étant bloquée, un Pi Pico basé sur RP2040 a été placé à l’intérieur et relié directement à l’UART et à la flash SPI, afin de créer avec CircuitPython et serprog un dispositif temporaire de débogage et de flashage appelé PicoCCD
- Les premiers journaux de démarrage ont été récupérés en utilisant IO port 0x80, journalisé par l’EC de ChromeOS, comme canal de sortie temporaire lent, car il était difficile d’utiliser directement l’UART 16550 MMIO derrière PCI
- La prise en charge de l’eMMC est passée par des différences d’initialisation SD/MMC, l’absence de contrôle d’alimentation SDHCI et la désactivation de commandes propres à SD, jusqu’à atteindre partiellement une session graphique ; les performances, la stabilité et la préparation des patchs restent toutefois à faire
Le Dell 3100 Chromebook choisi comme cible matérielle réelle
- En cherchant à contribuer plus en profondeur à SerenityOS, la première faiblesse qui sautait aux yeux était que le système s’exécutait sous QEMU, mais manquait de prise en charge du matériel réel
- La prise en charge UEFI était déjà en cours côté spholz, donc je n’y ai pas touché ; pour ce travail, il suffisait que le noyau de la branche master démarre via GRUB dans l’environnement d’exécution UEFI TianoCore
- Je voulais éviter de déboguer un OS sur la même machine que mon poste principal, et j’ai ciblé du matériel relativement récent, assez utilisable au quotidien
- En cherchant un Chromebook bon marché sur Allegro, j’ai acheté un Dell 3100 pour 95 PLN, soit environ 25 EUR
- Intel Celeron N4020, 2 cœurs, sans Hyper-Threading
- 4 Go de DDR4
- eMMC embarquée de 32 Go
- Écran TN 1366×768 piloté par l’IGP UHD600
- 2 ports USB-A, 2 ports USB-C, prise jack 3,5 mm
- Un clavier que j’ai trouvé meilleur que ceux des portables professionnels haut de gamme de Dell
- Cette machine est ensuite désignée par le nom d’hôte octopus
Attentes et échec du débogage basé sur Cr50
- L’une des grandes raisons du choix d’un Chromebook était que la puce de sécurité et contrôleur embarqué Cr50 offre des fonctions utiles pour le débogage boîtier fermé
- Presque tous les Chromebook depuis 2018 peuvent utiliser un port USB-C pour le débogage avec un câble SuzyQ, et Cr50 expose généralement trois périphériques ttyUSB
- La console interne Cr50
- La console AP, c’est-à-dire le port série du Chromebook
- La console cros_ec, c’est-à-dire le contrôleur embarqué
- L’objectif était d’accéder à une console série sans laisser l’ordinateur ouvert avec des fils qui dépassent ; avec l’émulation de frappes clavier et le contrôle de l’état d’alimentation de cros_ec, une forme simple de KVM semblait même possible
- Sur octopus, le CCD de Cr50 ne fonctionnait pas
- J’ai fabriqué un nouveau câble SuzyQ et vérifié les soudures avec un autre Chromebook, mais cela a échoué
- octopus était l’un des rares portables où Dell n’a pas monté certaines résistances sur la carte, ce qui empêche le fonctionnement du CCD
- Certains disaient avoir obtenu un succès limité avec une orientation précise du port et sous condition de chargeur branché, mais sur cette machine cela n’a jamais fonctionné
- Des informations vérifiées ensuite indiquaient aussi que les résistances manquantes ne devraient affecter que le flashage SPI, et non le pont USB lui-même ; la raison exacte pour laquelle le débogage Cr50 ne fonctionnait pas du tout reste donc floue
PicoCCD fabriqué avec un Pi Pico
- Une fois la voie Cr50 bloquée, j’ai vérifié s’il était possible d’insérer une carte Pi Pico standard dans l’espace libre à l’intérieur de la machine, et il y avait suffisamment de place
- Je me suis appuyé sur les schémas de portables similaires, mais aucun ne correspondait exactement à octopus
- Le grand port de débogage de la carte était lié au JTAG Intel et à des points de test, et ne convenait pas à l’objectif
- L’autre était Google Servo, mais Google a sorti plusieurs sondes de débogage sous le nom Servo et la documentation est limitée, ce qui rendait la recherche difficile
- La documentation Servo a servi de référence
- En activant l’applet UART de Glasgow et la détection de fréquence, puis en sondant directement les pastilles UART TX suspectes pendant que Linux écrivait en boucle sur
/dev/ttyS1- La pastille TX a été trouvée en quelques minutes
- RX était plus difficile, car il faut émettre activement et toucher la mauvaise ligne peut réinitialiser la carte ; cela a effectivement provoqué deux réinitialisations
- Les broches RX/TX de l’EC ont ensuite été trouvées en une dizaine de minutes
- Les fils soudés ont été fixés avec de l’époxy à durcissement UV, et aucun problème de connexion n’est apparu pendant six mois
- J’ai aussi utilisé le périphérique SPI du RP2040 pour souder six fils à la puce flash, puis coupé la piste allant vers la broche write-protect et l’ai reliée à GND afin d’obtenir un accès en écriture sans autorisation de Cr50
- Côté logiciel, le choix s’est porté sur CircuitPython
- Parce qu’il permet de téléverser scripts et données via un périphérique de stockage de masse USB
- Faire un pont UART vers un périphérique USB
cdc_acmétait simple - Comme la flash SPI était aussi connectée, une fonction de flashage était nécessaire
- J’ai utilisé flashrom, l’outil open source courant de flashage EEPROM/SPI, et serprog, qui proxy le SPI sur UART, correspondait bien au besoin
- Il existait l’implémentation C pico-serprog de stacksmashing, mais le fait de devoir reflasher le Pico à chaque flashage de BIOS ne convenait pas
- J’ai donc implémenté serprog en CircuitPython, en m’appuyant largement sur l’applet serprog de Glasgow
- Le code résultant a été rassemblé sous forme de solution rapide de débogage boîtier fermé appelée PicoCCD, dont le dépôt est PicoCCD sur Forgejo
- WeirdTreeThing a également écrit du code C pour RP2040 dans un but similaire, et il existe aussi sa version de PicoCCD
Récupération des journaux de démarrage de SerenityOS
- J’ai installé Alpine Linux pour le débogage et configuré les utilitaires de base permettant de récupérer un noyau SerenityOS construit sur une autre machine
- La configuration a ensuite évolué vers une structure qui télécharge automatiquement les artefacts depuis la machine de build, décompresse et remplace le noyau, et inclut même une entrée GRUB qui extrait le
.tarde l’espace utilisateur - Le temps d’itération entre une modification et le test était d’environ 20 secondes au moment de l’écriture, ce qui est plutôt correct pour du hacking bare metal
- La première entrée de démarrage GRUB était en pratique
multiboot /Kernel serial_debug, mais ni l’écran ni le port série n’affichaient quoi que ce soit - Pour le problème d’affichage, j’ai trouvé qu’il fallait ajouter
insmod all_videoà l’entrée de démarrage GRUB ; cela ne suffisait pas à résoudre le problème, mais c’était la bonne direction - L’absence de sortie série était un problème plus important
- Les journaux coreboot arrivaient encore quelques secondes auparavant
- L’UART de cette machine n’était pas un 16550 traditionnel mappé sur ports, mais un 16550A basé sur MMIO
- Les journaux Linux indiquaient
ttyS0etttyS1comme des 16550A à des adresses MMIO - Dans
lspci, l’Intel Celeron/Pentium Silver Processor Serial IO UART Host Controller apparaissait comme périphérique PCI
Contournement via l’UART 16550 et le port 0x80
- Traditionnellement, les périphériques externes des IBM PC étaient mappés sur les I/O ports des CPU x86, accessibles avec des instructions comme
outbetinb - Beaucoup de périphériques sont ensuite passés à MMIO, mais les ports série, pour lesquels la concurrence à haute vitesse n’est pas critique, ont souvent conservé l’ancienne approche, ce qui les rend utiles comme ports de débogage
- Dans un environnement courant, écrire quelque chose comme
outb 0x3f8, 0x41permet de recevoirAde l’autre côté, et l’initialisation est suffisamment courte pour être facile à implémenter dans de petits projets - L’UART d’octopus était un périphérique MMIO derrière PCI, et au tout début du démarrage de SerenityOS il était difficile de compter sur une initialisation PCI
- SerenityOS dispose d’une implémentation du bus PCI, mais l’étape de démarrage était beaucoup trop précoce
- Le
PCISerialDeviceexistant n’avait pas non plus été utilisé dans un contexte MMIO - Écrire ce pilote sans sortie de débogage n’était pas idéal
- Le contrôleur embarqué des appareils ChromeOS journalise toutes les écritures vers IO port 0x80
- Ce port sert traditionnellement au reporting d’état POST
- Les afficheurs de codes de démarrage à 7 segments des cartes mères fonctionnent en décodant le port 80
- J’ai testé l’hypothèse sous Linux avec un script écrivant des octets dans
/dev/port, et ces octets étaient lisibles dans la console cros_ec - En ajoutant du code comme
IO::out8(0x80, 1);autour deKernel/Arch/init.cpp, le point d’entrée de SerenityOS, j’ai suivi la progression et réduit le problème jusqu’à un crash dansMemory::MemoryManager::initialize(0); - J’ai ensuite essayé de remplacer l’adresse de la routine d’écriture série, de 0x3f8 vers 0x80
- Au début, beaucoup d’octets sortaient, mais cros_ec ne pouvait pas les relayer de façon fiable et débordait
- Plus loin, la sortie de journal cros_ec elle-même se corrompait
- Contrairement à une vraie puce série, il n’y a pas de gros tampon
- Le contournement a consisté à insérer de longs wait states basés sur des
nopentre chaque écriture- Afficher tous les messages de démarrage faisait passer le démarrage de quelques secondes à plusieurs minutes
- Pour du débogage bare metal, ce coût restait acceptable
- Pour parser automatiquement les lignes de journal cros_ec et les décoder en ASCII, j’ai utilisé une ligne de commande Bash combinant
picocom,watch,grep,sed,cutetxxd
Framebuffer et première sortie graphique
- Après avoir récupéré les journaux de démarrage, j’ai passé plusieurs jours à lire directement la base de code pour comprendre le problème, avant de finir par demander de l’aide à la communauté
- spholz m’a indiqué la PR SerenityOS #24435, alors ouverte, et en construisant cette branche, le generic framebuffer fonctionnait
- L’écran affichait un résultat ressemblant à une opération réussie alors qu’elle avait échoué, puis les problèmes de stockage sont devenus vraiment visibles
eMMC et problèmes d’initialisation SD/MMC
- Le crash existant dans StorageManagement venait d’un échec d’initialisation du SD Host Controller, suivi d’une assertion sur une liste de contrôleurs vide
- Les journaux affichaient
PCI: Failed to initialize SD Host ControlleretASSERTION FAILED: !m_controllers.is_empty() - Au final, un kernel panic survenait dans
StorageManagement::enumerate_storage_devices()
- Les journaux affichaient
- octopus possède une puce eMMC de 32 Go, et SerenityOS disposait déjà d’une partie des pilotes SD ; il semblait donc suffire d’ajouter la prise en charge MMC
- Pour utiliser une carte SD/MMC, il faut globalement trois éléments
- Host Controller : sur les machines modernes, généralement SDHCI, spécifié par la SD Association
- Le bus reliant le Host Controller : ici PCI
- L’implémentation du protocole de communication entre l’hôte et la carte
- D’après les journaux de crash, SerenityOS possédait les deux premiers éléments ; le problème restant se situait côté protocole
- Le protocole SD a une spécification publique, mais MMC, devenu un standard JEDEC en 2007, nécessite un accès officiel payant
- SD et MMC ont des séquences d’initialisation différentes
- SerenityOS commençait par envoyer CMD0 et attendre une réponse, ce qui devrait passer pour SD comme pour MMC
- Il envoyait ensuite CMD8 pour configurer la tension, mais MMC ne la prend pas en charge et doit donc renvoyer une erreur
- Certaines ressources suggèrent ensuite de réinitialiser la carte et de la considérer comme MMC
- D’autres proposent un flux plus complet combinant les résultats de CMD8 et CMD58 pour distinguer la version SD et le type de capacité
- Je n’ai pas implémenté tous les contrôles de compatibilité avancés, seulement les vérifications de base
Registre de contrôle d’alimentation manquant et résolution
- Le flux d’initialisation MMC se résumait aux étapes suivantes
- Après reset, régler l’horloge à 400 KHz
- Attendre 1 ms, puis 74 cycles d’horloge supplémentaires
- Envoyer CMD0 et attendre une réponse
- Répéter CMD1 jusqu’à ce que le bit 31 de la réponse passe à 1
- À la fin de la boucle, enregistrer la valeur comme registre Operating Conditions
- Continuer l’algorithme d’initialisation SD en excluant la lecture des registres propres à SD
- Optionnellement, détecter la compatibilité High-Speed et activer l’un des modes HS
- Le code arrivait environ jusqu’à l’étape 4, mais ensuite l’eMMC ne répondait plus à aucune requête
- Après plusieurs jours sans trouver la cause, la suppression du code lié au reset du contrôleur a fait répondre l’eMMC, ce qui a isolé le problème dans la fonction
reset_host_controller() - Ce qui était étrange dans cette fonction, c’est que je ne trouvais aucun registre
host_configurationdans le standard- L’ancien code avait regroupé arbitrairement plusieurs registres dans deux groupes
host_configuration - L’initialisation n’était pas non plus complète, et le premier groupe était simplement mis à 0
- L’ancien code avait regroupé arbitrairement plusieurs registres dans deux groupes
- Ce premier groupe incluait le registre Power Control, qui contrôle le régulateur d’alimentation de la carte
- Sur certains matériels, y compris toutes les implémentations utilisant l’eMMC, ce registre est nécessaire pour allumer la carte elle-même
- Sur d’autres conceptions où les rails d’alimentation sont directement connectés au slot, ce réglage peut être ignoré
- Comme solution temporaire, j’ai repris et utilisé la valeur initialement présente dans
host_configuration_0 - Le problème central était que le code essayait de communiquer avec une carte qui n’était pas alimentée
- Il a ensuite fallu encore quelques heures pour trouver et désactiver certaines commandes valides uniquement pour les cartes SD ; avec des sorties de débogage plus utiles côté contrôleur, le reste du travail s’est déroulé de façon relativement classique
État actuel et travail restant
- Au final, SerenityOS a réussi à lancer, très lentement, une session graphique partiellement corrompue, mais il se bloque peu après
- Le problème de cette session graphique et le processus qui a permis de remettre le framebuffer en état normal seront abordés dans un prochain billet
- L’ensemble du travail a été un processus d’apprentissage d’environ six mois, au cours duquel d’autres choses ont aussi avancé
- Le prochain objectif est de nettoyer les patchs et de les envoyer upstream d’ici la fin de l’année
1 commentaires
Avis sur Hacker News
J’ai lu quelque part qu’il était relativement facile d’adapter les pilotes de NetBSD à un noyau personnalisé ; je me demande si Serenity ne pourrait pas suivre cette voie.
Pour un OS naissant, les pilotes de périphériques sont un obstacle majeur.
La configuration matérielle y étant presque fixe, il devient plus facile de créer ou de porter des pilotes, et de tester le système.
Il permet d’exécuter des pilotes en espace utilisateur avec seulement un support de base minimal.
https://en.wikipedia.org/wiki/Rump_kernel
En revanche, je connais moins bien la partie pilotes.
C’est globalement ainsi qu’Apple a procédé depuis le début, et c’est le seul exemple de grand succès dans les Unix grand public.
System76 en est presque un exemple aussi, et Frame.work est similaire, même si l’accent y est moins mis sur l’OS lui-même.
Le faire fonctionner sur une machine où toutes les conditions étaient défavorables est vraiment un travail de hack impressionnant, et semble être le résultat d’un effort énorme de personnes talentueuses.
Quand je lis ce genre d’article, je me demande comment débuter dans le monde des pilotes et des OS.
Tout a l’air tellement complexe que je ne sais pas vraiment par où commencer.
C’est ce qu’on appelle les entrées/sorties mappées en mémoire (MMIO). Dans une application classique, ce n’est pas possible parce que le noyau empêche l’accès direct à la mémoire matérielle.
Pour commencer, il faut un langage capable de générer du code machine pour le CPU cible, comme Rust/C++/C/Zig, de préférence sans runtime ni GC. Si les langages bas niveau sont nouveaux pour vous, je recommande le C, car il y a beaucoup d’exemples.
Il faut aussi apprendre les bases de l’assembleur du CPU cible, car certaines instructions peuvent ne pas être exposées sous forme de fonctions intégrées dans les langages de plus haut niveau.
Ensuite, en écrivant un noyau « hello world », on apprend comment le CPU démarre le noyau, et comment les modes d’exécution et les niveaux de privilège sont organisés.
Puis, sur x86, on configure le CPU comme on le souhaite, par exemple en passant en long mode pour utiliser les instructions 64 bits ; c’est généralement à cette étape qu’on configure aussi la mémoire virtuelle.
Arrivé là, on commence à comprendre comment le CPU s’articule avec l’OS, comment énumérer les périphériques disponibles et trouver leurs emplacements mémoire ; il reste ensuite beaucoup de travail, comme les systèmes de fichiers ou l’ordonnanceur.
La différence entre un logiciel qui tourne au-dessus de l’OS et le noyau de l’OS tient finalement au mode CPU dans lequel le code s’exécute ; au niveau de privilège maximal, on peut utiliser des instructions indisponibles pour les applications ordinaires.
Plus tard, j’ai trouvé de véritables pépites cachées dans la documentation FreeBSD, notamment le FreeBSD Architecture Handbook et le FreeBSD Developers' Handbook, qui peuvent être particulièrement utiles.
https://lwn.net/Kernel/LDD3/
https://docs.freebsd.org/en/books/
Minix est écrit de façon très propre, son noyau fait autour de 5 000 lignes, et il est couvert par plusieurs manuels.
Nous avons implémenté un petit serveur et fait un peu de hack du noyau ; comme Minix est un micro-noyau, la plupart des pilotes fonctionnent de cette manière.
Je lisais les supports de cours à l’avance et j’allais à peine en amphi, mais j’ai quand même eu 8/10.
J’ai aussi entendu beaucoup de bien de NetBSD et de SerenityOS, et Andreas a fait une grande partie du développement en live streaming.
Une fois qu’on sait par où commencer, cela devient en fait plus facile.
Par exemple, un pilote de périphérique expose une interface permettant à d’autres programmes exécutés sur l’ordinateur d’accéder à un périphérique et de le contrôler.
https://m.youtube.com/watch?v=juGNPLdjLH4 est un bon cours accéléré.
On peut aussi créer un petit périphérique USB qui échange des informations avec un PC à l’aide de quelque chose comme Arduino. Exemple : https://m.youtube.com/watch?v=yTc2GLXfCOY
Ensuite, il suffit de comprendre ce que fait le sous-système qui vous intéresse, comment le faire fonctionner, puis d’écrire le code. Cela inclut par exemple les périphériques de stockage ou graphiques.
Le Raspberry Pi peut aussi être un bon point de départ pour ce type d’expérimentation. Exemple : Writing a bare metal operating system for the raspberry pi https://github.com/babbleberry/rpi4-osdev
https://wiki.osdev.org/Bare_Bones
J’aime le concept de SerenityOS et le navigateur Ladybird, donc je suis content de voir ces progrès.
Ladybird est non seulement devenu un projet indépendant, mais ne considère plus SerenityOS comme une plateforme cible.
Ladybird retire progressivement sa propre couche Serenity pour la remplacer par des alternatives plus mainstream.
En tant qu’utilisateur principalement sous Linux, je suis enthousiaste à l’idée que Ladybird devienne une vraie alternative sur Linux.
Mais en tant que fan de SerenityOS, je regrette que l’énergie et l’innovation qui allaient dans Ladybird quittent SerenityOS.
Si vous avez besoin d’aide pour hacker un Chromebook, vous pouvez demander sur la liste de diffusion chromium-os-dev.
Quelqu’un pourra probablement vous aider à faire fonctionner CCD.
https://groups.google.com/a/chromium.org/g/chromium-os-dev?p...
Le bootloader Depthcharge prend aussi en charge le démarrage réseau via TFTP.
Il faut le compiler soi-même et le flasher sur le SPI, mais c’est une fonctionnalité excellente quand on développe un noyau de façon itérative.
https://chromium.googlesource.com/chromiumos/platform/depthc...
Je pensais que SerenityOS tournait déjà sur du matériel réel ; est-ce qu’il tourne encore entièrement dans QEMU ?
Mais il n’y avait presque pas de pilotes dignes de ce nom, donc cela ne fonctionnait que dans le sens le plus basique, uniquement sur certains matériels, et probablement pas très bien.
Cette tentative vise à le faire fonctionner de manière fiable sur au moins une plateforme matérielle réelle.
Serenity reste impressionnant, même quand je ne suis pas d’accord avec certains choix d’implémentation.
C’est pour voir ce genre de truc terrifiant que j’étais venu.
doas dd seek=$((0x$1)) bs=1 count=1 of=/dev/port < <(xxd -p -r <<< "$2")