Crévindiou, c’est pas du bon C d’chez nous ça, cé du C deu’l ville !

Magazine
Marque
GNU/Linux Magazine
Numéro
267
Mois de parution
janvier 2024
Spécialité(s)


Résumé

IANAL (I Am Not A Linguist), mais quand j’entends du québécois, je ne comprends pas tout, mais je comprends. Mais qu’en est-il des dialectes du langage C ? Car oui, le langage C a des dialectes, et nous allons voyager un peu à travers l’un d’entre eux, le dialecte GNU, supporté principalement par GCC, mais aussi, en partie, par Clang.


Body

Si on essaie de compiler le code suivant avec le compilateur Clang en lui demandant d’être pédant :

void foo(int arg) {
    return arg ? : -1;
}

On obtient un message d’avertissement :

$ clang -c a.c -fpedantic
a.c:2:16: warning: use of GNU ?: conditional expression extension, omitting middle operand [-Wgnu-conditional-omitted-operand]

Clang a compris le sens de ce code, mais ce code n’est pas standard. D’ailleurs est-ce que toi, lecteur, tu comprends sa signification ? Si l’argument entre le point interrogation et le point-virgule est omis, il prend pour valeur le résultat de l’évaluation de la condition, dans notre cas arg. En ce sens, cela fournit une syntaxe assez proche de l’opérateur or en Python.

Dans la suite de cet article, nous explorerons plusieurs constructions propres au dialecte GNU C, tel que documenté dans https://gcc.gnu.org/onlinedocs/gcc/C-Extensions.html, en essayant de retrouver à chaque fois une construction similaire dans une version plus moderne du langage, ou dans un autre langage.

1. Statement Expression

En GNU C, la construction ({int result = foo(); result >= 0;}) est une expression dont la valeur est le résultat de l’évaluation de la dernière instruction. Cela permet de construire des macros élégantes avec variables intermédiaires, par exemple :

#define max(x, y) ({ __typeof(x) tmp_x = x; __typeof(y) tmp_y = y; tmp_x > tmp_y ? tmp_x : tmp_y; })

Cette macro utilise aussi une autre extension GNU, __typeof qui n’est pas sans rappeler le decltype de C++, et qui a été standardisée en C23 sous le nom typeof. À peine commence-t-on notre voyage qu’on est déjà assailli de verbiage inintelligible :-).

On notera que depuis C++17, le programmeur C++ a à sa disposition une construction semblable partout où l’on évalue une condition, par exemple :

if (auto res = foo(); res >= 0) {
    // stuff
}

2. Computed Goto

D’après le théorème fondamental de l’ingénierie logicielle, il n’est pas de problème qui ne puisse être résolu avec un niveau d’indirection supplémentaire. Si on considère que les goto sont un problème, on ne peut que se réjouir de la possibilité de faire des sauts indirects (possibilité offerte par le jmp de x86, d’ailleurs) en prenant l’adresse d’un label pour ensuite sauter à cette adresse. La syntaxe pour lire l’adresse d’un label est void* ptr = &&label_name, et on peut y sauter à travers goto *ptr. C’est souvent utilisé pour implémenter des automates, chaque état correspondant à un label. Par exemple, le code suivant implémente un switch pour les chaînes de caractères, avec un mix de C++ et de GNU C du plus bel effet :

void foo(std::string state) {
    static const std::unordered_map<std::string, void*> jumps = {
      {"case0", &&case0},
      {"case1", &&case1},
      {"case2", &&case1},
    };
    auto where = jumps.find(state);
    if(where == jumps.end()) return;
    goto *where->second;
 
  case0:
    // stuff
    return;
  case1:
    // other stuff
    return;
  case2:
    // yet another set of stuff
    return;
}

3. Nested Functions

Définir une fonction à l’intérieur d’une fonction, on connaît, ça existe en C++ depuis C++11, c’est une lambda. Mais avec GNU C, on peut définir une fonction à l’intérieur d’une fonction, elle capture alors toutes les variables de la fonction englobante par référence, et cela permet, au choix de limiter la portée d’une fonction, d’associer un état à une fonction, ou de se tirer une balle dans le pied. Voici un exemple d’usage sympathique :

