Voyage en C++ie : les symboles

Magazine
Marque
MISC
Numéro
93
Mois de parution
septembre 2017
Spécialité(s)


Résumé

Cet article est le premier d'une mini-série sur le C++, ou plutôt sur les binaires compilés depuis C++, leurs particularités et comment les concepts du langage se retrouvent parfois dans le binaire final.


Body

Comprendre le lien entre du code C++ et sa version compilée fournit parfois une aide précieuse à l’analyste. Dans cette optique, cette mini-série explore différentes facettes d’un binaire et leur lien avec le source d’origine quand ce dernier est du C++.

Pour commencer, nous allons nous intéresser à la table de symboles, aux sections, bref, à la carcasse ELF d'un code objet — on restera dans le monde Linux. Le compilateur utilisé est g++ (Debian 6.3.0-12) 6.3.0 20170406par conséquent, à moins qu’un drapeau différent soit utilisé dans les exemples, c’est la version gnu14 du standard qui est utilisée (C++14 avec quelques extensions GNU).

1. Rappel sur l’édition de liens

Le langage C++ supporte la compilation séparée, à savoir la compilation d’un programme par morceaux, généralement différents fichiers objets (.o) provenant de la compilation de codes sources (.cpp), mis en commun avec des archives (.a) et des bibliothèques dynamiques (.so) , le tout pour former, si ce n’est une nouvelle bibliothèque, un programme. Chacun de ces codes objet contient du code ou des données associées à un nom de symbole, et des références vers d’autres symboles qui peuvent être présents dans un autre code objet, une bibliothèque, plusieurs fois (et on risque alors un conflit) ou jamais (ce qui peut être problématique). Le rôle de l’édition de liens est de faire ce travail d’appariement.

2. Le Name Mangling, l'encodage des symboles

En C, chaque fonction externe se voit associer, une fois compilée, une entrée dans la table des symboles.

int foo() { return 0; }

compilé par :

> cc -c foo.c

0000000000000000 T foo

aura une table des symboles simple où l'on retrouve l'identifiant foo :

> nm foo.o

Le même code mis dans un fichier foo.cpp et compilé avec g++ (ou autre) donnera :

> g++ -c foo.cpp

> nm foo.o
0000000000000000 T _Z3foov

On ne retrouve plus l'identifiant d'origine, ou plutôt on le retrouve au milieu de toute une décoration, le mangling. Cette transformation de nom permet principalement de supporter la surcharge de fonction, comme l'illustre le code suivant :

int foo() { return 0; }

int foo(int n) { return n; }

compilé par :

> g++ -c foo.cpp
> nm foo.o
000000000000000b T _Z3fooi

0000000000000000 T _Z3foov

Le changement de signature est reflété par le changement d'identifiant, et on peut représenter toutes les surcharges de cette façon. D'ailleurs le drapeau -C de nm permet d'obtenir une version plus familière de cette même table des symboles :

> nm -C foo.o
000000000000000b T foo(int)
0000000000000000 T foo()

La commande c++filt peut être utilisée de façon similaire :

> c++filt _Z3fooi

foo(int)

On remarquera que le type de retour ne fait pas partie du mangling, puisque si on change le type de la fonction en void foo();, le symbole associé restera _Z3foov. Ce qui n'est pas si surprenant puisque, en C++, le type de retour ne rentre pas en compte dans la détermination des surcharges.

Donc si vous rencontrez un symbole répondant au doux nom de _Z3fooRSt6vectorIiSaIiEERKNSt7__cxx114listIiS0_EE, vous savez maintenant que sous sa cagoule, il s'appelle plutôt foo(std::vector<int, std::allocator<int> >&, std::__cxx11::list<int, std::allocator<int> > const&) !

Les symboles n'ont pas le même nom, seule leur version demangled est similaire. Eh bien cette duplication est une spécificité de g++ qu'on ne retrouve pas avec clang++.

