Поради щодо гольфу в машинному коді x86 / x64


27

Я помітив, що такого питання немає, тож ось:

Чи є у вас загальні поради щодо гольфу в машинному коді? Якщо порада стосується лише певного оточення або конвенції про виклики, вкажіть це у своїй відповіді.

Будь ласка, лише одна порада на відповідь (див. Тут ).

Відповіді:


11

mov-постійне дороге для констант

Це може бути очевидним, але я все-таки викладу це. Взагалі, це окупається, щоб подумати про розрядне представлення числа, коли потрібно ініціалізувати значення.

Ініціалізація eaxз 0:

b8 00 00 00 00          mov    $0x0,%eax

його слід скоротити (як для продуктивності, так і для розміру коду ) до

31 c0                   xor    %eax,%eax

Ініціалізація eaxз -1:

b8 ff ff ff ff          mov    $-1,%eax

можна скоротити до

31 c0                   xor    %eax,%eax
48                      dec    %eax

або

83 c8 ff                or     $-1,%eax

Або, загалом, будь-яке 8-бітове значення розширеного знаку може бути створене в 3 байти з push -12(2 байти) / pop %eax(1 байт). Це навіть працює для 64-розрядних регістрів без зайвих префіксів REX; push/ popза замовчуванням розмір операнда = 64.

6a f3                   pushq  $0xfffffffffffffff3
5d                      pop    %rbp

Або з урахуванням відомої константи в регістрі, ви можете створити іншу констант поблизу, використовуючи lea 123(%eax), %ecx(3 байти). Це зручно, якщо вам потрібен нульовий регістр та константа; xor-нуль (2 байти) + lea-disp8(3 байти).

31 c0                   xor    %eax,%eax
8d 48 0c                lea    0xc(%eax),%ecx

Див. Також Ефективно встановити всі біти в регістрі процесора на 1


Також для ініціалізації реєстру з невеликим (8-бітовим) значенням, відмінним від 0: використовуйте, наприклад, push 200; pop edx- 3 байти для ініціалізації.
anatolyg

2
BTW для ініціалізації регістра до -1, використовуйте dec, наприкладxor eax, eax; dec eax
anatolyg

