Функционални извиквания, част 1 (Основи)
(Статията на английски език е заимствана от тук, превод - Трубецкой А.)
В тази поредица от статии бих искал да разгледам вътрешната същност на механизма за извикване на функции. Какво се случва зад кулисите, защо кодът се генерира по този начин, както и други пикантни подробности. Тази статия е за компилатори на Microsoft и използвах Visual Studio 2005, въпреки че основната концепция може да се приложи към по-ранни версии на IDE. В допълнение, материалът на статията е приложим само за системи, базирани на процесор Intel x86.
Предполага се, че читателят е запознат с: 1. базова x86 архитектура и регистри. Можете да намерите добро ръководство тук 2. основи на програмирането на С/С++ 3. Среда за разработка Visual Studio 2005.
Опростен изглед на функциите
Няма да обяснявам какво е функция. Бих искал обаче да очертая основната му структура. Функцията има тяло, което съдържа кода, който функцията изпълнява. Функцията може или не може да има аргументи. Една функция може да върне нещо или да не върне нищо. Въпреки че всичко това изглежда очевидно, трябва да се отбележи, че когато някакъв код иска да използва функция, той всъщност се свива с тази функция. Все едно казва: - Хей, функцията сума! Ще ти се обадя и ще ти дам две цели числа, а ти ще ми върнеш сумата от тези цели числа. ДОБРЕ? И функцията sum отговаря на нещо подобно: - OK. Имам цяло число 1 и цяло число 2. Събирам ги. Имам резултат. Сега вземете този резултат.
Това, което току-що описах, се нарича "Calling convention" на жаргона на програмистите. Това е протокол, чрез който извикващият код и извиканата функция се съгласяват да комуникират помежду си.
Малко повече за функциите
Време е да погледнем темата малко по-надълбоко. Компютърът разбира само езика на процесора (машинния код). Следователно всеки код, който пишете, трябва да бъде компилиран до собствен код, преди процесорът да може да го изпълни. За да получите EXE файл, трябва да компилирате и свържете [свързващия] код с любимия си инструмент за разработка като Visual C++ (за да опростя нещата, ще взема предвид само EXE). Генерираният EXE може да съдържа повече от просто изпълним код. Може също да съдържа данни или ресурси. Секцията .text в EXE изображението съдържа изпълнимия код. Когато потребителят стартира програмата, операционната система извършва всички необходими действия, за да стартира машинния код, намиращ се в секцията .text. Това стартира основната нишка на приложението. EXE може също да създаде свои собствени нишки. Основната нишка обаче управлява живота на EXE. Когато основната нишка приключи, целият процес се прекратява.
Всяка нишка, създадена от EXE, се изпълнява независимо от това какво се случва с други нишки. По същество всяка нишка има указател към инструкцията, която следва да бъде изпълнена. При процесорите x86 този указател се намира в EIP регистъра. Указателят на регистъра на EIP просто сочи към местоположението в EXE, където се намира инструкцията за процесора. Процесорът зарежда инструкция от мястото, указано от EIP регистъра, и я изпълнява. Съдържанието на EIP регистъра не може да бъде променено изрично, но се актуализира в следните ситуации:
1. Процесорът е завършил изпълнението на инструкцията. Една инструкция може да съдържа много байтове изпълним код. Въпреки това, процесорът знае колко байта изисква една инструкция и по този начин може да измести показалеца с необходимия брой байтове след всяка инструкция. 2. Изпълнена е инструкция за връщане (връщане). 3. Инструкцията за повикване е изпълнена.
Какво ще кажете за данните, върху които работи кодът? Данните могат да бъдат локални или външни за тялото на функцията. Данните, които са външни за функцията (глобални или статични променливи), в повечето случаи се поставят в определен раздел на EXE файла (.data). Всички локални променливи се създават на специално определено място, което се нарича стек. Стекът е област от паметта, запазена от операционната система за нишка. Стекът се разширява или свива, когато функциите се извикват или завършват изпълнението. В допълнение към локалните променливи, аргументите, предадени на функцията, също се поставят в стека.
Пример за местоположението на EXE в паметта с една работеща нишка е показан по-долу. Това е пример на просто конзолно приложение с функции main и sum.

Когато се изпълнява EXE, зареждащото устройство на операционната система картографира EXE (и всички зависими DLL файлове) към разпределените му 4 гигабайта памет [4GB пясъчна кутия]. По време на жизнения цикъл на приложението EXE изпълнява всичко в рамките на тази специална област [sandbox] и по този начин не засяга други работещи процеси. Всички достъпи до паметта, които се случват по време на изпълнението на EXE, се отнасят до различни места в паметта в тези 4 гигабайта. Самият код се намира в тези 4 гигабайта. Стекът също е в тази област. Същото може да се каже за ресурсите, динамично разпределената памет и т.н.
Когато функцията за сумиране бъде извикана и изпълнението започне, EIP и ESP указателите се актуализират, както е показано на следващата фигура. Обърнете внимание на отместването на EIP указателя от основната функция към функцията за сумиране и отместването на ESP, като вземете предвид аргументите, предадени на функцията за сумиране.

Функции,поглед към нивото на дизасемблера
Стига теория, време е да проучим функциите в действие. За да получите желания резултат, трябва да използвате само конфигурацията Debug (в конфигурацията Release настройките на проекта са такива, че се извършва интензивна оптимизация, което затруднява анализирането на определени части от кода).
* Стартирайте Visual Studio 2005. Изберете типа проект Win32 и използвайте шаблона Win32 Console Application. Въведете сумата на името на проекта и щракнете върху Готово, за да завършите създаването на проекта. * Отворете свойствата на проекта и за да улесните четенето по-нататък, изключете тези настройки, които позволяват на компилатора да премахне част от кода, което ще направи тази статия по-трудна за разбиране. Ще се опитам да изброя необходимите настройки: - Отворете раздела Configuration Properties->C/C++->General. Тук Debug Information Format задайте Program Database(/Zi). - Отворете раздела Свойства на конфигурацията->C/C++->Генериране на код. Тук задайте Основни проверки по време на изпълнение на По подразбиране. - Отворете раздела Свойства на конфигурацията->Linker->Общи. Тук задайте Enable Incremental Linking на No. * Въведете кода, както е показано на следващата фигура, и задайте точка на прекъсване на 13-ия ред:

* Изграждане на проекта (елемент от менюто Изграждане на решение). * Натиснете F5, за да стартирате програмата под дебъгера. Изпълнението на програмата ще спре на 13-ти ред. * Натиснете Alt+5. Ще се появи прозорец, показващ съдържанието на [Прозорец на регистрите]. * Натиснете Alt+6. Появява се прозорец, показващ съдържанието на [Memory Watch Window]. * Отидете на ред 13, извикайте контекстното меню и изберете Go To Disassembly. * В дизасемблера отворете отново контекстното меню и се уверете, че следните са отметнатиелементи: - Показване на адрес - Показване на изходния код - Показване на кодови байтове
Сега да започнем нашето изследване. Помислете за следната фигура:

След това направете следното:

И така, ето какво научихме: