Я пишу ОС для хобби и столкнулся с множеством ошибок, которые требовали от меня понимания того, как функции вызывают друг друга в x86. Мне нужно вызывать функции C из сборки, вызывать функции сборки из C и полностью обходить вызовы функций при входе в обработчики прерываний или выходе из них.

Я решил немного поработать и сделать шпаргалку, которая поможет мне вспомнить, как все это работает. Я нашел действительно удобный веб-сайт под названием Compiler Explorer, который позволяет вам просматривать сборку любого кода C в вашем браузере. Я использовал его для этого краткого справочника:

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

Шаг за шагом

Я сейчас пройдусь по шагам, но это имеет смысл, когда вы также смотрите визуализацию.

Предположим, у вас есть одна функция foo (), которая вызывает другую функцию bar ():

int bar(int arg1, int arg2, int arg3) {
  return arg1;
}
int foo() {
  int local_var = bar(1, 2, 3);
  local_var += 5;
  return local_var;
}
  1. Первое, что делает foo () (и первое, что делает каждая функция) - это помещает базовый указатель в стек, а затем сохраняет указатель стека в базовый указатель. Базовый указатель указывает на начало кадра стека. Сразу после него идут локальные переменные текущей функции. Прямо перед ним обратный адрес.
  2. foo () увеличивает стек, чтобы освободить место для локальных переменных.
  3. foo () сохраняет аргументы функции bar () в регистрах. (Если бы аргументы были слишком велики, чтобы поместиться в регистр, вместо этого они были бы помещены в стек.
  4. foo () вызывает bar (). Это помещает адрес возврата в стек и переходит к первой инструкции bar ().
  5. Как и foo (), bar () первым делом помещает базовый указатель в стек и сохраняет указатель стека в базовом указателе. (Эта цепочка указателей также используется при трассировке стека).
  6. bar () перемещает свои аргументы из регистров в стек.
  7. bar () сохраняет возвращаемое значение в регистр eax.
  8. Инструкция leave копирует базовый указатель в указатель стека, а затем выталкивает сохраненный базовый указатель обратно в ebp.
  9. Инструкция ret извлекает адрес возврата из стека и переходит к нему.
  10. Вернувшись в foo (), возвращаемое значение перемещается из регистра eax в область стека, зарезервированную для локальных переменных. foo () завершает свои последние инструкции, затем следует тем же процедурам, что и bar (), чтобы вернуться к своему собственному вызывающему объекту.