2010-07-09 15 views
2

Je ne comprends pas correctement la compilation et la liaison de programmes C++. Y at-il un moyen, je peux regarder les fichiers objets générés en compilant un programme C++ (dans un format compréhensible). Cela devrait m'aider à comprendre le format des fichiers objets, comment les classes C++ sont compilées, quelles informations sont nécessaires au compilateur pour générer les fichiers objet et m'aider à comprendre les instructions comme:Besoin d'aide pour comprendre la compilation de programmes C++

si une classe est utilisée uniquement en tant que paramètres d'entrée et type de retour , nous n'avons pas besoin d'inclure le fichier d'en-tête de la classe entière. La déclaration forward est suffisante, mais si une classe dérivée dérive de la classe de base, nous devons inclure le fichier contenant la définition de la classe de base (Provenant de "C++ Exceptionnel"). Je lis le livre "Linking and Loading" pour comprendre le format des fichiers objet, mais je préférerais quelque chose de spécialement conçu pour le code source C++.

Merci,

Jagrati

Edit:

Je sais que avec nm je peux regarder les symboles présents dans les fichiers objet, mais je suis intéressé à en savoir plus sur les fichiers d'objets.

+4

Je ne pense pas que regarder les fichiers d'objets aidera à comprendre les points que vous avez mentionnés. Il est probablement plus utile de réfléchir à la question "qu'est-ce que le compilateur doit savoir pour créer du code machine pour cette entrée"? Par exemple, pour passer un 'A * a' à la fonction suivante, le compilateur n'aura pas besoin de savoir à quoi ressemble A, mais pour appeler' a-> foo() ', c'est le cas. Et pour dériver de 'A', au moins la taille et toutes les signatures de méthode de' A' doivent être connues. –

+0

Salut Christopher, je suis d'accord avec vous. En fait, c'est de là que je voulais partir. Mais, même des choses comme "dériver de A, au moins la taille de A doit être connue" n'est pas si évidente pour moi. Pourquoi la connaissance de la taille de A ne peut-elle pas être reportée avant l'heure d'exécution ou dire à l'heure de la liaison par rapport à l'heure de la compilation, quand les fichiers objets sont créés. Et donc, j'ai conclu que j'ai peut-être besoin de comprendre d'un point de vue différent ce que le compilateur d'information met dans les fichiers objets. – xyz

+1

Je pense que le livre de Stanley B Lippman: "A l'intérieur du modèle objet C++" peut vous aider à comprendre certains sujets –

Répondre

0

Avez-vous essayé d'inspecter vos binaires avec readelf (à condition que vous soyez sur une plate-forme Linux)? Cela fournit des informations assez complètes sur les fichiers objet ELF. Honnêtement, cependant, je ne suis pas sûr à quel point cela aiderait à comprendre la compilation et la liaison. Je pense que la bonne tactique est probablement de comprendre comment le code C++ est mappé à l'assemblage avant et après la liaison.

0

Vous n'avez normalement pas besoin de connaître en détail le format interne des fichiers Obj, car ils sont générés pour vous. Tout ce que vous devez savoir, c'est que pour chaque classe que vous créez, le compilateur génère le fichier Obj, qui est le code octet binaire de votre classe, adapté au système d'exploitation pour lequel vous compilez. Ensuite, l'étape suivante - linking - rassemblera les fichiers objets pour toutes les classes dont vous avez besoin pour votre programme dans un seul EXE ou DLL (ou tout autre format pour les OS non-Windows). Pourrait être également EXE + plusieurs DLL, selon vos souhaits.

Le plus important est que vous sépariez l'interface (déclaration) et l'implémentation (définition) de votre classe.

Toujours mettre dans les déclarations d'interface de fichier d'en-tête de votre classe uniquement. Rien d'autre - pas d'implémentations ici. Evitez également les variables membres, avec les types personnalisés, qui ne sont pas des pointeurs, car pour eux les déclarations anticipées ne suffisent pas et vous devez inclure d'autres en-têtes dans votre en-tête. Si vous avez inclus dans votre en-tête, alors le design sent et ralentit également le processus de construction.

Toutes les implémentations des méthodes de classe ou d'autres fonctions doivent se trouver dans le fichier CPP. Cela garantira que le fichier Obj, généré par le compilateur, ne sera pas nécessaire lorsque quelqu'un inclut votre en-tête et que vous pouvez inclure d'autres dans les fichiers CPP uniquement.

