pointer - ¿Cuál es la dirección de una función en un programa C++?
pointers in c (10)
¿Cuál es exactamente la dirección de una función en un programa C ++?
Al igual que otras variables, una dirección de una función es el espacio asignado para ella. En otras palabras, es la ubicación de la memoria donde se almacenan las instrucciones (código de máquina) para la operación realizada por la función.
Para comprender esto, eche un vistazo profundo al diseño de la memoria de un programa.
Las variables de un programa y el código / instrucciones ejecutables se almacenan en diferentes segmentos de la memoria (RAM). Las variables van a cualquiera de los segmentos STACK, HEAP, DATA y BSS mientras que el código ejecutable va al segmento CODE. Mira el diseño general de la memoria de un programa
Ahora puede ver que hay diferentes segmentos de memoria para variables e instrucciones. Se almacenan en diferentes ubicaciones de memoria. La dirección de función es la dirección que se encuentra en el segmento CODE.
Entonces, está confundiendo el término primera declaración con la primera instrucción ejecutable . Cuando se invoca llamada de función, el contador de programa se actualiza con la dirección de la función. Por lo tanto, el puntero de función apunta a la primera instrucción de la función almacenada en la memoria.
Como la función es conjunto de instrucciones almacenadas en un bloque contiguo de memoria.
Y la dirección de una función (punto de entrada) es la dirección de la primera instrucción en la función. (de mi conocimiento)
Y así podemos decir que la dirección de la función y la dirección de la primera instrucción en la función serán las mismas (en este caso, la primera instrucción es la inicialización de una variable).
Pero el programa a continuación contradice la línea anterior.
código:
#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 ejemplo de salida es:
Merry Christmas :)
Address of function = [0x400816]
Address of first variable created in fun() = [0x600e10]
Entonces, aquí la dirección de función y la dirección de la primera variable en función no son las mismas. ¿Porque?
Busqué en Google pero no puedo encontrar la respuesta exacta requerida y ser nuevo en este idioma. No puedo capturar algunos de los contenidos en la red.
Entonces, aquí la dirección de función y la dirección de la primera variable en función no son las mismas. ¿Porque?
¿Por qué sería así? Un puntero a la función es un puntero que apunta a la función. No apunta a la primera variable dentro de la función, de todos modos.
Para elaborar, una función (o subrutina) es una colección de instrucciones (incluida la definición de variable y diferentes declaraciones / operaciones) que realiza un trabajo específico, en su mayoría varias veces, según sea necesario. No es solo un puntero a los elementos presentes dentro de la función.
Las variables, definidas dentro de la función, no se almacenan en la misma área de memoria que el código de máquina ejecutable. Según el tipo de almacenamiento, las variables que están presentes dentro de la función se encuentran en alguna otra parte de la memoria del programa en ejecución.
Cuando se crea un programa (compilado en un archivo de objeto), diferentes partes del programa se organizan de una manera diferente.
Por lo general, la función (código ejecutable) reside en un segmento separado llamado segmento de código , generalmente una ubicación de memoria de solo lectura.
La variable asignada de tiempo de compilación , OTOH, se almacena en el segmento de datos .
Las variables locales de la función, por lo general, se llenan en la memoria de la pila, según sea necesario.
Por lo tanto, no existe una relación tal que un puntero de función arroje la dirección de la primera variable presente en la función, como se ve en el código fuente.
En este sentido, para citar el artículo de la wiki ,
En lugar de hacer referencia a los valores de los datos, un puntero a la función apunta a un código ejecutable dentro de la memoria.
Entonces, TL; DR, la dirección de una función es una ubicación de memoria dentro del segmento de código (texto) donde residen las instrucciones ejecutables.
Bien, esto va a ser divertido. Pasamos del concepto extremadamente abstracto de lo que es un puntero de función en C ++ hasta el nivel de código ensamblador, y gracias a algunas de las confusiones particulares que estamos teniendo, ¡incluso podemos hablar de apilamientos!
Comencemos por el lado altamente abstracto, porque ese es claramente el lado de las cosas de las que estás empezando. tienes una función char** fun()
que estás jugando. Ahora, en este nivel de abstracción, podemos ver qué operaciones están permitidas en los punteros de función:
- Podemos probar si dos indicadores de función son iguales. Dos indicadores de función son iguales si apuntan a la misma función.
- Podemos hacer pruebas de desigualdad en esos indicadores, lo que nos permite hacer la clasificación de dichos indicadores.
- Podemos deferencia un puntero a la función, lo que resulta en un tipo de "función" que es realmente confuso de trabajar, y elegiré ignorarlo por el momento.
- Podemos "llamar" un puntero a la función, usando la notación que
fun_ptr()
:fun_ptr()
. El significado de esto es idéntico a llamar a cualquier función a la que se apunta.
Eso es todo lo que hacen en el nivel abstracto. Debajo de eso, los compiladores son libres de implementarlo como mejor les parezca. Si un compilador quisiera tener un FunctionPtrType
que en realidad es un índice en una gran tabla de todas las funciones del programa, podría hacerlo.
Sin embargo, esto normalmente no es cómo se implementa. Al compilar C ++ hasta el código ensamblador / máquina, tendemos a aprovechar la mayor cantidad posible de trucos específicos de arquitectura para ahorrar tiempo de ejecución. En las computadoras de la vida real, casi siempre hay una operación de "salto indirecto", que lee una variable (generalmente un registro) y salta para comenzar a ejecutar el código que está almacenado en esa dirección de memoria. Es casi univeral que las funciones se compilan en bloques contiguos de instrucciones, por lo que si alguna vez saltas a la primera instrucción en el bloque, tiene el efecto lógico de llamar a esa función. La dirección de la primera instrucción satisface cada una de las comparaciones requeridas por el concepto abstracto de C ++ de un puntero de función y resulta ser exactamente el valor que el hardware necesita para usar un salto indirecto para llamar a la función. ¡Eso es tan conveniente, que prácticamente cada compilador elige implementarlo de esa manera!
Sin embargo, cuando comenzamos a hablar de por qué el puntero que creías que estabas viendo era el mismo que el puntero a la función, tenemos que entrar en algo un poco más matizado: los segmentos.
Las variables estáticas se almacenan por separado del código. Hay algunas razones para eso. Una es que quiere que su código sea lo más ajustado posible. No desea que su código salpique con los espacios de memoria para almacenar variables. Sería ineficiente. Tendría que omitir todo tipo de cosas, en lugar de solo atravesarlo. También hay una razón más moderna: la mayoría de las computadoras le permiten marcar algunos recuerdos como "ejecutables" y algunos "modificables". Hacer esto ayuda tremendamente para lidiar con algunos trucos de hackers realmente malvados. Intentamos nunca marcar algo como ejecutable y escribible al mismo tiempo, ¡en caso de que un pirata informático encuentre hábilmente la manera de engañar a nuestro programa para que sobrescriba algunas de nuestras funciones con las suyas propias!
En consecuencia, normalmente hay un segmento .code
(usando esa notación punteada simplemente porque es una forma popular de anotarlo en muchas arquitecturas). En este segmento, encontrará todo el código. Los datos estáticos entrarán en algún lugar como .bss
. Así que puede encontrar su secuencia estática almacenada bastante lejos del código que opera en ella (generalmente a 4kb de distancia, porque la mayoría de los equipos modernos le permiten configurar permisos de escritura en el nivel de la página: las páginas son de 4kb en muchos sistemas modernos) )
Ahora la última pieza ... la pila. Mencionaste almacenar cosas en la pila de una manera confusa, lo que sugiere que puede ser útil darle un vistazo rápido. Permítanme hacer una función recursiva rápida, porque son más efectivos para demostrar lo que está sucediendo en la pila.
int fib(int x) {
if (x == 0)
return 0;
if (x == 1)
return 1;
return fib(x-1)+fib(x-2);
}
Esta función calcula la secuencia de Fibonacci utilizando una forma bastante ineficiente pero clara de hacerlo.
Tenemos una función, fib
. Esto significa &fib
es siempre un puntero al mismo lugar, pero claramente llamamos fib muchas veces, por lo que cada uno necesita su propio espacio ¿verdad?
En la pila tenemos lo que se llama "marcos". Los marcos no son las funciones en sí mismas, sino que son secciones de memoria que esta invocación particular de la función puede usar. Cada vez que llamas a una función, como fib
, asignas un poco más de espacio en la pila para su marco (o, más pedantemente, lo asignará después de realizar la llamada).
En nuestro caso, fib(x)
claramente necesita almacenar el resultado de fib(x-1)
mientras ejecuta fib(x-2)
. No puede almacenar esto en la función en sí, o incluso en el segmento .bss
porque no sabemos cuántas veces va a recursarse. En cambio, asigna espacio en la pila para almacenar su propia copia del resultado de fib(x-1)
mientras que fib(x-2)
está operando en su propio marco (usando exactamente la misma función y la misma dirección de función). Cuando fib(x-2)
vuelve, fib(x)
simplemente carga ese valor anterior, que es seguro que no ha sido tocado por nadie más, agrega los resultados y lo devuelve.
¿Como hace esto? Prácticamente, todos los procesadores tienen algún soporte para una pila en el hardware. En x86, esto se conoce como el registro ESP (puntero de pila extendida). Los programas generalmente aceptan tratar esto como un puntero al siguiente punto en la pila donde puede comenzar a almacenar datos. Puedes mover este puntero para construir tu propio espacio para un marco y moverte hacia adentro. Cuando termines de ejecutar, se espera que muevas todo de regreso.
De hecho, en la mayoría de las plataformas, la primera instrucción en su función no es la primera instrucción en la versión final compilada. Los compiladores inyectan algunas operaciones adicionales para administrar este puntero de pila, para que nunca tenga que preocuparse por ello. En algunas plataformas, como x86_64, este comportamiento a menudo es incluso obligatorio y especificado en el ABI.
Entonces en todo lo que tenemos:
- segmento
.code
- donde se almacenan las instrucciones de su función. El puntero de función apuntará a la primera instrucción aquí. Este segmento generalmente está marcado como "ejecutar / solo leer", lo que impide que su programa escriba después de que se haya cargado. - segmento
.bss
: donde se almacenarán los datos estáticos, ya que no pueden formar parte del segmento.code
"execute only" si se quiere que sean datos. - la pila, donde tus funciones pueden almacenar marcos, que hacen un seguimiento de los datos necesarios solo para esa instantánea, y nada más. (La mayoría de las plataformas también usan esto para almacenar la información sobre a dónde volver después de que termina una función)
- el montón - Esto no apareció en esta respuesta, porque su pregunta no incluye ninguna actividad de montón. Sin embargo, para completar, lo dejé aquí para que no te sorprenda más tarde.
Como dices, la dirección de la función puede ser (dependerá del sistema) la dirección de la primera instrucción de la función.
Es la respuesta. La instrucción no compartirá la dirección con las variables en un entorno típico en el que se utiliza el mismo espacio de direcciones para las instrucciones y los datos.
Si comparten la misma dirección, ¡la instrumentación se destruirá asignando las variables!
En el texto de tu pregunta dices:
Y así podemos decir que la dirección de la función y la dirección de la primera instrucción en la función serán las mismas (en este caso, la primera instrucción es la inicialización de una variable).
pero en el código no se obtiene la dirección de la primera instrucción en la función, sino la dirección de alguna variable local declarada en la función.
Una función es código, una variable son datos. Se almacenan en diferentes áreas de memoria; ni siquiera residen en el mismo bloque de memoria. Debido a las restricciones de seguridad impuestas por los sistemas operativos hoy en día, el código se almacena en bloques de memoria que están marcados como de solo lectura.
Hasta donde yo sé, el lenguaje C no proporciona ninguna forma de obtener la dirección de una declaración en la memoria. Incluso si proporcionara tal mecanismo, el inicio de la función (la dirección de la función en la memoria) no es la misma que la dirección del código máquina generado a partir de la primera declaración C.
Antes del código generado a partir de la primera instrucción C, el compilador genera un prólogo de función que (al menos) guarda el valor actual del puntero de la pila y deja espacio para las variables locales de la función. Esto significa varias instrucciones de ensamblaje antes de cualquier código generado a partir de la primera declaración de la función C.
La dirección de una función normal es donde comienzan las instrucciones (si no hay ninguna tabla involucrada).
Para las variables depende:
- las variables estáticas se almacenan en otro lugar.
- los parámetros se insertan en la pila o se guardan en registros.
- las variables locales también se presionan en la pila o se guardan en registros.
A menos que la función esté en línea u optimizada.
La dirección de una función es solo una forma simbólica de darle esta función, como pasarla en una llamada o similar. Potencialmente, el valor que obtiene para la dirección de una función ni siquiera es un puntero a la memoria.
Las direcciones de las funciones son buenas para exactamente dos cosas:
comparar para la igualdad
p==q
, ypara desreferenciar y llamar
(*p)()
Cualquier otra cosa que intentes hacer es indefinida, podría funcionar o no, y es la decisión del compilador.
Otras respuestas aquí ya explican qué es un puntero de función y qué no lo es. Explicaré específicamente por qué su prueba no prueba lo que creía que era.
Y la dirección de una función (punto de entrada) es la dirección de la primera instrucción en la función. (de mi conocimiento)
Esto no es obligatorio (como explican otras respuestas), pero es común, y generalmente también es una buena intuición.
(En este caso, la primera instrucción es la inicialización de una variable).
De acuerdo.
printf("/n Address of first variable created in fun() = [%p]", (void*)ptr);
Lo que está imprimiendo aquí es la dirección de la variable. No es la dirección de la instrucción que establece la variable.
Estos no son lo mismo. De hecho, no pueden ser lo mismo.
La dirección de la variable existe en una ejecución particular de la función. Si la función se llama varias veces durante la ejecución del programa, la variable puede estar en diferentes direcciones cada vez. Si la función se llama recursivamente, o más generalmente si la función llama a otra función que llama ... que llama a la función original, entonces cada invocación de la función tiene su propia variable, con su propia dirección. Lo mismo ocurre en un programa multiproceso si hay varios hilos que invocan esa función en un momento determinado.
Por el contrario, la dirección de la función es siempre la misma. Existe independientemente de si se está llamando a la función en ese momento: después de todo, el punto de utilizar un puntero de función suele ser llamar a la función. Llamar a la función varias veces no cambiará su dirección: cuando llama a una función, no necesita preocuparse de si ya se está llamando.
Como la dirección de la función y la dirección de su primera variable tienen propiedades contradictorias, no pueden ser las mismas.
(Nota: es posible encontrar un sistema donde este programa pueda imprimir los mismos dos números, aunque puede pasar fácilmente por una carrera de programación sin encontrar uno. Existen arquitecturas de Harvard , donde el código y los datos se almacenan en diferentes memorias. el número cuando imprime un puntero de función es una dirección en la memoria de código, y el número cuando imprime un puntero de datos es una dirección en la memoria de datos. Los dos números podrían ser iguales, pero sería una coincidencia, y en otra llamada a la misma función, el puntero a la función sería el mismo, pero la dirección de la variable podría cambiar).
Si no me equivoco, un programa se carga en dos ubicaciones en la memoria. El primero es el ejecutable de compilaciones que incluye funciones y variables predefinidas. Esto comienza con la memoria más baja que ocupa la aplicación. Con algunos sistemas operativos modernos esto es 0x00000 ya que el administrador de memoria los traducirá según sea necesario. La segunda parte del código es el montón de aplicaciones donde está la fecha asignada en tiempo de ejecución, como los punteros que residen, por lo que cualquier memoria de tiempo de ejecución tendrá una ubicación diferente en la memoria.
las variables declaradas dentro de una función no están asignadas donde usted ve en el código las variables automáticas (variables definidas localmente en una función) reciben un lugar adecuado en la memoria de la pila cuando la función está a punto de ser llamada, esto es hecho durante el tiempo de compilación por el compilador , por lo tanto, la dirección de la primera instrucción no tiene nada que ver con las variables, se trata de las instrucciones ejecutables