web-dev-qa-db-fra.com

Quelle est l'adresse d'une fonction dans un programme C ++?

Comme la fonction est un ensemble d'instructions stockées dans un bloc de mémoire contigu.

Et l'adresse d'une fonction (point d'entrée) est l'adresse de la première instruction de la fonction. (de ma connaissance)

Et ainsi nous pouvons dire que l'adresse de la fonction et l'adresse de la première instruction dans la fonction seront les mêmes (dans ce cas la première instruction est l'initialisation d'une variable.).

Mais le programme ci-dessous contredit la ligne ci-dessus.

code:

#include<iostream>
#include<stdio.h>
#include<string.h>
using namespace std;
char ** fun()
{
    static char * z = (char*)"Merry Christmas :)";
    return &z;
}
int main()
{
    char ** ptr = NULL;

    char ** (*fun_ptr)(); //declaration of pointer to the function
    fun_ptr = &fun;

    ptr = fun();

    printf("\n %s \n Address of function = [%p]", *ptr, fun_ptr);
    printf("\n Address of first variable created in fun() = [%p]", (void*)ptr);
    cout<<endl;
    return 0;
}

Un exemple de sortie est:

 Merry Christmas :) 
 Address of function = [0x400816]
 Address of first variable created in fun() = [0x600e10]

Donc, ici l'adresse de la fonction et l'adresse de la première variable en fonction ne sont pas les mêmes. Pourquoi

J'ai cherché sur google mais je ne peux pas trouver la réponse exacte requise et étant nouveau dans cette langue, je ne peux pas attraper certains contenus sur le net.

32
Amit Upadhyay

Donc, ici, l'adresse de la fonction et l'adresse de la première variable dans la fonction ne sont pas les mêmes. Pourquoi?

Pourquoi en serait-il ainsi? Un pointeur de fonction est un pointeur qui pointe vers la fonction. De toute façon, il ne pointe pas vers la première variable à l'intérieur de la fonction.

Pour élaborer, une fonction (ou sous-routine) est un ensemble d'instructions (y compris la définition de variable et différentes instructions/opérations) qui effectue un travail spécifique, la plupart du temps plusieurs fois, selon les besoins. Ce n'est pas seulement un pointeur sur les éléments présents à l'intérieur de la fonction.

Les variables définies à l'intérieur de la fonction ne sont pas stockées dans la même zone mémoire que celle du code machine exécutable. En fonction du type de stockage, les variables présentes à l'intérieur de la fonction sont situées dans une autre partie de la mémoire du programme en cours d'exécution.

Lorsqu'un programme est construit (compilé dans un fichier objet), différentes parties du programme sont organisées de manière différente.

  • Habituellement, la fonction (code exécutable) réside dans un segment distinct appelé segment de code , généralement un emplacement de mémoire en lecture seule.

  • La variable de temps de compilation allouée , OTOH, est stockée dans le segment de données .

  • Les variables locales de la fonction sont généralement remplies dans la mémoire de la pile, au fur et à mesure des besoins.

Il n'y a donc pas de relation telle qu'un pointeur de fonction produira l'adresse de la première variable présente dans la fonction, comme vu dans le code source.

À cet égard, pour citer l'article wiki ,

Au lieu de faire référence à des valeurs de données, un pointeur de fonction pointe vers du code exécutable dans la mémoire.

Ainsi, TL; DR, l'adresse d'une fonction est un emplacement mémoire à l'intérieur du segment de code (texte) où résident les instructions exécutables.

52
Sourav Ghosh

L'adresse d'une fonction n'est qu'un moyen symbolique de transmettre cette fonction, comme la passer dans un appel ou autre. Potentiellement, la valeur que vous obtenez pour l'adresse d'une fonction n'est même pas un pointeur vers la mémoire.

Les adresses des fonctions sont bonnes pour exactement deux choses:

  1. comparer pour l'égalité p==q, et

  2. déréférencer et appeler (*p)()

Tout ce que vous essayez de faire est indéfini, peut ou peut ne pas fonctionner, et c'est la décision du compilateur.

17
Aganju

D'accord, ça va être amusant. Nous pouvons passer du concept extrêmement abstrait de ce qu'est un pointeur de fonction en C++ jusqu'au niveau du code Assembly, et grâce à certaines des confusions particulières que nous avons, nous pouvons même discuter des piles!

