Почему панель GCC работает с NOP?


81

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

В дизассемблере objdump есть код, но после ret нет:

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

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

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


Продолжаются ли эти NOP? Если они останавливаются на 80483af, возможно, это заполнение, чтобы выровнять следующую функцию до 8 или 16 байтов.
Mysticial

нет, после 4-х нопок он переходит прямо в функцию: __libc_csu_fini
olly

1
Если бы NOP были вставлены gcc, я не думаю, что он будет использовать только 0x90, так как существует много NOP с переменной размером от 1 до 9 байтов (10, если используется синтаксис газа )
phuclv

Ответы:


89

Во-первых, так 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-байтовой границы в первых нескольких инструкциях после записи критического цикла или записи подпрограммы.

(Цитата из «Оптимизация подпрограмм на ассемблере» Агнера Фога.)

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

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

Уточнение -falign-functionsдает:


1
Я не использовал никаких флагов -O, просто "gcc -o test test.c".
olly

1
@olly: Я тестировал его с помощью gcc 4.4.5 на 64-битной Ubuntu, и в моих тестах нет заполнения по умолчанию, и есть заполнение с -falign-functions.
NPE

@aix: я использую CentOS 6.0 (32-разрядную версию) и без каких-либо флагов есть отступы. Кто-нибудь хочет, чтобы я сбросил полный вывод "objdump -j .text -d ./test"?
olly

1
При дальнейшем тестировании, когда я компилирую его как объект: "gcc -c test.c". Поля нет, но когда я связываю: «gcc -o test test.o», оно появляется.
olly

2
@olly: этот отступ вставляется компоновщиком, чтобы удовлетворить требованиям выравнивания функции, которая следует mainв исполняемом файле (в моем случае это функция __libc_csu_fini).
NPE

15

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

Из «Оптимизация подпрограмм на ассемблере» А.Фога:

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

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

[...]

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


Это разница между 25-29 байтами (для основных), вы о чем-то большем? Как и текстовый раздел, через readelf я обнаружил, что это 364 байта? Еще я заметил 14 nops на _start. Почему это не делает «as»? Я новичок, извиняюсь.
olly

@olly: Я видел системы разработки, которые выполняют оптимизацию всей программы на скомпилированном машинном коде. Если адрес функции foo0x1234, то код, который использует этот адрес в непосредственной близости от литерала 0x1234, может в конечном итоге создать машинный код, mov ax,0x1234 / push ax / mov ax,0x1234 / push axкоторый оптимизатор затем может заменить на mov ax,0x1234 / push ax / push ax. Обратите внимание, что функции не должны перемещаться после такой оптимизации, поэтому удаление инструкций улучшит скорость выполнения, но не размер кода.
supercat

5

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


@ninjalj: А? Этот вопрос касается x86, который является конвейерным (как сказал mco). Многие современные процессоры x86 также предположительно выполняют инструкции, которые «не должны» выполняться, возможно, включая эти nops. Возможно, вы хотели оставить комментарий в другом месте?
Дэвид Кэри

3
@DavidCary: в x86 это полностью прозрачно для программиста. Ошибочно угаданные, предположительно выполненные инструкции просто отбрасывают их результаты и эффекты. В MIPS вообще нет «спекулятивной» части, инструкция в слоте задержки перехода всегда выполняется, и программист должен заполнить слоты задержки (или позволить ассемблеру сделать это, что, вероятно, приведет к nops).
ninjalj

@ninjalj: Да, эффект от ошибочно предполагаемых, предположительно выполненных операций и невыровненных инструкций прозрачен в том смысле, что они не влияют на значения выходных данных. Однако они оба влияют на время выполнения программы, что может быть причиной того, что gcc добавляет nops в код x86, что и было задано в исходном вопросе.
Дэвид Кэри

1
@DavidCary: если бы это было причиной, вы бы увидели его только после условных переходов, а не после безусловного ret.
ninjalj 07

1
Не поэтому. Прогнозирование отката косвенного перехода (при промахе BTB) - это следующая инструкция, но если это мусор, не связанный с инструкциями, рекомендуемая оптимизация для предотвращения неправильных предположений - это инструкции типа ud2или, int3которые всегда дают сбой, поэтому интерфейсная часть знает, что вместо этого нужно остановить декодирование например, подачи потенциально дорогостоящей divили ложной загрузки TLB-промаха в конвейер. Это не нужно после retили прямого jmpхвостового вызова в конце функции.
Питер Кордес
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.