Есть ли разница между отправкой регистров до создания фрейма стека или после?

Предположим, у меня есть функция с именем func:

PROC func:
    ;Bla bla
    ret
ENDP func

Теперь предположим, что я использую, например, регистры ax и bx, поэтому, чтобы сохранить их начальное значение, я помещаю их в стек внутри функции.

Теперь к вопросу: есть ли какая-то большая разница между нажатием регистров перед созданием кадра стека:

PROC func:
    push bp
    push ax
    push bx
    mov bp, sp
    ;Bla bla
    ret
ENDP func

Или после?

PROC func:
    push bp
    mov bp, sp
    push ax
    push bx
    ;Bla bla
    ret
ENDP func

И что я должен использовать в своих программах? Является ли один метод лучше или «правильнее» другого? Потому что я использую первый метод в настоящее время.


person Kidsm    schedule 01.12.2019    source источник
comment
нет, вы бы сделали push bp, а затем push bp, sp . Преимущество заключается в том, что если вы сделаете это в начале, то в 16-битном коде первый параметр всегда будет иметь значение [bp+4] , второй параметр — значение [bp+6] и т. делаю mov bp, sp. Так людям будет проще поддерживать код. С точки зрения компилятора высокого уровня это не имеет большого значения. Преимущество: ремонтопригодность.   -  person Michael Petch    schedule 01.12.2019
comment
Значит, мой подход действительно был лучше? :D Рад слышать, Мой друг пытается убедить меня, что второй метод правильный, и теперь я могу доказать, что он не прав :) Спасибо, сэр.   -  person Kidsm    schedule 01.12.2019
comment
Нет, с точки зрения удобочитаемости и ремонтопригодности для ассемблера, созданного человеком, подход ваших друзей будет лучше, ИМХО. На самом деле большая часть кода, сгенерированного компиляторами и людьми, следует шаблону: сначала проталкивается bp, затем выполняется mov bp, sp, а затем выполняется обратное действие непосредственно перед ret. Обычно разработчики находят параметры чтения с положительным смещением от BP и локальные переменные с отрицательным смещением. Это более распространенное соглашение.   -  person Michael Petch    schedule 01.12.2019
comment
Я думаю, вы запутались в том, что случай push 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.2019
comment
Мой подход заключался в пуше перед мобом bp, sp XD, он предложил сделать mov перед пушем. Я использовал первый подход, потому что мне легче работать с положительными значениями для регистров и отрицательными для локальных переменных, как вы сказали: D   -  person Kidsm    schedule 01.12.2019
comment
@Kidsm: Чтобы упростить ассемблер, вы можете написать функции, которые просто не сохраняют все регистры, которые они используют. В 32-битных соглашениях о вызовах функциям разрешено уничтожать EAX, ECX и EDX без их сохранения/восстановления. Наличие нескольких чистых регистров call-clobbed aka volatile означает меньше push/ поп для простых функций.   -  person Peter Cordes    schedule 01.12.2019
comment
Нет, я говорю, что вы НАЖИМАЕТЕ ТОЛЬКО BP, затем mov bp, sp, а затем делаете все остальные нажатия, которые вам нужно сохранить. И прежде чем вы когда-либо изменяете bp, вам нужно сохранить его, чтобы его можно было восстановить до исходного значения. Ваш второй фрагмент кода неверен, потому что он даже не нажимает bp.   -  person Michael Petch    schedule 01.12.2019
comment
@MichaelPetch и в чем причина?   -  person Kidsm    schedule 01.12.2019
comment
Ты имеешь в виду для пуш бп? Если вы имеете в виду, в чем причина push bp и mov bp, sp, это создание кадра стека.   -  person Michael Petch    schedule 01.12.2019
comment
@Kidsm: помните, что bp является энергонезависимым; вы должны сохранить значение вашего вызывающего абонента в этом регистре. (Было бы довольно неудобно, если бы вам приходилось сохранять BP в другом регистре при вызовах функций, которые делает ваш собственный код). Было бы бесполезно push bp после уничтожения значения вызывающего объекта с помощью mov bp,sp. Сохранение/восстановление AX, но не BP — безумие.   -  person Peter Cordes    schedule 01.12.2019
comment
@MichaelPetch за отправку bp, затем создание фрейма стека, а затем за отправку регистров   -  person Kidsm    schedule 01.12.2019
comment
Мой последний комментарий отвечает на вопрос, который вы задавали Майклу: P   -  person Peter Cordes    schedule 01.12.2019
comment
Я отредактировал код в вопросе, неправильно написал функции   -  person Kidsm    schedule 02.12.2019
comment
@PeterCordes Посмотрите на изменения, которые я внес в вопрос, я случайно забыл написать push bp во второй функции:/   -  person Kidsm    schedule 02.12.2019
comment
Какой ассемблер вы используете (MASM/TASM/JWASM?) и какой версии. Или вы используете EMU8086? или что-то другое?   -  person Michael Petch    schedule 02.12.2019