Commençons par le côté très abstrait, car c'est clairement le côté des choses dont vous partez. vous avez une fonction char** fun() avec laquelle vous jouez. Maintenant, à ce niveau d'abstraction, nous pouvons regarder quelles opérations sont autorisées sur les pointeurs de fonction:

  • Nous pouvons tester si deux pointeurs de fonction sont égaux. Deux pointeurs de fonction sont égaux s'ils pointent vers la même fonction.
  • Nous pouvons effectuer des tests d'inégalité sur ces pointeurs, ce qui nous permet de trier ces pointeurs.
  • Nous pouvons déférer un pointeur de fonction, ce qui se traduit par un type de "fonction" qui est vraiment déroutant pour travailler avec, et je vais choisir de l'ignorer pour l'instant.
  • Nous pouvons "appeler" un pointeur de fonction, en utilisant la notation que vous avez utilisée: fun_ptr(). La signification de ceci est identique à l'appel de n'importe quelle fonction pointée.

C'est tout ce qu'ils font au niveau abstrait. En dessous, les compilateurs sont libres de l'implémenter comme bon leur semble. Si un compilateur voulait avoir un FunctionPtrType qui est en fait un index dans une grande table de chaque fonction du programme, ils le pourraient.

Cependant, ce n'est généralement pas la façon dont il est mis en œuvre. Lors de la compilation de C++ en assembleur/code machine, nous avons tendance à tirer parti d'autant d'astuces spécifiques à l'architecture que possible pour économiser le temps d'exécution. Sur les ordinateurs réels, il y a presque toujours une opération de "saut indirect", qui lit une variable (généralement un registre), et saute pour commencer à exécuter le code stocké à cette adresse mémoire. Il est presque universel que les fonctions sont compilées en blocs d'instructions contigus, donc si jamais vous passez à la première instruction du bloc, cela a pour effet logique d'appeler cette fonction. L'adresse de la première instruction arrive à satisfaire chacune des comparaisons requises par le concept abstrait C++ d'un pointeur de fonction et il se trouve que c'est exactement la valeur dont le matériel a besoin pour utiliser un saut indirect pour appeler la fonction ! C'est tellement pratique que pratiquement tous les compilateurs choisissent de l'implémenter de cette façon!

Cependant, lorsque nous commençons à expliquer pourquoi le pointeur que vous pensiez regarder était le même que le pointeur de fonction, nous devons entrer dans quelque chose d'un peu plus nuancé: les segments.

Les variables statiques sont stockées séparément du code. Il y a plusieurs raisons à cela. La première est que vous voulez que votre code soit aussi serré que possible. Vous ne voulez pas que votre code soit moucheté avec les espaces mémoire pour stocker des variables. Ce serait inefficace. Il faudrait sauter toutes sortes de choses, plutôt que de se contenter de les parcourir. Il y a aussi une raison plus moderne: la plupart des ordinateurs vous permettent de marquer de la mémoire comme "exécutable" et certains "inscriptible". Faire cela aide énormément pour faire face à des trucs de hackers vraiment mauvais. Nous essayons de ne jamais marquer quelque chose comme à la fois exécutable et accessible en écriture, au cas où un pirate trouverait habilement un moyen de tromper notre programme pour écraser certaines de nos fonctions par les leurs!