void my_sort(float *data, size_t n, bool reverse)
{
    int cmp(const void *a, const void *b)
    {
        int r = *(float *)a > *(float *)b;
        return reverse ? -r : r;
    }
    qsort(data, n, sizeof(*data), cmp);
}

Le code généré est assez astucieux, il repose sur l’utilisation d’un trampoline pour éviter tout appel de fonction, je laisse les curieux investiguer.

4. La foire à la saucisse des nombres flottants

Vous avez besoin de types de données particulières pour représenter une certaine classe de nombres à virgule ? GNU C est fait pour vous : fractions, nombres décimaux, nombres à virgule flottante sur 80 bits ou 128 bits, description d’un litteral flottant par sa représentation en hexadécimal (syntaxe d’ailleurs reprise en C++17 !). Tout cela est possible, je vous laisse tenter de retrouver le type de chacun des littéraux suivants :

12.3W; /* non ce ne sont pas des Watts */
12.3q;
12.3df;
12.3dd;
12.3dl; /* pas plus que des décilitres ici */;
0x1.fp3; /* le prédécesseur du mp3 ? */

Depuis C++11, il est possible de définir ses propres opérateurs de conversion de littéraux.

5. Le bon vieux struct hack

Le motif suivant n’est pas standard, mais est tellement courant que vous l’avez sûrement déjà rencontré :

struct array {
    int size;
    char data[0];
};
struct array * myarray = malloc(sizeof(struct array) + my_size);
myarray->size = my_size;

Une version de cette extension a été normalisée en C99 sous le nom de « flexible array member » et dans ce cas, la déclaration de data est juste char data[];.

6. Tableau de taille variable

Ce n’est que depuis le C99 que l’on peut déclarer un tableau de taille variable, ce qui a souvent pour effet d’agrandir la pile d’une valeur dynamique (ce qui peut avoir des implications en termes de sécurité, mais passons, nous ne sommes pas dans MISC ici). Par exemple, on peut déclarer un tableau en vue de préparer un strcpy : char buffer[strlen(input) + 1];. Cette syntaxe est invalide en C++ et optionnelle depuis C11, mais ça reste du GNU C valide !

7. Les case range

Il arrive de devoir écrire un switch pour lequel plusieurs cases marquent le même fragment de code, ce qui conduit au relativement disgracieux :

case 0:
case 1:
case 2:
case 3:
   // stuff

En GNU C, on bénéficie d’un raccourci de langage pour exprimer cela :

case 0 .. 3:
  // stuff

Plutôt élégant, non ? Et pourtant, rien de tel en C moderne ni en C++, mais c’est un motif qu’on peut retrouver dans les match de Python.

8. Les attributs

Rien que par leur syntaxe, on sent qu’on est dans du dialecte pur : __attribute__((always_inline)), ce n’est clairement pas du C standard ! Et pourtant, certains de ces attributs sont tellement utiles qu’ils trouvent leur chemin dans de nombreuses bases de code. Et une fois de plus, C++ a fini par normaliser certaines de ces expressions, comme par exemple __attribute__((noreturn)) qui s’exprime avec un joli [[noreturn]] en C++11 (et qui signifie que la fonction ne rend jamais la main, par exemple parce qu’elle appelle systématiquement abort(), ou parce qu’elle lève systématiquement une exception).