Ответы (3)


Второй способ, 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
comment
Соглашения о вызовах для DOS (не включая свертывание собственных) немного отличаются и отличаются от современных и варьируются от компилятора к компилятору. Следует отметить, что функция в 16-битном коде, доступ к которой осуществляется через дальний вызов, будет иметь первый параметр в bp+6. Так что это не обязательно всегда верно, и это зависит от характера создаваемой вами функции. Как и в случае с MASM с соглашением о вызовах на основе стека, вы можете использовать директивы MASM в PROC и после него, чтобы указать параметры и локальные переменные (по имени) и позволить ассемблеру справиться с тяжелой работой по вычислению смещений BP. - person Michael Petch; 02.12.2019
comment
В случае программы .COM модель по умолчанию крошечная (похожая на маленькую), поэтому это будет почти вызов. В других моделях по умолчанию может использоваться адрес удаленного вызова (сегмент: смещение). Использование локальных директив PROC и MASM для определения функций может уменьшить эти головные боли, поскольку он знает из модели по умолчанию (или переопределения PROC), если что-то близко или далеко, и изменяет ret на retn или retf. Гораздо проще написать код, который можно собрать в разные модели. - person Michael Petch; 02.12.2019
comment
@MichaelPetch: хорошие моменты. Я думал отредактировать начальный абзац, чтобы упомянуть дальние процедуры, но решил не загромождать его и рассказать о самом простом случае. Может быть, сноска. Есть ли у вас предложения по поводу каких-либо хороших 16-битных соглашений о вызовах с хорошо подобранным набором регистров с затиранием вызовов? - person Peter Cordes; 02.12.2019
comment
Я использую соглашение Watcom C 16-bit. Они создали первый компилятор с соглашением о передаче по регистру (позже Microsoft подражала ему в своей собственной версии). Я считаю, что соглашения о вызовах Agner Fog включают в себя особенности этого соглашения. Вы всегда можете направить людей к документу Агнера с помощью соглашений о вызовах. Что бы вы ни выбрали, проще быть последовательным, и вам придется выбрать подходящий вариант при взаимодействии с языком, который имеет определенное соглашение. - person Michael Petch; 02.12.2019
comment
Вопрос с ответом, который отдаленно связан с этим обсуждением, но дает идеи о том, как вы можете использовать директивы MASM для упрощения передачи параметров, можно найти здесь: stackoverflow.com/questions/36293714/ . Предостережение в том, что некоторые старые версии MASM не поддерживают все директивы, а EMU8086 также имеет ограничения. Я не знаю, что этот человек использует. - person Michael Petch; 02.12.2019

В своих программах я обычно использую второй метод, то есть сначала создаю кадр стека. Это делается с помощью push bp \ mov bp, sp, а затем, опционально, push ax один или два раза или lea sp, [bp - x] для резервирования места для неинициализированных переменных. (Я позволяю своим макросам кадра стека создавать эти инструкции.) Затем вы можете дополнительно поместить в стек, чтобы зарезервировать место для и в то же время инициализировать дополнительные переменные. После переменных могут быть помещены регистры для сохранения во время выполнения функции.

Есть третий способ, который вы не указали в качестве примера в своем вопросе. Это выглядит так:

PROC func:
    push ax
    push bx
    push bp
    mov bp, sp
    ;Bla bla
    ret
ENDP func

Для моего использования легко возможны второй и третий способы. Я мог бы использовать третий способ, если я сначала нажимаю вещи, а затем для создания фрейма стека указываю, что я называю "насколько велик адрес возврата и другие значения между bp и последним параметром" в моем вызове макроса lframe.

Но проще всегда проталкивать регистры после настройки фрейма (второй способ). В этом случае я всегда могу указать «тип кадра» как near, что почти полностью эквивалентно 2; это так, потому что почти 16-битный адрес возврата занимает 2 байта.

Вот пример кадра стека с регистрами сохраняется путем нажатия на них:

        lframe near, nested
        lpar word,      inp_index_out_segment
        lpar word,      out_offset
        lpar_return
        lenter
        lvar dword,     start_pointer
         push word [sym_storage.str.start + 2]
         push word [sym_storage.str.start]
        lvar word,      orig_cx
         push cx
        mov cx, SYMSTR_index_size

        ldup

        lleave ctx
        lleave ctx

                ; INP:  ?inp_index_out_segment = index
                ;       ?start_pointer = start far pointer of this area
                ;       ?orig_cx = what to return cx to
                ;       cx = index size
