Files
Victor Bodinaud 2ece6aadbc Faire tourner le simulateur iOS 6 sur macOS, avec souris et clavier
- Compilation sur macOS : en-têtes OpenSSL et libslirp de Homebrew, apps et
  agent compilés avec le clang de Xcode (cible 5.1, code ARM : le ld récent
  ne pose plus le bit Thumb sur LC_MAIN ni sur les IMP Objective-C).
- Sauvegarde du disque : copie par clone APFS (cp -c) au lieu des options GNU.
- Agent : commandes text et key (événements GS clavier matériel construits à
  la main, appui vide puis relâchement portant le caractère).
- Fenêtre macOS native (macos/, tools/build_mac_app.sh) : affichage via le VNC
  local, souris convertie en gestes, clavier transmis à l'agent.
- tools/update_agent.py remplace l'agent à chaud ; tools/vnc_input.py ajoute
  la souris et le clavier à un client VNC classique.
2026-10-05 11:49:27 +02:00
..
…
…
…
…

Simulateur expérimental iOS 6 — développement natif

Le vrai iOS 6.1.6 (10B500, iPhone 3GS) tourne dans QEMU, avec SpringBoard, UIKit et un écran 320 × 480. On peut compiler une app Objective-C ARM32, l’installer, la lancer, envoyer des gestes et vérifier ses fichiers et ses logs. Cet outil est destiné au pilotage direct par l’agent de développement : commandes locales, captures du framebuffer réel et tests automatisés.

Il vit dans le dossier ios6/ du fork Mahtan/qemu-ios ; le modèle de machine est hw/arm/ios6_n88.c, compilé avec le reste du dépôt. Sur le workbench, la copie de travail est le worktree /home/dev/qemu-ios-ios6 (branche agent/ios6-n88), et /home/dev/ios6-emu pointe vers son dossier ios6/.

Utiliser la session locale

cd /home/dev/ios6-emu
bin/ios6 start
bin/ios6 wait

L’agent utilise bin/ios6 pour installer et lancer les apps, envoyer des gestes, inspecter leur état et récupérer les preuves de test. Les fonctions de tools/simulator.py sont aussi accessibles depuis les scripts Python.

# Construire et installer l’exemple natif
.venv/bin/python tools/build_app.py
bin/ios6 install build/apps/SimulatorLab.app
bin/ios6 launch org.ios6emu.SimulatorLab

# Piloter et observer
bin/ios6 tap 160 296
bin/ios6 swipe 160 400 160 140 --ms 600
bin/ios6 front
bin/ios6 screenshot run/capture.png
bin/ios6 logs SimulatorLab
bin/ios6 get /var/mobile/Library/Caches/SimulatorLab-result.txt run/result.txt
bin/ios6 home

# Test automatisé de l’exemple installé
.venv/bin/python tools/smoke_test.py

# Conserver les données et arrêter
bin/ios6 stop

install copie le bundle sous /Applications/<identifiant>.app, sauvegarde le disque et redémarre iOS pour enregistrer l’app. Prévoir quelques minutes. Les bundles acceptés contiennent un exécutable Mach-O ARM32 signé ad hoc ; les identifiants Apple et les liens symboliques sont refusés. Une mise à jour remplace les fichiers fournis ; les anciens fichiers supplémentaires restent présents. Les apps sont installées comme applications système de développement, pas dans un conteneur App Store : les tests d’isolation ne sont pas représentatifs.

Persistance : utiliser bin/ios6 stop. Le disque invité est en RAM ; cette commande synchronise, suspend la VM, extrait le volume et rejoue son journal HFS dans une nouvelle copie avant de remplacer firmware/rootfs-dev.hfs. Tuer QEMU perd les changements depuis la dernière sauvegarde. stop --discard abandonne explicitement ces changements. Ne jamais modifier hors ligne le disque d’une VM active.

Le test smoke_test.py contrôle SpringBoard, le lancement de l’app, la remise à zéro et trois incréments UIKit, le transfert binaire, un aller-retour HTTP vers l’hôte et les traces NSLog. Il conserve capture, logs et rapport dans run/reports/. Les preuves de validation sont dans results/20261003-native-apps.

Sur macOS (non encore essayé)

Le disque et le noyau préparés ne sont pas dans Git : ce sont des fichiers Apple (1,8 Gio). Une archive prête est sur le NAS, dans le gestionnaire de fichiers UGOS : data/workbench/home/ios6-emu-mac/ios6-emu-firmware.tar.zst (930 Mio, SHA-256 dans le fichier .sha256 voisin). Elle contient firmware/rootfs-dev.hfs, firmware/kernel-rootfs.bin et firmware/boot-display-rootfs.bin, soit le nécessaire pour bin/ios6 start.

brew install ninja pkgconf glib pixman libpng jpeg-turbo openssl@3 libslirp zstd
git clone https://git.mahtan-melwasul.com/Mahtan/qemu-ios.git && cd qemu-ios/ios6
shasum -a 256 -c ~/Downloads/ios6-emu-firmware.tar.zst.sha256  # depuis ~/Downloads
zstd -dc ~/Downloads/ios6-emu-firmware.tar.zst | tar -xf -
export PKG_CONFIG_PATH="$(brew --prefix openssl@3)/lib/pkgconfig:$PKG_CONFIG_PATH"
bash tools/build.sh        # compile ce dépôt QEMU dans ios6/build, puis netd
bin/ios6 start && bin/ios6 wait
.venv/bin/python tools/smoke_test.py

