Я пишу ОС для хобби и столкнулся с множеством ошибок, которые требовали от меня понимания того, как функции вызывают друг друга в 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; }
- Первое, что делает foo () (и первое, что делает каждая функция) - это помещает базовый указатель в стек, а затем сохраняет указатель стека в базовый указатель. Базовый указатель указывает на начало кадра стека. Сразу после него идут локальные переменные текущей функции. Прямо перед ним обратный адрес.
- foo () увеличивает стек, чтобы освободить место для локальных переменных.
- foo () сохраняет аргументы функции bar () в регистрах. (Если бы аргументы были слишком велики, чтобы поместиться в регистр, вместо этого они были бы помещены в стек.
- foo () вызывает bar (). Это помещает адрес возврата в стек и переходит к первой инструкции bar ().
- Как и foo (), bar () первым делом помещает базовый указатель в стек и сохраняет указатель стека в базовом указателе. (Эта цепочка указателей также используется при трассировке стека).
- bar () перемещает свои аргументы из регистров в стек.
- bar () сохраняет возвращаемое значение в регистр
eax
. - Инструкция
leave
копирует базовый указатель в указатель стека, а затем выталкивает сохраненный базовый указатель обратно вebp
. - Инструкция
ret
извлекает адрес возврата из стека и переходит к нему. - Вернувшись в foo (), возвращаемое значение перемещается из регистра
eax
в область стека, зарезервированную для локальных переменных. foo () завершает свои последние инструкции, затем следует тем же процедурам, что и bar (), чтобы вернуться к своему собственному вызывающему объекту.