Выполнение программы — различия между версиями
(→Последовательное выполнение) |
Phil (обсуждение | вклад) |
||
| Строка 3: | Строка 3: | ||
==Последовательное выполнение== | ==Последовательное выполнение== | ||
| − | При старте процесса создается единица выполнения кода - ''поток''. | + | При старте процесса создается единица выполнения кода {{---}} ''поток''. Потоку передается точка входа {{---}} адрес нахождения первой команды программы в адресном пространстве. Можно считать, что поток передает команду и ее аргументы процессору, тот выполняет ее и сообщает результат, после этого поток переходит на следующую команду с помощью счетчика команд. Каждая команда в адресном пространстве занимает несколько байт, не обязательно постоянное число. |
| + | {{TODO|t=запилить пруфлинк на то, как на самом деле}} | ||
{{TODO|t=можно написать про переключение потоков}} | {{TODO|t=можно написать про переключение потоков}} | ||
| − | + | ==Вызов функций== | |
| − | {{ | + | ===Адрес возврата=== |
| + | [[Файл:Func.png|right|frame|<center>Вызов функции</center>]] | ||
| + | Представим, что в нашем коде есть функция, не принимающая аргументов, ничего не возвращающая, и ничего не выполняющая: | ||
| + | void f() | ||
| + | { | ||
| + | } | ||
| + | Для ее вызова в адресном пространстве будет использоваться команда <tex>call f</tex>, которая переведет поток на начало функции <tex>f</tex>, а для возврата обратно в место, откуда функция была вызвана {{---}} команда <tex>ret</tex>. Возникает вопрос: как найти адрес команды, к которой надо перейти после выполнения <tex>ret</tex>? Можно сохранить этот адрес в переменной, но, в таком случае, нельзя будет вызывать функции из других функций, или создавать рекурсии: | ||
| + | void f() | ||
| + | { | ||
| + | } | ||
| + | |||
| + | void g() | ||
| + | { | ||
| + | f(); | ||
| + | } | ||
| + | Поэтому, в адресном пространстве существует ''стек'', на который при вызове функции кладется адрес возврата. Команда <tex>ret</tex> удаляет текущий адрес возврата с вершины стека, и устанавливает счетчик команд по этому адресу. | ||
| + | ===Передача параметров=== | ||
| + | Представим, что теперь нам необходимо вызвать чуть более сложную функцию: | ||
| + | void f(int a, int b) | ||
| + | { | ||
| + | } | ||
| + | Передавать аргументы внутрь функции удобно, также используя стек. Во всех конвенциях вызова C++ сначала передаются аргументы в обратном порядке, а потом - адрес возврата, то есть, вызов <tex>f(239, 566)</tex> будет исполнен как | ||
| + | push 566 | ||
| + | push 239 | ||
| + | call f | ||
| + | , где <tex>push</tex> <tex>n</tex> {{---}} команда "положить на стек n". Адрес возврата положится на стек при вызове команды <tex>call</tex>. | ||
| + | ===Конвенции вызова=== | ||
| + | ====Отличия конвенций==== | ||
| + | Заметим, что мы положили аргументы на стек, но не удалили их оттуда, поэтому при следующем вызове <tex>ret</tex> будет взят неверный адрес возврата. В С++ существует несколько конвенций вызова функций, рассмотрим 2 из них: <tex>\_\_stdcall</tex> и <tex>\_\_cdecl</tex>. | ||
| + | |||
| + | При использовании <tex>\_\_stdcall</tex> аргументы удалятся из стека при выполнении команды <tex>ret</tex>, а при использовании <tex>\_\_cdecl</tex> после возврата из функции необходимо переместить вершину стека командой <tex>add</tex>. Рассмотрим пример: | ||
| + | void __cdecl f(int a, int b) | ||
| + | { | ||
| + | } | ||
| + | |||
| + | void __cdecl g(int a, int b, int c) | ||
| + | { | ||
| + | } | ||
| + | |||
| + | int main() | ||
| + | { | ||
| + | f(2, 3); | ||
| + | g(1, 2, 3); | ||
| + | return 0; | ||
| + | } | ||
| + | При выполнении этого кода можно будет сэкономить на количестве команд, не удаляя аргументы <tex>2</tex> и <tex>3</tex> со стека: | ||
| + | push 3 | ||
| + | push 2 | ||
| + | call f | ||
| + | push 1 | ||
| + | call g | ||
| + | add esp, 12 | ||
| + | ====Конфликт конвенций==== | ||
| + | По умолчанию в С++ используются конвенции вызова <tex>\_\_cdecl</tex> и <tex>\_\_thiscall</tex>, которая используется для методов класса и отличается тем, что кладет один аргумент в регистр процессора. | ||
| − | == | + | Посмотрим, что произойдет, если искусственно создать конфликт конвенций вызова: |
| + | void __decl f(int a) | ||
| + | { | ||
| + | } | ||
| + | |||
| + | int main() | ||
| + | { | ||
| + | void __stdcall (*g)(int); | ||
| + | g = (void __stdcall (*)(int)) &f; | ||
| + | g(239); | ||
| + | return 0; | ||
| + | } | ||
| + | Так как <tex>g</tex> задана конвенция вызова <tex>\_\_stdcall</tex>, после ее вызова вершина стека не будет сдвигаться. Однако, <tex>f</tex> задана конвенция вызова <tex>\_\_cdecl</tex>, поэтому аргумент не удалится со стека при вызове <tex>ret</tex>, и при следующем запросе адреса возврата будет выдан адрес <tex>239</tex>, скорее всего, не являющийся валидным. | ||
| + | ==Размещение локальных переменных== | ||
Версия 19:51, 11 июля 2011
Содержание
Последовательное выполнение
При старте процесса создается единица выполнения кода — поток. Потоку передается точка входа — адрес нахождения первой команды программы в адресном пространстве. Можно считать, что поток передает команду и ее аргументы процессору, тот выполняет ее и сообщает результат, после этого поток переходит на следующую команду с помощью счетчика команд. Каждая команда в адресном пространстве занимает несколько байт, не обязательно постоянное число.
TODO: запилить пруфлинк на то, как на самом деле
TODO: можно написать про переключение потоков
Вызов функций
Адрес возврата
Представим, что в нашем коде есть функция, не принимающая аргументов, ничего не возвращающая, и ничего не выполняющая:
void f()
{
}
Для ее вызова в адресном пространстве будет использоваться команда , которая переведет поток на начало функции , а для возврата обратно в место, откуда функция была вызвана — команда . Возникает вопрос: как найти адрес команды, к которой надо перейти после выполнения ? Можно сохранить этот адрес в переменной, но, в таком случае, нельзя будет вызывать функции из других функций, или создавать рекурсии:
void f()
{
}
void g()
{
f();
}
Поэтому, в адресном пространстве существует стек, на который при вызове функции кладется адрес возврата. Команда удаляет текущий адрес возврата с вершины стека, и устанавливает счетчик команд по этому адресу.
Передача параметров
Представим, что теперь нам необходимо вызвать чуть более сложную функцию:
void f(int a, int b)
{
}
Передавать аргументы внутрь функции удобно, также используя стек. Во всех конвенциях вызова C++ сначала передаются аргументы в обратном порядке, а потом - адрес возврата, то есть, вызов будет исполнен как
push 566 push 239 call f
, где — команда "положить на стек n". Адрес возврата положится на стек при вызове команды .
Конвенции вызова
Отличия конвенций
Заметим, что мы положили аргументы на стек, но не удалили их оттуда, поэтому при следующем вызове будет взят неверный адрес возврата. В С++ существует несколько конвенций вызова функций, рассмотрим 2 из них: и .
При использовании аргументы удалятся из стека при выполнении команды , а при использовании после возврата из функции необходимо переместить вершину стека командой . Рассмотрим пример:
void __cdecl f(int a, int b)
{
}
void __cdecl g(int a, int b, int c)
{
}
int main()
{
f(2, 3);
g(1, 2, 3);
return 0;
}
При выполнении этого кода можно будет сэкономить на количестве команд, не удаляя аргументы и со стека:
push 3 push 2 call f push 1 call g add esp, 12
Конфликт конвенций
По умолчанию в С++ используются конвенции вызова и , которая используется для методов класса и отличается тем, что кладет один аргумент в регистр процессора.
Посмотрим, что произойдет, если искусственно создать конфликт конвенций вызова:
void __decl f(int a)
{
}
int main()
{
void __stdcall (*g)(int);
g = (void __stdcall (*)(int)) &f;
g(239);
return 0;
}
Так как задана конвенция вызова , после ее вызова вершина стека не будет сдвигаться. Однако, задана конвенция вызова , поэтому аргумент не удалится со стека при вызове , и при следующем запросе адреса возврата будет выдан адрес , скорее всего, не являющийся валидным.
