¿Por qué los punteros de función y los punteros de datos son incompatibles en C / C ++?

130

He leído que convertir un puntero de función en un puntero de datos y viceversa funciona en la mayoría de las plataformas, pero no se garantiza que funcione. ¿Por qué es este el caso? ¿No deberían ser ambas direcciones simplemente en la memoria principal y, por lo tanto, ser compatibles?

gexicida
fuente
16
Indefinido en el estándar C, definido en POSIX. Cuidado con la diferencia.
Ephemient
Soy un poco nuevo en esto, pero ¿no se supone que debes hacer el reparto en el lado derecho de "="? Me parece que el problema es que estás asignando un puntero vacío. Pero veo que la página del manual hace esto, así que espero que alguien pueda educarme. Veo ejemplos en la 'red de personas que emiten el valor de retorno de dlsym, por ejemplo, aquí: daniweb.com/forums/thread62561.html
JasonWoof
9
Tenga en cuenta lo que POSIX dice en la sección sobre Tipos de datos : §2.12.3 Tipos de puntero. Todos los tipos de puntero de función tendrán la misma representación que el puntero de tipo void. La conversión de un puntero de función a void *no alterará la representación. Un void *valor resultante de dicha conversión se puede volver a convertir al tipo de puntero de función original, utilizando una conversión explícita, sin pérdida de información. Nota : El estándar ISO C no requiere esto, pero se requiere para la conformidad POSIX.
Jonathan Leffler
2
esta es la pregunta en la sección ACERCA DE este sitio web .. :) :) Nos vemos aquí
ZooZ
1
@KeithThompson: el mundo cambia, y POSIX también. Lo que escribí en 2012 ya no se aplica en 2018. El estándar POSIX cambió la palabrería. Ahora está asociado con dlsym(): observe el final de la sección 'Uso de la aplicación' donde dice: Tenga en cuenta que la conversión de un void *puntero a un puntero de función como en: fptr = (int (*)(int))dlsym(handle, "my_function"); no está definida por el estándar ISO C. Este estándar requiere que esta conversión funcione correctamente en implementaciones conformes.
Jonathan Leffler

Respuestas:

171

Una arquitectura no tiene que almacenar código y datos en la misma memoria. Con una arquitectura de Harvard, el código y los datos se almacenan en una memoria completamente diferente. La mayoría de las arquitecturas son arquitecturas de Von Neumann con código y datos en la misma memoria, pero C no se limita solo a ciertos tipos de arquitecturas si es posible.

Dirk Holsopple
fuente
15
Además, incluso si el código y los datos se almacenan en el mismo lugar en el hardware físico, el software y el acceso a la memoria a menudo impiden ejecutar los datos como código sin la "aprobación" del sistema operativo. DEP y similares.
Michael Graczyk
15
Al menos tan importante como tener diferentes espacios de direcciones (quizás más importante) es que los punteros de función pueden tener una representación diferente que los punteros de datos.
Michael Burr
14
Ni siquiera tiene que tener una arquitectura de Harvard para tener punteros de código y datos utilizando diferentes espacios de direcciones; el antiguo modelo de memoria "Pequeño" de DOS hizo esto (cerca de punteros con CS != DS).
caf
1
Incluso los procesadores modernos tendrían dificultades con la mezcla, ya que las instrucciones y la memoria caché de datos generalmente se manejan por separado, incluso cuando el sistema operativo le permite escribir código en alguna parte.
PypeBros
3
@EricJ. Hasta que llame VirtualProtect, lo que le permite marcar regiones de datos como ejecutables.
Dietrich Epp
37

Algunas computadoras tienen (tenían) espacios de direcciones separados para código y datos. En tal hardware simplemente no funciona.

El lenguaje está diseñado no solo para las aplicaciones de escritorio actuales, sino para permitir que se implemente en un gran conjunto de hardware.


Parece que el comité del lenguaje C nunca tuvo la intención void*de ser un puntero para funcionar, solo querían un puntero genérico para los objetos.

La justificación del C99 dice:

6.3.2.3 Los punteros
C ahora se han implementado en una amplia gama de arquitecturas. Si bien algunas de estas arquitecturas presentan punteros uniformes del tamaño de algún tipo de entero, el código máximamente portátil no puede asumir la correspondencia necesaria entre los diferentes tipos de puntero y los tipos de entero. En algunas implementaciones, los punteros pueden incluso ser más anchos que cualquier tipo de entero.

