Exécution de code exotique

Palaiseau, le dimanche 2021-03-14

Joyeuse journée de π !

J'ai passé le plus clair de l'an passé à faire de l'empaquetage pour le projet pure blend Debian Med. À ce titre, il m'est arrivé régulièrement d'intervenir sur des bugs sur d'autres architectures que l'omniprésente Intel x86_64. Par exemple :

Aujourd'hui, en dehors de mon radio réveil, qui embarque l'un des derniers processeur x86 à avoir été distribué au grand public dans un ordinateur personnel, la totalité de mes machines tournent avec des processeurs à architecture x86_64. L'accès à d'autres architectures ne m'est pour le moment matériellement pas possible. s390x est une architecture de grand système IBM ; je ne vais pas en installer un dans mon appartement, il bousillerait le parquet, et en plus mon foie n'est pas à vendre, j'en ai besoin en ce moment. Les processeurs POWER étaient plus ou moins synonymes de Macintosh à une époque révolue, de même que Motorola 68000 à une époque encore plus révolue, et je n'ai aucune idée d'où me procurer les autres puces. Ma dernière tentative de faire importer un processeur Risc-V depuis les Royaumes Unis a failli se solder par une visite de la douane ; je dois les faire importer des États-Unis d'Amérique, en admettant que ce circuit là ne soit pas sujet à régulations gouvernementales. Même problème pour les processeurs MIPS type Loongsoon, je ne m'attends pas à en avoir un entre les mains même en me rendant en Chine. Quand aux processeurs ARM, je n'en suis pas excessivement fan. Comme la vie peut être dure pour les porteurs de programmes !

Heureusement, il y a une solution pour avoir facilement accès à du matériel indisponible ou exotique : l'émulation de processeur ! Pour résoudre mes problèmes de portages, j'utilise Qemu. Ce programme peut fonctionner dans trois modes différents, comme indiqué sur la page d'accueil :

  1. le système est entièrement émulé : lancer Qemu dans ce mode permet d'imiter le comportement de n'importe quelle machine physique supportée par le programme, et de lancer dessus n'importe quel système d'exploitation ;
  2. l'émulation en mode utilisateur : c'est le mode dont j'use et j'abuse, au lieu de démarrer un système d'exploitation complet, il m'est possible de lancer des programmes issus d'architectures différentes de celle qui me sert à faire tourner mes machines ;
  3. la virtualisation accélérée : c'est le mode le plus communément utilisé, le système invité est isolé de l'hôte, comme dans le cas de l'émulation totale du système, à ceci près que, en admettant que le noyau et le processeur supportent ce mode de fonctionnement, le noyau hôte va être capable de décharger les instructions des programmes de la machine virtuelle sur le processeur natif afin de réduire au minimum la perte de performances liée à une quelconque émulation.

Les premier et troisième cas sont largement traités sur le web depuis au moins une bonne décennie, et je n'ai rien à ajouter sur le sujet ; quiconque a besoin de travailler avec des machines virtuelles peut travailler avec l'interface virt-manager et se laisser guider par l'interface graphique. Le cas qui m'intéresse est celui de l'émulation en mode utilisateur.

Comment fonctionne l'émulation en mode utilisateur ? Lors de l'installation du mode utilisateur de Qemu, par exemple via le paquet qemu-user-static sur Debian, une série d'émulateurs est enregistrée auprès du noyau afin d'être capable d'interpréter les programmes ciblant les architectures de processeur étrangères. Pour installer ce paquet, il n'y a rien de plus simple que de l'installer avec apt :

$ sudo apt install qemu-user-static

En Debian 11, Qemu est capable de gérer vingt-sept architectures différentes en mode utilisateur. Les interpréteurs capables d'exécuter les programmes compilés en architecture étrangère sont disponibles dans le répertoire /usr/libexec/qemu-binfmt/, il y en a vingt-sept :

