Pregunta

¿Alguien podría explicar qué está haciendo GCC con este código? ¿Qué se está inicializando? El código original es:

#include <stdio.h>
int main()
{

}

Y fue traducido a:

    .file   "test1.c"
    .def    ___main;    .scl    2;  .type   32; .endef
    .text
.globl _main
    .def    _main;  .scl    2;  .type   32; .endef
_main:
    pushl   %ebp
    movl    %esp, %ebp
    subl    $8, %esp
    andl    $-16, %esp
    movl    <*>, %eax
    addl    $15, %eax
    addl    $15, %eax
    shrl    $4, %eax
    sall    $4, %eax
    movl    %eax, -4(%ebp)
    movl    -4(%ebp), %eax
    call    __alloca
    call    ___main
    leave
    ret

Le agradecería que un compilador / gurú de ensamblaje me ayudara a comenzar explicando la pila, el registro y las inicializaciones de la sección. No puedo hacer la cabeza o la cola fuera del código.

EDITAR: Estoy usando gcc 3.4.5. y el argumento de la línea de comando es gcc -S test1.c

Gracias, kunjaan.

¿Fue útil?

Solución

Debería prologar todos mis comentarios diciendo que todavía estoy aprendiendo de manera ensamblada.

Ignoraré la inicialización de la sección. Una explicación para la inicialización de la sección y básicamente todo lo que cubro se puede encontrar aquí: http://en.wikibooks.org/wiki/X86_Assembly/GAS_Syntax

El registro ebp es el marco de pila puntero base, de ahí el BP. Almacena un puntero al principio de la pila actual.

El registro esp es el puntero de pila. Mantiene la ubicación de memoria de la parte superior de la pila. Cada vez que empujamos algo en la pila, se actualiza esp para que siempre apunte a una dirección en la parte superior de la pila.

Así que ebp apunta a la base y esp apunta a la parte superior. Así que la pila se ve como:

esp -----> 000a3   fa
           000a4   21
           000a5   66
           000a6   23
esb -----> 000a7   54

Si presionas e4 en la pila, esto es lo que sucede:

esp -----> 000a2   e4
           000a3   fa
           000a4   21
           000a5   66
           000a6   23
esb -----> 000a7   54

Note que la pila crece hacia direcciones más bajas, este hecho será importante a continuación.

Los dos primeros pasos se conocen como el procedimiento de prólogo o, más comúnmente, el function prolog , Preparan la pila para su uso por variables locales. Consulte la cita del prólogo del procedimiento en la parte inferior.

En el paso 1 guardamos el puntero en el marco de pila antiguo en la pila llamando al pushl% ebp. Como main es la primera función llamada, no tengo idea de cuál es el valor anterior de% ebp points también.

Paso 2, estamos ingresando un nuevo marco de pila porque estamos ingresando una nueva función (principal). Por lo tanto, debemos establecer un nuevo puntero base de marco de pila. Usamos el valor en esp para ser el comienzo de nuestro marco de pila.

Paso 3. Asigna 8 bytes de espacio en la pila. Como mencionamos anteriormente, la pila crece hacia direcciones más bajas, por lo tanto, al restar en 8, mueve la parte superior de la pila en 8 bytes.

Paso 4; Alinea la pila, he encontrado diferentes opiniones sobre esto. No estoy realmente seguro de qué es exactamente lo que se hace. Sospecho que se hace para permitir que las instrucciones grandes (SIMD) se asignen en la pila,

http://gcc.gnu.org/ml/gcc /2008-01/msg00282.html

  

Este código "y ESP" con 0xFFFF0000,   alineando la pila con la siguiente   límite más bajo de 16 bytes. Un   examen del código fuente de Mingw   revela que esto puede ser para SIMD   instrucciones que aparecen en el " _main "   rutina, que operan solo en alineados   direcciones Ya que nuestra rutina no   contienen instrucciones SIMD, esta linea   es innecesario.

http://en.wikibooks.org/wiki/X86_Assembly/GAS_Syntax

Los pasos 5 a 11 parecen no tener ningún propósito para mí. No pude encontrar ninguna explicación en google. ¿Podría alguien que realmente sabe esto proporcionar una comprensión más profunda? He escuchado rumores de que esto se usa para el manejo de excepciones de C.

El paso 5, almacena el valor de retorno del 0 principal, en eax.

Los pasos 6 y 7 añadimos 15 en hexadecimal a eax por razones desconocidas. eax = 01111 + 01111 = 11110

Paso 8 cambiamos los bits de eax 4 bits a la derecha. eax = 00001 porque los últimos bits se desplazan fuera del final 00001 | 111.

Paso 9 cambiamos los bits de eax 4 bits a la izquierda, eax = 10000.

Los pasos 10 y 11 mueven el valor de los primeros 4 bytes asignados en la pila a eax y luego lo mueven de eax back.

Los pasos 12 y 13 configuran la biblioteca c.

