Conflicto entre un tutorial de Stanford y GCC

82

Según esta película (alrededor del minuto 38), si tengo dos funciones con las mismas vars locales, usarán el mismo espacio. Entonces, el siguiente programa debería imprimirse 5. Compilándolo con gccresultados -1218960859. ¿por qué?

El programa:

#include <stdio.h>

void A()
{
    int a;
    printf("%i",a);
}

void B()
{
    int a;
    a = 5;
}

int main()
{
    B();
    A();
    return 0;
}

según lo solicitado, aquí está la salida del desensamblador:

0804840c <A>:
 804840c:   55                      push   ebp
 804840d:   89 e5                   mov    ebp,esp
 804840f:   83 ec 28                sub    esp,0x28
 8048412:   8b 45 f4                mov    eax,DWORD PTR [ebp-0xc]
 8048415:   89 44 24 04             mov    DWORD PTR [esp+0x4],eax
 8048419:   c7 04 24 e8 84 04 08    mov    DWORD PTR [esp],0x80484e8
 8048420:   e8 cb fe ff ff          call   80482f0 <printf@plt>
 8048425:   c9                      leave  
 8048426:   c3                      ret    

08048427 <B>:
 8048427:   55                      push   ebp
 8048428:   89 e5                   mov    ebp,esp
 804842a:   83 ec 10                sub    esp,0x10
 804842d:   c7 45 fc 05 00 00 00    mov    DWORD PTR [ebp-0x4],0x5
 8048434:   c9                      leave  
 8048435:   c3                      ret    

08048436 <main>:
 8048436:   55                      push   ebp
 8048437:   89 e5                   mov    ebp,esp
 8048439:   83 e4 f0                and    esp,0xfffffff0
 804843c:   e8 e6 ff ff ff          call   8048427 <B>
 8048441:   e8 c6 ff ff ff          call   804840c <A>
 8048446:   b8 00 00 00 00          mov    eax,0x0
 804844b:   c9                      leave  
 804844c:   c3                      ret    
 804844d:   66 90                   xchg   ax,ax
 804844f:   90                      nop
elyashiv
fuente
41
"Usan el mismo espacio", eso es incorrecto. Podrían. O puede que no. Y no puedes confiar en esto de ninguna manera.
Mat
17
Me pregunto qué uso tiene esto como ejercicio, si uno lo usara en el código de producción, recibiría un disparo.
AndersK
12
@claptrap ¿Quizás para aprender cómo funciona la pila de llamadas y entender qué hace la computadora debajo del capó? La gente se está tomando esto demasiado en serio.
Jonathon Reinhart
9
@claptrap Una vez más, es un ejercicio de aprendizaje . Los "aros que tienes que atravesar" tienen sentido si comprendes lo que está sucediendo en el nivel de la asamblea. Me seriamente en duda la OP tiene ninguna intención de utilizar algo como esto en un programa "real" (si es que lo hace, debe ser expulsado!)
Jonathon Reinhart
12
El ejemplo es engañoso para los desprevenidos, porque las dos variables locales tienen el mismo nombre; pero esto es irrelevante para lo que está sucediendo: solo importa el número y tipo de variables. Los diferentes nombres deberían funcionar exactamente igual.
Alexis

Respuestas:

130

Sí, sí, este es un comportamiento indefinido , porque está utilizando la variable 1 sin inicializar .

Sin embargo, en la arquitectura x86 2 , este experimento debería funcionar . El valor no se "borra" de la pila, y dado que no está inicializado B(), ese mismo valor debería estar allí, siempre que los marcos de la pila sean idénticos.

Me atrevería a adivinar que, dado int aque no se usa dentro de void B(), el compilador optimizó ese código y nunca se escribió un 5 en esa ubicación en la pila. Trate de añadir una printfen B()así - sólo se puede trabajar.

Además, los indicadores del compilador, es decir, el nivel de optimización, probablemente también afectarán este experimento. Intente deshabilitar las optimizaciones pasando -O0a gcc.

Editar: Acabo de compilar su código con gcc -O0(64 bits) y, de hecho, el programa imprime 5, como esperaría alguien familiarizado con la pila de llamadas. De hecho, funcionó incluso sin él -O0. Una compilación de 32 bits puede comportarse de manera diferente.

Descargo de responsabilidad: ¡Nunca, nunca uses algo como esto en código "real"!

1 - Hay un debate a continuación sobre si esto es oficialmente "UB" o simplemente impredecible.

2 - También x64, y probablemente cualquier otra arquitectura que use una pila de llamadas (al menos las que tengan una MMU)


Echemos un vistazo a la razón por la que no funcionó. Esto se ve mejor en 32 bits, por lo que compilaré con -m32.

$ gcc --version
gcc (GCC) 4.7.2 20120921 (Red Hat 4.7.2-2)

Compilé con $ gcc -m32 -O0 test.c(Optimizaciones deshabilitadas). Cuando ejecuto esto, imprime basura.

Mirando $ objdump -Mintel -d ./a.out:

