Изменения

Перейти к: навигация, поиск

Выполнение программы

14 495 байт добавлено, 06:03, 17 июля 2011
м
Проблема, возникающая при использовании многих библиотек
==Последовательное выполнение==
При старте процесса создается единица выполнения кода {{- --}} ''поток''. Поток имеет точку Потоку передается точка входа {{--- }} адрес нахождения первой команды программы в адресном пространстве, и счетчик команд. Каждая команда занимает несколько байт, не обязательно постоянное число. Можно считать, что поток передает команду и ее аргументы процессору, тот выполняет ее и сообщает результат, после этого поток переходит на следующую команду с помощью счетчика команд. Каждая команда в адресном пространстве занимает несколько байт, не обязательно постоянное число. {{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>. Хорошим объяснением того, почему конвенции вызова используют обратный порядок передачи аргументов являются эллипсисы (функции с неопределенным количеством аргументов).  {{TODO|t=запилить картинку пример программы с эллипсисом}} ===Возвращаемое значение===Возвращаемое функцией значение тоже храниться на стеке, рядом с локальными переменными.{{TODO|t=дописать}}{{TODO|t=это мы вроде так для простоты считаем, ссылка на правду}}===Конвенции вызова=======Отличия конвенций====Заметим, что мы положили аргументы на стек, но не удалили их оттуда, поэтому при следующем вызове <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 //смещает вершину стека на 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>, скорее всего, не являющийся валидным. =Вызов =Размещение локальных переменных== [[Файл:locals.png|right|thumb|200px|<center>Стек при выполнении функции</center>]] В функциях для выполнения промежуточных вычислений, временного хранения данных и пр. могут быть использованы локальные переменные. Эти переменные являются временными и создаются при вызове функции, а удаляются при завершении ее выполнения. Для использования локальных переменных необходимо выделить память. В С++ память для всех локальных переменных функции выделяется при начале ее выполнения, и удаляется перед выполнением команды <tex>ret</tex>, чтобы в вершине стека снова оказался адрес возврата. С помощью функции <tex>\_alloca()</tex> можно увеличить размер стека для размещения локальных данных. Так как объем стека ограничен,при использовании этой функции может возникнуть переполнение стека. Простейший пример кода, который вызовет переполнение: void f(int size) { for (;;) { _alloca(size); } }Однако, переполнение может возникнуть и в менее очевидном случае: void f(int size) { _alloca(size); } int main() { int size = 239; for(;;) { f(size); } }Для упрощения выполнения, компилятор может посчитать, что функция <tex>f</tex> имеет свойство <tex>inline</tex>, и преобразовать код в следующий: int main() { int size = 239; for(;;) { _alloca(size); } }, что, в свою очередь, вызовет переполнение. ==Архив== {{TODO|t=включить в главу про кучу}} Динамическая память - это память, получающаяся из свободной памяти при выделении памяти. Слово "динамическая" здесь означает "динамически распределяемая". Динамическая память выделяется и освобождается функциями <tex>VirtualAlloc</tex> и <tex>VirtualFree</tex>. Вызывая <tex>VirtualAlloc</tex>, указывая размер блока памяти и желаемый атрибут доступа (обычно: чтение-запись). Система выделяет от свободной памяти блок. Теперь в программе выделена память, и есть указатель на нее. Когда память надо освободить - вызывайте <tex>VirtualFree</tex>. Система переведёт память обратно в свободную.Чем плохо такое выделением памяти? Оно выделяет больше памяти, чем нужно, и происходит частое обращение к ядру. Для этого в языке c++ есть особенные функции, для работы с областью памяти - куча "heap". В стандартной библиотеке, пришедшей из языка C, <tex>libc</tex> реализованы функции <tex>malloc()</tex> и <tex>free()</tex>, соответственно для выделения и освобождения памяти. В самом C++ есть аналогичные функции <tex>new (new[])</tex> и <tex>delete(delete[])</tex>. Для каждого <tex>malloc/new/new[]</tex> должны вызываться <tex>free/delete/delete[]</tex>, т.к. память сама не освобождается при выходе из функций. Не вызвав эти функции, куча останется неосвобожденнной, и произойдут утечки памяти. {{TODO|t=запилить картиночки, поправить корявые фразочки}} Рассмотрим процесс выделения памяти в куче на примере простой программы: [[Файл:vmmap_before.png|right|thumb|200px|Память не выделена]][[Файл:vmmap_after.png||right|thumb|200px|Выделена память под массив]][[Файл:vmmap_delete.png||right|thumb|200px|Память освобождена]]  int main() { int *a =new int [1000000]; delete [] a; return 0; } До выполнения int *a = new int [1000000];куча в адресном пространстве программы занимает <tex>1152kB</tex>, а после выполнения команды - <tex>5060kB</tex>. Значит, память была выделена именно в куче, и объем выделенной памяти равен <tex>3908kB</tex>, что почти в точности соответствует объему массива <tex>int</tex> из <tex>1000000</tex> элементов при выделении <tex>4B</tex> на элемент.  После выполнения delete [] a;размер кучи снова равен <tex>1152kB</tex>, как и до выделение памяти под массив.  {{TODO|t=Показать в адресном пространстве ран-тайм, библиотеки ядра}} ==Проблема, возникающая при использовании многих библиотек=={{TODO|t=насколько я помню, это позиционировалось не столько как проблема, больше - просто фича. Поправить}}{{TODO|t=запилить картиночки, поправить корявые фразочки}} Для начала рассмотрим пример программы, где используются две библиотеки: пользовательская (<tex>a.dll</tex>) и стандартная (<tex>msvcrt.dll</tex>). В случае, когда библиотека <tex>a.dll</tex> использует для работы с кучей библиотеку <tex>msvcrt.dll</tex>, никаких проблем не возникнет, т.к. при работе программы куча будет одна и функции для работы с ней будут одинаковые, что в пользовательской библиотеке, что в нашей программе.  Теперь рассмотрим другую ситуацию, когда в программе используется всё та же <tex>msvcrt.dll</tex>, но в <tex>a.dll</tex> пусть используется другая стандартна библиотека для работы с кучей: <tex>msvcrtr.dll</tex>.Здесь может случится неприятная вещь: если мы будем пытаться освободить память из кучи, используя средства пользовательской библиотеки, у нас произойдет ошибка, т.к. кучи для библиотеки и программы будут разные и функция <tex>free</tex> может просто не понять, от куда ей надо освобождать, потому что в библиотеке она использовала <tex>new</tex> из <tex>msvcrtr.dll</tex>, а при вызове <tex>delete</tex> из программы мы обратимся к <tex>msvcrt.dll</tex>. Согласитесь, не очень приятная ошибка. Для борьбы с ней есть верный способ --- использовать статическое объявление функций из стандартных библиотек: мы "выдёргиваем" только те функции, которые нас интересуют и объявляем их через <tex>static</tex>. У нас будет явное обращение к ним и тогда не произойдет ошибки, возникающей во втором примере.
97
правок

Навигация