.common:
        push es
        push di
        push dx
        push bx
        push ax
%if _BUFFER_86MM_SLICE
        push si
        push ds
%endif

Здесь есть небольшое преимущество перед использованием второго способа: начальный кадр стека фактически создается несколько раз разными точками входа в функцию. Они легко разделяют сохранение, помещая регистры в обработку .common. Этого нельзя было бы достичь так просто, если бы отличающееся вступление для каждой точки входа следовало бы после нажатия регистров для сохранения их значений.


В остальном особой разницы нет, т. Однако сохранение предыдущего значения bp на уровне word [bp] (второй или третий способ) может быть полезным или даже необходимым для отладчиков или другого программного обеспечения, чтобы следовать цепочка кадров стека. Точно так же второй способ может быть полезен, поскольку он сохраняет адрес возврата word [bp + 2].

person ecm    schedule 01.12.2019
comment
Почему lea sp, [bp - x] вместо sub sp, x - 2*n_pushes? Я думаю, что в любом случае это одинаковый размер кода, если только sub не может использовать imm8, а lea нужен disp16, потому что несколько толчков имеют значение. Я думаю, что для производительности на современном Intel со стековым движком (Pentium M и более поздние версии) обоим потребуется синхронизация стека uop даже для использования SP только для записи в серверной части. Однако на PPro/PIII чтение BP вместо SP-after-more-push сократит цепочку зависимостей. Это было причиной? - person Peter Cordes; 02.12.2019
comment
@Peter Cordes: Во всяком случае, я оптимизирую размер. Я решил использовать lea, потому что он такой же короткий, но не изменяет флаги. Я редко ввожу флаги в функцию или устанавливаю флаги в точке входа, а затем использую макрос lreserve или возвращаю флаги через lleave lframe x, inner. Все это делается с использованием lea, за исключением упомянутого первого, который может использовать push ax (который также не изменяет флаги). Между прочим, мои стеки обычно находятся в диапазоне от 512 байт до 1 КиБ, поэтому создание фрейма стека с более чем 255 байтами было бы опрометчивым. - person ecm; 02.12.2019
comment
Ах, сохранение флагов - веская причина, я не думал об этой разнице. Думая о коде, сгенерированном компилятором, легко забыть обо всех других возможностях, даже если я стараюсь помнить о них. (Стандартные соглашения о вызовах C очень ограничены, например, возвращается только 1 значение, что приводит к ошибкам проектирования API, таким как memcmp, который отбрасывает позицию разницы. Или, может быть, потому, что они разработаны для такого языка, как C.) - person Peter Cordes; 02.12.2019
comment
@Peter Cordes: и сохранение флага, и другая функция использования lea являются задокументирован для макроса lenter, а другой заключается в том, что lenter после lenter early реализуется с использованием lea sp, [bp - x], поэтому не имеет значения, сколько переменных уже было инициализировано путем помещения в них. (Это верно и для lreserve.) Если вы хотите использовать вместо этого sub sp, x - y, вам придется отслеживать, сколько переменных уже зарезервировано в стеке, чтобы определить y. - person ecm; 02.12.2019
comment
@Peter Cordes: Интересно, что я на самом деле использовал sub для обычного lenter использования сначала. Это было до того, как были добавлены lenter early или lreserve. - person ecm; 02.12.2019
comment
@ecm Спасибо за четкий и информативный ответ: D - person Kidsm; 02.12.2019

Чаще всего сначала настраивают кадр стека. Это связано с тем, что параметры вашей функции обычно находятся в стеке. Вы можете получить к ним доступ с фиксированным (положительным) смещением от б.п. Если вы сначала поместите другие регистры, то положение параметров в кадре стека изменится.

Если вам нужно выделить локальную память в стеке, вы можете вычесть константу из sp, чтобы создать пустое пространство, а затем поместить другие регистры. Таким образом, ваше локальное хранилище имеет (отрицательное) смещение от bp, которое не изменится, если вы поместите больше или меньше регистров в стек.

person transconductance    schedule 01.12.2019
comment
Аргументы функции находятся над адресом возврата на положительном смещении от BP. Кроме того, вы резервируете место для местных жителей с sub sp, constant, а не sub bp, const, поэтому они ниже BP, выше SP. - person Peter Cordes; 02.12.2019
comment
Абсолютно правильно. Я работаю над PIC micro, который увеличивает стек вверх;) - person transconductance; 02.12.2019