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)

Garder ses parties privées

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

Il y a 19 ans, Ulrich Drepper, alors développeur chez Red Hat et un des contributeurs principaux de la glibc, ajoutait au changelog de la glibc la ligne suivante : sysdeps/i386/bsd-_setjmp.S: Use PTR_MANGLE for PC if defined, inaugurant ainsi l’arrivée de la protection des pointeurs de fonction stockés dans des structures internes à la glibc.

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 !

Les derniers articles Premiums

Les derniers articles Premium

PostgreSQL au centre de votre SI avec PostgREST

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

Dans un système d’information, il devient de plus en plus important d’avoir la possibilité d’échanger des données entre applications. Ce passage au stade de l’interopérabilité est généralement confié à des services web autorisant la mise en œuvre d’un couplage faible entre composants. C’est justement ce que permet de faire PostgREST pour les bases de données PostgreSQL.

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.

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 68 listes de lecture

Abonnez-vous maintenant

et profitez de tous les contenus en illimité

Je découvre les offres

Déjà abonné ? Connectez-vous