awesome-everything EN
↑ Обратно к восхождению

Базовый CS с нуля

Что такое функция

Суть Функция — именованный блок инструкций, к которому CPU может перейти и из которого вернуться. Имя делает блок многократно используемым и даёт его назначению одно слово.
◷ 18 min

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

Этот переход называется вызовом функции. Это механизм, стоящий за каждым подпрограммой, методом и процедурой в каждом языке программирования. Прежде чем смотреть, что происходит со стеком при вызове функции — это тема следующего урока — нужно понять, что такое функция на уровне машины: помеченный адрес в памяти, цель перехода и точка возврата.

Цель

После этого урока ты сможешь определить функцию как именованный блок инструкций, объяснить, что значит «вызвать» функцию в терминах перехода CPU, объяснить, что значит «вернуться» (прыжок назад к месту вызова), и назвать две практические причины существования функций: именование и повторное использование.

1

Инструкции живут по адресам, а функция — это метка для одного из них. Ты знаешь из Блока 02, что память — ряд пронумерованных ячеек. Инструкции — это просто данные, лежащие в этих ячейках. CPU читает их одну за другой, следуя счётчику команд. Функция — не что иное как блок последовательных инструкций, начинающийся с определённого адреса, и имя, присвоенное этому адресу.

В машинном коде имена в рантайме не существуют — CPU знает только адреса. Но компилятор и ассемблер переводят имя, написанное в исходном коде, в адрес, где живут эти инструкции. Когда ты пишешь greet() в TypeScript, компилятор создаёт инструкцию машинного кода CALL, содержащую адрес первой инструкции greet. Имя — удобство для людей; аппаратура видит адрес.

2

Вызов функции: CPU переходит к первой инструкции функции. Вызов — это особый переход. Как и условные и безусловные переходы из Блока 07 об управлении потоком, вызов изменяет счётчик команд, чтобы он указывал на другой адрес. Разница в том, что вызов также записывает, откуда он пришёл, — адрес инструкции сразу после вызова, — чтобы CPU мог найти дорогу обратно.

Конкретно:

  • До вызова: счётчик команд указывает на инструкцию CALL.
  • Инструкция CALL выполняется: счётчик команд устанавливается на первую инструкцию функции. Адрес возврата (инструкция сразу после CALL) сохраняется.
  • Инструкции функции выполняются одна за другой.
  • Когда функция завершается, срабатывает инструкция RET: счётчик команд восстанавливается до сохранённого адреса возврата. Выполнение продолжается ровно с того места, где остановилось.
3

Возврат: переход назад к вызывающей стороне. Инструкция RET — это выход из функции. Она читает сохранённый адрес возврата и устанавливает туда счётчик команд. С точки зрения CPU возврат — просто ещё один переход, но на этот раз адрес назначения был записан в момент вызова, а не написан в исходном коде.

Термин caller (вызывающая сторона) означает код, который выдал CALL. Термин callee (вызываемая сторона) означает вызываемую функцию. После срабатывания RET управление возвращается к вызывающей стороне, которая продолжает выполнение с инструкции сразу после вызова.

Почему это работает

Зачем аппаратуре сохранять адрес возврата, вместо того чтобы использовать обычный переход? Потому что одна и та же функция может быть вызвана из многих разных мест программы. Функция greet, вызванная на строке 10, должна вернуться к продолжению строки 10; та же greet, вызванная на строке 50, должна вернуться к продолжению строки 50. Фиксированная цель перехода могла бы вернуться только в одно место. Запись адреса возврата при каждом вызове позволяет одному телу функции обслуживать вызывающих из любого места программы.

4

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

Именование. Имя функции превращает последовательность низкоуровневых операций в понятное человеку намерение. sendEmail(to, subject, body) говорит, что делает, не перечисляя как. Читатель понимает цель, не читая реализацию. Детали уровня машины остаются изолированными внутри тела функции.

