3 points par GN⁺ 2024-07-01 | 1 commentaires | Partager sur WhatsApp
  • C est simple et expressif, mais les seules règles de l’instruction switch et des labels permettent déjà d’écrire du code capable de dérouter même des développeurs expérimentés
  • L’obfuscation à la manière de l’IOCCC cache le code avec des macros et de la mise en forme, mais la syntaxe propre à C permet aussi de produire du code lisible tout en étant déroutant
  • switch (...) n’exige pas d’accolades et, comme il saute vers le label case correspondant, il peut se comporter différemment du flux habituel d’initialisation d’un bloc
  • Les labels case n’ont pas besoin de se trouver au niveau supérieur d’un bloc switch, si bien qu’un switch sans accolades combinant if (0) case... et une chaîne else if compile aussi
  • L’extension GNU C des adresses de labels avec && permet même de créer son propre switch ou des boucles fondées sur des labels, mais certains exemples sont propres à GCC et peuvent ne pas être sûrs vis-à-vis des comportements indéfinis

Le code déroutant que permet la syntaxe de C

  • C a de nombreux défauts, mais sa syntaxe simple et son expressivité en ont fait un langage utilisé pour écrire de grands logiciels comme des systèmes d’exploitation
  • Sa syntaxe concise a aussi influencé la structure du code de nombreux langages grand public qui lui ont succédé, de Java à Go
  • L’exemple emblématique de l’obfuscation de code est l’IOCCC
    • Les programmes primés à l’IOCCC sont souvent composés de macros de préprocesseur, d’une mise en forme inhabituelle, de noms de variables peu utiles et d’expressions arithmétiques obscures
    • Ce type de code peut susciter l’admiration, mais il faut le rétroconcevoir en code normal, ce qui peut le rendre moins amusant à étudier

Les recoins profonds de C révélés par switch, case et goto

  • switch (...) peut s’écrire sans accolades, comme if (...) ou for (...)
    • switch (i) case 1: puts("i = 1"); compile correctement
    • Sans accolades, une seule instruction est attachée au switch, si bien que le case 2: qui suit n’est plus à l’intérieur du switch et provoque une erreur
  • switch est fondamentalement une structure proche de goto, qui se déplace vers le label case correspondant
    • Même si le bloc switch contient int a = 123; et puts(...), un saut vers default: n’exécute pas le code d’initialisation qui le précède
    • Dans ce cas, a n’est pas initialisée à 123 et, techniquement, le comportement est indéfini
    • L’exemple est consultable sur Godbolt
  • Les labels case n’ont pas besoin d’être uniquement au niveau supérieur du bloc switch auquel ils sont associés
    • Une forme comme if (0) case 0: puts("i = 0"); fonctionne aussi
    • Comme switch saute directement vers ce case, le if (0) qui le précède est ignoré
    • Les autres instructions d’affichage qui suivent restent bloquées par la condition if (0), ce qui permet d’éviter le fallthrough même sans break
    • L’exemple est consultable sur Godbolt
  • Une chaîne if ... else se comporte syntaxiquement comme une seule instruction de niveau supérieur ; on peut donc la combiner avec switch pour créer un switch étrange sans accolades
    • L’exemple inclut des case de plage case 1 ... 10 ainsi qu’un default
    • L’exemple est consultable sur Godbolt
  • L’opérateur &&, une extension GNU, permet d’obtenir l’adresse d’un label puis de faire un goto vers cette adresse
    • On peut l’utiliser pour implémenter un switch maison avec une approche comme goto *(void*[]){ &&case_0, &&case_1, &&case_2 }[i];
    • L’exemple est consultable sur Godbolt
  • La même extension GNU permet d’implémenter une boucle fondée sur des labels dans une déclaration de variable, même sans for (...)
    • L’exemple construit avec des labels et goto *&&_ une boucle qui affiche de i = 0 à i = 5
    • L’exemple est consultable sur Godbolt
    • Ce dernier extrait est propre à GCC et peut ne pas être sûr vis-à-vis des comportements indéfinis
  • En C, même sans obfusquer volontairement le code, on peut créer des formes totalement inhabituelles et déroutantes