Petite sélection du chef :

  • __attribute__((malloc)) indique que la valeur de retour de la fonction a les mêmes propriétés que la valeur de retour de malloc, ce qui aide l’analyse d’aliasing.
  • __attribute__((flatten)) demande au compilateur d’inliner tous les appels de fonction à l’intérieur de la fonction décorée, ce qui revient à mettre son graphe d’appel à plat.
  • __attribute__((deprecated)) est un autre classique (normalisé sous la forme [[deprecated]] en C++14) qui enjoint (oui, qui enjoint) le compilateur à émettre un message d’avertissement si la fonction associée est référencée. Existe aussi sous la forme __attribute__((unavailable)) pour générer une erreur.
  • __attribute__((pure)) indique au compilateur que la fonction marquée n’a pas d’effets de bord, ce qui permet au compilateur d’appliquer diverses optimisations (p. ex. élimination d’expressions communes).
  • __attribute__((hot)) (et son pendant __attribute__((cold))) permet de se substituer aux informations de profiling si celles-ci ne sont pas présentes pour indiquer au compilateur s’il doit optimiser plus agressivement et placer la fonction dans une section regroupant toutes les fonctions utilisées fréquemment, ou au contraire s’il peut optimiser plus légèrement la fonction et la placer dans une section qui a peu de chances d’être chargée en mémoire (typiquement, une fonction qui affiche un message d’erreur avant d’interrompre le programme).

9. #pragma GCC

Le langage C a ceci d’amusant qu’il a prévu une façon standard d’écrire des dialectes, à travers les directives #pragma. Le résultat est moins divertissant qu’une nouvelle syntaxe, mais au moins c’est portable (dans le sens où un compilateur tiers peut ignorer cette directive sans lâcher une erreur de syntaxe). On y retrouve des classiques comme #pragma GCC unroll n pour dérouler une boucle d’un pas de n, mais aussi le support de directives venant d’autres compilateurs, comme #pragma push_macro("macro_name") qui nous vient de MSVC et qui permet avec son compagnon #pragma pop_macro("macro_name") de gérer une pile de définitions de macros. J’utilise personnellement de temps en temps #pragma GCC optimize (string...) pour changer le niveau d’optimisation d’une fonction, très pratique quand on cherche à isoler une régression du compilateur !

10. [[noreturn]] void terminate() noexcept

Au-delà de la gratification intellectuelle de découvrir de nouvelles constructions syntaxiques — et nous n’avons fait qu’effleurer le sujet, cet article avait pour but d’illustrer deux aspects, qui sont d’ailleurs communs aux langages informatiques et naturels : quand on cherche à exprimer quelque chose, on crée le vocabulaire pour le faire, quelle que soit la forme pure du langage (-pedantic disait le compilateur). Et de manière amusante, cet usage rentre dans les mœurs pour finir par se standardiser :-).

Je tiens par ailleurs à remercier Lancelot Six pour sa relecture aussi agréable que précise de cet article.



Article rédigé par

Par le(s) même(s) auteur(s)

Des soucis à la chaîne

Magazine
Marque
MISC
HS n°
Numéro
30
Mois de parution
octobre 2024
Spécialité(s)
Résumé

L’histoire, ou plutôt l’Histoire, est une coquine. Et quand Dennis Ritchie et Ken Thompson inventent le langage C en 1972, ils prennent une décision anodine, une micro-optimisation qui fait gagner quelques octets, mais qui aura un impact important sur la sécurité de nombreux systèmes : en C, les chaînes de caractères sont terminées par un octet positionné à zéro.

Édito

Magazine
Marque
MISC
HS n°
Numéro
30
Mois de parution
octobre 2024
Résumé

En regardant la liste des 25 failles les plus dangereuses éditées par MITRE chaque année, on ne peut qu’être frappé par la présence (ou la persistance) de thèmes bien connus : écriture illégale dans une zone mémoire, utilisation d’une zone mémoire désallouée, lecture illégale d’une zone mémoire, déréférencement de pointeur NULL, dépassement de la capacité d’un entier… Autant de sujets qui sont pourtant abordés dans les premiers chapitres de tout bouquin traitant de la sécurité logicielle. Ce qui n’en fait pas pour autant des sujets faciles dès lors que les considérations de base de code existant et de performances rentrent en compte. C’est compliqué l’optimisation multicritère !

Smash Bros

Magazine
Marque
MISC
HS n°
Numéro
30
Mois de parution
octobre 2024
Spécialité(s)
Résumé

