asm - use assembler in c
¿Por qué GCC pad funciona con NOPs? (3)
He estado trabajando con C por un tiempo breve y recientemente comencé a ingresar en ASM. Cuando compilo un programa:
int main(void)
{
int a = 0;
a += 1;
return 0;
}
El desmontaje objdump tiene el código, pero nops después del ret:
...
08048394 <main>:
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 hastan
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 se especifica 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 compila utilizando gcc 4.4.5 con la configuración predeterminada, obtengo:
align.o: file format elf64-x86-64
Disassembly of section .text:
0000000000000000 <f>:
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 <g>:
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 <f>:
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 <g>
d: 90 nop
e: 90 nop
f: 90 nop
0000000000000010 <g>:
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.