$ ls /usr/libexec/qemu-binfmt/
aarch64-binfmt-P     mips64-binfmt-P     riscv64-binfmt-P
alpha-binfmt-P       mips64el-binfmt-P   s390x-binfmt-P
arm-binfmt-P         mipsel-binfmt-P     sh4-binfmt-P
armeb-binfmt-P       mipsn32-binfmt-P    sh4eb-binfmt-P
cris-binfmt-P        mipsn32el-binfmt-P  sparc-binfmt-P
hppa-binfmt-P        ppc-binfmt-P        sparc32plus-binfmt-P
m68k-binfmt-P        ppc64-binfmt-P      sparc64-binfmt-P
microblaze-binfmt-P  ppc64le-binfmt-P    xtensa-binfmt-P
mips-binfmt-P        riscv32-binfmt-P    xtensaeb-binfmt-P

Bien que ces composants soient nécessaires pour l'exécution de programmes en architecture étrangère, ils ne sont pas suffisants. Si en l'état, j'essaie de lancer un programme, comme le /bin/ls de l'architecture Risc-V 64 bits :

$ file /mnt/archs/sid-riscv64/bin/ls
/mnt/archs/sid-riscv64/bin/ls: ELF 64-bit LSB pie executable, UCB RISC-V, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux-riscv64-lp64d.so.1, BuildID[sha1]=bd00c8fc6273108a31db91b40b0ed3ec2922ae70, for GNU/Linux 4.15.0, stripped

alors j'ai un plantage :

$ /mnt/archs/sid-riscv64/bin/ls
riscv64-binfmt-P: Could not open '/lib/ld-linux-riscv64-lp64d.so.1': No such file or directory

Les programmes sur ma machine ne sont pas juste de simples copies des instructions et données qui seront exécutées sur le processeur de ma machine, il sont également empaqueté dans un format de fichier : le format ELF. Les programmes peuvent être compilés au choix : en embarquant statiquement toutes les bibliothèques dont il dépend, ou bien en embarquant des informations permettant à l'éditeur de liens de retrouver dynamiquement les bouts de code manquant en cherchant dans les bibliothèques systèmes. Le projet Debian pousse à ce que tous les programmes utilisent l'édition de lien dynamique. L'une des raisons principales étant que ça fait un seul paquet à mettre à jour quand un problème de sécurité doit être corrigé dans une bibliothèque. Si un programme est compilé en statique, et que des vulnérabilités de sécurité se présentent dans une bibliothèque, alors il faut penser à recompiler ce programme avec la nouvelle version. En pratique, trop de programmes statiques continuent d'embarquer des copies de bibliothèques vulnérables même sur des systèmes à jour.

Le programme /bin/ls est un exemple simple de fichier ELF lié dynamiquement. Comme indiqué par objdump, ce programme a besoin de la bibliothèque C standard et de la bibliothèque NSA SELinux pour fonctionner correctement :

$ objdump -p /bin/ls | grep NEEDED
  NEEDED               libselinux.so.1
  NEEDED               libc.so.6

En exécutant /bin/ls avec ldd, on peut également noter la présence de la bibliothèque libdl, qui permet de gérer le chargement de bibliothèques dynamiques supplémentaires au cours de l'exécution du programme, notamment via la fonction dlopen(3), et à ce titre : sont chargées les fonctions de threads d'une part, et d'expressions rationnelles en Perl d'autre part :

$ ldd /bin/ls
        linux-vdso.so.1 (0x00007ffccfdc8000)
        libselinux.so.1 => /lib/x86_64-linux-gnu/libselinux.so.1 (0x00007f4699b0c000)
        libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f4699947000)
        libpcre2-8.so.0 => /lib/x86_64-linux-gnu/libpcre2-8.so.0 (0x00007f46998af000)
        libdl.so.2 => /lib/x86_64-linux-gnu/libdl.so.2 (0x00007f46998a9000)
        /lib64/ld-linux-x86-64.so.2 (0x00007f4699b90000)
        libpthread.so.0 => /lib/x86_64-linux-gnu/libpthread.so.0 (0x00007f4699887000)

Afin de fonctionner, l'émulateur doit avoir accès à l'intégralité de ces bibliothèques dans leur architecture cible. Il est donc nécessaire d'avoir pratiquement un système auxiliaire complet installé dans cette architecture. Même si un système complet peut être relativement lourd à manipuler, il est en fait relativement facile d'en construire un avec debootstrap, par exemple pour la plateforme arm64, qui est officiellement supportée par Debian :

$ sudo debootstrap --arch=arm64 bullseye /mnt/archs/db11-arm64
I: Target architecture can be executed
[...]
I: Configuring libc-bin...
I: Base system installed successfully.

