Skip to content

Comment fonctionnent les appels système macOS en 2026

Ce qui se passe vraiment quand un programme arm64 sur Mac émet une instruction SVC — du stub utilisateur jusqu'au dispatcher BSD, et retour.

Publié le 5 min de lecture

Un appel système est la frontière où le code utilisateur cesse d'être maître et où le noyau prend la main pour un instant. Sur macOS moderne — Apple Silicon comme Intel — presque tout ce qu'un programme fait d'intéressant finit par franchir cette frontière : ouvrir un fichier, allouer de la mémoire, signer un hash, dessiner à l'écran. Comprendre comment cette frontière est franchie est le socle de toute programmation système, de tout reverse engineering, et de toute recherche en sécurité sérieuse sur la plateforme.

Ce guide parcourt le chemin complet : comment la requête quitte votre processus, comment XNU la classe et la dispatch, et comment le résultat revient.

Trois familles, une seule instruction de trap

XNU expose trois familles distinctes d'appels système, toutes acheminées par la même instruction de trap sur arm64 :

  • Syscalls BSD — la surface façon POSIX que tout programmeur Unix connaît : open, read, write, kqueue, et ~480 autres. Numéros positifs.
  • Mach traps — héritage du micro-noyau Mach de Carnegie Mellon, toujours bien vivant dans XNU. mach_msg_trap, task_for_pid, semaphore_wait_trap, et ~60 autres. Numéros négatifs.
  • Appels diagnostic / privés — calls de debug et internes Apple, utilisés par le noyau lui-même et quelques daemons de confiance.

L'instruction qui franchit la frontière est la même dans chaque cas :

svc #0x80   ; le Supervisor Call qui déclenche un syscall macOS

Ce qui les distingue : le registre x16 — le numéro du syscall — et un encodage de classe dans ses bits hauts.

Étape 1 : le stub utilisateur

Quand vous appelez open() en C, vous n'appelez pas le noyau directement. Vous appelez un petit stub dans libsystem_kernel.dylib. Sur arm64 il ressemble à ceci :

_open:
    mov     x16, #5          ; SYS_open
    svc     #0x80            ; trap vers le noyau
    b.cs    __cerror         ; si carry, gère errno
    ret

#5 est le numéro BSD de open. Le stub reste discret : il place le numéro dans x16, exécute le SVC, et transmet le résultat. Les arguments restent dans x0–x7 au passage ; le noyau les lit directement depuis la frame de trap.

Sur x86_64 (Intel et Rosetta 2), l'équivalent utilise l'instruction syscall et un numéro 32-bit qui encode famille + index :

mov     eax, 0x2000005       ; SYSCALL_CLASS_UNIX << 24 | 5
syscall

Les quatre classes :

ClasseCodeFamille
SYSCALL_CLASS_UNIX2syscalls BSD
SYSCALL_CLASS_MACH1traps Mach
SYSCALL_CLASS_MDEP3dépendant-machine
SYSCALL_CLASS_DIAG4diagnostic

Sur arm64 l'encodage équivalent vit dans le signe de x16 : positif pour UNIX, négatif pour MACH.

Étape 2 : le handler de trap

Quand le SVC se déclenche, le CPU saute dans le vecteur d'exception de XNU. Le handler dans osfmk/arm64/sleh.c (sleh_synchronous) décode l'exception, voit que c'est un SVC, et regarde x16 :

  • x16 > 0 → chemin syscall BSD. Le handler appelle unix_syscall64() dans bsd/dev/arm64/systemcalls.c, qui cherche le syscall dans la table sysent[] générée depuis bsd/kern/syscalls.master.
  • x16 < 0 → chemin Mach trap. Le handler appelle mach_call_munger64() dans osfmk/arm64/bsd_arm64.c, qui indexe mach_trap_table[] par la valeur absolue.

C'est cette séparation qui fait qu'une même instruction SVC peut signifier deux choses totalement différentes — le noyau lit d'abord le signe de x16.

Étape 3 : dispatch

Pour les syscalls BSD, unix_syscall64() fait trois choses :

  1. Valider le numéro par rapport à sysent_size. Hors limites → nosys(), qui retourne ENOSYS.
  2. Copier les arguments depuis la frame utilisateur vers la struct d'arguments du syscall, avec extension de signe typée et validation de pointeurs.
  3. Appeler le handlersysent[N].sy_call(p, &args, &retval). Ce handler est l'implémentation réelle (ex. sys_open() pour le syscall #5).

Le handler tourne en mode noyau, mais l'espace d'adressage du processus reste mappé, pour pouvoir copyin() / copyout() entre tampons utilisateur et noyau en sécurité.

Pour les Mach traps, mach_call_munger64() joue le même rôle sur mach_trap_table[] — sauf que la table est bien plus courte, et chaque entrée encode le nombre d'arguments.

Étape 4 : le retour

Quand le handler termine, le chemin s'inverse :

  1. La valeur de retour va dans x0 (et x1 pour les syscalls dont le type est plus large).
  2. Si le handler BSD a retourné non-zéro, le noyau positionne le flag carry dans le PSTATE de la frame.
  3. Le noyau émet eret (Exception Return), qui restaure l'état utilisateur et reprend après le SVC.
  4. Dans le stub, b.cs __cerror branche si carry est positionné ; __cerror met x0 dans errno et retourne -1.

Les Mach traps n'utilisent pas la convention du carry flag. Ils retournent un kern_return_t directement — KERN_SUCCESS (0) ou un code KERN_* — et l'appelant compare manuellement.

Pourquoi c'est utile en pratique

Connaître ce chemin compte pour trois publics :

  • Travail de performance : chaque syscall coûte au minimum une entrée noyau, un copyin, un copyout, et une exception return. Sur Apple Silicon c'est ~150 ns d'overhead avant même le travail réel. Le batching avec readv / writev / kevent enlève l'essentiel.
  • Reverse engineering : quand vous voyez svc #0x80 précédé de mov x16, #N, vous identifiez immédiatement le syscall par numéro. Idem pour mov eax, 0x2nnnnnn sur binaires Intel. Le catalogue /syscall est un lookup numéro → nom.
  • Sécurité : chaque syscall est observable côté noyau. L'injection de code finit toujours par task_for_pid suivi de mach_vm_write et thread_create_running — trois Mach traps qui, combinés, donnent l'exécution arbitraire.

Connaître la frontière, c'est aussi savoir où regarder quand quelque chose cloche.

Pour aller plus loin

Et si vous voulez juste la donnée : chaque syscall de chaque release XNU livrée par Apple est dans le catalogue de référence.

Articles associés

Guide pratique pour tracer syscalls BSD et Mach traps avec DTrace sur macOS moderne — prérequis SIP, scripts qui marchent, et que faire quand les probes Apple disparaissent.
XNU mélange un micro-noyau Mach 3 et une personnalité BSD : à quoi sert chaque famille de syscalls et comment elles diffèrent en pratique.
Correspondance pratique entre les syscalls macOS et les événements Endpoint Security pour la détection en EDR moderne.