En 1996, Aleph One publiait dans l’e-zine Phrack un article intitulé « Smashing the Stack for Fun and Profit ». C’était il y a plus de 25 ans et les principes énoncés dans cet article sont toujours valides, même si leur exploitation est devenue plus technique.De manière plus conventionnelle, l’écriture dans une zone mémoire non-autorisée est un vecteur d’attaque classique connu sous le doux nom de CWE-787.Dans cet article, on se concentrera sur les attaques ciblant la pile, en détaillant quelques bugs classiques, leur exploitation historique et quelques contre-mesures qui ont été mises en place au fil du temps.

Les derniers articles Premiums

Les derniers articles Premium

La place de l’Intelligence Artificielle dans les entreprises

Magazine
Marque
Contenu Premium
Spécialité(s)
Résumé

L’intelligence artificielle est en train de redéfinir le paysage professionnel. De l’automatisation des tâches répétitives à la cybersécurité, en passant par l’analyse des données, l’IA s’immisce dans tous les aspects de l’entreprise moderne. Toutefois, cette révolution technologique soulève des questions éthiques et sociétales, notamment sur l’avenir des emplois. Cet article se penche sur l’évolution de l’IA, ses applications variées, et les enjeux qu’elle engendre dans le monde du travail.

Petit guide d’outils open source pour le télétravail

Magazine
Marque
Contenu Premium
Spécialité(s)
Résumé

Ah le Covid ! Si en cette période de nombreux cas resurgissent, ce n’est rien comparé aux vagues que nous avons connues en 2020 et 2021. Ce fléau a contraint une large partie de la population à faire ce que tout le monde connaît sous le nom de télétravail. Nous avons dû changer nos habitudes et avons dû apprendre à utiliser de nombreux outils collaboratifs, de visioconférence, etc., dont tout le monde n’était pas habitué. Dans cet article, nous passons en revue quelques outils open source utiles pour le travail à la maison. En effet, pour les adeptes du costume en haut et du pyjama en bas, la communauté open source s’est démenée pour proposer des alternatives aux outils propriétaires et payants.

Sécurisez vos applications web : comment Symfony vous protège des menaces courantes

Magazine
Marque
Contenu Premium
Spécialité(s)
Résumé

Les frameworks tels que Symfony ont bouleversé le développement web en apportant une structure solide et des outils performants. Malgré ces qualités, nous pouvons découvrir d’innombrables vulnérabilités. Cet article met le doigt sur les failles de sécurité les plus fréquentes qui affectent même les environnements les plus robustes. De l’injection de requêtes à distance à l’exécution de scripts malveillants, découvrez comment ces failles peuvent mettre en péril vos applications et, surtout, comment vous en prémunir.

Bash des temps modernes

Magazine
Marque
Contenu Premium
Spécialité(s)
Résumé

Les scripts Shell, et Bash spécifiquement, demeurent un standard, de facto, de notre industrie. Ils forment un composant primordial de toute distribution Linux, mais c’est aussi un outil de prédilection pour implémenter de nombreuses tâches d’automatisation, en particulier dans le « Cloud », par eux-mêmes ou conjointement à des solutions telles que Ansible. Pour toutes ces raisons et bien d’autres encore, savoir les concevoir de manière robuste et idempotente est crucial.

Les listes de lecture

9 article(s) - ajoutée le 01/07/2020
Vous désirez apprendre le langage Python, mais ne savez pas trop par où commencer ? Cette liste de lecture vous permettra de faire vos premiers pas en découvrant l'écosystème de Python et en écrivant de petits scripts.
11 article(s) - ajoutée le 01/07/2020
La base de tout programme effectuant une tâche un tant soit peu complexe est un algorithme, une méthode permettant de manipuler des données pour obtenir un résultat attendu. Dans cette liste, vous pourrez découvrir quelques spécimens d'algorithmes.
10 article(s) - ajoutée le 01/07/2020
À quoi bon se targuer de posséder des pétaoctets de données si l'on est incapable d'analyser ces dernières ? Cette liste vous aidera à "faire parler" vos données.
Voir les 65 listes de lecture

Abonnez-vous maintenant

et profitez de tous les contenus en illimité

Je découvre les offres

Déjà abonné ? Connectez-vous