Стек: push и pop

Урок знакомит со стеком — временным складом данных, на котором держатся вызовы функций.

Стек — область памяти, работающая по принципу «последним пришёл — первым вышел» (LIFO), с вершиной в регистре rsp.

Зачем нужен стек

Регистров всего 16, а данных в программе много. Стек — это удобное временное хранилище: можно положить значение «наверх» командой push и забрать его позже командой pop. На стеке держатся локальные переменные, сохранённые регистры и адреса возврата из функций.

push и pop

push rax уменьшает rsp на 8 и записывает rax по новому адресу. pop rbx читает значение с вершины и увеличивает rsp на 8:

push rax        ; rsp -= 8; [rsp] = rax
push rbx        ; положили ещё одно значение сверху
pop  rcx        ; rcx = [rsp]; rsp += 8 (забрали rbx)
pop  rdx        ; rdx = [rsp]; rsp += 8 (забрали rax)

Порядок важен: что положили последним, то и заберём первым. Поэтому здесь rcx получит бывшее rbx, а rdx — бывшее rax.

Стек растёт вниз

Любопытная деталь: при push адрес уменьшается. Стек растёт от старших адресов к младшим — навстречу остальной памяти. Схема:

  высокие адреса
     |  ... старые данные ...
     |  [значение 1]   <- положили первым
     |  [значение 2]   <- rsp (вершина) после второго push
     v
  низкие адреса

Как работает под капотом

Смоделируем стек обычным списком Python, где append/pop ведут себя как push/pop:

stack = []
stack.append("rax=10")   # push
stack.append("rbx=20")   # push
print("вершина:", stack[-1])
first = stack.pop()      # pop -> забираем последнее
second = stack.pop()     # pop -> забираем предыдущее
print("забрали:", first)
print("потом  :", second)

Вывод:

вершина: rbx=20
забрали: rbx=20
потом  : rax=10

Видно правило LIFO: последним положили rbx, его же первым и достали. Эта дисциплина — основа того, как функции аккуратно сохраняют и восстанавливают данные.

Частые ошибки

  • Несбалансированные push/pop. Сколько положили, столько и заберите, иначе rsp съедет и ret уведёт не туда.
  • Думать, что стек растёт вверх. На x86 push уменьшает адрес — стек растёт вниз.
  • Хранить адрес локальных данных после возврата. После pop/возврата это место уже не ваше.

Итог

  • Стек — временное LIFO-хранилище, вершина в rsp.
  • push уменьшает rsp и пишет значение, pop читает и увеличивает rsp.
  • Стек растёт вниз: push уменьшает адрес.
  • push и pop должны быть сбалансированы, иначе rsp собьётся.
Проверьте себя
1. По какому принципу работает стек?
AПервым пришёл — первым вышел (FIFO)
BПоследним пришёл — первым вышел (LIFO)
CСлучайный доступ
DПо возрастанию значений
2. Что делает push rax с регистром rsp?
AУвеличивает rsp на 8
BУменьшает rsp на 8 и пишет rax по новому адресу
CНе меняет rsp
DОбнуляет rsp
3. В какую сторону растёт стек на x86-64?
AВверх, к старшим адресам
BВниз, к младшим адресам
CНе растёт, фиксированный размер
DВ обе стороны одновременно