resueltos - punteros en c++ ejemplos sencillos
¿Por qué este programa, que define main como un puntero a función, falla? (2)
En el código que se ejecuta antes de main
, hay algo como:
extern "C" int main(int argc, char **argv);
El problema con su código es que si tiene un puntero de función llamado main
, no es lo mismo que una función (a diferencia de Haskell, donde una función y un puntero de función son prácticamente intercambiables, al menos con mi conocimiento del 0.1% de Haskell).
Mientras que el compilador aceptará alegremente:
int (*func)() = ...;
int x = func();
como una llamada válida a la función puntero de func
. Sin embargo, cuando el compilador genera un código para llamar a la func
, en realidad lo hace de una manera diferente [aunque el estándar no dice cómo se debe hacer, y varía en diferentes arquitecturas de procesador, en la práctica, carga el valor en el puntero variable, y luego llama a este contenido].
Cuando tengas:
int func() { ... }
int x = func();
la llamada a func
solo se refiere a la dirección de func
sí, y llama a eso.
Entonces, suponiendo que su código realmente se compile, el código de inicio antes de main
llamará a la dirección de su variable main
lugar de leer indirectamente el valor en main
y luego llamará a eso. En los sistemas modernos, esto causará una falla de seguridad debido a que la vida main
en el segmento de datos que no es ejecutable, pero en los sistemas operativos más antiguos, es probable que se bloquee debido a que el sistema main
no contiene código real (pero puede ejecutar algunas instrucciones antes de que se caiga). en este caso - en el pasado oscuro y distante, accidentalmente he ejecutado todo tipo de "basura" con causas bastante difíciles de descubrir ...)
Pero como main
es una función "especial", también es posible que el compilador diga "No, no puedes hacer esto".
Solía funcionar, hace muchos años para hacer esto:
char main[] = { 0xXX, 0xYY, 0xZZ ... };
pero, de nuevo, esto no funciona en un sistema operativo moderno, porque main
termina en la sección de datos, y no es ejecutable en esa sección.
Edición: Después de probar el código publicado, al menos en mi Linux de 64 bits, el código realmente compila, pero se bloquea, como era de esperar, cuando intenta ejecutar main.
Ejecutar en GDB da esto:
Program received signal SIGSEGV, Segmentation fault.
0x0000000000600950 in main ()
(gdb) bt
#0 0x0000000000600950 in main ()
(gdb) disass
Dump of assembler code for function main:
=> 0x0000000000600950 <+0>: and %al,0x40(%rip) # 0x600996
0x0000000000600956 <+6>: add %al,(%rax)
End of assembler dump.
(gdb) disass stuff
Dump of assembler code for function stuff():
0x0000000000400520 <+0>: push %rbp
0x0000000000400521 <+1>: mov %rsp,%rbp
0x0000000000400524 <+4>: sub $0x10,%rsp
0x0000000000400528 <+8>: lea 0x400648,%rdi
0x0000000000400530 <+16>: callq 0x400410 <puts@plt>
0x0000000000400535 <+21>: mov $0x0,%ecx
0x000000000040053a <+26>: mov %eax,-0x4(%rbp)
0x000000000040053d <+29>: mov %ecx,%eax
0x000000000040053f <+31>: add $0x10,%rsp
0x0000000000400543 <+35>: pop %rbp
0x0000000000400544 <+36>: retq
End of assembler dump.
(gdb) x main
0x400520 <stuff()>: 0xe5894855
(gdb) p main
$1 = (int (*)(void)) 0x400520 <stuff()>
(gdb)
Entonces, podemos ver que main
no es realmente una función, es una variable que contiene un puntero a stuff
. El código de inicio llama a main
como si fuera una función, pero no ejecuta las instrucciones allí (porque son datos, y los datos tienen el bit de "no ejecutar" establecido, no es que pueda ver eso aquí, pero sé que funciona) camino).
Edit2:
Inspeccionando dmesg
muestra:
a.out [7035]: segfault a 600950 ip 0000000000600950 sp 00007fff4e7cb928 error 15 en a.out [600000 + 1000]
En otras palabras, la falla de segmentación ocurre inmediatamente con la ejecución de main
, porque no es ejecutable.
Edit3:
Ok, entonces es un poco más complicado que eso (al menos en mi biblioteca en tiempo de ejecución de C), ya que el código que llama main es una función que toma el puntero a main como argumento y lo llama a través de un puntero. Sin embargo, esto no cambia el hecho de que cuando el compilador construye el código, produce un nivel de direccionamiento indirecto inferior al que necesita e intenta ejecutar la variable llamada main
lugar de la función a la que apunta la variable.
Listado __libc_start_main
en GDB:
87 STATIC int
88 LIBC_START_MAIN (int (*main) (int, char **, char ** MAIN_AUXVEC_DECL),
89 int argc, char *__unbounded *__unbounded ubp_av,
90 #ifdef LIBC_START_MAIN_AUXVEC_ARG
91 ElfW(auxv_t) *__unbounded auxvec,
92 #endif
En este punto, la impresión main
nos da un puntero a la función que apunta a 0x600950, que es la variable llamada main
(igual que lo que desmonté arriba)
(gdb) p main
$1 = (int (*)(int, char **, char **)) 0x600950 <main>
Tenga en cuenta que esta es una variable main
diferente a la llamada main
en la fuente publicada en la pregunta.
El siguiente programa compila perfectamente sin errores ni advertencias (incluso con -Wall
) en g ++, pero se bloquea de inmediato.
#include <cstdio>
int stuff(void)
{
puts("hello there.");
return 0;
}
int (*main)(void) = stuff;
Este es un intento (obviamente horriblemente equivocado) de ejecutar un programa C ++ sin declarar explícitamente main como una función. Mi intención era que el programa ejecutara stuff
enlazándolas con el símbolo main
. Me sorprendió mucho que se compilara, pero ¿por qué falla exactamente, después de haber compilado? He mirado el ensamblaje generado pero no sé lo suficiente como para entenderlo.
Soy plenamente consciente de que hay muchas restrictions sobre cómo se puede definir / usar main
, pero no estoy claro de cómo mi programa las rompe. No he sobrecargado main ni lo he llamado dentro de mi programa ... así que, ¿qué regla estoy infringiendo al definir main
esta manera?
Nota: esto no era algo que intentaba hacer en el código real. En realidad, fue el comienzo de un intento de escribir Haskell en C ++ .
No hay nada especial aquí por ser main (). Lo mismo ocurrirá si haces esto para cualquier función. Considera este ejemplo:
archivo1.cpp:
#include <cstdio>
void stuff(void)
{
puts("hello there.");
}
void (*func)(void) = stuff;
file2.cpp:
extern "C" {void func(void);}
int main(int argc, char**argv)
{
func();
}
Esto también se compilará, y luego segfault. Esencialmente, está haciendo lo mismo para la función func, pero como la codificación es explícita, ahora aparentemente parece más incorrecta. main () es una función de tipo C simple sin alteración de nombres, y simplemente aparece como un nombre en la tabla de símbolos. Si lo hace algo distinto de una función, obtendrá un error de seguridad cuando ejecuta un puntero.
Supongo que la parte interesante es que el compilador le permitirá definir un símbolo llamado main cuando ya esté declarado implícitamente con un tipo diferente.