1 commentaires

 
GN⁺ 2024-07-01
Commentaires sur Hacker News
  • L’exemple ci-dessus affiche la valeur de a, tout en disant qu’elle n’est pas initialisée à 123, mais en C cela peut effectivement arriver
    Utiliser une variable non initialisée ne signifie pas lire « la valeur qui se trouvait par hasard encore dans cette mémoire » : c’est un comportement indéfini, donc le compilateur peut faire ce qu’il veut
    Par exemple, il pourrait toujours initialiser cette mémoire à 123, ou considérer que tout le fragment relève d’un comportement indéfini et supprimer toutes les instructions, de sorte que rien ne soit affiché. Il peut même optimiser en supprimant le return qui suit, ou des instructions précédentes, ce qui peut donner l’impression qu’un comportement indéfini a des effets « à rebours dans le temps »

    • En C, un comportement indéfini ne remonte pas réellement le temps
      Il peut affecter des instructions précédentes, mais les réordonnancements de code et les transformations complexes peuvent aussi se produire sans comportement indéfini
    • En C, l’accès à un objet à durée de stockage automatique non initialisé est le seul cas où il s’agit d’un comportement indéfini
      Les objets statiques sont toujours initialisés, donc cette situation ne se présente pas
      Restent les objets dynamiques, par exemple un membre non initialisé d’une structure allouée avec malloc ; lire de la mémoire dynamique non initialisée n’est pas un comportement indéfini en C, et l’on obtient la valeur représentée par les bits non initialisés. Si le type concerné n’a pas de représentation piège, cela ne peut pas échouer
  • Ce genre de syntaxe est aussi possible
    switch(k) { if (0) case 0: x = 1; if (0) case 1: x = 2; if (0) default: x = 3; }
    On peut en faire une sorte de switch qui n’a pas besoin d’un break à la fin de chaque clause, et même un macro du type #define brkcase if (0) case. Le compilateur n’aimera probablement pas le flot de contrôle, mais il devrait en général bien l’éliminer

    • #define brkcase break;case semble fonctionner de manière similaire, mais l’objectif du macro disparaît alors un peu
    • Le fonctionnement de cette approche est très peu clair ; il vaudrait mieux écrire le même flot de contrôle avec goto plutôt que d’abuser du switch à la manière du Duff's device
    • Cette approche ne fonctionne que lorsque le corps du label case tient sur une ligne ou est entouré d’accolades
      Par le passé, j’ai utilisé cette structure pour signifier « ne sauter que la première ligne du label case suivant, puis continuer en fallthrough sur le reste »
      Tout devient cohérent si l’on considère les labels case non pas comme des séparateurs entre instructions, mais comme de simples labels
  • On peut utiliser cette technique pour implémenter des coroutines en C : https://stackoverflow.com/questions/24202890/switch-based-co...

  • Je ne sais pas pourquoi j’ignorais que case 1 ... 10: était du C valide
    J’ai utilisé C pendant des années, et je me demande de quelle norme cela vient

    • Si cela n’a pas été standardisé récemment, ce n’est pas du C valide mais une extension GNU
    • Cela ressemble à une extension GNU C : https://gcc.gnu.org/onlinedocs/gcc/Case-Ranges.html
      Je n’ai pas trouvé l’historique de cette extension, et à ma connaissance elle n’existe pas dans le C standard. Pour clang, je ne suis pas sûr
  • Il y a quelque temps, pour m’amuser, j’ai écrit un bout de C bizarre qui compte à rebours de 10 à 1
    Les versions C, Python et shell peuvent toutes tenir en une ligne en utilisant && et des appels récursifs

    • Petite remarque : sys.stdout.write(f"{n}\n") peut être remplacé par print(n)
      Le code actuel, si l’on retire les f-string, ressemble à du style Python 2, où print était une instruction. En Python 3, print est une fonction normale et renvoie None, qui est évalué comme faux ; il faut donc aussi remplacer le premier and par un or
  • Une autre chose surprenante est que 4[arr] est identique à arr[4]

    • Comme un tableau est converti en pointeur, c’est essentiellement *(array_label+offset), et ici cela devient *(offset+array_label)
      Autrement dit, *(arr+4) et *(4+arr) sont identiques
    • Par le même principe, arr[i][j] et j[i[arr]] sont exactement identiques
      Il suffit de savoir que a[x][y] est équivalent à (a[x])[y], et que a[x] est équivalent à x[a]
      arr[i][j](arr[i])[j](i[arr])[j]j[i[arr]]
  • Le dernier fragment de code obscurci de l’article montre encore une autre extension GCC : https://stackoverflow.com/questions/34559705/ternary-conditi...

  • J’avais d’abord vu ces techniques ludiques de l’auteur de ce blog sur Twitter
    On peut aussi créer des boucles avec une instruction switch : https://twitter.com/lcamtuf/status/1807129116980007037

  • J’ai l’impression que ce genre de bidouille avec switch est une partie importante du Duff's device

    • Le Duff's device repose sur le fait qu’avec la syntaxe de K&R C, on peut mélanger un bloc switch et une boucle, sur le choix de laisser les case tomber en fallthrough si l’on n’écrit pas explicitement break, et sur le fait qu’en C une boucle peut sauter de nouveau à l’intérieur du switch
      Duff cherchait à optimiser des entrées/sorties à mémoire mappée (MMIO), et on ne ferait probablement plus cela ainsi en C aujourd’hui. Les MMIO ne sont plus aussi rapides que les instructions CPU, et dès qu’il y a un peu de données, on peut utiliser le DMA
      Un langage moderne ne traiterait sans doute pas non plus les MMIO comme de simples déréférencements de pointeurs ; en C, on a continué à ajouter au système de types des échappatoires pour préserver cela
      À titre personnel, je considère que le mécanisme des iterate loops de WUFFS est l’héritier du « Device » de Tom Duff. Il permet de préciser comment dérouler partiellement N étapes d’une boucle, tout en promettant un résultat équivalent à N exécutions du corps principal de la boucle, mais potentiellement plus rapide. La vectorisation comprend plus facilement l’intention, et les cas limites pénibles comme M % N != 0 sont correctement gérés par l’outil, pas par l’humain