Повторное использование. Без функций каждое место в программе, которому нужна одна и та же логика, требовало бы её копии. Копии расходятся, когда баг исправляется в одной, но не в других. С функцией существует ровно одна копия инструкций по ровно одному адресу в памяти. Каждый вызывающий прыгает на этот адрес. Исправление функции исправляет всех вызывающих сразу.

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

...
10
CALL
11
...
12
...
13
...
...
ADD
40
MUL
41
RET
42
CALL по адресу 11 устанавливает счётчик команд на адрес 40 (тело функции). RET по адресу 42 восстанавливает счётчик команд на адрес 12 (инструкция сразу после CALL).
Разбор примера

Трассировка вызова и возврата по адресам.

Предположим, память содержит:

  • Адрес 20: CALL 80 (вызвать функцию, тело которой начинается по адресу 80)
  • Адрес 21: ADD R0, 1 (инструкция, выполняемая после возврата из функции)
  • Адрес 80: MUL R1, 2 (первая инструкция функции)
  • Адрес 81: RET (последняя инструкция функции)

Шаг 1 — Счётчик команд на 20. CPU читает CALL 80. Он сохраняет 21 как адрес возврата (следующая инструкция после вызова), затем устанавливает счётчик команд на 80.

Шаг 2 — Счётчик команд на 80. CPU выполняет MUL R1, 2. Счётчик команд продвигается на 81.

Шаг 3 — Счётчик команд на 81. CPU выполняет RET. Читает сохранённый адрес возврата (21) и устанавливает счётчик команд на 21.

Шаг 4 — Счётчик команд на 21. CPU выполняет ADD R0, 1. Нормальное выполнение продолжается. Тело функции по адресам 80–81 полностью завершено.

Тело функции по адресам 80–81 может быть вызвано из любого адреса программы. Каждый вызов сохраняет собственный адрес возврата, поэтому каждый вызов возвращается в правильное место.

Частая ошибка

Распространённая ошибка в ментальной модели — думать, что вызов функции похож на копирование тела функции в точку вызова. Это не так. Тело функции существует один раз по одному фиксированному адресу. Инструкция CALL — это переход на этот адрес. Когда функция возвращается, управление возвращается туда, где был вызывающий. Ничего не дублируется. Это различие важно при рассуждении о рекурсии позже: несколько вызовов одной функции — это несколько переходов по одному и тому же адресу, каждый со своим сохранённым адресом возврата.

Практика 0 / 5

Тело функции начинается по адресу 200 и заканчивается RET по адресу 210. Сколько адресов занимает эта функция?

Инструкция CALL находится по адресу 50. Инструкция сразу после CALL — по адресу 51. Какой адрес возврата сохраняет CALL?

Одна и та же функция вызывается из адреса 30 и из адреса 90. Сколько копий тела функции существует в памяти?

Функция вызывается 5 раз за время выполнения программы. Сколько раз выполняется её инструкция RET?

После выполнения RET счётчик команд устанавливается на адрес возврата, сохранённый в момент вызова. Если CALL был по адресу 100, на какой адрес устанавливается счётчик команд после RET?

Проверь себя
Викторина

Что делает CPU, выполняя инструкцию CALL?

Итог

Функция — именованный блок инструкций по фиксированному адресу в памяти. Вызов функции означает выполнение инструкции CALL, которая сохраняет адрес возврата (инструкция сразу после вызова) и затем устанавливает счётчик команд на первую инструкцию функции. Возврат означает, что инструкция RET читает сохранённый адрес возврата и устанавливает счётчик команд обратно на него — управление возвращается к вызывающей стороне ровно с того места, где она остановилась. Одно и то же тело функции может быть вызвано из любого количества мест, потому что каждый вызов сохраняет свой адрес возврата. Функции существуют по двум причинам: именование (имя функции выражает намерение, не раскрывая реализацию) и повторное использование (одна копия тела обслуживает каждого вызывающего).

Продолжить восхождение ↑Стек вызовов
хоткеи развернуть
поиск
K
пред. пьеса
k
след. пьеса
j
тиры
t
это меню
?
sources3
expand
  1. 01
  2. 02
  3. 03

Trademarks belong to their respective owners. Editorial reference only.