Почему функция GCC работает с NOP?

Я работал с C на короткое время и совсем недавно начал заниматься ASM. Когда я скомпилирую программу:

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

Демонстрация objdump имеет код, но nops после 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
...

Из того, что я узнал, nops ничего не делают, и поскольку после ret даже не будет выполнен.

Мой вопрос: зачем беспокоиться? Не может ли ELF (linux-x86) работать с секцией .text(+ main) любого размера?

Буду признателен за любую помощь, просто пытаясь научиться.

Ответ 1

Прежде всего, gcc не всегда это делает. Заполнение контролируется -falign-functions, которое автоматически включается -O2 и -O3:

-falign-functions
-falign-functions=n

Совместите начало функций со следующей мощностью-два больше, чем n, пропустив до n байт. Например, -falign-functions=32 выравнивает функции к следующей 32-байтовой границе, но -falign-functions=24 будет выравниваться только с следующей 32-байтовой границей если это можно сделать, пропустив 23 байта или меньше.

-fno-align-functions и -falign-functions=1 эквивалентны и означают, что функции не будут выровнены.

Некоторые ассемблеры поддерживают этот флаг только тогда, когда n является степенью двух; в в этом случае он округляется.

Если n не указано или равно нулю, используйте зависящее от машины значение по умолчанию.

Включено на уровнях -O2, -O3.

Для этого может быть несколько причин, но главная из них на x86, вероятно, такова:

Большинство процессоров извлекают инструкции в выровненных 16-байтных или 32-байтных блоках. Может быть выгодно выровнять записи критического цикла и подпрограммы на 16, чтобы минимизировать число 16-байтных границ в коде. В качестве альтернативы, убедитесь, что в первых нескольких инструкциях после 16-байтовой границы нет 16-байтовой границы после записи критического цикла или подпрограммы.

(Цитата из раздела "Оптимизация подпрограмм в сборке" язык "от Agner Fog.)

edit: Вот пример, демонстрирующий дополнение:

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

При компиляции с использованием gcc 4.4.5 с настройками по умолчанию я получаю:

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   

Задание -falign-functions дает:

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   

Ответ 2

Это делается для выравнивания следующей функции с помощью 8, 16 или 32-байтовой границы.

В разделе "Оптимизация подпрограмм на языке ассемблера" A.Fog:

11.5 Выравнивание кода

Большинство микропроцессоров извлекают код в выровненных 16-байтных или 32-байтных блоках. Если запись importantubroutine или метка перехода находится ближе к концу 16-байтового блока, тогда на микропроцессор будет извлекаться только несколько полезных байтов кода при извлечении этого блока кода. Itmay должен получить следующие 16 байт, прежде чем он сможет декодировать первые инструкции после метки. Этого можно избежать, выровняв важные записи подпрограмм и записи циклов на 16.

[...]

Выравнивание записи подпрограммы так же просто, как и установка NOP  по мере необходимости, перед вводом подпрограммы, чтобы сделать адрес, делящийся на 8, 16, 32 или 64, по желанию.

Ответ 3

Насколько я помню, инструкции конвейерны в процессоре, а разные блоки процессора (загрузчик, декодер и т.д.) обрабатывают последующие инструкции. Когда выполняются инструкции RET, несколько инструкций уже загружаются в конвейер cpu. Это предположение, но вы можете начать копать здесь, и если вы узнаете (возможно, определенное количество NOP, которое безопасно, поделитесь своими результатами, пожалуйста.