Вызов функций: call и ret
Урок объясняет, как процессор вызывает функцию и возвращается обратно через стек.
call сохраняет на стеке адрес следующей команды и прыгает в функцию; ret забирает этот адрес и возвращается.
Проблема возврата
Когда мы прыгаем в функцию через jmp, мы не помним, откуда пришли. А функцию вызывают из разных мест, и вернуться нужно именно туда, откуда позвали. Решение — сохранять адрес возврата на стеке.
Что делает call
Команда call func — это два действия в одной: push адреса следующей команды и jmp в функцию:
; call func эквивалентен:
push rip_next ; сохранить адрес возврата на стек
jmp func ; прыгнуть в функцию
; ret эквивалентен:
pop rip ; забрать адрес возврата и прыгнуть на негоТаким образом стек запоминает «хлебные крошки» — цепочку адресов, куда возвращаться. Это и есть стек вызовов, который вы видите в отладчике.
Простой пример
_start:
call dvoin ; вызвать функцию, rax удвоится
; ... сюда вернёт ret
dvoin:
add rax, rax ; rax = rax * 2
ret ; вернуться по адресу со стекаКак работает под капотом
Промоделируем call/ret на Python, используя список как стек адресов возврата:
call_stack = []
def call(return_addr):
call_stack.append(return_addr) # push адрес возврата
print("вошли в функцию, на стеке:", call_stack)
def ret():
addr = call_stack.pop() # pop адрес возврата
print("вернулись по адресу:", addr)
call(0x401050) # call func
ret() # retВывод:
вошли в функцию, на стеке: [4198480] вернулись по адресу: 4198480
Адрес 0x401050 равен 4198480 в десятичном виде — это и есть точка, куда вернётся ret. Когда функция вызывает другую функцию, на стеке копится несколько таких адресов: получается стек вызовов, по которому отладчик строит «трассировку».
Частые ошибки
- Менять
rspвнутри функции и не восстановить передret. Тогдаretзаберёт «не тот» адрес и прыгнет в никуда — крах. - Делать
pushбез парногоpopдоret. Адрес возврата окажется погребён под лишними данными. - Вызывать функцию через
jmpвместоcall. Тогда адрес возврата не сохранится, иretуведёт неизвестно куда.
Итог
call= сохранить адрес возврата на стек + прыгнуть в функцию.ret= забрать адрес возврата со стека и прыгнуть на него.- Стек хранит цепочку адресов возврата — это стек вызовов.
- Баланс стека критичен: лишний
pushбезpopломает возврат.