El uso de void*("puntero a void") como un tipo de puntero de objeto genérico es una invención del Comité C89. La adopción de este tipo fue estimulada por el deseo de especificar argumentos de prototipo de función que conviertan silenciosamente punteros arbitrarios (como en fread) o se quejen si el tipo de argumento no coincide exactamente (como en strcmp). No se dice nada sobre punteros a funciones, que pueden ser inconmensurables con punteros de objetos y / o enteros.

Nota No se dice nada sobre los punteros a las funciones en el último párrafo. Pueden ser diferentes de otros indicadores, y el comité lo sabe.

Bo Persson
fuente
El estándar podría hacerlos compatibles sin meterse con esto simplemente haciendo que los tipos de datos tengan el mismo tamaño y garantizando que asignarlos a uno y luego regresarlos dará como resultado el mismo valor. Lo hacen con void *, que es el único tipo de puntero compatible con todo.
Edward Strange
15
@CrazyEddie No puede asignar un puntero de función a un void *.
ouah
44
Podría estar equivocado en void * al aceptar punteros de función, pero el punto sigue siendo. Los bits son bits. El estándar podría requerir que el tamaño de los diferentes tipos pueda acomodar los datos entre sí y se garantizaría que la asignación funcione incluso si se usan en diferentes segmentos de memoria. La razón por la que existe esta incompatibilidad es que esto NO está garantizado por el estándar y, por lo tanto, los datos se pueden perder en la asignación.
Edward Strange
55
Pero requerir sizeof(void*) == sizeof( void(*)() )desperdiciará espacio en el caso en que los punteros de función y los punteros de datos sean de diferentes tamaños. Este fue un caso común en los años 80, cuando se escribió el primer estándar C.
Robᵩ
8
@RichardChambers: los diferentes espacios de dirección también pueden tener diferentes anchos de dirección , como un AVR Atmel que usa 16 bits para instrucciones y 8 bits para datos; en ese caso, sería difícil convertir datos (8 bits) a punteros funcionales (16 bits) y viceversa. Se supone que las C son fáciles de implementar; parte de esa facilidad proviene de dejar los punteros de datos e instrucciones incompatibles entre sí.
John Bode
30

Para aquellos que recuerdan MS-DOS, Windows 3.1 y versiones anteriores, la respuesta es bastante fácil. Todo esto solía admitir varios modelos de memoria diferentes, con diferentes combinaciones de características para punteros de código y datos.

Entonces, por ejemplo, para el modelo compacto (código pequeño, datos grandes):

sizeof(void *) > sizeof(void(*)())

y viceversa en el modelo Medio (código grande, datos pequeños):

sizeof(void *) < sizeof(void(*)())

En este caso, no tenía un almacenamiento separado para el código y la fecha, pero aún no podía convertir entre los dos punteros (salvo el uso de modificadores no estándar __near y __far).

Además, no hay garantía de que, incluso si los punteros son del mismo tamaño, apunten a lo mismo: en el modelo de memoria pequeña de DOS, tanto el código como los datos se usan cerca de los punteros, pero apuntan a diferentes segmentos. Por lo tanto, convertir un puntero de función en un puntero de datos no le daría un puntero que tuviera relación alguna con la función y, por lo tanto, no era útil para dicha conversión.

