Второй способ, push bp
;mov bp, sp
перед добавлением других регистров означает, что ваш первый аргумент стека равен всегда при [bp+4]
независимо от того, сколько еще нажатий вы сделаете 1 . Это не имеет значения, если вы передали все аргументы в регистрах, а не в стеке, , что проще и эффективнее в большинстве случаев, если у вас есть только пара.
Этохорош для ремонтопригодности людьми;Вы можете изменить количество сохраняемых / восстанавливаемых регистров без изменения способа доступа к аргументам. Но вы все равно должны избегать места прямо под BP;сохранение большего числа регистров означает, что вы можете поместить наивысшую локальную переменную в [bp-6]
вместо [bp-4]
.
Сноска: «Дальний процесс» имеет 32-битный адрес возврата CS: IP, поэтому аргументы начинаются с [bp+6]
в этом случае. См. Комментарии @ MichaelPetch о том, как инструменты типа MASM могли бы разобраться с символическими именами для аргументов и локальных переменных.
Кроме того, для возвращение вверх по стеку вызовов ,это означает, что значение bp
вашего вызывающего абонента указывает на сохраненное значение BP в стековом кадре вашего вызывающего абонента, формируя связанный список значений BP / ret-addr, за которым отладчик может следовать . Если сделать больше толчков до mov bp,sp
, BP останется в другом месте. См. Также Когда мы создадим базовый указатель в функции - до или после локальных переменных? для получения более подробной информации об этом по очень похожему вопросу для 32-битного режима. (Обратите внимание, что 32- и 64-разрядный код может использовать [esp +- x]
режимы адресации, но 16-разрядный код не может. 16-разрядный код в основном вынужден устанавливать BP в качестве указателя кадра для доступа к своему собственному стековому кадру.)
I-трассировка стека является одной из основных причин того, что mov bp,sp
сразу после push bp
является стандартным соглашением. В отличие от некоторых других не менее действительных соглашений, таких как выполнение всех ваших толчков и , тогда mov bp,sp
.
Если вы push bp
последний , вы можете использовать leave
инструкция перед pop / pop / ret в эпилоге. (Это зависит от того, как BP указывает на сохраненное значение BP).
Инструкция leave
может сохранить размер кода в виде компактной версии mov sp,bp
;pop bp
. (Это не волшебство, это все, что он делает. Совершенно нормально не использовать его. И enter
очень медленный на современном x86, никогда не используйте его.) Вы не можете действительно использовать leave
, если у вас есть другие всплывающие окна, чтобы сделатьпервый. После add sp, whatever
, чтобы указать SP на сохраненное значение BX, вы делаете pop bx
, а затем вы можете просто использовать pop bp
вместо leave
. Так что leave
полезен только в функции, которая создает кадр стека, но не помещает другие регистры после. Но резервирует некоторое дополнительное пространство, например, с помощью sub sp, 20
, поэтому sp
по-прежнему не указывает на то, что вы хотите pop
.
Или вы можете использовать что-то подобное, чтобы смещатьАргументы стека и локальные значения не зависят от того, сколько регистров вы добавляете / извлекаете, кроме BP. Я не вижу никаких очевидных недостатков в этом, но, возможно, есть какая-то причина, по которой я пропустил, почему это не обычное соглашение.
func:
push bp
mov bp,sp
sub sp, 16 ; space for locals from [bp-16] to [bp-1]
push bx ; save some call-preserved regs *below* that
push si
... function body
pop si
pop bx
leave ; mov sp, bp; pop bp
ret
Современный GCC имеет тенденцию сохранять любые сохраненные вызовы регистры до sub esp, imm
. например,
void ext(int); // non-inline function call to give GCC a reason to save/restore a reg
void foo(int arg1) {
volatile int x = arg1;
ext(1);
ext(arg1);
x = 2;
// return x;
}
gcc9.2 -m32 -O3 -fno-omit-frame-pointer -fverbose-asm на Godbolt
foo(int):
push ebp #
mov ebp, esp #,
push ebx # save a call-preserved reg
sub esp, 32 #,
mov ebx, DWORD PTR [ebp+8] # arg1, arg1 # load stack arg
push 1 #
mov DWORD PTR [ebp-12], ebx # x = arg1
call ext(int) #
mov DWORD PTR [esp], ebx #, arg1
call ext(int) #
mov DWORD PTR [ebp-12], 2 # x,
mov ebx, DWORD PTR [ebp-4] #, ## restore EBX with mov instead of pop
add esp, 16 #, ## missed optimization, let leave do this
leave
ret
Восстановление сохраненных вызовов регистров с помощью mov
вместо pop
позволяет GCC по-прежнему использовать leave
. Если вы настраиваете функцию для возврата значения, GCC избегает потраченных впустую add esp,16
.
BTW, вы можете сократить код, позволяя функциям уничтожать как минимум AX без сохранения / восстановления . т. е. относиться к ним как к замкнутому, называемому летучим . Обычные 32-битные соглашения о вызовах имеют изменчивую EAX, ECX и EDX (как то, для чего GCC компилирует в приведенном выше примере: Linux i386 System V), но существует много различных 16-битных соглашений, которые различаются.
Наличие одной из волатильности SI, DI или BX позволило бы функциям получать доступ к памяти без необходимости нажимать / выдавать копию ее вызывающей стороны.
Руководство по соглашениям о вызовах Agner Fog включает некоторые стандартные 16-разрядные соглашения о вызовах , см. Таблицу в начале главы 7 для используемых 16-разрядных соглашенийсуществующими компиляторами C / C ++. @MichaelPetch предлагает соглашение Watcom: AX и ES всегда с замкнутым вызовом, но аргументы передаются в AX, BX, CX, DX. Любой регистр, используемый для передачи аргументов, также называется clobbered. Как и SI, когда он используется для передачи указателя на то место, где функция должна хранить большое возвращаемое значение.
Или, в крайнем случае, выберите пользовательское соглашение о вызовах для каждой функции в зависимости от того, что наиболее эффективно. для этой функции и для ее абонентов. Но это быстро стало бы кошмаром обслуживания;если вы хотите такого рода оптимизацию, просто используйте компилятор и дайте ему встроить короткие функции и оптимизируйте их в вызывающую функцию, или проведите межпроцедурную оптимизацию, основанную на том, какие регистры фактически используются функцией.