Эффективный вызов нескольких функций с одинаковыми аргументами в сборке - PullRequest
0 голосов
/ 04 июля 2011

Я внутри функции с некоторыми аргументами. Допустим,

__cdecl функция (int a, int b, long c)

(Давайте пока забудем __stdcall) .

Теперь в сборке я хочу вызвать несколько функций, которые принимают аргументы a, b, c. Первое, что я должен знать, это то, что, если я делаю «вызовы», стек будет дислоцирован в зависимости от того, как я его получил, так как сначала он будет использовать текущие EIP и EBP. Я предполагаю, что могу сохранить старый EIP и старый EBP и вычислить текущие EIP и EBP, заменить их в стеке, а затем просто перейти. Это нормально, если это предположение неверно и, возможно, оно принесет больше проблем (не стесняйтесь указывать на это), но это не имеет значения, потому что я могу снова выдвинуть аргументы. Теперь реальный вопрос: могу ли я, просто нажав один раз аргументы (или используя стек, как я его получил), вызвать несколько раз несколько функций с одним и тем же стеком? Или это может вызвать проблемы?

Пример:

push 2
push 3
push 4
call X
call Y
Call Z
add esp, 12

Итак, я хочу, чтобы X, Y, Z имели одинаковые аргументы, и таким образом, особенно для нескольких функций, это будет намного эффективнее, чем с «обычным кодом», который будет выдвигать все аргументы один раз для каждого вызова функции.

Ответы [ 3 ]

3 голосов
/ 04 июля 2011

В полностью основанном на стеке соглашении о вызовах нет ничего плохого в реализации на стороне сборки вызовов, таких как:

void myfunction(void)
{
    call_some_func(1, 2, 3);
    call_another_func(1, 2, 3);
    call_more_stuff(1, 2, 3);
    call_even_more_stuff(0, 1, 2, 3);
    call_yet_more(2, 3);
    ...
}

в виде последовательности (синтаксис AT & T, 32-битный x86, я UN *X guy):

myfunction:
    pushl   $3
    pushl   $2
    pushl   $1
    call    call_some_func
    call    call_another_func
    call    call_more_stuff
    pushl   $0
    call    call_even_more_stuff
    addl    $8, %esp
    call    call_yet_more_stuff
    ...
    addl    $8, %esp
    ret

Соглашения о вызовах (поскольку, как Microsoft называет это, стиль cdecl, как это также используется в UN * X i386 ABI) для передачи параметров на основе стека, все имеютсвойство, что указатель стека не изменяется после того, как call вернет .Это означает, что если вы поместили ряд аргументов в стек и выполнили call, они будут по-прежнему находиться в стеке после того, как любая функция, которую вы вызвали, вернется.Так что ничто не мешает вам снова использовать их на месте;как показано, в некоторых случаях вы можете даже использовать повторно то, что уже находится в стеке, если вы вызываете функции с большим / меньшим количеством аргументов, чем вы использовали в предыдущих вызовах.

После возврата из функциистек снова ваш;у вас нет для очистки (выполните addl $..., %esp непосредственно после call), если то, что уже есть, полезно для вас, просто оставьте это.

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

myfunction:
    stmfd   sp!, {lr}
    mov    r0, #1
    mov    r1, #2
    mov    r2, #3
    stmfd  sp!, {r0-r2}
    bl     call_some_func
    ldmfd  sp, {r0-r2}
    bl     call_another_func
    ldmfd  sp, {r0-r2}
    bl     call_more_stuff
    ldmfd  sp!, {r1-r3}
    mov    r0, #0
    stmfd  sp!, {r2, r3}
    bl     call_even_more_stuff
    ldmfd  sp!, {r0, r1, lr}
    b      call_yet_more_stuff

Т.е. вы сохраняете содержимое в стеке и загружаете его оттуда без изменения указателя стека для нагрузок (sp! в ARM делает различие между изменением и просто использованием регистра стека).

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

Редактировать:
Если вы думаете о следующем:

void myfunc(void *a1, void *a2, void *a3)
{
    func1(a1, a2, a3);
    func2(a1, a2, a3);
    func3(a1, a2, a3);
}

тогда вы можете «сыграть в Ханойские башни» со стеком и переупорядочить его;адрес возврата в вызывающем myfunc является самым верхним в стеке, и аргументы следуют;поэтому используйте регистры clobber (%eax, %ecx и %edx для UN * X) для временного хранения значений, пока вы перемещаете адрес возврата в самое нижнее место стека.С тремя аргументами это достаточно просто, как это делает один раунд «Ханоя»:

myfunc:
    popl    %eax          ; return address now in EAX
    popl    %ecx          ; arg[1]
    popl    %edx          ; arg[2]
    xchgl   %eax, (%esp)  ; swap return address and arg[3]
    pushl   %eax          ; re-push arg[3]
    pushl   %edx          ; and arg[2]
    pushl   %ecx          ; and arg[1]
    call    func1
    call    func2
    call    func3
    popl    %ecx          ; pop of dummy, gets %esp to pre-call
    jmpl    0xc(%esp)     ; use jmpl to return - address at "bottom"

Edit2 : я изначально допустил ошибку, используя энергонезависимый регистр (%ebx) удерживать обратный адрес;как правильно отметили комментаторы, которые бы забили значение в регистре и вызвали бы проблемы у нашего вызывающего.Чтобы предотвратить это, можно использовать описанный выше метод переупорядочения вещей в стеке.

1 голос
/ 04 июля 2011

Технически нет, но обычно да

Функция позволяет изменять параметры стека;они похожи на локальные объекты кадра.

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

При вызове функций библиотеки вы быстро узнаете, изменились ли ваши параметры.Если нет, то вы в безопасности, если какая-то будущая ревизия библиотеки не начнет изменять параметры.Это маловероятно, но поскольку вы нарушаете API, вы не сможете жаловаться.Это риск, хотя и довольно маленький.

1 голос
/ 04 июля 2011

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

Более традиционный способ - использовать мир выделенной памяти длиной 3 * 4 байта (переменные x, y, z) и использовать указатель на него в качестве входных данных для вызова функций.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...