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