Cette commande permet de construire la racine d'un système d'exploitation Debian Bullseye minimal dans le répertoire /mnt/archs/db11-arm64, et compatible avec l'architecture arm64. Première remarque, le système de base d'une architecture étrangère doit être normalement installé en deux étapee, avec les options --foreign, et puis --second-stage. Mais comme qemu-user-static est déjà disponible sur le système hôte, la procédure d'installation du système de base est capable d'arriver au bout sans erreur.

Juste pour l'expérience, je vérifie à nouveau que l'exécution directe des programmes ciblant l'architecture ARM 64 bits ne passe toujours pas :

$ /mnt/archs/db11-arm64/bin/ls /
aarch64-binfmt-P: Could not open '/lib/ld-linux-aarch64.so.1': No such file or directory

Si toutefois, je change de racine système avec la commande chroot(8), alors cette fois ci, l'exécution de la commande passe :

$ sudo chroot /mnt/archs/db11-arm64 /bin/ls /
bin   dev  home  media	opt   root  sbin  sys  usr
boot  etc  lib	 mnt	proc  run   srv   tmp  var

Un changement un peu plus permanent de racine système permet d'avoir un prompt et de commencer à travailler un peu comme si j'étais sur une machine aarch64 :

$ uname -m
x86_64

$ file /bin/ls
/bin/ls: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=6461a544c35b9dc1d172d1a1c09043e487326966, for GNU/Linux 3.2.0, stripped

$ sudo chroot /mnt/archs/db11-arm64

# uname -m
aarch64

# apt-get update && apt-get install --yes file
[...]

# file /bin/ls
/bin/ls: ELF 64-bit LSB pie executable, ARM aarch64, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux-aarch64.so.1, BuildID[sha1]=9c1a4999161b8b2da681b80d8bf351e40afc40ad, for GNU/Linux 3.7.0, stripped

Toutefois, l'exécution de programmes aarch64 est réduite à l'espace de travail du chroot. Dans le contexte d'un environnement de compilation isolé et éphémère, c'est plutôt bienvenu. Mais pour faire des tests plus poussés, je devrais effectuer un montage lié de mon répertoire home dans la nouvelle racine, y rajouter mon utilisateur, etc. Je ne détaille pas ces étapes, elles ne sont pas très intéressantes pour le propos, et j'utilise un autre outil pour gérer mes multiples racines systèmes auxiliaires avec architectures de processeur étrangères : schroot. Combiné à Qemu, schroot fournit des capacités très similaires, mais non équivalentes, à des conteneurs capables d'exécuter du binaire à destination de n'importe quel type d'architecture de processeur. De l'aveu de l'auteur de schroot lui-même, les technologies de conteneurs aujourd'hui à la mode devraient prendre le pas sur les cas d'utilisation de schroot. Toutefois, je ne suis pas encore capable de lâcher schroot pour docker ou équivalent aujourd'hui, probablement parce que je suis dans le cas « super-niche » du développeur qui veut s'en servir pour faire du portage avec Qemu en mode utilisateur.

En bonus pour le petit malin qui se demanderait comment se comporte le système si on copiait l'éditeur de lien du système aarch64 sur le système hôte en x86_64, car après tout les noms sont différents : l'un est ld-linux-aarch64.so.1, et l'autre est ld-linux.so.2. Voici ce que cela donnerait :

$ /mnt/archs/sid-arm64/bin/ls /
/mnt/archs/sid-arm64/bin/ls: error while loading shared libraries: libselinux.so.1: cannot open shared object file: No such file or directory

L'éditeur de lien est capable d'interpréter le fichier ELF Aarch64, mais toutes les bibliothèques Aarch64 sont manquantes, y compris la bibliothèque C, ou, comme indiqué par le message d'erreur, la bibliothèque NSA SELinux. Et là, manque de bol, le nom entrerait en conflit avec la bibliothèque existante pour le système hôte, donc tenter de copier les bibliothèques étrangères sur l'hôte ne réussirait qu'a tout casser. Mais peut-être qu'on pourrait s'en sortir avec le support de la gestion des architectures multiples ?

[ICO]NameLast modifiedSize
[PARENTDIR]Parent Directory  -

  —