Сборка x86 Зачем использовать Push / Pop вместо Mov? - PullRequest
3 голосов
/ 16 июня 2019

У меня есть пример кода из полезной нагрузки шелл-кода, показывающий цикл for и использующий push / pop для установки счетчика:

push 9
pop ecx

Почему он не может просто использовать mov?

mov ecx, 9

Ответы [ 3 ]

6 голосов
/ 16 июня 2019

Да, обычно вы всегда должны использовать mov ecx, 9 из соображений производительности. Он работает более эффективно, чем push / pop`, как инструкция с одним входом и может выполняться на любом порту.(Это верно для всех существующих процессоров, которые тестировал Agner Fog: https://agner.org/optimize/)


Обычная причина для push imm8 / pop r32 состоит в том, что машинный код не содержит нулевых байтов. Это важно для шелл-кода , который должен переполнять буфер через strcpy или любым другим методом, который обрабатывает его как часть строки C неявной длины, оканчивающейся байтом 0.

mov ecx, immediate доступен только с 32-битным немедленным, поэтому машинный код будет выглядеть как B9 09 00 00 00. Против 6a 09 push 9; 59 pop ecx.

(ECX - это номер регистра 1, отсюда B9 и 59: младшие 3 бита инструкции = 001)


Другой вариант использованияимеет чисто кодовый размер : mov r32, imm32 составляет 5 байтов (используя кодировку без ModRM, которая помещает номер регистра в младшие 3 бита кода операции), потому что x86, к сожалению, не имеет расширенного знака кода операции imm8 для mov(нет mov r/m32, imm8). Это существует почти для всех инструкций ALU, относящихся к 8086 году.

В 16-битном 8086 это кодирование не сэкономило бы места: 3-байтовая краткая форма mov r16, imm16 была бы столь же хороша, как и гипотетическая mov r/m16, imm8 почти для всего, кроме перемещения непосредственно в память, гденеобходима форма mov r/m16, imm16 (с байтом ModRM).

Поскольку в 32-битном режиме 386 не добавлялись новые коды операций, просто изменили размер операнда по умолчанию и непосредственную ширину, эта «пропущенная оптимизация» вISA в 32-битном режиме началась с 386. С немедленной шириной сразу на 2 байта длина add r32,imm32 теперь длиннее add r/m32, imm8.См. x86 сборка 16 бит против 8-битного кодирования непосредственного операнда .Но у нас нет этой опции для mov, потому что нет никакого кода операции MOV, который расширяет (или расширяет ноль) его немедленное значение.

Интересный факт: clang -Oz (оптимизация под размер даже за счетскорости) скомпилирует int foo(){return 9;} до push 9;pop rax.

См. Также Советы по игре в гольф с машинным кодом x86 / x64 на Codegolf.SE (сайт, посвященный оптимизации размера, обычно для развлечения, а не для того, чтобы поместить код в маленькийПЗУ или загрузочный сектор. Но для машинного кода оптимизация по размеру иногда имеет практическое применение, даже в ущерб производительности.)

Если у вас уже был другой регистр с известным содержимым, создание 9 в другом регистре может бытьсделано с 3-байтовым lea ecx, [eax-0 + 9] (если EAX содержит 0).Просто Opcode + ModRM + disp8.Таким образом, вы можете избежать взлома push / pop, если вы уже собирались обнулять любой другой регистр.lea чуть менее эффективен, чем mov, и вы могли бы учитывать это при оптимизации для скорости, потому что меньший размер кода имеет незначительные преимущества в скорости в широком масштабе: попадание в кэш L1i, а иногда и декодирование, если кэш UOP еще негорячий.

2 голосов
/ 16 июня 2019

У этого могут быть разные причины.

В этом случае это, кажется, делается, потому что код меньше:

Вариант с комбинацией push и pop равен 3длиной в байты, инструкция mov имеет длину 5 байтов.

Однако я бы предположил, что вариант mov быстрее ...

0 голосов
/ 16 июня 2019

По сути, точно такая же вещь.нажмите 9 в стек, затем вставьте его в регистр ecx, который в основном совпадает с mov ecx, 9. Лично я думаю, что 9 в ecx, вероятно, более эффективен, чем нажатие 9 в стек, а затем вставка его в ecx, но я думаю, что время обработкине проблема, поэтому они оба одинаково быстры, учитывая, насколько мал код в любом случае.

...