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)

Les options de compilation : Fun with Flags

Magazine
Marque
MISC
Numéro
121
Mois de parution
mai 2022
Spécialité(s)
Résumé

Machines et développeurs ont parfois du mal à communiquer. D’un côté l’esprit humain, avec sa volonté d’abstraire et de penser fonctionnel. De l’autre côté, la machine et sa froide rigueur électronique. Entre les deux, il y a un pont : le compilateur. Mais comment prend-il en compte les aspects sécurité ?

Cheval de Troie : retour aux sources

Magazine
Marque
MISC
Numéro
121
Mois de parution
mai 2022
Spécialité(s)
Résumé

Le premier novembre 2021, les CVE-2021-42574 et CVE-2021-42694 étaient rendues publiques. Ces deux CVE rendent comptent des limitations de l’utilisation de caractères Unicode dans les identifiants, commentaires et/ou chaînes de caractères littérales de codes sources. Elles sont intéressantes par deux aspects : elles sont relativement agnostiques au langage de programmation sous-jacent, et elles utilisent comme cheval de Troie le rendu fait par certains outils, notamment les forges logicielles en ligne, de certains caractères Unicode.

La compilation statique démythifiée - Une plongée dans les entrailles de mon compilo

Magazine
Marque
MISC
Numéro
120
Mois de parution
mars 2022
Spécialité(s)
Résumé

Dans le hors-série 24 de MISC [0], nous avions montré que les outils et techniques de la compilation sont aujourd'hui des éléments incontournables dans le domaine de la sécurité des applications, que ce soit pour les protéger, les obfusquer, et même de manière plus surprenante les reverser.Nous plongeons ici plus en détail dans les entrailles d'un compilateur, Clang/LLVM [LLVM] : ses différentes étapes, son architecture, sa représentation interne, ses optimisations. Nous illustrerons les transformations les plus communes par un exemple concret, et nous verrons que si certaines structures de code observées dans les binaires sont facilement reconnaissables, d'autres peuvent surprendre...

Les derniers articles Premiums

Les derniers articles Premium

Stubby : protection de votre vie privée via le chiffrement des requêtes DNS

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

Depuis les révélations d’Edward Snowden sur l’espionnage de masse des communications sur Internet par la NSA, un effort massif a été fait pour protéger la vie en ligne des internautes. Cet effort s’est principalement concentré sur les outils de communication avec la généralisation de l’usage du chiffrement sur le web (désormais, plus de 90 % des échanges se font en HTTPS) et l’adoption en masse des messageries utilisant des protocoles de chiffrement de bout en bout. Cependant, toutes ces communications, bien que chiffrées, utilisent un protocole qui, lui, n’est pas chiffré par défaut, loin de là : le DNS. Voyons ensemble quels sont les risques que cela induit pour les internautes et comment nous pouvons améliorer la situation.

Surveillez la consommation énergétique de votre code

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

Être en mesure de surveiller la consommation énergétique de nos applications est une idée attrayante, qui n'est que trop souvent mise à la marge aujourd'hui. C'est d'ailleurs paradoxal, quand on pense que de plus en plus de voitures permettent de connaître la consommation instantanée et la consommation moyenne du véhicule, mais que nos chers ordinateurs, fleurons de la technologie, ne le permettent pas pour nos applications... Mais c'est aussi une tendance qui s'affirme petit à petit et à laquelle à terme, il devrait être difficile d'échapper. Car même si ce n'est qu'un effet de bord, elle nous amène à créer des programmes plus efficaces, qui sont également moins chers à exécuter.

Donnez une autre dimension à vos logs avec Vector

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

Avoir des informations précises et détaillées sur ce qu’il se passe dans une infrastructure, et sur les applications qu'elle héberge est un enjeu critique pour votre business. Cependant, ça demande du temps, temps qu'on préfère parfois se réserver pour d'autres tâches jugées plus prioritaires. Mais qu'un système plante, qu'une application perde les pédales ou qu'une faille de sécurité soit découverte et c'est la panique à bord ! Alors je vous le demande, qui voudrait rester aveugle quand l'observabilité a tout à vous offrir ?

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

Abonnez-vous maintenant

et profitez de tous les contenus en illimité

Je découvre les offres

Déjà abonné ? Connectez-vous