Question Visibilité des symboles, exceptions, erreur d'exécution


J'essaie de mieux comprendre la visibilité des symboles. Le wiki GCC (http://gcc.gnu.org/wiki/Visibility) contient une section sur "Problèmes avec les exceptions C ++". Selon GCC Wiki, il est possible que temps d'exécution erreur en raison d'exceptions non exportées. Les erreurs d'exécution sans erreur / avertissement de compilation sont très dangereuses, alors j'ai essayé de mieux comprendre le problème. J'ai fait quelques expériences mais je ne peux toujours pas le reproduire. Des idées sur la façon de reproduire le problème?

Le Wiki mentionne trois bibliothèques qui s’utilisent mutuellement, alors j'ai fait trois petites bibliothèques.

Je lance les commandes suivantes:

Classe d'exception sans vtable (fonctionne comme prévu):

make
./dsouser

Classe d'exception avec vtable mais elle ne s'exporte pas (ne compile même pas):

make HAS_VIRTUAL=1

Classe d'exception exportée vtable (fonctionne comme prévu):

make HAS_VIRTUAL=1 EXCEPTION_VISIBLE=1
./dsouser

Makefile:

CXX=g++-4.7.1
CFLAGS=-ggdb -O0 -fvisibility=hidden
ifdef EXCEPTION_VISIBLE
  CFLAGS+=-DEXCEPTION_VISIBLE
endif
ifdef HAS_VIRTUAL
  CFLAGS+=-DHAS_VIRTUAL
endif
all: dsouser

libmydso.so: mydso.cpp mydso.h
    $(CXX) $(CFLAGS) -fPIC -shared -Wl,-soname,$@ -o $@ $<

libmydso2.so: mydso2.cpp mydso.h mydso2.h libmydso.so
    $(CXX) $(CFLAGS) -L.  -fPIC -shared -Wl,-soname,$@ -o $@ $< -lmydso

libmydso3.so: mydso3.cpp mydso.h mydso2.h mydso3.h libmydso2.so
    $(CXX) $(CFLAGS) -L.  -fPIC -shared -Wl,-soname,$@ -o $@ $< -lmydso -lmydso2

dsouser: dsouser.cpp libmydso3.so
    $(CXX) $< $(CFLAGS) -L. -o $@ -lmydso -lmydso2 -lmydso3

clean:
    rm -f *.so *.o dsouser

.PHONY: all clean

mydso.h:

#ifndef DSO_H_INCLUDED
#define DSO_H_INCLUDED
#include <exception>
#define SYMBOL_VISIBLE __attribute__ ((visibility ("default")))
namespace dso
{
  class
#ifdef EXCEPTION_VISIBLE
    SYMBOL_VISIBLE
#endif
    MyException : public std::exception
  {
  public:
#ifdef HAS_VIRTUAL
    virtual void dump();
#endif
    void SYMBOL_VISIBLE foo();
  };
}
#endif

mydso.cpp:

#include <iostream>
#include "mydso.h"
namespace dso
{

#ifdef HAS_VIRTUAL
void MyException::dump()
{
}
#endif

void MyException::foo()
{
#ifdef HAS_VIRTUAL
  dump();
#endif
}

}

mydso2.h:

#ifndef DSO2_H_INCLUDED
#define DSO2_H_INCLUDED
#define SYMBOL_VISIBLE __attribute__ ((visibility ("default")))
namespace dso2
{
  void SYMBOL_VISIBLE some_func();
}
#endif

mydso2.cpp:

#include <iostream>
#include "mydso.h"
#include "mydso2.h"
namespace dso2
{
  void some_func()
  {
    throw dso::MyException();
  }
}

mydso3.h:

#ifndef DSO3_H_INCLUDED
#define DSO3_H_INCLUDED
#define SYMBOL_VISIBLE __attribute__ ((visibility ("default")))
namespace dso3
{
  void SYMBOL_VISIBLE some_func();
}
#endif

mydso3.cpp:

#include <iostream>

#include "mydso2.h"
#include "mydso3.h"

#include <iostream>

namespace dso3
{

  void some_func()
  {
    try
    {
      dso2::some_func();
    } catch (std::exception e)
    {
      std::cout << "Got exception\n";
    }
  }

}

dsouser.cpp:

#include <iostream>
#include "mydso3.h"
int main()
{
  dso3::some_func();
  return 0;
}

Merci, Dani


18
2018-01-10 22:51


origine


Réponses:


Je suis l'auteur du correctif d'origine de GCC, qui ajoute le support de visibilité de classe, et mon guide original, cloné par GCC, est à http://www.nedprod.com/programs/gccvisibility.html. Merci à VargaD de m'avoir envoyé un mail pour me parler de cette question SO.