L’affichage est accessible en VNC sur 127.0.0.1:5906 (Partage d’écran : vnc://127.0.0.1:5906). bin/ios6 stop sauvegarde le disque dans firmware/rootfs-dev.hfs du Mac ; il reste indépendant de celui du workbench.

Restent propres au workbench Linux : la préparation du disque (prepare_rootfs.py, prepare_dev.py, qui utilisent ipod-emu), la compilation de l’agent invité (build_agent.sh) et celle des apps (build_app.py), qui demandent /opt/cctools et les SDK locaux. Compiler les apps sur le workbench puis les copier sur le Mac pour bin/ios6 install.

Préparer un autre projet natif

L’exemple est dans apps/SimulatorLab/main.m. Le petit script de compilation assemble les fichiers .m à la racine d’un projet, crée son Info.plist et signe son exécutable avec ldid :

.venv/bin/python tools/build_app.py apps/MonApp --bundle-id org.exemple.MonApp
bin/ios6 install build/apps/MonApp.app
bin/ios6 launch org.exemple.MonApp

Le SDK iOS 6.0 est local sous references/sdk/iPhoneOS6.0.sdk. Le script tools/prepare_sdk.py récupère les trois archives du dépôt Sn0wCooder/theos-sdks, à une révision fixe, et vérifie le SHA-256 de l’archive assemblée. Un SDK existant peut être choisi avec --sdk ou IOS6_SDK. La compilation utilise le cross-compilateur /opt/cctools/bin/arm-apple-darwin9-gcc et ldid déjà présents dans cet environnement. Ce script est un point de départ pour les projets Objective-C simples ; il ne gère pas les projets Xcode, les storyboards ou la copie des ressources. Pas de prise en charge Swift.

Pour préparer un disque de développement après la préparation du firmware et du profil complet décrite plus bas :

python3 tools/prepare_boot.py --profile display --rootfs --software-rendering
bash tools/build.sh
python3 tools/prepare_dev.py
python3 tools/prepare_sdk.py

prepare_dev.py conserve le disque de démonstration et ajoute l’agent au nouveau rootfs-dev.hfs. Il exige --reset pour remplacer un disque de développement existant et refuse d’opérer sur une VM active. L’agent est compilé avec le SDK 2.2.1 local pour ses API C communes ; les apps utilisent le SDK 6.0. Le relais hôte nécessite les en-têtes et la bibliothèque libslirp via pkg-config.

Réseau et automatisation

UART1 (0x82600000, IRQ 23, /dev/tty.debug) transporte PPP en mode PIO. L’agent publie la configuration réseau dans configd. L’invité utilise 10.0.2.15, la passerelle hôte 10.0.2.2 et le relais DNS 10.0.2.3. Un serveur HTTP local peut donc être testé depuis une app via http://10.0.2.2:PORT. Les anciens protocoles TLS d’iOS 6 peuvent être incompatibles avec les services modernes.

L’agent root est accessible sur 127.0.0.1:7796 et l’affichage VNC sur 127.0.0.1:5906. Le tactile passe par GraphicsServices, pas par VNC. Ces interfaces de développement restent liées à la boucle locale. bin/ios6 exec /chemin/programme arguments lance un processus invité et transmet son code de sortie ; put fichier-local /chemin/invité et get transfèrent les fichiers. Les journaux de session et les PID sont sous run/. L’autorisation com.apple.springboard.launchapplications de l’agent permet le lancement avec SBSLaunchApplicationWithIdentifier.

Portée et limites

La chaîne a été validée avec l’app UIKit fournie. La compatibilité de chaque nouvelle app reste à tester. Le GPU, l’audio, la caméra, le modem, le GPS et le stockage sécurisé matériel ne sont pas reproduits. L’accéléromètre est fixe. Il n’y a pas encore de saisie directe au clavier hôte, de multitouch, de débogueur source intégré, ni d’installation IPA/App Store. Ce prototype permet des tests fonctionnels ; ses performances et son matériel ne représentent pas fidèlement un iPhone physique.

App native de test

Historique du démarrage système

Le premier écran d’accueil a été reproduit depuis le firmware original en approximativement 220 secondes, puis vers 115 secondes depuis un volume initialisé. Les captures proviennent du framebuffer invité.

SpringBoard iOS 6

Deux profils permettent de distinguer les résultats :

  • Le profil initial exécute le noyau sans patch, avec le disque de restauration. Il reste conservé comme référence et bloque sur le coprocesseur ARM7M.
  • Le profil expérimental display --rootfs charge le système complet, omet les dépendances audio/IOP et la boussole, et adapte le memdev au stockage physique. Il emploie quatre modifications d'opcodes documentées dans prepare_boot.py (deux pour le disque, deux pour borner l'attente de l'horloge), avec vérification des octets originaux. Le DeviceTree désactive le protocole NAND secure-root-prefix selon le chemin prévu par AppleARMPE et renseigne le panneau Pinot.

L'horloge, le PMU et le panneau LCD sont reconnus grâce aux modèles expérimentaux I²C/CLCD/DSIM. La sortie graphique traduit les tables legacy-DART remplies par AppleH2PDART et émet l'interruption LCD 37 à 60 Hz. Ces modèles sont partiels : ils ne constituent pas une émulation fidèle du matériel, ni du GPU. Un trousseau temporaire en RAM est accepté par AppleKeyStore ; le stockage sécurisé matériel reste absent.

iBoot et la BootROM ne sont pas exécutés : le noyau est lancé directement. Le texte « iBoot version » vient d'une propriété du DeviceTree.

Essai du système complet

python3 tools/prepare_rootfs.py  # recrée uniquement la copie rootfs-vm.hfs
python3 tools/prepare_boot.py --profile display --rootfs --software-rendering
bash tools/build.sh
python3 tools/probe.py --seconds 300 --boot-data boot-display-rootfs.bin \
  --rootfs --capture-interval 10 --dump-disk

Le disque original reste dans firmware/rootfs.hfs. Sa copie expérimentale rootfs-vm.hfs est agrandie à 1776 Mio et reçoit un fstab pour /dev/md0, la journalisation ASL. --software-rendering omet le GPU SGX du DeviceTree. Les variables CA_NO_ACCEL=1, CA_ENABLE_OGL=0 sont également transmises aux processus graphiques. Le paramètre launchctl_enforce_codesign=0, lu par le launchctl original, permet le chargement des plists locales au lieu des configurations précompilées du cache xpcd. Leur prise en compte a été vérifiée dans l'environnement des processus invités. Le programme expérimental src/keybag_init.c, installé comme ios6-session, initialise un trousseau de session sans clé matérielle, via les fonctions Apple du cache 10B500. Le démon keybagd original est conservé et son job est écarté. Le service de session n'enregistre pas son port IPC : les clients n'attendent donc pas une réponse d'un serveur absent. Le profil ajoute amfi_allow_any_signature=1 pour exécuter cet outil local. Le service vérifie l'absence de code de verrouillage avant d'envoyer le geste initial. Il réveille ensuite régulièrement l'écran pendant six minutes pour la démonstration, puis cesse toute entrée automatique. Le bundle CommCenterMigrator.migrator est conservé dans /System/Library/EmulatorDisabledMigrators : la migration cellulaire attendait indéfiniment le modem absent. Les autres migrations Apple restent exécutées. Les capacités cellulaires de N88AP.plist sont désactivées pour éviter les fonctions liées au modem. Cela ne suffit pas à supprimer les appels CoreTelephony de SpringBoard : les jobs CommCenter* sont aussi conservés hors de LaunchDaemons, dans EmulatorDisabledDaemons. Le job mediaserverd est également écarté, l'audio n'étant pas émulé. Les tables de polices Apple U/UR remplacent les tables H/HR qui désignent des fichiers absents de ce firmware. Le modèle LIS302DL acquitte son reset et fournit une accélération verticale fixe. Le bundle Setup.app est conservé dans EmulatorDisabledApplications, hors du répertoire des applications : l'assistant exige des services d'activation absents de cette VM. L'état d'activation n'est pas modifié. La sonde probe.py utilise un mapping privé : les écritures invitées disparaissent à l'arrêt, sauf si --dump-disk conserve un instantané guest-disk.hfs dans les preuves.

--diagnostic-panic provoque volontairement une panique à la fin d'un essai pour relever les processus et piles. Le journal précédant cette intervention est sauvegardé séparément ; ce diagnostic ne compte pas comme une panique spontanée. La sonde conserve aussi screen.ppm, sans modifier ses pixels. --capture-interval 30 ajoute des captures pendant le démarrage ; --dump-ram conserve les 256 Mio de RAM. Un essai est borné à 300 secondes au maximum. python3 tools/verify.py --rootfs vérifie les artefacts et le plan mémoire ; python3 tools/verify_display.py teste séparément le périphérique graphique avec des pixels synthétiques, sans démarrer iOS.

Un premier démarrage a terminé les migrations Apple sur le volume agrandi. Son instantané local est conservé dans firmware/rootfs-initialized.hfs ; son journal HFS est encore actif. Les modifications hors ligne de cet instantané ont produit des erreurs de lecture au démarrage suivant. tools/recover_hfs.py source.hfs nouvelle-copie.hfs vérifie et rejoue ses transactions dans une nouvelle copie avant toute modification hors ligne. Il contrôle les sommes de contrôle du journal et des blocs ; il ne remplace pas une vérification complète du système de fichiers. Le premier essai de cette récupération a permis de reprendre l'état initialisé et de modifier les configurations sans reproduire l'erreur de lecture du profil. prepare_rootfs.py repart toujours du firmware original et réinitialise cette copie de travail. Le fichier evidence/initialized-rootfs.json trace l'instantané.

Reproduire dans cet environnement

Les commandes se lancent depuis ios6/ (/home/dev/ios6-emu sur le workbench). L'émulateur iPhone OS 2 et sa session sous /srv/homes/iphoneos2/ipod-emu ne sont pas modifiés.

cd /home/dev/ios6-emu
python3 tools/verify.py
python3 tools/probe.py --seconds 10 --trace \
  --expect-panic 'ARM7M not stopped for some reason'

La sonde crée un nouveau dossier evidence/probe-<date>/ par essai, conserve serial.log, qemu.log, stderr.log, la commande et les registres dans result.json, puis arrête uniquement la VM qu'elle a lancée. Elle utilise un socket QMP privé, sans ouvrir de port TCP. La durée est bornée à 300 secondes et la trace à environ 32 Mio (contrôle toutes les 50 ms).

Sans --expect-panic, une panique invitée produit le code de sortie 2. Avec cette option, le code 0 signifie que le blocage attendu a été retrouvé, pas que le système a fini de démarrer. guest_status et milestones décrivent séparément les progrès observés.

Une trace ciblée est possible :

python3 tools/probe.py --seconds 5 \
  --trace-range 0x8065f600..0x8065f800 \
  --expect-panic 'ARM7M not stopped for some reason'
.venv/bin/python tools/kernel_address.py 0x8065f7c0

Préparation et compilation

Les fichiers Apple, les téléchargements de référence, l'environnement Python et les sorties de compilation restent locaux et sont exclus de Git.

cd /home/dev/ios6-emu
python3 tools/prepare_firmware.py
python3 tools/prepare_boot.py
bash tools/build.sh
python3 tools/verify.py

prepare_firmware.py récupère l'IPSW directement chez Apple, vérifie son SHA-256, extrait les conteneurs IMG3 avec OpenSSL et vérifie la taille et l'Adler32 du noyau LZSS. Le dernier bloc AES doit être lu avec le padding du tag IMG3 : l'ancien xpwntool disponible ici tronquait 45 octets à la décompression.

tools/build.sh configure le dépôt parent dans ios6/build (cible arm-softmmu seule) : il ne touche pas au build/ de l'émulateur 2.2.1. Le modèle hw/arm/ios6_n88.c est déclaré dans hw/arm/meson.build sous CONFIG_IPOD_TOUCH_2G ; le binaire contient donc aussi la machine iPod.

Dépendances de compilation : GCC, Git, Ninja, pkg-config, Python avec venv, en-têtes GLib/Pixman/OpenSSL et dépendances habituelles de ce fork QEMU. Les paquets Python sont isolés dans .venv. L'image du workbench ne fournit pas encore Ninja, pkgconf, diffutils ni les bibliothèques d'exécution (pixman, libpng, libjpeg-turbo, alsa-lib, libslirp) : les installer avec sudo pacman -S après chaque redémarrage du conteneur.

Matériel et chargement réalisés

Élément Modèle actuel
CPU Cortex-A8 ARMv7 de QEMU, un cœur
RAM 256 Mio physiques à 0x40000000, dont 240 Mio annoncés utilisables
Vecteurs Page de RAM expérimentale à l'adresse physique zéro
Console UART Samsung à 0x82500000, réutilisée du fork iPod
Interruptions Trois PL192 à 0xbf200000, 0xbf210000, 0xbf220000
Temps Compteur 24 MHz à 0xbf100200/204 ; timer one-shot 208/220, ligne VIC 6
Noyau Segments Mach-O chargés à partir de 0x40001000, entrée 0x40086084
Paramètres boot_args à 0x41000000, format Version 5 requis par 10B500
DeviceTree À 0x41004000, issu du firmware et complété comme par un chargeur
RAMDisk Disque de restauration Apple à 0x42000000
Autres MMIO Réponses nulles et traces bornées ; périphériques non émulés

Le profil rootfs relocalise la RAM à 0x70000000, les paramètres à 0x71000000 et la VRAM à 0x7f000000. Son disque de 1776 Mio commence à 0x00100000 et ne chevauche pas la RAM ni les MMIO.

Le compteur et le timer sont des modèles minimaux validés pour ce démarrage, pas une implémentation exhaustive du contrôleur S5L8920. La page de vecteurs est également un aménagement de démarrage direct, à préciser avant de viser une émulation fidèle de la chaîne de boot.

Le DeviceTree reçoit les fréquences CPU, la mémoire, la console et les régions réservées pram/vram, ainsi qu'une partition NVRAM common vide et valide. Les placeholders NVRAM nuls du firmware entraînaient une boucle infinie dans IODTNVRAM. La graine aléatoire fixe est réservée à ces essais reproductibles. Dans le profil initial, le code du noyau n'est jamais réécrit ; verify.py compare chaque segment et vérifie la mise à zéro du BSS ainsi que les limites des allocations.

Preuves du premier démarrage

Le dossier results/20261003-springboard conserve la capture de l'écran d'accueil. Le dossier results/20261003-first-boot conserve le rapport de validation, les empreintes des composants et le journal série (aux retours de ligne normalisés ; original brut dans evidence/). Deux essais avec la NVRAM corrigée ont reproduit le blocage ARM7M. La trace du dernier essai contient des FIQ et IRQ réellement prises par le processeur.

Ces preuves concernent le premier jalon, antérieur au démarrage de launchd. Les essais plus récents et leurs instantanés se trouvent dans evidence/. Les adresses et les symboles se contrôlent avec kernel_address.py, kext_info.py et cache_address.py (cache dyld avec glissement explicite).

Références : QEMU-iOS, openiBoot S5L8920, structure boot_args Apple, IODTNVRAM Apple. Les adresses sont recoupées avec le DeviceTree et le noyau exacts du firmware.