Notez que le mangling n'est pas unique ! C'est là qu'intervient la notion d'ABI (Application Binary Interface), et un même compilateur peut utiliser différents mangling suivant l'ABI ciblée. Par exemple, le compilateur msvc peut utiliser l'ABI MS pour Windows 64 bits, GCC utilise on ABI pour Linux 32 ou 64 bits, qui peut être reprise (et c'est le cas par défaut) par Clang, etc. Mais ces ABI évoluent ! Rien que pour GCC, il existe plus de 10 versions mineures de l'ABI C++, comme documenté dans la page info gcc sur le drapeau -fabi-version=N. Le lecteur curieux pour s’essayer au décodage manuel en suivant l’ABI itanium suivie par GCC : https://itanium-cxx-abi.github.io/cxx-abi/abi.html#mangling.

3. Lien C : C++

Le mangling explique à lui seul le besoin du extern "C" utilisé pour déclarer une fonction venant d'une bibliothèque C appelée par un code C++, ou pour déclarer une fonction C++ ayant vocation à être utilisée depuis un code C : il force l'utilisation du mangling C pour ce symbole (ou ce groupe si on utilise la notation avec accolades) :

int foo() { return 0;}

extern "C" int bar() { return 0;}

Une fois compilé et analysé :

> nm bar.o

000000000000000b T bar
0000000000000000 T _Z3foov

On notera que puisque le mangling n'utilise pas de symboles réservés, il est tout à fait possible de déclarer depuis C un symbole importable sans extern "C" :

int _Z3foov() { return 0; }

Et une déclaration de fonction C valide, que l'on peut utiliser depuis C++ à travers extern "C" int _Z3foov() bien sûr, ou, de façon non portable (car dépendant de l’ABI utilisée), int foo();.

Le C peut a priori être remplacé par d'autres langages, mais le standard n'impose que ce dernier. Il y a bien gcc qui permette d'utiliser extern "java" pour une utilisation avec gcj, mais ça reste bien anecdotique.

4. Le mot-clé static

Ce mot-clé respecte l'héritage du C : une définition marquée static apparaît dans la table des symboles, mais n'est pas visible lors de l'étape de lien :

static int foo() { return 0;}

int bar() { return foo();}

Compilé et examiné :

> g++ -c static.cpp
> nm static.o
000000000000000b T _Z3barv

0000000000000000 t _ZL3foov

On notera que le drapeau --extern-only de nm ne liste bien que le symbole bar :

> nm --extern-only static.o

000000000000000b T _Z3barv

C'est ce qui permet de définir deux symboles static dans deux unités de compilation différentes sans qu'elles rentrent en conflit.

5. One Definition Rule

Considérons le code suivant :

template<class T> T bar() { return {};}

int foo() { return bar<int>();}

Une fois compilé sans optimisation, on a une table des symboles assez surprenante :

> g++ template.cpp -c
> nm template.o
0000000000000000 W _Z3barIiET_v

0000000000000000 T _Z3foov

La fonction bar est instanciée une fois pour le type int, ce qui crée un nouveau symbole (en l'absence d'optimisation). Or il arrive souvent qu'une même fonction template soit instanciée dans plusieurs unités de compilation, ne serait-ce que celles de la bibliothèque standard. Eh bien tout se passe bien, car le symbole est marqué comme weak, identifié d'un W dans la sortie de nm. Un symbole normal gagne sur un symbole weak et si deux symboles du même nom marqués weak sont présents lors du lien, l'un des deux est choisi, à la discrétion de l'implémentation, ce qui est tout à fait l'effet attendu ici !

Ce principe de « plusieurs définitions du moment qu'elles sont identiques » est plus connu sous le nom d'ODR, One Definition Rule. Il est aussi valable pour une fonction membre quand celle-ci est définie dans la classe :

struct foo {
   void bar() {}
};
void foobar() {
   foo().bar();

}

La table des symboles comprend des entrées pour foobar et l'appel à foo::bar (le constructeur par défaut ne faisant rien, il semble avoir été ignoré) :

> g++ class.cpp -c
> nm class.o
0000000000000000 T foobar()

0000000000000000 W foo::bar()

Là encore, foo::bar est marqué weak, ce qui est en accord avec l'usage de mettre ce genre de petite méthode dans le fichier d'en-tête.

6. Le mot-clé inline

Il n'est pas rare, dans des fichiers d'en-tête, d'avoir des fonctions marquées inline. Comme ces fichiers sont potentiellement inclus dans plusieurs sources qui peuvent être liées ensembles, on devrait avoir un conflit de symboles lors de l'édition de liens. Et on l'aurait si le mot-clef inline n'était pas utilisé. Regardons la table des symboles pour deux fonctions identiques, l'une étant marquée inline et l'autre non :

inline int foo() { return 0;}

int bar() { return foo();}

Une fois compilé sans optimisation, on a la table des symboles suivante :

> g++ inline.cpp -c
> nm inline.o
0000000000000000 T _Z3barv

0000000000000000 W _Z3foov

La fonction marquée inline est bien présente, mais elle est marquée du sceau de la OneDefinitionRule vue précédemment, ce qui est en accord avec la pratique de mettre ses fonctions dans les fichiers d'en-tête pour qu'un compilateur puisse les expanser un peu partout, sans créer d'erreur de liens.

Notons qu'il est possible de marquer une fonction static inline, dans ce cas elle aura une visibilité statique et sera en plus weak.

7. COMDAT Section

Si on s'attarde un peu plus sur le code objet généré à partir de inline.cpp, on découvre que la fonction _Z3barv n'est pas définie dans la section .text comme sa comparse _Z3foov, mais dans une section suffixée par son nom, la section [6] dans l’exemple ci-dessous :

> readelf --sections --section-groups inline.o
[Nr] Name Type Address Offset Size EntSize Flags Link Info Align
[...]
[ 1] .group GROUP 0000000000000000 00000040 0000000000000008 0000000000000004 11 10 4
[ 2] .text PROGBITS 0000000000000000 00000048 000000000000000b 0000000000000000 AX 0 0 1
[...]
[ 6] .text._Z3barv PROGBITS 0000000000000000 00000053 000000000000000b 0000000000000000 AXG 0 0 1

[...]

COMDAT group section [ 1] `.group' [_Z3barIiET_v] contains 1 sections:

[Index] Name

[ 6] .text._Z3barIiET_v

La section [1] est intéressante, puisqu'elle introduit une COMDATgroupsection qui contient une valeur... [6] .text._Z3barv. On apprend donc que cette section séparée est en fait une COMDAT section, qui a quelques particularités : si l'éditeur de lien rencontre deux sections COMDAT avec le même nom, il peut en jeter une (discarded) ; et quand une section COMDAT est jetée, tous les symboles associés sont aussi jetés. C'est donc une version sous stéroïdes des symboles marqués weak.

La version Windows des COMDAT permet également de spécifier une stratégie associée à la fusion des COMDAT, mais ça dépasse le cadre de cet article.

8. Initialisation statique

La code suivant définit un symbole global possédant un petit code d'initialisation :

struct foo {

int m;
 foo() : m{1} {}
} f;

L'inspection de sa table des symboles est pleine de surprises !

> g++ static.cpp -c
> nm -C static.o
0000000000000000 B f
000000000000002c t _GLOBAL__sub_I_f
0000000000000000 t __static_initialization_and_destruction_0(int, int)
0000000000000000 W foo::foo()
0000000000000000 W foo::foo()
0000000000000000 n foo::foo()

On retrouve le symbole f, jusque-là rien de bien surprenant. Une méthode définie dans sa classe est sujette à la One Definition Rule là encore, le lien weak n'est pas surprenant. Il est aussi marqué n ce qui semble être un symbole de debug d'après la page de manuel de nm. Plus intéressant, il y a ces deux symboles _GLOBAL__sub_I_f et __static_initialization_and_destruction_0(int, int). En inspectant le .o avec plus d'attention, on apprend que ce sont tous deux des symboles de fonction :

> readelf --syms static.o
[...]
6: 0000000000000000 44 FUNC LOCAL DEFAULT 2 _Z41__static_initializati
7: 000000000000002c 21 FUNC LOCAL DEFAULT 2 _GLOBAL__sub_I_f

[...]

 

Et en les désassemblant, on apprend que _GLOBAL__sub_I_f appelle __static_initialization_and_destruction_0. Et en inspectant le fichier ELF, on voit que :

> readelf -a static.o
[...]
Relocation section '.rela.init_array' at offset 0x390 contains 1 entries:
 Offset Info Type Sym. Value Sym. Name + Addend
 000000000000 000200000001 R_X86_64_64 0000000000000000 .text + 2c

ce qui nous apprend que _GLOBAL__sub_I_f est dans la section .init_array. Or les entrées de cette section sont exécutées au chargement du binaire, avant la fonction main, comme se doivent de l'être les constructeurs statiques. Ouf !

On retrouve d'ailleurs ici les embryons du Static Initialization Order Fiasco, puisque ces différents symboles vont se retrouver dans le même binaire, et puisqu'aucune information n'est disponible dans le code pour préciser leur ordre relatif, un ordre sera choisi parmi les possibles, peut-être en fonction de l'ordre des arguments de l'éditeur de lien ?

Conclusion

À peine quelques concepts élémentaires, et déjà pas mal d'interactions avec le format ELF, C++ ne faillit pas à sa réputation et est plein de surprises ! Dans un opus ultérieur, on s'intéressera à d'autres aspects du langage…

Remerciements

J'en profite pour remercier chaleureusement Lancelot Six, Adrien Guinet et Juan Martinez pour leur relecture, merci les gars o/



Article rédigé par

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

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.

Les derniers articles Premiums

Les derniers articles Premium

Quarkus : applications Java pour conteneurs

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

Initié par Red Hat, il y a quelques années le projet Quarkus a pris son envol et en est désormais à sa troisième version majeure. Il propose un cadre d’exécution pour une application de Java radicalement différente, où son exécution ultra optimisée en fait un parfait candidat pour le déploiement sur des conteneurs tels que ceux de Docker ou Podman. Quarkus va même encore plus loin, en permettant de transformer l’application Java en un exécutable natif ! Voici une rapide introduction, par la pratique, à cet incroyable framework, qui nous offrira l’opportunité d’illustrer également sa facilité de prise en main.

De la scytale au bit quantique : l’avenir de la cryptographie

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

Imaginez un monde où nos données seraient aussi insaisissables que le célèbre chat de Schrödinger : à la fois sécurisées et non sécurisées jusqu'à ce qu'un cryptographe quantique décide d’y jeter un œil. Cet article nous emmène dans les méandres de la cryptographie quantique, où la physique quantique n'est pas seulement une affaire de laboratoires, mais la clé d'un futur numérique très sécurisé. Entre principes quantiques mystérieux, défis techniques, et applications pratiques, nous allons découvrir comment cette technologie s'apprête à encoder nos données dans une dimension où même les meilleurs cryptographes n’y pourraient rien faire.

Les nouvelles menaces liées à l’intelligence artificielle

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

Sommes-nous proches de la singularité technologique ? Peu probable. Même si l’intelligence artificielle a fait un bond ces dernières années (elle est étudiée depuis des dizaines d’années), nous sommes loin d’en perdre le contrôle. Et pourtant, une partie de l’utilisation de l’intelligence artificielle échappe aux analystes. Eh oui ! Comme tout système, elle est utilisée par des acteurs malveillants essayant d’en tirer profit pécuniairement. Cet article met en exergue quelques-unes des applications de l’intelligence artificielle par des acteurs malveillants et décrit succinctement comment parer à leurs attaques.

Les listes de lecture

11 article(s) - ajoutée le 01/07/2020
Clé de voûte d'une infrastructure Windows, Active Directory est l'une des cibles les plus appréciées des attaquants. Les articles regroupés dans cette liste vous permettront de découvrir l'état de la menace, les attaques et, bien sûr, les contre-mesures.
8 article(s) - ajoutée le 13/10/2020
Découvrez les méthodologies d'analyse de la sécurité des terminaux mobiles au travers d'exemples concrets sur Android et iOS.
10 article(s) - ajoutée le 13/10/2020
Vous retrouverez ici un ensemble d'articles sur les usages contemporains de la cryptographie (whitebox, courbes elliptiques, embarqué, post-quantique), qu'il s'agisse de rechercher des vulnérabilités ou simplement comprendre les fondamentaux du domaine.
Voir les 66 listes de lecture

Abonnez-vous maintenant

et profitez de tous les contenus en illimité

Je découvre les offres

Déjà abonné ? Connectez-vous