Чому панель GCC працює з NOP?


81

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

Розбирання objdump має код, але nops після ret:

З того, що я дізнався, nops нічого не робить, а оскільки після ret навіть не буде страчено.

Моє питання: навіщо турбуватися? Чи не може ELF (linux-x86) працювати з розділом .text (+ main) будь-якого розміру?

Буду вдячний за будь-яку допомогу, просто намагаючись навчитися.


Чи продовжують ці NOP? Якщо вони зупиняються на 80483af, то, можливо, це доповнення, щоб вирівняти наступну функцію до 8 або 16 байт.
Містичний

немає після того, як 4 NOP , вона йде по обидві сторони протоки до функції: __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-байтової межі.

(Цитується з "Оптимізація підпрограм мовою збірки" Агнера Фога.)

редагувати: Ось приклад, який демонструє відступ:

При компіляції за допомогою 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 сну на _start. Чому ці речі не роблять "як"? Я новачок, перепрошую.
olly

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

5

Наскільки я пам’ятаю, інструкції складаються в процесор, а різні блоки процесора (завантажувач, декодер тощо) обробляють подальші інструкції. Коли RETінструкції виконуються, у конвеєр процесора вже завантажується декілька наступних інструкцій. Це припущення, але ви можете почати копати тут, і якщо ви дізнаєтесь (можливо, конкретну кількість NOPбезпечних слів, поділіться своїми результатами, будь ласка.


@ninjalj: Так? Це питання задається щодо x86, який є конвеєрним (як сказав mco). Багато сучасних процесорів x86 також спекулятивно виконують інструкції, які "не слід" виконувати, можливо, включаючи ці nops. Можливо, ви мали намір коментувати деінде?
Девід Кері

3
@DavidCary: у x86 це повністю прозоро для програміста. Неправильно здогадані спекулятивно виконані інструкції просто відкидають результати та ефекти. На MIPS взагалі не існує "спекулятивної" частини, інструкція в слоті затримки гілки завжди виконується, і програміст повинен заповнити слоти затримки (або дозволити це зробити асемблеру, що, ймовірно, призведе до nops).
ninjalj

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

1
@DavidCary: якби це було причиною, ви побачили б це лише після умовних стрибків, а не після безумовних ret.
ninjalj

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