Hemos llegado al epilogo de funciones . Es decir, la parte de la función que devuelve los punteros de pila, esp y ebp al estado en que estaban antes de que se llamara esta función.

Paso 14, deja los conjuntos esp al valor de ebp, moviendo la parte superior de la pila a la dirección que era antes de que se llamara main. Luego, configura ebp para que apunte a la dirección que guardamos en la parte superior de la pila durante el paso 1.

La licencia solo se puede reemplazar con las siguientes instrucciones:

mov  %ebp, %esp
pop  %ebp

Paso 15, devuelve y sale de la función.

1.    pushl       %ebp
2.    movl        %esp, %ebp
3.    subl        $8, %esp
4.    andl        $-16, %esp
5.    movl        <*>, %eax
6.    addl        $15, %eax
7.    addl        $15, %eax
8.    shrl        $4, %eax
9.    sall        $4, %eax
10.   movl        %eax, -4(%ebp)
11.   movl        -4(%ebp), %eax
12.   call        __alloca
13.   call        ___main
14.   leave
15.   ret

Procedimiento Prolog:

  

Lo primero que tiene que hacer una función   Se llama el procedimiento de prólogo. Eso   Primero guarda el puntero base actual.   (ebp) con la instrucción pushl% ebp   (recuerda que ebp es el registro usado para   acceder a los parámetros de la función y   variables locales). Ahora copia la   apilar el puntero (esp) a la base   puntero (ebp) con la instrucción   movl% esp,% ebp. Esto te permite   acceder a los parámetros de la función como   Índices desde el puntero base. Local   Las variables son siempre una resta.   desde ebp, como -4 (% ebp) o   (% ebp) -4 para la primera variable local,   el valor de retorno es siempre en 4 (% ebp)   o (% ebp) +4, cada parámetro o   el argumento está en N * 4 + 4 (% ebp) tal como   8 (% ebp) para el primer argumento mientras   el antiguo ebp está en (% ebp).

http://www.milw0rm.com/papers/52

Existe un gran hilo de desbordamiento de pila que responde a gran parte de esta pregunta. ¿Por qué hay instrucciones adicionales en mi salida de gcc?

Una buena referencia sobre las instrucciones del código de máquina x86 se puede encontrar aquí: http://programminggroundup.blogspot.com/2007/ 01 / appendix-b-common-x86-instructions.html

Esta es una conferencia que contiene algunas de las ideas utilizadas a continuación: http://csc.colstate.edu/bosworth/cpsc555/jpg MySlides / CPSC5155_L23.htm

Aquí hay otra opinión sobre cómo responder a tu pregunta: http://www.phiral.net/linuxasmone.htm

Ninguna de estas fuentes lo explica todo.

Otros consejos

Aquí hay un buen desglose paso a paso de una función simple main () compilada por GCC, con mucha información detallada: Sintaxis de GAS (Wikipedia)

Para el código que pegó, las instrucciones se desglosan de la siguiente manera:

  • Primeras cuatro instrucciones (pushl through andl): configura un nuevo marco de pila
  • Próximas cinco instrucciones (pasar de una a otra): generar un valor extraño para eax, que se convertirá en el valor de retorno (no tengo idea de cómo decidió hacerlo)
  • Próximas dos instrucciones (ambas movl): almacene el valor de retorno calculado en una variable temporal en la pila
  • Próximas dos instrucciones (ambas llamadas): invocar las funciones de inicio de la biblioteca C
  • instrucción leave : arranca el marco de la pila
  • instrucción ret : regresa a la persona que llama (la función de tiempo de ejecución externa, o quizás la función del núcleo que invocó su programa)

Bueno, no sé mucho sobre GAS, y estoy un poco oxidado con el ensamblaje de Intel, pero parece que se está inicializando el marco de la pila principal.

si echas un vistazo, __main es una especie de macro, debe estar ejecutando inicializaciones. Luego, como el cuerpo de main está vacío, llama a abandonar la instrucción, para volver a la función que llamó main.

De http: //en.wikibooks .org / wiki / X86_Assembly / GAS_Syntax # .22hello.s.22_line-by-line :

Esta línea declara el " _main " etiqueta, marcando el lugar que se llama desde el código de inicio.

    pushl   %ebp
    movl    %esp, %ebp
    subl    $8, %esp

Estas líneas guardan el valor de EBP en la pila, luego mueven el valor de ESP a EBP, luego restan 8 de ESP. El " l " El final de cada código de operación indica que queremos usar la versión del código de operación que funciona con " long " (32 bits) operandos;

    andl    $-16, %esp

Este código " y el ESP con 0xFFFF0000, alineando la pila con el siguiente límite más bajo de 16 bytes. (necesario cuando se usan instrucciones simd, no es útil aquí)

    movl    
    call    __alloca
    call    ___main
, %eax movl %eax, -4(%ebp) movl -4(%ebp), %eax