080483ec <A>:
 80483ec:   55                      push   ebp
 80483ed:   89 e5                   mov    ebp,esp
 80483ef:   83 ec 28                sub    esp,0x28
 80483f2:   8b 45 f4                mov    eax,DWORD PTR [ebp-0xc]
 80483f5:   89 44 24 04             mov    DWORD PTR [esp+0x4],eax
 80483f9:   c7 04 24 c4 84 04 08    mov    DWORD PTR [esp],0x80484c4
 8048400:   e8 cb fe ff ff          call   80482d0 <printf@plt>
 8048405:   c9                      leave  
 8048406:   c3                      ret    

08048407 <B>:
 8048407:   55                      push   ebp
 8048408:   89 e5                   mov    ebp,esp
 804840a:   83 ec 10                sub    esp,0x10
 804840d:   c7 45 fc 05 00 00 00    mov    DWORD PTR [ebp-0x4],0x5
 8048414:   c9                      leave  
 8048415:   c3                      ret    

Vemos que en B, el compilador reservó 0x10 bytes de espacio de pila e inicializó nuestra int avariable en [ebp-0x4]5.

En ASin embargo, el compilador coloca int aal [ebp-0xc]. Entonces, en este caso, nuestras variables locales no terminaron en el mismo lugar. Al agregar una printf()llamada Atambién, los marcos de pila para Ay Bserán idénticos y se imprimirán 55.

Jonathon Reinhart
fuente
7
¡Buen descargo de responsabilidad!
Tobias Wärre
5
Incluso si funciona una vez, no será confiable en algunas arquitecturas: un preámbulo de interrupción puede destruir todo lo que está debajo del puntero de la pila en cualquier momento.
Martin James
6
Muchos votan a favor de una respuesta que ni siquiera menciona "comportamiento indefinido". Además, también se acepta.
BЈовић
25
Además, se acepta porque realmente responde a la pregunta .
slebetman
8
@ BЈовић ¿Viste algo del video? Mira, todos y su hermano saben que no debes hacer esto en código real, e invoca un comportamiento indefinido . Ese no es el punto. La cuestión es que una computadora es una máquina predecible y bien definida. En una caja x86 (y probablemente en la mayoría de las otras arquitecturas), con un compilador sano y potencialmente algunos mensajes de código / bandera, esto funcionará como se esperaba. Este código, junto con el video, es simplemente una demostración de cómo funciona la pila de llamadas. Si te molesta tanto, te sugiero que vayas a otro lado. A algunos de nosotros los curiosos nos gusta entender las cosas.
Jonathon Reinhart
36

Es un comportamiento indefinido . Una variable local no inicializada tiene un valor indeterminado y su uso dará lugar a un comportamiento indefinido.

Algún tipo programador
fuente
6
Para ser más precisos, usar una variable unitaria de la que nunca se toma la dirección es un comportamiento indefinido.
Jens Gustedt
@JensGustedt Buen comentario. ¿Tiene algo que decir sobre la sección "El siguiente ejemplo" de blog.frama-c.com/index.php?post/2013/03/13/… ?
Pascal Cuoq
@ PascalCuoq, esto incluso parece ser una discusión en curso en el comité de estándares. Hay situaciones en las que tiene sentido inspeccionar la memoria que obtiene a través de un puntero, incluso si no puede saber si está inicializada o no. Dejarlo sin definir en todos los casos es demasiado restrictivo.
Jens Gustedt
@JensGustedt: ¿Cómo es que tomar su dirección hace que su uso tenga un comportamiento definido? { int uninit; &uninit; printf("%d\n", uninit); }Aún tiene un comportamiento indefinido. Por otro lado, puede tratar cualquier objeto como una matriz de unsigned char; ¿Eso es lo que tenías en mente?
Keith Thompson
@KeithThompson, no, es al revés. Tener una variable tal que su dirección nunca se toma y no se inicializa conduce a UB. Leer un valor indeterminado en sí mismo no es un comportamiento indefinido, el contenido es simplemente impredecible. De 6.3.2.1 p2: Si el lvalue designa un objeto de duración de almacenamiento automático que podría haber sido declarado con la clase de almacenamiento de registro (nunca se tomó su dirección), y ese objeto no está inicializado (no declarado con un inicializador y sin asignación a él se ha realizado antes de su uso), el comportamiento no está definido.
Jens Gustedt
12

Una cosa importante para recordar: ¡ nunca confíe en algo así y nunca lo use en código real! Es solo algo interesante (que incluso no siempre es cierto), no una característica o algo así. Imagínese tratando de encontrar un error producido por ese tipo de "característica": pesadilla.

Por cierto. - C y C ++ están llenos de ese tipo de "características", aquí hay una GRAN presentación de diapositivas al respecto: http://www.slideshare.net/olvemaudal/deep-c Entonces, si desea ver más "características" similares, comprenda qué bajo el capó y cómo está funcionando, solo mire esta presentación de diapositivas; no se arrepentirá y estoy seguro de que incluso la mayoría de los programadores experimentados de c / c ++ pueden aprender mucho de esto.

cyriel
fuente
7

En la función A, la variable ano se inicializa, imprimir su valor conduce a un comportamiento indefinido.