Mais pourquoi s'embêter? La réponse est que si vous avez de telles séparations, la liaison est plus rapide, car chacun de vos fichiers Obj est utilisé une fois par classe. De plus, si vous changez de classe, cela changera aussi une petite quantité d'autres fichiers objets lors de la prochaine génération. Si vous avez inclus dans l'en-tête, cela signifie que lorsque le compilateur génère le fichier Obj pour votre classe, il doit d'abord générer le fichier Obj pour les autres classes incluses dans votre en-tête, ce qui peut nécessiter d'autres fichiers Obj, etc. .Peut-être même une dépendance circulaire et vous ne pouvez pas compiler! Ou si vous changez quelque chose dans votre classe, alors le compilateur aura besoin de régénérer beaucoup d'autres fichiers Obj, car ils deviennent très dépendants après un certain temps, si vous ne vous séparez pas.

+0

RE: "Evitez aussi les variables membres, avec des types personnalisés" - comment éviter cela avec des pointeurs bruts? Je devine serait des pointeurs intelligents, mais d'autres idées? – msi

+0

@msiemeri: Je pense que ce conseil est de toute façon exagéré. Vous pourriez vouloir le faire dans quelques cas pour casser un cycle de dépendance, mais c'est mauvais comme conseiller général. Et oui, dans ce cas, il faut utiliser un scoped_ptr ou similaire. – peterchen

0

nm est un outil Unix qui vous montrera les noms des symboles dans un fichier objet.

objdump est un outil GNU qui vous montrera plus d'informations. Mais les deux outils vous montreront des informations assez brutes qui sont utilisées par l'éditeur de liens, mais qui ne sont pas conçues pour être lues par des êtres humains. Cela ne vous aidera probablement pas à mieux comprendre ce qui se passe au niveau C++.

1

D'abord, d'abord. Le désassemblage de la sortie du compilateur ne vous aidera probablement pas à comprendre les problèmes que vous avez. La sortie du compilateur n'est plus un programme C++, mais un assemblage simple et c'est vraiment difficile à lire si vous ne savez pas quel est le modèle de mémoire.

Sur les questions particulières pourquoi la définition de base requise lorsque vous déclarez être une classe de base de derived il y a quelques raisons différentes (et probablement plus que j'oubliais):

  1. Lorsque un objet de type derived est créé, le compilateur doit réserver la mémoire pour l'instance complète et toutes les sous-classes: il doit connaître la taille base
  2. Lorsque vous accédez à un attribut de membre, le compilateur doit connaître le décalage du pointeur implicite this, et ce décalage nécessite une connaissance de la taille prise par le e base sous-objet.
  3. Lorsqu'un identificateur est analysé dans le contexte de derived et que l'identificateur n'est pas trouvé dans la classe derived, le compilateur doit savoir s'il est défini dans base avant de rechercher l'identificateur dans les espaces de noms englobants. Le compilateur ne peut pas savoir si foo(); est un appel valide à l'intérieur de derived::function() si foo() est déclaré dans la classe base.
  4. Le nombre et les signatures de toutes les fonctions virtuelles définies dans base doivent être connus lorsque le compilateur définit la classe derived. Il a besoin de cette information pour construire le mécanisme de distribution dynamique - normalement vtable--, et même pour savoir si un membre fonctionne en derived est lié pour l'envoi dynamique ou non - si base::f() est virtuel, alors derived::f() sera virtuel indépendamment du fait que le déclaration dans derived a le mot-clé virtual.
  5. L'héritage multiple ajoute quelques autres exigences - comme les décalages relatifs de chaque baseX qui doivent être réécrits avant que les surcharges finales des méthodes soient appelées (un pointeur de type base2 qui pointe vers un objet de multiplyderived ne pointe pas vers le début de l'instance, mais au début du base2 subobject dans l'instance, ce qui pourrait être offsetted par d'autres bases déclarées avant base2 dans la liste d'héritage

pour la dernière question dans les commentaires.

Donc l'instanciation des objets (sauf pour les objets globaux) ne peut pas attendre jusqu'à l'exécution et donc la taille et le décalage, etc., pourraient attendre le temps de liaison et nous ne devrions pas forcément y faire face au moment de générer des fichiers objet?

void f() { 
    derived d; 
    //... 
} 

Les alloue de code précédents et objet de type derived dans la pile. Le compilateur ajoutera des instructions assembleur pour réserver une certaine quantité de mémoire pour l'objet dans la pile. Après que le compilateur a analysé et généré l'assembly, il n'y a aucune trace de l'objet, en particulier (en supposant un constructeur trivial pour un type POD: c'est-à-dire rien n'est initialisé), ce code produira exactement le même assembleur. Lorsque le compilateur génère l'instruction qui réservera l'espace, il doit savoir combien.