Este código mueve cero a EAX, luego mueve EAX a la ubicación de memoria EBP-4, que se encuentra en el espacio temporal que reservamos en la pila al comienzo del procedimiento. Luego mueve la ubicación de la memoria EBP-4 de nuevo a EAX; claramente, este no es un código optimizado.

<*>

Estas funciones son parte de la configuración de la biblioteca C. Ya que estamos llamando a las funciones en la biblioteca C, probablemente las necesitemos. Las operaciones exactas que realizan varían según la plataforma y la versión de las herramientas GNU que están instaladas.

Aquí hay un enlace útil.

http://unixwiz.net/techtips/win32-callconv-asm.html

Realmente ayudaría saber qué versión de gcc está utilizando y qué libc. Parece que tienes una versión gcc muy antigua o una plataforma extraña o ambas. Lo que está pasando es algo extraño con las convenciones de convocatoria. Te puedo decir algunas cosas:

Guarde el puntero del marco en la pila de acuerdo con la convención:

pushl       %ebp
movl        %esp, %ebp

Haga espacio para las cosas en el extremo antiguo del marco, y redondee el puntero de la pila hasta un múltiplo de 4 (por qué es necesario, no lo sé):

subl        $8, %esp
andl        $-16, %esp

A través de una loca canción y baile, prepárate para regresar 1 desde main :

movl        
call        __alloca
, %eax addl $15, %eax addl $15, %eax shrl $4, %eax sall $4, %eax movl %eax, -4(%ebp) movl -4(%ebp), %eax

Recupere cualquier memoria asignada con alloca (GNU-ism):

call        ___main

Anuncie a libc que main está saliendo (más GNU-ism):

leave

Restaurar los punteros de marco y pila:

ret

Volver:

        .file   "main.c"
        .text
        .p2align 4,,15
.globl main
        .type   main, @function
main:
        leal    4(%esp), %ecx
        andl    $-16, %esp
        pushl   -4(%ecx)
        pushl   %ebp
        movl    %esp, %ebp
        pushl   %ecx
        popl    %ecx
        popl    %ebp
        leal    -4(%ecx), %esp
        ret
        .size   main, .-main
        .ident  "GCC: (Debian 4.3.2-1.1) 4.3.2"
        .section        .note.GNU-stack,"",@progbits

Esto es lo que sucede cuando compilo el mismo código fuente con gcc 4.3 en Debian Linux:

        .file   "main.c"

Y lo desgloso de esta manera:

Dígale al depurador y otras herramientas el archivo fuente:

        .text

El código va en la sección de texto:

        .p2align 4,,15

Me supera:

.globl main
        .type   main, @function

main es una función exportada:

main:
Punto de entrada de

main :

        leal    4(%esp), %ecx
        andl    $-16, %esp
        pushl   -4(%ecx)

Agarre la dirección de retorno, alinee la pila en una dirección de 4 bytes y guarde la dirección de retorno nuevamente (por qué no puedo decir):

        pushl   %ebp
        movl    %esp, %ebp

Guarde el puntero del cuadro usando la convención estándar:

        pushl   %ecx
        popl    %ecx

Locura inescrutable:

        popl    %ebp
        leal    -4(%ecx), %esp

Restaure el puntero del marco y el puntero de la pila:

        ret

Volver:

        .size   main, .-main
        .ident  "GCC: (Debian 4.3.2-1.1) 4.3.2"
        .section        .note.GNU-stack,"",@progbits

¿Más información para el depurador ?:

int f(void) {
  return 17;
}

Por cierto, main es especial y mágico; cuando compilo

        .file   "f.c"
        .text
        .p2align 4,,15
.globl f
        .type   f, @function
f:
        pushl   %ebp
        movl    $17, %eax
        movl    %esp, %ebp
        popl    %ebp
        ret
        .size   f, .-f
        .ident  "GCC: (Debian 4.3.2-1.1) 4.3.2"
        .section        .note.GNU-stack,"",@progbits

Obtengo algo un poco más sano:

<*>

Todavía hay un montón de elementos decorativos, y aún estamos guardando el puntero del marco, moviéndolo y restaurándolo, lo cual es completamente inútil, pero el resto del código tiene sentido.

Parece que GCC está actuando como si estuviera bien editar main () para incluir el código de inicialización CRT. Acabo de confirmar que obtengo el mismo listado de ensamblaje de MinGW GCC 3.4.5 aquí, con su texto de origen.

La línea de comando que utilicé es:

gcc -S emptymain.c

Curiosamente, si cambio el nombre de la función a qqq () en lugar de main () , obtengo el siguiente ensamblaje:

        .file   "emptymain.c"
        .text
.globl _qqq
        .def    _qqq;      .scl    2;      .type   32;     .endef
_qqq:
        pushl   %ebp
        movl    %esp, %ebp
        popl    %ebp
        ret

que tiene mucho más sentido para una función vacía sin optimizaciones activadas.

Licenciado bajo: CC-BY-SA con atribución
No afiliado a StackOverflow
scroll top