En conséquence, il existe généralement un segment .code (En utilisant cette notation en pointillés simplement parce que c'est un moyen populaire de le noter dans de nombreuses architectures). Dans ce segment, vous trouverez tout le code. Les données statiques iront quelque part comme .bss. Vous pouvez donc trouver votre chaîne statique stockée assez loin du code qui l'utilise (généralement au moins à 4 Ko, car la plupart des matériels modernes vous permettent de définir des autorisations d'exécution ou d'écriture au niveau de la page: les pages font 4 Ko dans de nombreux systèmes modernes )

Maintenant, le dernier morceau ... la pile. Vous avez mentionné le stockage de choses sur la pile de manière confuse, ce qui suggère qu'il pourrait être utile de lui donner un aperçu rapide. Permettez-moi de créer une fonction récursive rapide, car ils sont plus efficaces pour démontrer ce qui se passe dans la pile.

int fib(int x) {
    if (x == 0)
        return 0;

    if (x == 1)
        return 1;

    return fib(x-1)+fib(x-2);
}

Cette fonction calcule la séquence de Fibonacci en utilisant une manière plutôt inefficace mais claire de le faire.

Nous avons une fonction, fib. Cela signifie que &fib Est toujours un pointeur vers le même endroit, mais nous appelons clairement fib plusieurs fois, donc chacun a besoin de son propre espace, n'est-ce pas?

Sur la pile, nous avons ce qu'on appelle des "cadres". Les trames sont pas les fonctions elles-mêmes, mais ce sont plutôt des sections de mémoire que cette invocation particulière de la fonction est autorisée à utiliser. Chaque fois que vous appelez une fonction, comme fib, vous allouez un peu plus d'espace sur la pile pour son cadre (ou, plus pédantiquement, il l'allouera après avoir effectué l'appel).

Dans notre cas, fib(x) doit clairement stocker le résultat de fib(x-1) lors de l'exécution de fib(x-2). Il ne peut pas le stocker dans la fonction elle-même, ni même dans le segment .bss Car nous ne savons pas combien de fois cela va se reproduire. Au lieu de cela, il alloue de l'espace sur la pile pour stocker sa propre copie du résultat de fib(x-1) tandis que fib(x-2) fonctionne dans son propre cadre (en utilisant exactement la même fonction et la même adresse de fonction ). Lorsque fib(x-2) retourne, fib(x) charge simplement cette ancienne valeur, dont il est certain qu'elle n'a été touchée par personne d'autre, ajoute les résultats et la renvoie!

Comment fait-il cela? Pratiquement tous les processeurs prennent en charge une pile de matériel. Sur x86, ceci est connu sous le nom de registre ESP (pointeur de pile étendue). Les programmes acceptent généralement de le traiter comme un pointeur vers l'endroit suivant de la pile où vous pouvez commencer à stocker des données. Vous Je vous invite à déplacer ce pointeur pour créer de l'espace pour un cadre et à vous déplacer. Une fois l'exécution terminée, vous devez tout replacer.

En fait, sur la plupart des plateformes, la première instruction de votre fonction est pas la première instruction de la version finale compilée. Les compilateurs injectent quelques opérations supplémentaires pour gérer ce pointeur de pile pour vous, afin que vous n'ayez même jamais à vous en soucier. Sur certaines plateformes, comme x86_64, ce comportement est même souvent obligatoire et spécifié dans l'ABI!

Donc, dans tout ce que nous avons:

  • Segment .code - où sont stockées les instructions de votre fonction. Le pointeur de fonction pointera vers la première instruction ici. Ce segment est généralement marqué "exécution/lecture seule", empêchant votre programme d'écrire dessus après son chargement.
  • Segment .bss - où vos données statiques seront stockées, car elles ne peuvent pas faire partie du segment "exécuter uniquement" .code Si elles veulent être des données.
  • la pile - où vos fonctions peuvent stocker des trames, qui gardent une trace des données nécessaires uniquement pour cette seule instanciation, et rien de plus. (La plupart des plates-formes utilisent également cela pour stocker les informations sur l'endroit où retourner à une fois la fonction terminée)
  • le tas - Cela n'apparaissait pas dans cette réponse, car votre question n'inclut aucune activité de tas. Cependant, pour être complet, je l'ai laissé ici pour qu'il ne vous surprenne pas plus tard.
10
Cort Ammon

Dans le texte de votre question, vous dites:

Et ainsi nous pouvons dire que l'adresse de la fonction et l'adresse de la première instruction dans la fonction seront les mêmes (dans ce cas la première instruction est l'initialisation d'une variable.).

mais dans le code, vous n'obtenez pas l'adresse de la première instruction dans la fonction mais l'adresse d'une variable locale déclarée dans la fonction.

Une fonction est du code, une variable est des données. Ils sont stockés dans différentes zones de mémoire; ils ne résident même pas dans le même bloc de mémoire. En raison des restrictions de sécurité imposées par les systèmes d'exploitation de nos jours, le code est stocké dans des blocs de mémoire marqués en lecture seule.

Pour autant que je sache, le langage C ne fournit aucun moyen d'obtenir l'adresse d'une instruction dans la mémoire. Même s'il fournissait un tel mécanisme, le début de la fonction (l'adresse de la fonction en mémoire) n'est pas la même que l'adresse du code machine généré à partir de la première instruction C.

Avant le code généré à partir de la première instruction C, le compilateur génère un fonction prologue qui (au moins) enregistre la valeur actuelle du pointeur de pile et fait de la place pour les variables locales de la fonction. Cela signifie plusieurs instructions d'assemblage avant tout code généré à partir de la première instruction de la fonction C.

9
axiac

Comme vous le dites, l'adresse de la fonction peut être (cela dépendra du système) l'adresse de la première instruction de la fonction.

C'est la réponse. L'instruction ne partagera pas l'adresse avec des variables dans un environnement typique dans lequel le même espace d'adressage est utilisé pour les instructions et les données.

S'ils partagent la même adresse, l'instruction sera détruite en assignant aux variables!

7
MikeCAT

Quelle est exactement l'adresse d'une fonction dans un programme C++?

Comme les autres variables, l'adresse d'une fonction est l'espace qui lui est alloué. En d'autres termes, c'est l'emplacement de la mémoire où sont stockées les instructions (code machine) pour l'opération effectuée par la fonction.

Pour comprendre cela, jetez un œil sur la disposition de la mémoire d'un programme.

Les variables d'un programme et le code/les instructions exécutables sont stockés dans différents segments de mémoire (RAM). Les variables vont à l'un des segments STACK, HEAP, DATA et BSS tandis que le code exécutable va au segment CODE. Regardez la disposition générale de la mémoire d'un programme

enter image description here

Vous pouvez maintenant voir qu'il existe différents segments de mémoire pour les variables et les instructions. Ils sont stockés dans différents emplacements de mémoire. L'adresse de fonction est l'adresse qui se trouve dans le segment CODE.

Donc, vous confondez le terme première instruction avec première instruction exécutable . Lorsque l'appel de fonction est appelé, le compteur de programme est mis à jour avec l'adresse de la fonction. Par conséquent, le pointeur de fonction pointe vers la première instruction de la fonction stockée en mémoire.

enter image description here

7
haccks

D'autres réponses ici expliquent déjà ce qu'est un pointeur de fonction et ce qu'il n'est pas. J'expliquerai spécifiquement pourquoi votre test ne teste pas ce que vous pensiez qu'il a fait.

Et l'adresse d'une fonction (point d'entrée) est l'adresse de la première instruction de la fonction. (de ma connaissance)

Ce n'est pas obligatoire (comme d'autres réponses l'expliquent), mais c'est courant, et c'est généralement aussi une bonne intuition.

(Dans ce cas, la première instruction est l'initialisation d'une variable.).

D'accord.

printf("\n Address of first variable created in fun() = [%p]", (void*)ptr);

Ce que vous imprimez ici est l'adresse de la variable. Pas l'adresse de l'instruction qui définit la variable.

Ce ne sont pas les mêmes. En fait, ils ne peuvent pas être les mêmes.

L'adresse de la variable existe dans une exécution particulière de la fonction. Si la fonction est appelée plusieurs fois pendant l'exécution du programme, la variable peut être à des adresses différentes à chaque fois. Si la fonction s'appelle récursivement, ou plus généralement si la fonction appelle une autre fonction qui appelle… qui appelle la fonction d'origine, alors chaque invocation de la fonction a sa propre variable, avec sa propre adresse. Il en va de même dans un programme multithread si plusieurs threads se trouvent invoquer cette fonction à un moment particulier.

En revanche, l'adresse de la fonction est toujours la même. Il existe indépendamment du fait que la fonction est actuellement appelée: après tout, le point d'utiliser un pointeur de fonction est généralement d'appeler la fonction. Appeler la fonction plusieurs fois ne changera pas son adresse: lorsque vous appelez une fonction, vous n'avez pas à vous soucier si elle est déjà appelée.

Puisque l'adresse de la fonction et l'adresse de sa première variable ont des propriétés contradictoires, elles ne peuvent pas être les mêmes.

(Remarque: il est possible de trouver un système où ce programme pourrait imprimer les deux mêmes nombres, bien que vous puissiez facilement passer par une carrière de programmation sans en rencontrer un. Il y a architectures Harvard , où le code et les données sont stockés dans différentes mémoires. Sur ces machines, le nombre lorsque vous imprimez un pointeur de fonction est une adresse dans la mémoire de code et le nombre lorsque vous imprimez un pointeur de données est une adresse dans la mémoire de données. Les deux nombres peuvent être identiques, mais il serait une coïncidence, et lors d'un autre appel à la même fonction, le pointeur de fonction serait le même, mais l'adresse de la variable pourrait changer.)

4
Gilles

Si je ne me trompe pas Un programme se charge dans deux emplacements en mémoire. Le premier est l'exécutable de compilation comprenant des fonctions et des variables prédéfinies. Cela commence par la mémoire la plus faible occupée par l'application. Avec certains systèmes d'exploitation modernes, il s'agit de 0x00000 car le gestionnaire de mémoire les traduira au besoin. La deuxième partie du code est le tas d'applications, où la date allouée au moment de l'exécution, tels que les pointeurs, telle que toute mémoire d'exécution aura un emplacement différent dans la mémoire

4
Robert Humiston

La adresse d'une fonction normale est l'endroit où les instructions commencent (si aucune table virtuelle n'est impliquée).

Pour les variables cela dépend:

  • variables statiques sont stockées dans un autre endroit.
  • paramètres sont poussés sur la pile ou conservés dans les registres.
  • variables locales sont également poussées sur la pile ou conservées dans les registres.

Sauf si la fonction est intégrée ou optimisée.

4
Danny_ds

les variables déclarées dans une fonction ne sont pas allouées là où vous voyez dans le code les variables automatiques (variables définies localement dans une fonction) se voient attribuer une place appropriée dans la pile mémoire lorsque la fonction est sur le point d'être appelée, cela se fait pendant la compilation par le compilateur, donc l'adresse de la première instruction n'a rien à voir avec les variables il s'agit des instructions exécutables

2
Amir ElAttar