Tomek
fuente
Re: "la conversión de un puntero de función a un puntero de datos no le daría un puntero que tuviera relación alguna con la función y, por lo tanto, no había uso para tal conversión": Esto no se sigue del todo. La conversión de int*a a le void*da un puntero con el que realmente no puede hacer nada, pero sigue siendo útil para poder realizar la conversión. (Esto se debe a que void*puede almacenar cualquier puntero de objeto, por lo que puede usarse para algoritmos genéricos que no necesitan saber qué tipo contienen. Lo mismo podría ser útil también para los punteros de función, si estuviera permitido).
ruakh
44
@ruakh: en el caso de convertir int *a void *, void *se garantiza que al menos apunte al mismo objeto que el original int *, por lo que esto es útil para algoritmos genéricos que acceden al objeto señalado, como int n; memcpy(&n, src, sizeof n);. En el caso de que convertir un puntero de función en a void *no produzca un puntero apuntando a la función, no es útil para tales algoritmos; lo único que podría hacer es convertir el void *puntero de nuevo a una función nuevamente, por lo que podría bueno, simplemente use un puntero que unioncontenga void *ay función.
caf
@ café: bastante justo. Gracias por señalar eso. Y para el caso, incluso si la void* hicieron punto a la función, supongo que sería una mala idea para las personas que pasan a memcpy. :-P
ruakh
Copiado de lo anterior : tenga en cuenta lo que POSIX dice en Tipos de datos : §2.12.3 Tipos de puntero. Todos los tipos de puntero de función tendrán la misma representación que el puntero de tipo void. La conversión de un puntero de función a void *no alterará la representación. Un void *valor resultante de tal conversión se puede volver a convertir al tipo de puntero de función original, utilizando una conversión explícita, sin pérdida de información. Nota : El estándar ISO C no requiere esto, pero se requiere para la conformidad POSIX.
Jonathan Leffler
@caf Si solo se pasa a una devolución de llamada que conoce el tipo correcto, solo estoy interesado en la seguridad de ida y vuelta, no en ninguna otra relación que esos valores convertidos puedan tener.
Deduplicador
23

Se supone que los punteros a anular pueden acomodar un puntero a cualquier tipo de datos, pero no necesariamente un puntero a una función. Algunos sistemas tienen requisitos diferentes para punteros a funciones que punteros a datos (por ejemplo, hay DSP con diferente direccionamiento para datos frente a código, el modelo medio en MS-DOS utiliza punteros de 32 bits para código pero solo punteros de 16 bits para datos) .

Jerry Coffin
fuente
1
pero entonces no debería la función dlsym () devolver algo distinto de un vacío *. Quiero decir, si el vacío * no es lo suficientemente grande para el puntero de función, ¿no estamos ya borrados?
Manav
1
@Knickerkicker: Sí, probablemente. Si la memoria funciona, el tipo de retorno de dlsym se discutió extensamente, probablemente hace 9 o 10 años, en la lista de correo electrónico de OpenGroup. Sin embargo, no recuerdo qué (si algo) salió de eso.
Jerry Coffin
1
tienes razón. Esto parece un resumen bastante agradable (aunque anticuado) de su punto.
Manav
2
@LegoStormtroopr: Interesante cómo 21 personas están de acuerdo con la idea de votar, pero solo alrededor de 3 lo han hecho. :-)
Jerry Coffin
13

Además de lo que ya se dijo aquí, es interesante mirar POSIX dlsym():

El estándar ISO C no requiere que los punteros a las funciones se puedan enviar de un lado a otro a los punteros a los datos. De hecho, el estándar ISO C no requiere que un objeto de tipo void * pueda contener un puntero a una función. Sin embargo, las implementaciones que admiten la extensión XSI requieren que un objeto de tipo void * pueda contener un puntero a una función. Sin embargo, el resultado de convertir un puntero a una función en un puntero a otro tipo de datos (excepto void *) aún no está definido. Tenga en cuenta que los compiladores que cumplen con el estándar ISO C deben generar una advertencia si se intenta una conversión de un puntero nulo * a un puntero de función como en:

 fptr = (int (*)(int))dlsym(handle, "my_function");

Debido al problema señalado aquí, una versión futura puede agregar una nueva función para devolver punteros de función, o la interfaz actual puede quedar en desuso en favor de dos nuevas funciones: una que devuelve punteros de datos y la otra que devuelve punteros de función.

Maxim Egorushkin
fuente
¿eso significa que usar dlsym para obtener la dirección de una función no es seguro actualmente? ¿Existe actualmente una forma segura de hacerlo?
Gexicida
44
Significa que actualmente POSIX requiere de una plataforma ABI que tanto los punteros de función como los de datos se puedan transmitir de forma segura void*.
Maxim Egorushkin
@gexicide Significa que las implementaciones que cumplen con POSIX han hecho una extensión al lenguaje, dando un significado definido por la implementación a lo que es un comportamiento indefinido según el estándar en sí mismo. Incluso aparece como una de las extensiones comunes al estándar C99, sección J.5.7 Conversiones de puntero de función.
David Hammen
1
@DavidHammen No es una extensión del lenguaje, sino un nuevo requisito adicional. C no requiere void*ser compatible con un puntero de función, mientras que POSIX sí.
Maxim Egorushkin
9

