Выполнение программы — различия между версиями

Материал из Викиконспекты
Перейти к: навигация, поиск
(Размещение локальных переменных)
(Размещение локальных переменных)
Строка 91: Строка 91:
 
Так как стек ограничен в своем обьеме, то можно увидеть его переполнение запустив примерно следующий код:
 
Так как стек ограничен в своем обьеме, то можно увидеть его переполнение запустив примерно следующий код:
  
  f()
+
  void f()
     _alloca
+
{
  g()
+
     _alloca(size);
 +
}
 +
   
 +
void g()
 +
{
 
     for(;;)
 
     for(;;)
       f()
+
       f();
  g
+
}
 +
 +
  g();
  
 
==Архив==
 
==Архив==

Версия 01:57, 12 июля 2011

Эта статья находится в разработке!

Последовательное выполнение

При старте процесса создается единица выполнения кода — поток. Потоку передается точка входа — адрес нахождения первой команды программы в адресном пространстве. Можно считать, что поток передает команду и ее аргументы процессору, тот выполняет ее и сообщает результат, после этого поток переходит на следующую команду с помощью счетчика команд. Каждая команда в адресном пространстве занимает несколько байт, не обязательно постоянное число.


TODO: запилить пруфлинк на то, как на самом деле

TODO: можно написать про переключение потоков

Вызов функций

Адрес возврата

Вызов функции

Представим, что в нашем коде есть функция, не принимающая аргументов, ничего не возвращающая, и ничего не выполняющая:

void f()
{
}

Для ее вызова в адресном пространстве будет использоваться команда [math]call f[/math], которая переведет поток на начало функции [math]f[/math], а для возврата обратно в место, откуда функция была вызвана — команда [math]ret[/math]. Возникает вопрос: как найти адрес команды, к которой надо перейти после выполнения [math]ret[/math]? Можно сохранить этот адрес в переменной, но, в таком случае, нельзя будет вызывать функции из других функций, или создавать рекурсии:

void f()
{
}

void g()
{
  f();
}

Поэтому, в адресном пространстве существует стек, на который при вызове функции кладется адрес возврата. Команда [math]ret[/math] удаляет текущий адрес возврата с вершины стека, и устанавливает счетчик команд по этому адресу.

Передача параметров

Представим, что теперь нам необходимо вызвать чуть более сложную функцию:

void f(int a, int b)
{
}

Передавать аргументы внутрь функции удобно, также используя стек. Во всех конвенциях вызова C++ сначала передаются аргументы в обратном порядке, а потом - адрес возврата, то есть, вызов [math]f(239, 566)[/math] будет исполнен как

push 566
push 239
call f

, где [math]push[/math] [math]n[/math] — команда "положить на стек n". Адрес возврата положится на стек при вызове команды [math]call[/math].

Конвенции вызова

Отличия конвенций

Заметим, что мы положили аргументы на стек, но не удалили их оттуда, поэтому при следующем вызове [math]ret[/math] будет взят неверный адрес возврата. В С++ существует несколько конвенций вызова функций, рассмотрим 2 из них: [math]\_\_stdcall[/math] и [math]\_\_cdecl[/math].

При использовании [math]\_\_stdcall[/math] аргументы удалятся из стека при выполнении команды [math]ret[/math], а при использовании [math]\_\_cdecl[/math] после возврата из функции необходимо переместить вершину стека командой [math]add[/math]. Рассмотрим пример:

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;
}

При выполнении этого кода можно будет сэкономить на количестве команд, не удаляя аргументы [math]2[/math] и [math]3[/math] со стека:

push 3
push 2
call f
push 1
call g
add esp, 12 //смещает вершину стека на 12 байт

Конфликт конвенций

По умолчанию в С++ используются конвенции вызова [math]\_\_cdecl[/math] и [math]\_\_thiscall[/math], которая используется для методов класса и отличается тем, что кладет один аргумент в регистр процессора.

Посмотрим, что произойдет, если искусственно создать конфликт конвенций вызова:

void __decl f(int a)
{
}

int main()
{
  void __stdcall (*g)(int);
  g = (void __stdcall (*)(int)) &f;
  g(239);
  return 0;
}

Так как [math]g[/math] задана конвенция вызова [math]\_\_stdcall[/math], после ее вызова вершина стека не будет сдвигаться. Однако, [math]f[/math] задана конвенция вызова [math]\_\_cdecl[/math], поэтому аргумент не удалится со стека при вызове [math]ret[/math], и при следующем запросе адреса возврата будет выдан адрес [math]239[/math], скорее всего, не являющийся валидным.

Размещение локальных переменных

Локальные переменные в стеке

В функциях также могут быть использованы локальные переменные, для выполнения промежуточных вычислений. Эти переменные являются временными и создаются при вызове функции. Для использования локальных переменных необходимо выделить память. Эта память также выделяется на стеке, прибавлением памяти на нем.

С помощью функции

_alloca

можно прибавлять память на стек.

Так как стек ограничен в своем обьеме, то можно увидеть его переполнение запустив примерно следующий код:

void f()
{
   _alloca(size);
}

void g()
{
   for(;;)
      f();
}

g();

Архив

Рассмотрим процесс выделения памяти в куче на примере простой программы:

Память не выделена
Выделена память под массив
Память освобождена
int main()
{
    int *a = new int [1000000];
    delete [] a;
    return 0;
}

До выполнения

int *a = new int [1000000];

куча в адресном пространстве программы занимает [math]1152kB[/math], а после выполнения команды - [math]5060kB[/math]. Значит, память была выделена именно в куче, и объем выделенной памяти равен [math]3908kB[/math], что почти в точности соответствует объему массива [math]int[/math] из [math]1000000[/math] элементов при выделении [math]4B[/math] на элемент.

После выполнения

delete [] a;

размер кучи снова равен [math]1152kB[/math], как и до выделение памяти под массив.


TODO: Показать в адресном пространстве ран-тайм, библиотеки ядра