Есть ли разница между вставкой регистров перед созданием стекового фрейма или после? - PullRequest
3 голосов
/ 01 декабря 2019

Предположим, у меня есть функция 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

А что мне использовать в моих программах? Один метод лучше или "более правильный", чем другой? Потому что я использую первый метод в настоящее время.

Ответы [ 3 ]

5 голосов
/ 02 декабря 2019

Второй способ, 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, когда он используется для передачи указателя на то место, где функция должна хранить большое возвращаемое значение.

Или, в крайнем случае, выберите пользовательское соглашение о вызовах для каждой функции в зависимости от того, что наиболее эффективно. для этой функции и для ее абонентов. Но это быстро стало бы кошмаром обслуживания;если вы хотите такого рода оптимизацию, просто используйте компилятор и дайте ему встроить короткие функции и оптимизируйте их в вызывающую функцию, или проведите межпроцедурную оптимизацию, основанную на том, какие регистры фактически используются функцией.

2 голосов
/ 02 декабря 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 и последним параметром " в моем макросе lframeinvocation.

Но легче всегда выдвигать регистры после настройки фрейма (второй метод). В этом случае я всегда могу указать «тип кадра» как 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].

1 голос
/ 02 декабря 2019

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

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

...