Низкий уровень: как выглядят вызовы функций по указателю на ASM



В языках высокого уровня, таких как C или C++, часто используются указатели на функции. Это позволяет динамически выбирать, какую функцию вызвать в runtime. Но как это выглядит на уровне ассемблера? Давайте разберемся.



Для начала рассмотрим простой пример на C, где используется указатель на функцию:



int callme() { 

return 1;

}



void main() {

int (*func_ptr)() = callme;

func_ptr();

}


Здесь мы создаем указатель на функцию func_ptr, который указывает на функцию callme, и затем вызываем функцию через этот указатель.



Как это выглядит в ассемблере?



Используем Compiler Explorer, чтобы преобразовать этот код в ассемблер. Вот что получилось:



callme: 

push rbp

mov rbp, rsp

mov eax, 1

pop rbp

ret

main:

push rbp

mov rbp, rsp

sub rsp, 16

mov QWORD PTR [rbp-8], OFFSET FLAT:callme

mov rax, QWORD PTR [rbp-8]

call rax

nop

leave

ret


Что здесь происходит?



Создание указателя на функцию:



В функции main мы видим, что адрес функции callme сохраняется в памяти по адресу [rbp-8]:



mov QWORD PTR [rbp-8], OFFSET FLAT:callme


Здесь OFFSET FLAT:callme — это адрес функции callme в памяти.



Загрузка указателя в регистр:



Затем этот адрес загружается в регистр rax:



mov rax, QWORD PTR [rbp-8]


Вызов функции по указателю:



После этого происходит вызов функции через регистр rax:



call rax


Инструкция call использует значение в регистре rax как адрес функции, на которую нужно перейти.



Пролог и эпилог



Как и в случае с обычным вызовом функции, здесь также присутствуют пролог и эпилог:



Пролог:



push rbp

mov rbp,

rsp sub rsp, 16


Здесь сохраняется значение rbp, устанавливается новый кадр стека и выделяется место для локальных переменных.



Эпилог:



leave ret


Здесь восстанавливается значение rbp и выполняется возврат из функции.



Пример с массивом в функции



Давайте добавим массив в функцию callme и посмотрим, как это повлияет на ассемблерный код:



int callme() { 

char a[128];

return 1;

}


В ассемблере это будет выглядеть так:



callme:

push rbp

mov rbp, rsp

sub rsp, 8 ; <-- обратите внимание, тут сработала Red Zone

mov eax, 1

leave

ret


Здесь видно, что в прологе добавилась инструкция sub rsp, 128, которая выделяет место на стеке для массива a[128].



Вывод



Сегодня мы узнали, как вызов функций по указателю выглядит на уровне ассемблера.



Основные моменты:



Указатель на функцию — это просто адрес функции в памяти.

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



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



Таким образом, даже такие высокоуровневые конструкции, как указатели на функции, имеют свое прямое отражение в ассемблерном коде.