Второй способ, 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 в качестве указателя кадра для доступа к собственному кадру стека.)
Трассировки стека являются одной из основных причин того, что mov bp,sp
сразу после push bp
является стандартным соглашением. В отличие от некоторых других равноценных соглашений, таких как выполнение всех ваших push-уведомлений и затем 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
.
Кстати, вы можете сократить свой код, позволив функциям уничтожить как минимум AX без сохранения/восстановления. т. е. рассматривать их как забитые вызовами, также известные как volatile. Обычные 32-битные соглашения о вызовах имеют EAX, ECX и EDX volatile (например, для чего компилируется GCC в приведенном выше примере: Linux i386 System V), но существует множество различных 16-битных соглашений, которые отличаются.
Наличие одного из SI, DI или BX volatile позволит функциям обращаться к памяти без необходимости вталкивать/выталкивать копию вызывающего объекта.
Руководство Agner Fog по соглашениям о вызовах включает некоторые стандартные 16-битные соглашения о вызовах, см. таблицу в начале главы 7 для 16-битных соглашений используется существующими компиляторами C/C++. @MichaelPetch предлагает соглашение Watcom: AX и ES всегда затираются вызовами, но аргументы передаются в AX, BX, CX, DX. Любой регистр, используемый для передачи аргументов, также затирается вызовами. То же самое относится и к SI, когда он используется для передачи указателя на то место, где функция должна хранить большое возвращаемое значение.
Или, в крайнем случае, выберите пользовательское соглашение о вызовах для каждой функции в соответствии с тем, что наиболее эффективно для этой функции и для ее вызывающих объектов. Но это быстро превратилось бы в кошмар обслуживания; если вам нужна такая оптимизация, просто используйте компилятор и позвольте ему встроить короткие функции и оптимизировать их в вызывающем объекте или выполнить межпроцедурную оптимизацию на основе того, какие регистры фактически используются функцией.
person
Peter Cordes
schedule
01.12.2019
push bp
, а затемpush bp, sp
. Преимущество заключается в том, что если вы сделаете это в начале, то в 16-битном коде первый параметр всегда будет иметь значение[bp+4]
, второй параметр — значение[bp+6]
и т. делаюmov bp, sp
. Так людям будет проще поддерживать код. С точки зрения компилятора высокого уровня это не имеет большого значения. Преимущество: ремонтопригодность. - person Michael Petch   schedule 01.12.2019push bp
mov bp, sp
выполняется до всех остальных толчков. Таким образом, это будет выглядеть какpush bp
mov bp, sp
push ax
push bx
и т. д. Большинство людей, которых я знаю, считают, что это предпочтительнее, чемpush ax
push bx
push bp
mov bp, sp
- person Michael Petch   schedule 01.12.2019mov bp, sp
, а затем делаете все остальные нажатия, которые вам нужно сохранить. И прежде чем вы когда-либо изменяетеbp
, вам нужно сохранить его, чтобы его можно было восстановить до исходного значения. Ваш второй фрагмент кода неверен, потому что он даже не нажимает bp. - person Michael Petch   schedule 01.12.2019push bp
иmov bp, sp
, это создание кадра стека. - person Michael Petch   schedule 01.12.2019bp
является энергонезависимым; вы должны сохранить значение вашего вызывающего абонента в этом регистре. (Было бы довольно неудобно, если бы вам приходилось сохранять BP в другом регистре при вызовах функций, которые делает ваш собственный код). Было бы бесполезноpush bp
после уничтожения значения вызывающего объекта с помощьюmov bp,sp
. Сохранение/восстановление AX, но не BP — безумие. - person Peter Cordes   schedule 01.12.2019