Le comportement que vous observez est valable pour les GCC récents, mais ce n’est pas toujours le cas. Lors de la mise à jour initiale de GCC en 2004, j'ai envoyé une requête à GCC bugzilla pour que l’exception GCC gère l’exécution afin de comparer les types lancés par comparaison de chaînes de leurs symboles mutilés au lieu de comparer les adresses de ces chaînes - cela a été rejeté à ce moment par les responsables du GCC en tant que coût d'exécution inacceptable, malgré le fait que ce comportement est ce que MSVC fait, et que les performances lors des exceptions ne sont généralement pas considérées importantes. J'ai donc dû ajouter une exception spécifique à mon guide de visibilité pour dire que tout type lancé ne doit jamais être masqué, pas une seule fois, dans un fichier binaire car "hiddeness" l'emporte sur "default". le même symbole dans un binaire donné.

Ce qui est arrivé ensuite, je suppose qu'aucun d'entre nous ne s'y attendait - KDE a adopté publiquement ma fonctionnalité de contribution. Cela s'est répercuté dans presque tous les grands projets utilisant GCC dans un délai incroyablement court. Soudain, la dissimulation de symboles était la norme, pas l'exception.

Malheureusement, un petit nombre de personnes n’ont pas appliqué correctement mon guide pour les types d’exception lancés, et les rapports de bogues constants sur la gestion des exceptions d’objets croisés incorrects dans GCC ont finalement conduit les responsables GCC à abandonner pour la correspondance de type lancée, comme je l'avais demandé à l'origine. Par conséquent, dans les nouveaux GCC, la situation est un peu meilleure. Je n'ai pas changé mon guide ni les instructions car cette approche est toujours la plus sûre sur tous les GCC depuis la v4.0, et si les nouveaux GCC sont plus fiables dans le traitement des exceptions à cause de la comparaison de chaînes, suivre les règles du guide ne nuit pas cette.

Cela nous amène au problème de typeinfo. Un grand problème est que les meilleures pratiques C ++ vous obligent à toujours hériter virtuellement dans les types pouvant être lancés, parce que si vous composez deux types d'exception héritant à la fois de std :: exception, avoir deux classes de base std :: exception équidistantes provoquera un appel à interception (std :: exception &) à terminate () car ne peut pas résoudre la classe de base à faire correspondre, vous devez donc toujours avoir une classe de base std :: exception et la même logique s'applique à toute composition possible de type pouvant être lancé. Cette pratique exemplaire est particulièrement requise dans toute bibliothèque C ++, car vous ne pouvez pas savoir ce que les utilisateurs tiers feront avec vos types d'exception.

En d'autres termes, cela signifie que tous les types d'exceptions lancés dans la meilleure pratique seront toujours accompagnés d'une chaîne de RTTI successive pour chaque classe de base, et que cette correspondance d'exception consiste à effectuer en interne un Dynamic_cast <> correspondant au type correspondant, une opération O (nombre de classes de base). Et pour que dynamic_cast <> fonctionne sur une chaîne de types pratiquement hérités, vous l'avez compris, vous avez besoin de chacun de cette chaîne pour avoir une visibilité par défaut. Si même un seul est caché du code exécutant le catch (), tout le caboodle se retourne et vous obtenez un terminate (). Je serais très intéressé si vous avez retravaillé votre exemple de code ci-dessus pour hériter virtuellement et voir ce qui se passe - un de vos commentaires dit qu'il refuse de lier, ce qui est génial. Mais disons que DLL A définit le type A, les sous-classes DLL B de type A à B, les sous-classes de DLL C de type B et le programme D tente d’attraper une exception de type A lorsqu’il a été lancé. Le programme D aura le type info de A disponible, mais il devrait y avoir une erreur lors de la tentative d'extraction de RTTI pour les types B et C. Peut-être, cependant, que les GCC récents ont également résolu ce problème? Je ne sais pas, mon attention ces dernières années est sur le point que c'est l'avenir de tous les compilateurs C ++.

Evidemment, c'est un gâchis, mais c'est un gâchis spécifique à ELF - rien de tout cela n'affecte PE ou MachO, qui obtiennent tout ce qui précède en n'utilisant pas les tables de symboles globales de processus en premier lieu. Cependant, le groupe d’étude WG21 SG2 Modules travaillant sur C ++ 17 doit implémenter efficacement les modèles exportés pour que les modules fonctionnent afin de résoudre les violations ODR, et C ++ 17 est le premier standard proposé avec un LLVM dans esprit. En d'autres termes, les compilateurs C ++ 17 devront transférer un AST complexe sur un disque comme le fait le clang. Et cela implique une augmentation considérable des garanties de disponibilité des RTTI - en fait, c’est la raison pour laquelle nous avons le groupe d’étude SG7 Reflection, car l’AST de C ++ Modules permet une augmentation considérable des possibilités d’autoréflexion. En d'autres termes, attendez-vous à ce que les problèmes ci-dessus disparaissent rapidement avec l'adoption de C ++ 17.

Donc, en bref, continuez à suivre mon guide original pour le moment. Nous espérons que les choses iront beaucoup mieux au cours de la prochaine décennie. Et merci à Apple pour le financement de cette solution, cela a pris beaucoup de temps à cause de sa dureté.

Niall


23
2018-01-16 17:19