C ++ 11 tiene una solución al desajuste de larga data entre C / C ++ y POSIX con respecto a dlsym(). Se puede usar reinterpret_castpara convertir un puntero de función a / desde un puntero de datos siempre que la implementación admita esta característica.

De la norma, 5.2.10 párr. 8, "la conversión de un puntero de función a un tipo de puntero de objeto o viceversa es compatible condicionalmente". 1.3.5 define "condicionalmente soportado" como una "construcción de programa que no es necesaria una implementación para soportar".

David Hammen
fuente
Se puede, pero no se debe. Un compilador conforme debe generar una advertencia para eso (que a su vez debería provocar un error, cf. -Werror). Una solución mejor (y no UB) es recuperar un puntero al objeto devuelto por dlsym(es decir void**) y convertirlo en un puntero para que funcione . Aún definido por la implementación, pero ya no causa una advertencia / error .
Konrad Rudolph
3
@KonradRudolph: en desacuerdo. La redacción "condicionalmente compatible" fue escrita específicamente para permitir dlsymy GetProcAddresscompilar sin previo aviso.
MSalters
@MSalters ¿Qué quieres decir con "en desacuerdo"? O tengo razón o no. La documentación de dlsym dice explícitamente que "los compiladores que cumplen con el estándar ISO C deben generar una advertencia si se intenta una conversión de un puntero void * a un puntero de función". Esto no deja mucho espacio para la especulación. Y GCC (con -pedantic) no advierten. Nuevamente, no es posible especular.
Konrad Rudolph
1
Seguimiento: creo que ahora entiendo. No es UB. Está definido por la implementación. Todavía no estoy seguro de si la advertencia se debe generar o no, probablemente no. Oh bien.
Konrad Rudolph el
2
@KonradRudolph: No estoy de acuerdo con tu "no debería", que es una opinión. La respuesta mencionó específicamente C ++ 11, y yo era miembro del C ++ CWG en el momento en que se solucionó el problema. C99 de hecho tiene una redacción diferente, condicionalmente compatible es una invención de C ++.
MSalters
7

Dependiendo de la arquitectura de destino, el código y los datos pueden almacenarse en áreas de memoria fundamentalmente incompatibles y físicamente distintas.

Graham Borland
fuente
"físicamente distinto" lo entiendo, pero ¿puede explicar más sobre la distinción "fundamentalmente incompatible"? Como dije en la pregunta, ¿no se supone que un puntero vacío es tan grande como cualquier tipo de puntero, o es una presunción errónea de mi parte?
Manav
@KnickerKicker: void *es lo suficientemente grande como para contener cualquier puntero de datos, pero no necesariamente ningún puntero de función.
Ephemient
1
Regreso
5

indefinido no necesariamente significa no permitido, puede significar que el implementador del compilador tiene más libertad para hacerlo como ellos quieran.

Por ejemplo, puede que no sea posible en algunas arquitecturas: indefinido les permite tener una biblioteca 'C' conforme, incluso si no puede hacerlo.

Martin Beckett
fuente
5

Otra solución:

Suponiendo que POSIX garantiza que los punteros de función y datos tengan el mismo tamaño y representación (no puedo encontrar el texto para esto, pero el ejemplo OP citado sugiere que al menos pretendieron hacer este requisito), lo siguiente debería funcionar:

double (*cosine)(double);
void *tmp;
handle = dlopen("libm.so", RTLD_LAZY);
tmp = dlsym(handle, "cos");
memcpy(&cosine, &tmp, sizeof cosine);

Esto evita violar las reglas de alias al pasar por la char []representación, que permite alias de todos los tipos.

Otro enfoque más:

union {
    double (*fptr)(double);
    void *dptr;
} u;
u.dptr = dlsym(handle, "cos");
cosine = u.fptr;

Pero recomendaría el memcpyenfoque si desea una C 100% correcta.

R .. GitHub DEJA DE AYUDAR AL HIELO
fuente
5

Pueden ser diferentes tipos con diferentes requisitos de espacio. Asignar a uno puede cortar irreversiblemente el valor del puntero para que la asignación de resultados resulte en algo diferente.

Creo que pueden ser de diferentes tipos porque el estándar no quiere limitar las posibles implementaciones que ahorran espacio cuando no es necesario o cuando el tamaño podría hacer que la CPU tenga que hacer más basura para usarlo, etc.

Edward extraño
fuente
3

