¿Por qué la almohadilla GCC funciona con NOP?

81

He estado trabajando con C por un tiempo y recientemente comencé a entrar en ASM. Cuando compilo un programa:

El desmontaje de objdump tiene el código, pero nops después de ret:

Por lo que aprendí, los nops no hacen nada, y dado que después de ret ni siquiera serían ejecutados.

Mi pregunta es: ¿por qué molestarse? ¿No podría ELF (linux-x86) funcionar con una sección .text (+ main) de cualquier tamaño?

Agradecería cualquier ayuda, solo tratando de aprender.

olly
fuente
¿Siguen esos NOP? Si se detienen en 80483af, entonces tal vez sea un relleno para alinear la siguiente función a 8 o 16 bytes.
Mysticial
no después de los 4 nops va directo a una función: __libc_csu_fini
olly
1
Si los NOP fueron insertados por gcc, entonces no creo que use solo 0x90 ya que hay muchos NOP con una variable de tamaño de 1-9 bytes (10 si usa sintaxis de gas )
phuclv

Respuestas:

89

En primer lugar, gccno siempre hace esto. El relleno está controlado por -falign-functions, que se activa automáticamente por -O2y -O3:

-falign-functions
-falign-functions=n

Alinee el inicio de funciones a la siguiente potencia de dos mayor que n, saltando hasta nbytes. Por ejemplo, -falign-functions=32alinea funciones con el siguiente límite de 32 bytes, pero -falign-functions=24se alinearía con el siguiente límite de 32 bytes solo si esto se puede hacer omitiendo 23 bytes o menos.

-fno-align-functionsy -falign-functions=1son equivalentes y significan que las funciones no estarán alineadas.

Algunos ensambladores solo admiten esta bandera cuando n es una potencia de dos; en ese caso, se redondea.

Si no se especifica n o es cero, utilice un valor predeterminado dependiente de la máquina.

Habilitado en los niveles -O2, -O3.

Puede haber varias razones para hacer esto, pero la principal en x86 es probablemente esta:

La mayoría de los procesadores obtienen instrucciones en bloques alineados de 16 o 32 bytes. Puede ser ventajoso alinear las entradas de bucle críticas y las entradas de subrutina en 16 para minimizar el número de límites de 16 bytes en el código. Alternativamente, asegúrese de que no haya un límite de 16 bytes en las primeras instrucciones después de una entrada de ciclo crítica o una entrada de subrutina.

(Citado de "Optimización de subrutinas en lenguaje ensamblador" por Agner Fog.)

editar: Aquí hay un ejemplo que demuestra el relleno:

Cuando se compila usando gcc 4.4.5 con la configuración predeterminada, obtengo:

Especificar -falign-functionsda:

NPE
fuente
1
No utilicé ningún indicador -O, simplemente "gcc -o test test.c".
olly
1
@olly: Lo probé con gcc 4.4.5 en Ubuntu de 64 bits y en mis pruebas no hay relleno por defecto, y hay relleno con -falign-functions.
NPE
@aix: estoy en centOS 6.0 (32 bits) y sin ningún indicador tengo el relleno. ¿Alguien quiere que descargue mi salida completa "objdump -j .text -d ./test"?
olly
1
En más pruebas, cuando lo compilo como un objeto: "gcc -c test.c". No hay relleno, pero cuando enlace: "gcc -o test test.o" aparece.
olly
2
@olly: Ese relleno lo inserta el vinculador, para satisfacer los requisitos de alineación de la función que sigue mainen el ejecutable (en mi caso, esa función es __libc_csu_fini).
NPE
15

Esto se hace para alinear la siguiente función con un límite de 8, 16 o 32 bytes.

De "Optimización de subrutinas en lenguaje ensamblador" por A.Fog:

11.5 Alineación de código

La mayoría de los microprocesadores obtienen el código en bloques alineados de 16 o 32 bytes. Si una entrada de subrutina importante o una etiqueta de salto se encuentran cerca del final de un bloque de 16 bytes, entonces el microprocesador solo obtendrá unos pocos bytes de código útiles cuando obtenga ese bloque de código. También puede tener que buscar los siguientes 16 bytes antes de poder decodificar las primeras instrucciones después de la etiqueta. Esto se puede evitar alineando las entradas de subrutinas importantes y las entradas de bucle en 16.

[...]

Alinear una entrada de subrutina es tan simple como colocar tantos NOP como sea necesario antes de la entrada de subrutina para hacer que la dirección sea divisible por 8, 16, 32 o 64, según se desee.

hámstergene
fuente
Es la diferencia entre 25-29 bytes (para main), ¿estás hablando de algo mayor? Al igual que la sección de texto, a través de readelf encontré que era de 364 bytes. También noté 14 nops en _start. ¿Por qué "como" no hace estas cosas? Soy un novato, disculpas.
olly
@olly: He visto sistemas de desarrollo que realizan la optimización de todo el programa en código de máquina compilado. Si la dirección de la función fooes 0x1234, entonces el código que usa esa dirección muy cerca de un 0x1234 literal podría terminar generando un código de máquina como el mov ax,0x1234 / push ax / mov ax,0x1234 / push axque el optimizador podría reemplazar mov ax,0x1234 / push ax / push ax. Tenga en cuenta que las funciones no deben reubicarse después de dicha optimización, por lo que la eliminación de instrucciones mejoraría la velocidad de ejecución, pero no el tamaño del código.
supercat
5

Por lo que recuerdo, las instrucciones se canalizan en la CPU y diferentes bloques de la CPU (cargador, decodificador y demás) procesan las instrucciones posteriores. Cuando RETse están ejecutando instrucciones, algunas de las siguientes instrucciones ya están cargadas en la canalización de la CPU. Es una suposición, pero puede comenzar a investigar aquí y si lo descubre (tal vez el número específico de correos NOPelectrónicos que son seguros, comparta sus hallazgos, por favor.

mco
fuente
@ninjalj: ¿Eh? Esta pregunta se refiere a x86, que está canalizado (como dijo mco). Muchos procesadores x86 modernos también ejecutan de forma especulativa instrucciones que "no deberían" ejecutarse, tal vez incluyendo estos nops. ¿Quizás quisiste comentar en otra parte?
David Cary
3
@DavidCary: en x86, eso es totalmente transparente para el programador. Las instrucciones ejecutadas especulativamente mal adivinadas simplemente tienen sus resultados y efectos descartados. En MIPS, no hay ninguna parte "especulativa", la instrucción en una ranura de retardo de bifurcación siempre se ejecuta y el programador tiene que llenar las ranuras de retardo (o dejar que el ensamblador lo haga, lo que probablemente resultaría en nops).
ninjalj
@ninjalj: Sí, el efecto de operaciones ejecutadas especulativamente erróneamente e instrucciones no alineadas es transparente, en el sentido de que no tienen ningún efecto sobre los valores de los datos de salida. Sin embargo, ambos tienen un efecto en la sincronización del programa, lo que puede ser la razón por la que gcc agrega nops al código x86, que es lo que se hacía en la pregunta original.
David Cary
1
@DavidCary: si ese fuera el motivo, solo lo verías después de saltos condicionales, no después de un incondicional ret.
ninjalj
1
Ésta no es la razón. La predicción de reserva de un salto indirecto (en un error de BTB) es la siguiente instrucción, pero si eso no es una instrucción basura, la optimización recomendada para detener la especulación errónea es una instrucción como ud2o int3que siempre falla, por lo que el front-end sabe que debe detener la decodificación en su lugar de alimentar una divcarga de TLB potencialmente cara o falsa en la tubería, por ejemplo. Esto no es necesario después de un tailcall retdirecto o jmpal final de una función.
Peter Cordes