Параметры функции.
Функции в ассемблере — это часть кода, которая решает конкретную задачу или несколько, объединённых одной целью задач. Функция может вызываться без дополнительного дублирования кода. Человек способен помнить, воспринимать и использовать ограниченное число информации. Для облегчения понимания и создания кода его структурируют — дробят на определенные части. Функция — один из вариантов дробления кода — первый шаг к абстракции программы, упрощающий её структуру. Функция включает параметры.
Параметры функции:
- Параметры ввода (Input Parameters или просто In) — может быть сколько угодно.
- Параметры вывода (Output Parameters или просто Out) — может быть сколько угодно.
- Возвращаемое значение (Return value или просто Return) — только одно.
Таким образом, «стандартная» функция «MyFunc» имеет вид:
- return MyFunc (In, In, Out, Out, In, Out,…);
Указанная (Си-подобная) структура функции условна. Любой из параметров может отсутствовать. Функция вообще может не иметь параметров.
Реализация вызовов функции в ассемблере.
Кроме параметров имеет значение алгоритм работы функции, так называемое «соглашение о вызове функции». Оно связано с построением работы в различных операционных системах различных высокоуровневых языков программирования.
В настоящее время существуют целый ряд принятых соглашений (конвенций) о вызове функций (подпрограмм): cdecl (язык Си, С++), pascal (язык Pascal), stdcall (WinApi), fastcall, safecall, thiscall. Все они начинали использоваться в разное время и с разными целями, решая определённые задачи, наиболее приемлемым способом.
Необходимо помнить, что для функции в ассемблере нет определённых договорённостей — вызывать можно так, как удобнее и оптимальнее. Мы рассмотрим наиболее используемые конвенции высокоуровневых языков программирования и их реализации на ассемблере.
Изучить примеры работы компиляторов языков высокого уровня крайне полезно, если вас интересует кодокопание в любом виде (крэкинг, реверс-программирование и др.) .
Передача параметров через регистры.
Самым очевидным, быстрым и простым способом передачи параметров функции в ассемблере может показаться способ передачи параметров через регистры:
Функция (процедура) содержит два параметра:
myFunc (a,b)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | ;Ассемблерный код: mov ax, a; Первый параметр (самый левый) - сверху mov bx, b; Второй параметр call myFunc myFunc: ... (команды, которые могут использовать стек): add ax,1 ; используем первый параметр ; Его адрес в сегменте стека ВР + 4, потому что при выполнении ; команды CALL при вызове функции, в стек поместили адрес возврата - 2 байта для процедуры ; типа NEAR (или 4 - для FAR), а потом еще и ВР - 2 байта (push bp - в начале нашей функции) mov bx,ax ; используем второй параметр (ещё команды) ret ; Возвращаемся к коду вызова функции (call) из основного кода |
Такой код имеет несколько недостатков, основной из которых — ограничение количества параметров (не больше, чем регистров). Есть и другие, менее очевидные сложности, которые мы не будем обсуждать в рамках этой статьи.
Из плюсов передачи параметров (и возврата значений) функции в ассемблере через регистры можно назвать скорость работы кода.
Теперь перейдём к рассмотрению более совершенных методов организации работы функций, которые используются языками высокого уровня.
Конвенция | Передача параметров | Освобождение стека |
CDECL | загоняются в стек слева направо — снизу вверх | вызывающая программа |
PASCAL | загоняются в стек слева направо — сверху вниз | сама процедура |
STDCAL | загоняются в стек слева направо — снизу вверх | сама процедура |
Конвенция вызова функций PASCAL (Pascal, Basic, Fortran и др.).
Параметры загоняются в стек слева направо — сверху вниз, стек очищается вызываемой функцией:
Функция (процедура) содержит пять параметров:
myFunc (a,b,c,d,e)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 | ;Ассемблерный код: push a; Первый параметр (самый левый) - сверху push b; Второй параметр push c; push d; push e; Пятый параметр - снизу call myFunc ... ;Функция содержит пять параметров: myFunc: push bp mov bp,sp; Создаём стековый кадр. В bp - указатель на стековый кадр, регистр bp использовать нельзя! a equ [bp+12]; Первый параметр - сверху ([bp+12]) b equ [bp+10] c equ [bp+8] d equ [bp+6] e equ [bp+4]; Пятый параметр ... ;команды, которые могут использовать стек: mov ax,e ; Cчитать параметр 5 - [bp+4]. Можно и так, но это менее понятно: mov ax,[bp+4] ; Его адрес в сегменте стека ВР + 4, потому что при выполнении ; команды CALL при вызове функции, в стек поместили адрес возврата - 2 байта для процедуры ; типа NEAR (или 4 - для FAR), а потом еще и ВР - 2 байта (push bp - в начале нашей функции) mov bx,с ; считать параметр 3 - [bp+8]. Можно и так, но это менее понятно: mov bx,[bp+8] ; ... ещё команды ... pop bp ret 10 ; Из стека дополнительно извлекается 10 байт - стек освобождает вызываемая функция |
Конвенция вызова функций CDECL (Си, С++ и др.).
Параметры загоняются в стек слева направо — снизу вверх, стек очищается вызывающая функция:
Функция (процедура) содержит пять параметров:
myFunc (a,b,c,d,e)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | ;Ассемблерный код: push e; Пятый параметр - сверху push d; push c; push b;Второй параметр push a; Первый параметр (самый левый) - снизу call myFunc add sp,10; Стек освобождает вызывающая функция ... ;Функция содержит пять параметров: myFunc: push bp mov bp,sp; Создаём стековый кадр. В bp - указатель на стековый кадр, регистр bp использовать нельзя! e equ [bp+12]; Последний параметр - сверху ([bp+12]) d equ [bp+10] c equ [bp+8] b equ [bp+6] a equ [bp+4] ... ;команды, которые могут использовать стек: mov ax,e ; считать пятый параметр - [bp+12]. Можно и так, но это менее понятно: mov ax,[bp+12] ; Его адрес в сегменте стека ВР + 4, потому что при выполнении ; команды CALL при вызове функции, в стек поместили адрес возврата - 2 байта для процедуры ; типа NEAR (или 4 - для FAR), а потом еще и ВР - 2 байта (push bp - в начале нашей функции) mov bx,с ; считать третий параметр - [bp+8]. Можно и так, но это менее понятно: mov bx,[bp+8] ;... ещё команды ... pop bp ret ; Из стека дополнительные байты не извлекаются - стек освободит код вызывающей программы (add sp,10) |
Конвенция вызова функций STDCALL (WinApi : Windows 95 — Windows 10).
Операционная система Windows содержит набор встроенных функций, которые обеспечивают удобство программирования, обслуживания и работы системы — так называемые WinApi — Windows Application programming interfaces.
Функций огромное количество. Они входят в стандартный пакет Windows любой версии и содержаться в библиотеках *.dll, расположенных в системных директориях Windows (System, System32, SysWOW64 — для 64 битной системы). Например, kernel32.dll содержит огромное количество функций, входящих в т.н. «Ядро операционной системы», например MoveFile — «переместить файл».
Для операционной системы Windows (WinApi) был разработана отдельная конвенция вызова функций. Она включила в себя преимущества PASCAL и С (Си) конвенций.
Конвенция STDCALL (WinApi) имеет следующие особенности.
Параметры загоняются в стек слева направо — снизу вверх, стек очищается вызываемая функция.
К слову можно сказать, что возвращаемое функцией WinApi значение содержиться в 32 битном регистре eax (нам он пока не известен, но это расширенный до 32 бит регистр ax).
Функция (процедура) содержит пять параметров:
myFunc (a,b,c,d,e)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | ;Ассемблерный код: push e; Пятый параметр - сверху push d; push c; push b;Второй параметр push a; Первый параметр (самый левый) - снизу call myFunc ... ;---------------------------------------------------------------------- ;Функция содержит пять параметров: myFunc: push bp mov bp,sp; Создаём стековый кадр. В bp - указатель на стековый кадр, регистр bp использовать нельзя! e equ [bp+12]; Последний параметр - сверху ([bp+12]) d equ [bp+10] c equ [bp+8] b equ [bp+6] a equ [bp+4] ... ;команды, которые могут использовать стек: mov ax,e ; считать параметр "a" - [bp+4]. Можно и так, но это менее понятно: mov ax,[bp+4] ; Команды CALL при вызове функции, в стек поместили адрес возврата - 2 байта для процедуры типа NEAR (или 4 - для FAR), а потом еще и ВР - 2 байта (push bp - в начале нашей функции) mov bx,с ; считать параметр "c" - [bp+8]. Можно и так, но это менее понятно: mov bx,[bp+8] ;...ещё команды ... pop bp ret 10 ; Из стека дополнительно извлекается 10 байт - стек освобождает вызываемая функция |
Преимущество C (Си) конвенции по сравнению с PASCAL.
1. Освобождение стека от параметров возлагается на вызывающую процедуру. Это позволяет отимизировать код.
Например, если вызываются несколькл функций подряд, принимающих одни и теже параметры, можно не заполнять стек заново:
1 2 3 4 5 6 7 | .... push b;Второй параметр push a; Первый параметр - снизу call myFunc1 call myFunc2 add sp,4; Стек освобождает вызывающая функция .... |
2. Более просто создавать функции с изменяемым числом параметров (printf).
Преимущество PASCAL конвенции по сравнению с C (Си).
1. Освобождение стека производится внутри кода функции, что упрощает создание исходного кода, несколько уменьшает размер кода при многократном вызове функций.
Реализация конвенции PASCAL встроена в TASM с помощью макро-возможностей. Практическая реализация и код рассмотрены в статье «MS-DOS и TASM 2.0. Часть 18. Упрощаем вызов функции в TASM«.
Преимущество STDCALL конвенции.
1. Создана специально для Windows API, достаточно удобна, поддерживается макрокомандами наиболее используемых для программирования современных Windows приложений ассемблерами MASM32, FASM.
Очень скоро мы перейдём от 16 битного DOS программирования к 32 битному программированию для Windows, где будет использоваться преимущественно именно конвенция STDCALL (WinApi), хотя никто не запрещает Вам использовать любые способы организации вызова функции в ассемблере с любым видом передачи параметров и освобождения стека.