La única solución verdaderamente portátil es no usar dlsympara funciones, y en su lugar usar dlsympara obtener un puntero a datos que contengan punteros de función. Por ejemplo, en tu biblioteca:

struct module foo_module = {
    .create = create_func,
    .destroy = destroy_func,
    .write = write_func,
    /* ... */
};

y luego en su aplicación:

struct module *foo = dlsym(handle, "foo_module");
foo->create(/*...*/);
/* ... */

Por cierto, esta es una buena práctica de diseño de todos modos, y hace que sea fácil admitir la carga dinámica a través dlopeny la vinculación estática de todos los módulos en sistemas que no admiten la vinculación dinámica, o donde el usuario / integrador del sistema no quiere utilizar la vinculación dinámica.

R .. GitHub DEJA DE AYUDAR AL HIELO
fuente
2
¡Agradable! Si bien estoy de acuerdo en que esto parece más fácil de mantener, todavía no es obvio (para mí) cómo utilizo el enlace estático además de esto. ¿Puedes elaborar?
Manav
2
Si cada módulo tiene su propia foo_moduleestructura (con nombres únicos), simplemente puede crear un archivo adicional con una matriz struct { const char *module_name; const struct module *module_funcs; }y una función simple para buscar en esta tabla el módulo que desea "cargar" y devolver el puntero correcto, luego use esto en lugar de dlopeny dlsym.
R .. GitHub DEJA DE AYUDAR AL HIELO
@R .. Es cierto, pero agrega costos de mantenimiento al tener que mantener la estructura del módulo.
user877329
3

Un ejemplo moderno de dónde los punteros de función pueden diferir en tamaño de los punteros de datos: punteros de función miembro de clase C ++

Citado directamente de https://blogs.msdn.microsoft.com/oldnewthing/20040209-00/?p=40713/

class Base1 { int b1; void Base1Method(); };
class Base2 { int b2; void Base2Method(); };
class Derived : public Base1, Base2 { int d; void DerivedMethod(); };

Ahora hay dos posibles thispunteros.

Un puntero a una función miembro de Base1puede usarse como un puntero a una función miembro de Derived, ya que ambos usan el mismo this puntero. Pero un puntero a una función miembro de Base2no se puede usar tal cual como puntero a una función miembro de Derived, ya que el this puntero debe ajustarse.

Hay muchas formas de resolver esto. Así es como el compilador de Visual Studio decide manejarlo:

Un puntero a una función miembro de una clase de herencia múltiple es realmente una estructura.

[Address of function]
[Adjustor]

El tamaño de una función de puntero a miembro de una clase que usa herencia múltiple es el tamaño de un puntero más el tamaño de a size_t.

tl; dr: cuando se usa la herencia múltiple, un puntero a una función miembro puede (dependiendo del compilador, versión, arquitectura, etc.) realmente almacenarse como

struct { 
    void * func;
    size_t offset;
}

que obviamente es más grande que a void *.

Andrew Sun
fuente
2

En la mayoría de las arquitecturas, los punteros a todos los tipos de datos normales tienen la misma representación, por lo que la conversión entre tipos de punteros de datos no es una opción.

Sin embargo, es concebible que los punteros de función requieran una representación diferente, tal vez sean más grandes que otros punteros. Si void * pudiera contener punteros de función, esto significaría que la representación de void * tendría que ser de mayor tamaño. Y todos los moldes de punteros de datos a / desde void * tendrían que realizar esta copia adicional.

Como alguien mencionó, si necesita esto, puede lograrlo utilizando una unión. Pero la mayoría de los usos de void * son solo para datos, por lo que sería oneroso aumentar todo el uso de memoria en caso de que sea necesario almacenar un puntero de función.

Barmar
fuente
-1

Sé que esto no ha sido comentada desde 2012, pero pensé que sería útil añadir que yo hago conocer una arquitectura que tiene muy punteros incompatibles para datos y funciones desde una llamada en la que la arquitectura cheques privilegio y lleva la información adicional. Ninguna cantidad de casting ayudará. Es el molino .

phorgan1
fuente
Esta respuesta es incorrecta. Por ejemplo, puede convertir un puntero de función en un puntero de datos y leerlo (si tiene permisos para leer desde esa dirección, como de costumbre). El resultado tiene tanto sentido como lo hace, por ejemplo, en x86.
Manuel Jacob