En algunos compiladores, la variable ain Ay ain Bestán en la misma dirección, por lo que puede imprimirse 5, pero nuevamente, no se puede confiar en un comportamiento indefinido.

Yu Hao
fuente
1
El tutorial es 100% correcto, pero si los resultados en el póster original s machine will be the same depends on the assembly generated by the compiler. As @JonathonReinhart pointed out the call to B () `pueden haber sido optimizados.
Lloyd Crawley
1
Tengo un problema con la palabrería de "ese tutorial está mal". ¿De verdad fuiste a ver el tutorial? No trata de enseñarle cómo hacer una locura como esta, sino de demostrar cómo funciona la pila de llamadas. En ese caso, el tutorial es completamente correcto.
Jonathon Reinhart
@JonathonReinhart No vi el tutorial, pensé que este ejemplo era del tutorial, eliminaré esta parte.
Yu Hao
@LloydCrawley He eliminado la parte sobre el tutorial. Sé que se trata de arquitectura de pila, eso es lo que quise decir con que están en la misma dirección cuando se imprimió 5, pero aparentemente Jonathon Reinhart tiene una explicación mucho mejor.
Yu Hao
7

Compile su código con gcc -Wall filename.cVerá estas advertencias.

In function 'B':
11:9: warning: variable 'a' set but not used [-Wunused-but-set-variable]

In function 'A':
6:11: warning: 'a' is used uninitialized in this function [-Wuninitialized]  

En c La impresión de una variable no inicializada conduce a un comportamiento indefinido.

La sección 6.7.8 Inicialización del estándar C99 dice

Si un objeto que tiene una duración de almacenamiento automático no se inicializa explícitamente, su valor es indeterminado. Si un objeto que tiene una duración de almacenamiento estático no se inicializa explícitamente, entonces:

if it has pointer type, it is initialized to a null pointer;
— if it has arithmetic type, it is initialized to (positive or unsigned) zero;
— if it is an aggregate, every member is initialized (recursively) according to these rules;
— if it is a union, the first named member is initialized (recursively) according to these rules.

Editar1

Como @Jonathon Reinhart Si desactiva la optimización mediante el uso de la -Obandera gcc-O0 , es posible que obtenga la salida 5.

Pero esto no es una buena idea, nunca lo use en el código de producción.

-Wuninitialized Esta es una de las advertencias valiosas. Debería considerar esta. No debe desactivar ni omitir esta advertencia que genera un gran daño en la producción, como causar fallas mientras se ejecutan demonios.


Editar2

Explicación de las diapositivas de Deep C ¿Por qué el resultado es 5 / basura? Agregando esta información de las diapositivas con modificaciones menores para que esta respuesta sea un poco más efectiva.

Caso 1: sin optimización

$ gcc -O0 file.c && ./a.out  
5

Quizás este compilador tiene un grupo de variables con nombre que reutiliza. Por ejemplo, se usó y se liberó la variable a B(), luego, cuando A()necesite un nombre entero a, obtendrá la variable obtendrá la misma ubicación de memoria. Si cambia el nombre de la variable B()a, digamos b, entonces no creo que lo obtenga 5.

Caso 2: con optimización

Pueden suceder muchas cosas cuando el optimizador se activa. En este caso, supongo que la llamada a B()puede omitirse ya que no tiene efectos secundarios. Además, no me sorprendería que A()esté insertado main(), es decir, sin llamada de función. (Pero dado que A ()tiene visibilidad de vinculador, el código de objeto para la función aún debe crearse en caso de que otro archivo de objeto quiera vincularse con la función). De todos modos, sospecho que el valor impreso será diferente si optimiza el código.

gcc -O file.c && ./a.out
1606415608  

¡Basura!

Gangadhar
fuente
1
Su lógica en la Edición 2, Caso 1 es completamente incorrecta. No es así en absoluto como funciona. El nombre de la variable local no significa absolutamente nada.
Jonathon Reinhart
@JonathonReinhart Como se mencionó en la respuesta, agregué esto de las diapositivas de deepc, explique sobre qué base es incorrecto.
Gangadhar
3
No existe ninguna asociación entre el espacio de la pila y los nombres de las variables. El ejemplo se basa en el hecho de que, conceptualmente, el marco de pila en la segunda llamada de función simplemente se superpondrá al marco de pila de la segunda llamada de función. No importa cuáles sean los nombres, siempre que las firmas de ambos métodos sean iguales, podría suceder lo mismo. Como otros han señalado, si estuviera en un sistema embebido y una interrupción de hardware fuera atendida entre las llamadas a A () y B (), la pila contendría valores aleatorios. Herramientas antiguas como Code Guard para Borland permitían escribir ceros para apilar antes de cada llamada.
Dan Haynes
@DanHaynes Tu comentario me convence. El marco de pila en la segunda llamada de función puede superponerse al marco de pila de la Primera llamada de función en cuanto al tipo de variable y el prototipo de función mismo. Yo también estoy de acuerdo porque no hay nada que ver con los nombres de variable.
Gangadhar