¿Por qué GCC pad funciona con NOPs?

He estado trabajando con C por un tiempo breve y recientemente comencé a ingresar en ASM. Cuando compilo un progtwig:

int main(void) { int a = 0; a += 1; return 0; } 

El desassembly objdump tiene el código, pero nops después del ret:

 ... 08048394 : 8048394: 55 push %ebp 8048395: 89 e5 mov %esp,%ebp 8048397: 83 ec 10 sub $0x10,%esp 804839a: c7 45 fc 00 00 00 00 movl $0x0,-0x4(%ebp) 80483a1: 83 45 fc 01 addl $0x1,-0x4(%ebp) 80483a5: b8 00 00 00 00 mov $0x0,%eax 80483aa: c9 leave 80483ab: c3 ret 80483ac: 90 nop 80483ad: 90 nop 80483ae: 90 nop 80483af: 90 nop ... 

Por lo que aprendí, nops no hace nada, y desde luego, ret incluso no se ejecutará.

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.

En primer lugar, gcc no siempre hace esto. El relleno está controlado por -falign-functions , que es activado automáticamente por -O2 y -O2 :

-falign-functions
-falign-functions=n

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

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

Algunos ensambladores solo admiten esta bandera cuando n es un poder de dos; en ese caso, se redondea hacia arriba.

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

Habilitado en 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 bytes o 32 bytes. Puede ser ventajoso alinear entradas de bucle críticas y 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 bucle crítico 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:

 // align.c int f(void) { return 0; } int g(void) { return 0; } 

Cuando se comstack utilizando gcc 4.4.5 con la configuración predeterminada, obtengo:

 align.o: file format elf64-x86-64 Disassembly of section .text: 0000000000000000 : 0: 55 push %rbp 1: 48 89 e5 mov %rsp,%rbp 4: b8 00 00 00 00 mov $0x0,%eax 9: c9 leaveq a: c3 retq 000000000000000b : b: 55 push %rbp c: 48 89 e5 mov %rsp,%rbp f: b8 00 00 00 00 mov $0x0,%eax 14: c9 leaveq 15: c3 retq 

Especificar -falign-functions da:

 align.o: file format elf64-x86-64 Disassembly of section .text: 0000000000000000 : 0: 55 push %rbp 1: 48 89 e5 mov %rsp,%rbp 4: b8 00 00 00 00 mov $0x0,%eax 9: c9 leaveq a: c3 retq b: eb 03 jmp 10  d: 90 nop e: 90 nop f: 90 nop 0000000000000010 : 10: 55 push %rbp 11: 48 89 e5 mov %rsp,%rbp 14: b8 00 00 00 00 mov $0x0,%eax 19: c9 leaveq 1a: c3 retq 

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

Desde “Optimización de subrutinas en lenguaje ensamblador” por A.Fog:

11.5 Alineación del código

La mayoría de los microprocesadores recuperan el código en bloques alineados de 16 bytes o de 32 bytes. Si una entrada de importación o una etiqueta de salto pasa a estar cerca del final de un bloque de 16 bytes, entonces su microprocesador solo obtendrá unos pocos bytes útiles de código cuando busque ese bloque de código. Puede que también tenga que buscar los siguientes 16 bytes antes de que pueda decodificar las primeras instrucciones después de la etiqueta. Esto se puede evitar alineando las entradas de subrutinas importantes y las entradas de bucle por 16.

[…]

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

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