@anatolyg: 200 - це поганий приклад, він не вписується у знаки, розширені-імун8. Але так, push imm8/ pop regце 3 байти, і це фантастично для 64-бітних констант на x86-64, де dec/ incє 2 байти. І push r64/ pop 64(2 байти) навіть може замінити 3 байти mov r64, r64(3 байти на REX). Дивіться також Встановлення всіх бітів у регістрі процесора на 1 ефективно для речей, таких як lea eax, [rcx-1]дане відоме значення в eax(наприклад, якщо потрібен нульовий регістр та інша константа, просто використовуйте LEA замість push / pop
Peter Cordes

10

У багатьох випадках інструкції на основі акумулятора (тобто ті, що приймаються (R|E)AXза операнд призначення) на 1 байт коротші, ніж інструкції загального випадку; перегляньте це питання в StackOverflow.


Зазвичай найбільш корисними є al, imm8спеціальні випадки, наприклад, or al, 0x20/ sub al, 'a'/ cmp al, 'z'-'a'/ ja .non_alphabeticмає по 2 байти замість 3. Використання alдля символьних даних також дозволяє lodsbта / або stosb. Або скористайтеся, alщоб перевірити щось про низький байт EAX, як lodsd/ test al, 1/ setnz clробить cl = 1 або 0 для непарних / парних. Але в рідкісному випадку, коли вам потрібен 32-бітний негайний, тоді впевнений op eax, imm32, як у моїй ключовій відповіді
Пітер Кордес,

8

Виберіть умову виклику, щоб розмістити аргументи там, де ви їх хочете.

Мовою вашої відповіді є ASM (фактично машинний код), тому трактуйте її як частину програми, написаної в ASM, а не C-comlated-for-x86. Ваша функція не повинна легко телефонувати з C за допомогою будь-якого стандартного режиму виклику. Це приємний бонус, якщо він не коштує вам зайвих байтів.

У чистій програмі asm для деяких допоміжних функцій нормально використовувати умовний виклик, який є зручним для них та для їх абонента. Такі функції документують свою угоду про виклики (входи / виходи / клобери) з коментарями.

У реальному житті навіть програми asm (я думаю) схильні використовувати послідовні умови викликів для більшості функцій (особливо для різних вихідних файлів), але будь-яка важлива функція може зробити щось особливе. У коді-гольф ви оптимізуєте лайно з однієї єдиної функції, тому очевидно, що це важливо / особливо.


Щоб перевірити свою функцію за допомогою програми C, ви можете написати обгортку, яка розміщує аргументи в потрібних місцях, зберігає / відновлює будь-які зайві регістри, які ви клобуть, і встановлює повернене значення, e/raxякщо його ще не було.


Межі розумного: все, що не накладає необґрунтованого тягаря на абонента:

  • ESP / RSP має зберігатися у викликах; інші цілі регістри - це чесна гра. (RBP і RBX, як правило, зберігаються у звичайних умовах, але ви можете пограбувати обом.)
  • Будь-який аргумент у будь-якому реєстрі (крім RSP) є розумним, але просити абонента скопіювати той самий аргумент у кілька регістрів - це не так.
  • Потрібно, щоб DF (прапор напряму рядка для lods/ stos/ тощо) був чітким (вгору) під час виклику / повернення нормально. Не дозволяти це визначитись під час виклику / повернення, було б нормально. Вимагаючи, щоб він був очищений або встановлений під час вступу, але потім залишати його модифікованим, коли ви повернетесь, було б дивним.

  • Повернення значень FP в x87 st0є розумним, але повернення st3зі сміттям в інший регістр x87 не є. Абонент повинен був би очистити стек x87. Навіть повернення в st0не порожні регістри вищих стеків також буде сумнівним (якщо ви не повертаєте кілька значень).

  • Ваша функція буде викликана call, [rsp]як і ваша зворотна адреса. Ви можете уникнути call/ retна x86, використовуючи регістр посилань, як lea rbx, [ret_addr]/ jmp functionі повертатися з jmp rbx, але це не "розумно". Це не настільки ефективно, як call / ret, тому ви не правдоподібно знайдете в реальному коді.
  • Зниження необмеженої пам’яті над RSP не є розумним, але клобірування ваших функцій аргументів на стеку дозволено в звичайних режимах викликів. x64 Windows вимагає 32 байтів тіньового простору над зворотною адресою, тоді як x86-64 System V дає вам 128-байтну червону зону нижче RSP, тому будь-який із них є розумним. (Або навіть набагато більшу червону зону, особливо в автономній програмі, а не функції.)

Прикордонні випадки: запишіть функцію, яка виробляє послідовність у масиві, задавши перші 2 елементи як аргументи функції . Я вирішив, щоб абонент зберігав початок послідовності в масив і просто передав вказівник на масив. Це, безумовно, зводить вимоги до питання. Я подумав взяти аргументи, запаковані xmm0для movlps [rdi], xmm0, що також було б дивною умовою закликання.


Повернути булеве значення у FLAGS (коди умов)

Системні виклики OS X роблять це ( CF=0означає відсутність помилок): Чи вважається поганою практикою використання регістра прапорів як булевого повернення? .

Будь-яка умова, яку можна перевірити за допомогою одного СКК, цілком розумна, особливо якщо ви можете обрати той, який має якесь семантичне значення для проблеми. (наприклад, функція порівняння може встановити прапори, тому jneвони будуть прийняті, якщо вони не були рівними).


Потрібні, щоб вузькі аргументи (наприклад, а char) були знаковими або нульовими, розширеними до 32 або 64 біт.

Це нерозумно; використання movzxабо movsx уникнення часткового реєстру сповільнення нормально в сучасних x86 asm. Насправді clang / LLVM вже робить код, який залежить від незадокументованого розширення до системи x86-64 System V виклику: аргументи, вужчі за 32 біти, є знаковими або нульовими, дозволеними абонентом до 32 біт .

Ви можете документувати / описати розширення до 64 біт, написавши uint64_tабо int64_tв своєму прототипі, якщо хочете. наприклад, ви можете використовувати loopінструкцію, яка використовує цілі 64 біти RCX, якщо ви не використовуєте префікс розміру адреси для зміни розміру до 32-бітного ECX (так, дійсно, розмір адреси не розмір операнду).

Зауважте, що longв 64-розрядному ABI для Windows та 32 x біт Linux x32 є лише 32-розрядний тип ; uint64_tє однозначним і коротшим, ніж тип unsigned long long.


Існуючі умови виклику:

  • 32-бітний Windows __fastcall, вже запропонований іншою відповіддю : цілі аргументи в ecxі edx.

  • x86-64 Система V : передає безліч аргументів у регістри та має безліч регістрів з клобуванням викликів, які можна використовувати без префіксів REX. Що ще важливіше, він був фактично обраний, щоб дозволити компіляторам memcpyвбудовувати чи запам'ятовувати так само rep movsbлегко: перші 6 цілочисельних / вказівних аргументів передаються в RDI, RSI, RDX, RCX, R8, R9.

    Якщо у вашій функції використовується цикл lodsd/ stosdвсередині циклу, який працює за rcxчасом (з loopінструкцією), ви можете сказати "дзвонити з C, як int foo(int *rdi, const int *rsi, int dummy, uint64_t len)і в системі V86 x86-64". приклад: хромакей .

  • 32-бітний GCC regparm: Цілі аргументи в EAX , ECX, EDX, повернення в EAX (або EDX: EAX). Наявність першого аргументу в тому ж регістрі, що і повернене значення, дозволяє здійснити деякі оптимізації, як , наприклад, з прикладом виклику та прототипом з атрибутом функції . І звичайно, AL / EAX є спеціальним для деяких інструкцій.

  • Linux X32 ABI використовує 32-бітні покажчики в тривалому режимі, тому ви можете зберегти префікс REX під час зміни вказівника ( приклад використання-випадку ). Ви все ще можете використовувати 64-розрядний розмір адреси, якщо у вас в регістрі 32-бітове негативне ціле число, розширене (так що це було б велике неподписане значення, якщо ви це зробили [rdi + rdx]).

    Зауважте, що push rsp/ pop raxє 2 байти, що еквівалентно mov rax,rsp, тому ви можете скопіювати повноцінні 64-бітні регістри в 2 байти.


Коли проблеми вимагають повернути масив, чи вважаєте ви повернення на стек розумним? Я думаю, що це робитимуть компілятори, повертаючи структуру за значенням.
qwr

@qwr: ні, конвенції основного виклику передають прихований покажчик на повернене значення. (Деякі конвенції передають / повертають невеликі структури в регістри). C / C ++, що повертається структура за значенням під кришкою , і дивіться кінець розділу Як працюють об'єкти в x86 на рівні складання? . Зауважте, що передаючі масиви (всередині структур) копіюють їх у стек для x86-64 SysV: Який тип даних C11 є масивом відповідно до AMD64 ABI , але Windows x64 передає покажчик non-const.
Пітер Кордес

так що ви думаєте про розумне чи ні? Чи вважаєте ви x86 за цим правилом codegolf.meta.stackexchange.com/a/8507/17360
qwr

1
@qwr: x86 не є "мовою на основі стека". x86 - це реєстр-машина з оперативною пам'яттю , а не стекова машина . Машина стека схожа на позначення зворотного шліфування, як регістри x87. fld / fld / faddp. x86 стек викликів не відповідає цій моделі: всі звичайні умови викликів залишають RSP немодифікованим або запускають аргументи ret 16; вони не спливають зворотну адресу, натискають масив, а потім push rcx/ ret. Абонент повинен був знати розмір масиву або врятував RSP десь поза стеком, щоб знайти себе.
Пітер Кордес

Виклик натисніть адресу інструкції після дзвінка в стеку jmp, щоб функція викликалася; ret спливає адресу зі стека та jmp на цю адресу
RosLuP

7

Використовуйте спеціальні кодування короткої форми для AL / AX / EAX та інших коротких форм та однобайтових інструкцій

Приклади передбачають 32/64-бітний режим, де розмір операнду за замовчуванням становить 32 біта. Префікс розміру операнда змінює інструкцію на AX замість EAX (або реверсу в 16-бітному режимі).

  • inc/decрегістр (крім 8-бітового): inc eax/ dec ebp. (Не x86-64: 0x4xбайти опкоду були переставлені у вигляді префіксів REX, тому inc r/m32єдине кодування.)

    8-бітний inc bl- 2 байти, використовуючи inc r/m8кодування opcode + ModR / M операнду . Тому використовуйте inc ebxприріст bl, якщо це безпечно. (наприклад, якщо результат ZF не потрібен у випадках, коли верхні байти можуть бути не нульовими).

  • scasd: e/rdi+=4, вимагає, щоб регістр вказував на читабельну пам'ять. Іноді корисно, навіть якщо вам не байдуже результат FLAGS (як cmp eax,[rdi]/ rdi+=4). І в 64-бітному режимі scasbможе працювати як 1-байтinc rdi , якщо lodsb або stosb не корисні.

  • xchg eax, r32: Це де 0x90 NOP прийшли: xchg eax,eax. Приклад: переупорядкуйте 3 регістри з двома xchgінструкціями в cdq/ idivциклі для GCD у 8 байт, де більшість інструкцій є однобайтовими, включаючи зловживання inc ecx/ loopзамість test ecx,ecx/jnz

  • cdq: знак-розширення EAX в EDX: EAX, тобто копіювання високого біта EAX на всі біти EDX. Щоб створити нуль з відомими негативними або отримати 0 / -1 для додавання / суб або маски за допомогою. x86 урок історії: cltqvs.movslq , а також мнемоніка AT&T vs. Intel для цього та пов'язаного з цим cdqe.

  • lodsb / d : як mov eax, [rsi]/ rsi += 4без прапорів, що клобують . (Припустимо, що DF зрозуміло, які стандартні умови викликів вимагають для введення функції.) Також stosb / d, іноді scas і рідше movs / cmps.

  • push/ pop reg. наприклад, у 64-бітному режимі push rsp/ pop rdiстановить 2 байти, але mov rdi, rspпотребує префікса REX і становить 3 байти.

xlatbіснує, але рідко корисний. Велика таблиця пошуку - чого уникати. Я також ніколи не знаходив застосування для AAA / DAA або інших пакунків із BCD або 2-ASCII-знаками.

1-байт lahf/ sahfрідко корисні. Ви могли б lahf / and ah, 1як альтернативу setc ah, але зазвичай це не корисно.

А для CF конкретно, там sbb eax,eaxможна отримати 0 / -1 або навіть бездокументований, але універсально підтримуваний 1-байт salc(встановити AL від Carry), що ефективно робить, sbb al,alне впливаючи на прапори. (Вилучено у x86-64). Я використовував SALC у виклику оцінок користувача №1: Dennis ♦ .

1-байтовий cmc/ clc/ stc(фліп ("доповнення"), очищення або встановлення CF) рідко корисний, хоча я знайшов застосування дляcmc додавання з розширеною точністю з базовим 10 ^ 9 шматками. Щоб беззастережно встановити / очистити CF, зазвичай домовляйтеся, щоб це відбулося як частина іншої інструкції, наприклад, xor eax,eaxочищає CF та EAX. Не існує жодних еквівалентних інструкцій для інших прапорів стану, лише DF (напрямок рядка) та IF (переривання). Прапор для перенесення спеціальний для багатьох інструкцій; зрушень встановити його, adc al, 0можна додати його до AL у 2-х байтах, і я згадував раніше недокументований SALC.

std/ cldрідко здається, варто . Особливо для 32-бітного коду, краще просто скористатися decпокажчиком та movоперандом джерела пам'яті або інструкцією ALU, а не встановити DF так lodsb/ stosbспуститися вниз, а не вгору. Зазвичай, якщо вам взагалі потрібно вниз, у вас все ще є інший вказівник, що піднімається вгору, тому для використання / для обох вам знадобиться більше одного stdі cldвсієї функції . Замість цього просто використовуйте строкові інструкції для напрямку вгору. (Стандартні умови виклику гарантують DF = 0 при введенні функції, тому ви можете вважати це безкоштовно, не використовуючи .)lodsstoscld


Історія 8086: чому ці кодування існують

В оригінальних 8086, AX було дуже особливим: інструкції подобаються lodsb/ stosb, cbw, mul/ divі інші використовують його неявно. Це все одно так; поточний x86 не скинув жодного з 8086 опкодів (принаймні, жодного з офіційно задокументованих). Але пізніше процесори додали нові вказівки, які давали кращі / ефективніші способи робити речі, не скопіюючи їх і не замінюючи їх в AX спочатку. (Або в EAX в 32-бітному режимі.)

наприклад, у 8086 бракувало пізніших доповнень, таких як movsx/ movzxдля завантаження або переміщення + розширення знаків, або 2 та 3-операнди imul cx, bx, 1234, які не дають результату з високою половиною та не мають явних операндів.

Крім того, основним вузьким місцем 8086 було отримання інструкцій, тому оптимізація розміру коду була важливою для тогочасної продуктивності . Дизайнер ISA 8086 (Стівен Морз) витратив чимало простору кодування коду на спеціальні випадки для AX / AL, включаючи спеціальні (E) AX / AL призначення призначення для всіх основних ALU-інструкцій негайних негайних програм, просто опкодування + негайне без байта ModR / M. 2-байт add/sub/and/or/xor/cmp/test/... AL,imm8або AX,imm16або (в 32-бітному режимі) EAX,imm32.

Але особливого випадку для цього немає EAX,imm8, тому звичайне кодування ModR / M add eax,4коротше.

Припущення полягає в тому, що якщо ви збираєтеся працювати над деякими даними, ви захочете це в AX / AL, тому ви можете замінити реєстр на AX, можливо, навіть частіше, ніж копіювати реєстр в AX за допомогою mov.

Все, що стосується кодування інструкцій 8086, підтримує цю парадигму - від таких інструкцій, як lodsb/wвсі кодування у спеціальному випадку для безпосередніх даних з EAX, до їх неявного використання навіть для множення / ділення.


Не захоплюйся; це не автоматично виграш, щоб поміняти все на EAX, особливо якщо вам потрібно використовувати безпосередні 32-бітні регістри замість 8-бітних. Або якщо вам потрібно переплести операції над декількома змінними в регістрах одночасно. Або якщо ви використовуєте інструкції з 2-ма регістрами, а не безпосередньо.

Але завжди майте на увазі: чи я роблю щось, що було б коротше в EAX / AL? Чи можу я переставити, щоб у мене це було в AL, чи я зараз краще переважаю, якщо я вже використовую його.

Вільно перемішуйте 8-бітні та 32-бітні операції, щоб скористатися ними, коли це безпечно робити (вам не потрібно проводити їх до повного реєстру чи будь-чого іншого).


cdqє корисним, для divякого потреби нульові edxу багатьох випадках.
qwr

1
@qwr: правильно, ви можете зловживати cdqраніше, ніж без підписання, divякщо знаєте, що ваш дивіденд нижче 2 ^ 31 (тобто невід'ємний, якщо трактуватись як підписаний), або якщо ви використовуєте його перед встановленням eaxпотенційно великого значення. Зазвичай (за межами коду-гольфу), який ви використовувалиcdqidivxor edx,edxdiv
Пітер Кордес,

5

Використовуйте fastcallконвенції

Платформа x86 має багато умовних вимог . Ви повинні використовувати ті, які передають параметри в регістри. На x86_64 перші кілька параметрів все-таки передаються в регістри, так що проблем там немає. На 32-бітних платформах звичайний режим виклику за замовчуванням ( cdecl) передає параметри в стеці, що не годиться для гольфу - доступ до параметрів на стеці вимагає довгих інструкцій.

При використанні fastcallна 32-бітних платформах два та перші параметри зазвичай передаються в ecxі edx. Якщо у вашої функції є 3 параметри, ви можете розглянути можливість її застосування на 64-бітній платформі.

Прототипи функцій C для fastcallконвенції (взяті з цього прикладу відповіді ):

extern int __fastcall SwapParity(int value);                 // MSVC
extern int __attribute__((fastcall)) SwapParity(int value);  // GNU   

Або використовуйте повністю користувальницьку конвенцію про дзвінки , оскільки ви пишете в чистому форматі asm, не обов'язково писати код, який потрібно викликати з C. Повернення булевих файлів у FLAGS часто зручно.
Пітер Кордес

5

Віднімаємо -128 замість додавання 128

0100 81C38000      ADD     BX,0080
0104 83EB80        SUB     BX,-80

Точно ж додайте -128 замість віднімайте 128


1
Зрозуміло, це також працює в іншому напрямку: додайте -128 замість суб 128. Факт забав: компілятори знають цю оптимізацію, а також роблять пов'язану оптимізацію перетворення < 128на <= 127зменшення масштабу безпосереднього операнда для cmp, або gcc завжди віддає перевагу перестановці порівнює для зменшення величини, навіть якщо це не -129 проти -128.
Пітер Кордес

4

Створіть 3 нулі за допомогою mul(тоді inc/ decщоб отримати +1 / -1, а також нуль)

Ви можете нуль eax та edx, помноживши на нуль у третьому регістрі.

xor   ebx, ebx      ; 2B  ebx = 0
mul   ebx           ; 2B  eax=edx = 0

inc   ebx           ; 1B  ebx=1

це призведе до того, що EAX, EDX та EBX будуть нульовими лише у чотирьох байтах. Ви можете занулювати EAX та EDX у трьох байтах:

xor eax, eax
cdq

Але з цієї відправної точки ви не можете отримати третій нульовий регістр в одному ще байті або реєстр +1 або -1 в інших 2 байтах. Натомість використовуйте техніку муль.

Приклад використання-випадок: об'єднання чисел Фібоначчі у двійкові .

Зауважте, що після LOOPзакінчення циклу ECX буде дорівнює нулю, і його можна використовувати для нуля EDX та EAX; не завжди потрібно створювати перший нуль за допомогою xor.


1
Це трохи заплутано. Чи можете ви розширитись?
NoOneIsHere

@NoOneIsHere Я вважаю, що він хоче встановити три регістри до 0, включаючи EAX та EDX.
NieDzejkob

4

Реєстри та прапорці процесора знаходяться у відомих стартапах

Можна припустити, що процесор знаходиться у відомому та задокументованому стані за замовчуванням на основі платформи та ОС.

Наприклад:

DOS http://www.fysnet.net/yourhelp.htm

Linux x86 ELF http://asm.sourceforge.net/articles/startup.html


1
Правила Code Golf кажуть, що ваш код повинен працювати принаймні однією реалізацією. Linux вибирає нуль всіх регістрів (крім RSP) і стека перед тим, як ввести процес свіжого простору користувача, навіть якщо документи i386 і x86-64 System V ABI кажуть, що вони "не визначені" при вході в _start. Так що так, це справедлива гра, щоб скористатися цим, якщо ви пишете програму замість функції. Я робив це в Екстремальних Фібоначчі . (В динамічно виконується файлі, ld.so біжить перед стрибком до вашого _start, і робить відпустку сміття в регістрах, а статичний тільки ваш код.)
Пітер Кордес

3

Щоб додати або відняти 1, використовуйте один байт incабо decвказівки, менші за багатобайтові вказівки додавання та підзарядки.


Зауважте, що в 32-бітному режимі є 1-байт inc/dec r32із номером регістра, закодованим у коді. Так inc ebxце 1 байт, але inc blце 2. Все-таки менше, ніж add bl, 1звичайно, для реєстрів, крім al. Також зауважте, що inc/ decзалиште CF без змін, але оновіть інші прапори.
Пітер Кордес

1
2 для +2 & -2 в x86
l4m2

3

lea з математики

Це, мабуть, одне з перших речей, про які дізнається x86, але я залишаю це як нагадування. leaможна використовувати для множення на 2, 3, 4, 5, 8 або 9 та додавання зміщення.

Наприклад, для обчислення ebx = 9*eax + 3в одній інструкції (в 32-бітному режимі):

8d 5c c0 03             lea    0x3(%eax,%eax,8),%ebx

Ось це без компенсації:

8d 1c c0                lea    (%eax,%eax,8),%ebx

Оце Так! Звичайно, leaможна також використовувати математику, як ebx = edx + 8*eax + 3для обчислення індексації масивів.


1
Можливо, варто згадати, що lea eax, [rcx + 13]це версія без префіксів для 64-бітного режиму. 32-розрядний розмір операнду (для результату) та розмір 64-бітного адреси (для входів).
Пітер Кордес

3

Інструкції циклу та рядка менші, ніж альтернативні послідовності інструкцій. Найбільш корисним є те, loop <label>що менше, ніж дві послідовності інструкцій dec ECXі jnz <label>, і lodsbменше, ніж mov al,[esi]і inc si.


2

mov невеликі негативні речовини в нижчі регістри, коли це застосовується

Якщо ви вже знаєте, що верхні біти регістру дорівнюють 0, ви можете скористатися коротшою інструкцією для негайного переміщення в нижні регістри.

b8 0a 00 00 00          mov    $0xa,%eax

проти

b0 0a                   mov    $0xa,%al

Використовуйте push/ popдля імунітету8 до нуля верхніх бітів

Заслуга Петру Кордесу. xor/ movстановить 4 байти, але push/ popлише 3!

6a 0a                   push   $0xa
58                      pop    %eax

mov al, 0xaдобре, якщо він вам не потрібен, нульовий розширений до повного рег. Але якщо ви робите, xor / mov - 4 байти проти 3 для push imm8 / pop або leaвід іншої відомої константи. Це може бути корисно в поєднанні з mulнульовими 3 регістрами в 4 байти , або cdq, якщо вам потрібно багато констант, хоча.
Пітер Кордес

Інший випадок використання буде для констант від [0x80..0xFF], які не є представницькими як ознака, розширена на імунітет8. Або якщо ви вже знаєте верхні байти, наприклад, mov cl, 0x10після loopінструкції, тому що єдиний спосіб loopне стрибати - це коли він зроблений rcx=0. (Я думаю, ви сказали це, але ваш приклад використовує xor). Ви навіть можете використовувати низький байт реєстру для чогось іншого, доки щось інше поверне його до нуля (або будь-якого іншого), коли ви закінчите. наприклад, моя програма «Фібоначчі» зберігає -1024в ebx і використовує bl.
Пітер Кордес

@PeterCordes Я додав вашу техніку push / pop
qwr

Мабуть, варто зайти в існуючу відповідь про константи, де анатоліг вже запропонував це у коментарі . Я відредагую цю відповідь. IMO, вам слід переробити цей варіант, щоб запропонувати використовувати 8-бітний розмір операнду для більшої кількості матеріалів (крім xchg eax, r32), наприклад mov bl, 10// dec bl/, jnzщоб ваш код не переймався високими байтами RBX.
Пітер Кордес

@PeterCordes хм. Я досі не впевнений у тому, коли слід використовувати 8-бітні операнди, тому я не впевнений, що в цьому відповісти.
qwr

2

У ПРАПОРИ встановлюються після багатьох інструкцій

Після багатьох арифметичних інструкцій прапор перенесення (без підпису) та прапор переповнення (підписаний) встановлюються автоматично ( детальніше ). Знаковий прапор та нульовий прапор встановлюються після багатьох арифметичних та логічних операцій. Це можна використовувати для умовного розгалуження.

Приклад:

d1 f8                   sar    %eax

ZF встановлюється цією інструкцією, тому ми можемо використовувати її для умовного розгалуження.


Коли ви коли-небудь використовували прапор паритету? Ви знаєте, що це горизонтальний xor з низьких 8 біт результату, правда? (Незалежно від розміру операнда, PF встановлюється лише з низьких 8 біт ; див. Також ). Не парне число / непарне число; для цього перевірити ZF після test al,1; ти зазвичай не отримуєш це безкоштовно. (Або and al,1створити ціле число 0/1 залежно від непарного / парного.)
Пітер Кордес

У будь-якому випадку, якби ця відповідь сказала "використовувати прапори, які вже встановлені іншими інструкціями, щоб уникнути test/ cmp", то це було б досить базовим x86 для початківців, але все-таки варто підкреслити.
Пітер Кордес

@PeterCordes Так, я, здається, неправильно зрозумів прапор паритету. Я досі працюю над своєю іншою відповіддю. Я відредагую відповідь. І як ви, напевно, можете сказати, я початківець, тому основні поради допомагають.
qwr

2

Використовуйте петлі do-while замість циклів while

Це не специфічно для x86, але широко застосовується підказка для збірки початківців. Якщо ви знаєте, що цикл у той час як цикл буде працювати хоча б один раз, переписуючи цикл як цикл виконання часу, з перевіркою стану циклу в кінці, часто зберігається інструкція про перехід на 2 байти. У окремому випадку ви можете навіть використовувати loop.


2
Пов'язане: Чому петлі завжди складаються так? пояснює, чому do{}while()природна циклічна ідіома в зборах (особливо для ефективності). Зауважте також, що 2-байт jecxz/ jrcxzперед циклом дуже добре працює з тим, loopщоб "ефективно" запустити нуль разів "(на рідкісних процесорах, де loopне повільно). jecxzтакож можна використовувати всередині циклу для реалізації awhile(ecx){} , jmpвнизу.
Пітер Кордес

@PeterCordes - це дуже добре написана відповідь. Я хотів би знайти застосування для стрибків в середину петлі в програмі з кодовим гольфом.
qwr

Використовуйте goto jmp та відступ ...
Продовжуйте

2

Користуйтеся будь-якими зручними умовами виклику

System V x86 використовує стек і System V x86-64 використовує rdi, rsi, rdx, rcxі т.д. для вхідних параметрів, а також в raxякості значення, що повертається, але це цілком розумно використовувати своє власне угоду про виклики. __fastcall використовує ecxі в edxякості вхідних параметрів, а також інші компілятори / операційки використовувати свої власні угоди . Використовуйте стек і будь-які регістри, коли це зручно.

Приклад: Повторний байтовий лічильник , використовуючи розумний режим виклику для 1-байтового рішення.

Мета: Введення входів до регістрів , Написання виводу до регістрів

Інші ресурси: Примітки Агнера Фога щодо конвенцій про виклики


1
Нарешті я дійшов до публікації власної відповіді на це питання щодо складання конвенцій про виклики, і що розумно проти нерозумно.
Пітер Кордес

@PeterCordes не пов'язаний між собою, який найкращий спосіб друку в x86? Поки я уникав проблем, які потребують друку. DOS виглядає так, що він має корисні переривання для вводу-виводу, але я планую лише написати 32/64 бітові відповіді. Єдиний спосіб, про який я знаю, це те, int 0x80що потрібно купувати налаштування.
qwr

Так, int 0x8032-бітний код або syscall64-бітний код sys_write- це єдиний хороший спосіб. Це те, що я використовував для Extreme Fibach . У 64-бітному коді __NR_write = 1 = STDOUT_FILENO, так що ви можете mov eax, edi. Або якщо верхні байти EAX дорівнюють нулю, mov al, 4в 32-бітному коді. Ви можете також call printfабо puts, я думаю, і написати відповідь "x86 asm для Linux + glibc". Я думаю, що розумно не рахувати простір для входу PLT чи GOT чи сам код бібліотеки.
Пітер Кордес

1
Я б більше схильний, щоб абонент передав a char*bufі створив рядок у цьому, з ручним форматуванням. наприклад, як це (незручно оптимізовано для швидкості) ASM FizzBuzz , де я отримав рядкові дані в реєстр і потім зберігав їх mov, тому що рядки були короткими і фіксованої довжини.
Пітер Кордес

1

Використовуйте умовні рухи CMOVccта набориSETcc

Це більше нагадування про себе, але існують інструкції з умовного набору та існують інструкції щодо умовного переміщення на процесорах P6 (Pentium Pro) або новіших. Існує багато інструкцій, що базуються на одному або декількох прапорах, встановлених у EFLAGS.


1
Я виявив, що розгалуження зазвичай менше. Бувають випадки, коли це природне пристосування, але cmovмає 2-байтовий код коду ( 0F 4x +ModR/M), тому це 3 байти як мінімум. Але джерело - r / m32, тому ви можете умовно завантажувати в 3 байти. Окрім розгалуження, setccкорисний у більшій кількості випадків, ніж cmovcc. Все ж врахуйте весь набір інструкцій, а не лише базові 386 інструкцій. (Хоча інструкція SSE2 та BMI / BMI2 настільки велика, що вони рідко корисні. rorx eax, ecx, 32Це 6 байт, довше mov + ror. Приємно для продуктивності, а не для гольфу, якщо POPCNT або PDEP не економить багато островів)
Peter Cordes

@PeterCordes спасибі, я додав setcc.
qwr

1

Збережіть на jmpбайтах, упорядкувавшись на if / then, а не if / then / else

Це, звичайно, дуже елементарно, я просто думав, що я опублікую це як щось, про що варто подумати, коли займається гольфом. Як приклад, розглянемо наступний прямий код для розшифровки шістнадцяткового символу:

    cmp $'A', %al
    jae .Lletter
    sub $'0', %al
    jmp .Lprocess
.Lletter:
    sub $('A'-10), %al
.Lprocess:
    movzbl %al, %eax
    ...

Це можна скоротити на два байти, дозволивши випадку "тоді" потрапити у "інший" випадок:

    cmp $'A', %al
    jb .digit
    sub $('A'-'0'-10), %eax
.digit:
    sub $'0', %eax
    movzbl %al, %eax
    ...

Ви часто робите це зазвичай під час оптимізації для продуктивності, особливо коли додаткова subзатримка на критичному шляху для одного випадку не є частиною ланцюга залежностей, що переносяться циклом (наприклад, де кожна вхідна цифра є незалежною до об'єднання 4-бітових фрагментів ). Але я гадаю +1 у будь-якому випадку. До речі, у вашому прикладі є окрема пропущена оптимізація: якщо вам все movzxодно знадобиться кінець, тоді використовуйте sub $imm, %alне EAX, щоб скористатися 2-байтним кодуванням no-modrm op $imm, %al.
Пітер Кордес

Крім того , ви можете усунути cmp, виконавши sub $'A'-10, %al; jae .was_alpha; add $('A'-10)-'0'. (Я думаю, я правильно зрозумів логіку). Зауважте, що 'A'-10 > '9'тому двозначності немає. Віднімаючи виправлення на букву, буде зафіксовано десяткову цифру. Отже, це безпечно, якщо ми припускаємо, що наше введення є правильним шістнадцятковим, як і ваш.
Пітер Кордес

0

Ви можете отримати послідовні об'єкти зі стека, встановивши esi на esp та виконавши послідовність reg-lodsd / xchg, eax.


Чому це краще, ніж pop eax/ pop edx/ ...? Якщо вам потрібно залишити їх у стеці, ви можете pushповернути їх назад, щоб відновити ESP, все-таки 2 байти на об'єкт без потреби mov esi,esp. Або ви мали на увазі для 4-байтних об'єктів у 64-бітовому коді, де popбуло б 8 байт? До речі, ви навіть можете використовувати popпетлю над буфером з кращими характеристиками, ніж lodsd, наприклад, додавання
Peter Cordes

це корисніше після "lea esi, [esp + size ret address" ", яке б перешкоджало використанню pop, якщо у вас немає запасного реєстру.
peter ferrie

О, для аргументів функції? Досить рідко ви хочете більше аргументів, ніж є регістри, або щоб ви хотіли, щоб абонент залишив один у пам'яті, а не передавати їх усім у регістри. (У мене є напівфабрикатна відповідь про використання користувацьких конвенцій про дзвінки, якщо одна із стандартних конвенцій про реєстрацію дзвінків не відповідає ідеально.)
Пітер Кордес,

cdecl замість fastcall залишить параметри на стеці, і легко мати безліч параметрів. Дивіться, наприклад, github.com/peterferrie/tinycrypt.
peter ferrie

0

Для codegolf та ASM: Використовуйте вказівки, використовуйте лише регістри, push-поп, мінімізуйте пам'ять реєстру або пам'ять негайно


0

Щоб скопіювати 64-розрядний регістр, використовуйте push rcx; pop rdxзамість 3-байтного mov.
Типовий розмір операнду push / pop - 64-розрядний, не потребуючи префікса REX.

  51                      push   rcx
  5a                      pop    rdx
                vs.
  48 89 ca                mov    rdx,rcx

(Префікс розміру операнда може змінити розмір push / pop на 16-розрядний, але 32-розрядний push / pop розмір операнду не кодується в 64-бітному режимі навіть при REX.W = 0.)

Якщо один або обидва регістри є r8.. r15, використовуйте, movтому що для push і / або pop потрібен префікс REX. Найгірший випадок, що насправді втрачається, якщо обом потрібні префікси REX. Очевидно, що зазвичай у коді гольфу слід уникати r8..r15.


Ви можете зберігати своє джерело легше для читання, розробляючи цей макрос NASM . Просто пам’ятайте, що він крокує на 8 байт нижче RSP. (У червоній зоні в системі x86-64 Система V). Але в нормальних умовах це замінна плата для 64-бітових mov r64,r64абоmov r64, -128..127

    ; mov  %1, %2       ; use this macro to copy 64-bit registers in 2 bytes (no REX prefix)
%macro MOVE 2
    push  %2
    pop   %1
%endmacro

Приклади:

   MOVE  rax, rsi            ; 2 bytes  (push + pop)
   MOVE  rbp, rdx            ; 2 bytes  (push + pop)
   mov   ecx, edi            ; 2 bytes.  32-bit operand size doesn't need REX prefixes

   MOVE  r8, r10             ; 4 bytes, don't use
   mov   r8, r10             ; 3 bytes, REX prefix has W=1 and the bits for reg and r/m being high

   xchg  eax, edi            ; 1 byte  (special xchg-with-accumulator opcodes)
   xchg  rax, rdi            ; 2 bytes (REX.W + that)

   xchg  ecx, edx            ; 2 bytes (normal xchg + modrm)
   xchg  rcx, rdx            ; 3 bytes (normal REX + xchg + modrm)

xchgЧастина прикладу тому , що іноді вам потрібно отримати значення в EAX або RAX і не піклуються про збереження старої копії. push / pop не допомагає вам фактично обмінятися.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.