Что происходит, когда запускается компьютерная программа?

Я знаю общую теорию, но я не могу вписаться в детали.

Я знаю, что программа находится во вторичной памяти компьютера. После запуска программы она полностью копируется в ОЗУ. Затем процессор извлекает несколько инструкций (это зависит от размера шины) за раз, помещает их в регистры и выполняет их.

Я также знаю, что компьютерная программа использует два типа памяти: стек и кучу, которые также являются частью первичной памяти компьютера. Стек используется для нединамической памяти, а куча для динамической памяти (например, все, что связано с оператором new в С++)

Я не могу понять, как эти две вещи соединяются. В какой момент используется стек для выполнения инструкций? Инструкции переходят из ОЗУ в стек, в регистры?

Ответ 1

Это действительно зависит от системы, но современные ОС с виртуальной памятью имеют тенденцию загружать изображения процессов и выделять память примерно так:

+---------+
|  stack  |  function-local variables, return addresses, return values, etc.
|         |  often grows downward, commonly accessed via "push" and "pop" (but can be
|         |  accessed randomly, as well; disassemble a program to see)
+---------+
| shared  |  mapped shared libraries (C libraries, math libs, etc.)
|  libs   |
+---------+
|  hole   |  unused memory allocated between the heap and stack "chunks", spans the
|         |  difference between your max and min memory, minus the other totals
+---------+
|  heap   |  dynamic, random-access storage, allocated with 'malloc' and the like.
+---------+
|   bss   |  Uninitialized global variables; must be in read-write memory area
+---------+
|  data   |  data segment, for globals and static variables that are initialized
|         |  (can further be split up into read-only and read-write areas, with
|         |  read-only areas being stored elsewhere in ROM on some systems)
+---------+
|  text   |  program code, this is the actual executable code that is running.
+---------+

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

Обратите внимание, что позиции, например, стека и кучи, могут быть в другом порядке на некоторых системах (см. Billy O'Neal answer ниже для получения дополнительной информации о Win32).

Другие системы могут быть разными. Например, DOS запускается в режиме реального режима, а распределение памяти при запуске программ выглядит по-другому:

+-----------+ top of memory
| extended  | above the high memory area, and up to your total memory; needed drivers to
|           | be able to access it.
+-----------+ 0x110000
|  high     | just over 1MB->1MB+64KB, used by 286s and above.
+-----------+ 0x100000
|  upper    | upper memory area, from 640kb->1MB, had mapped memory for video devices, the
|           | DOS "transient" area, etc. some was often free, and could be used for drivers
+-----------+ 0xA0000
| USER PROC | user process address space, from the end of DOS up to 640KB
+-----------+
|command.com| DOS command interpreter
+-----------+ 
|    DOS    | DOS permanent area, kept as small as possible, provided routines for display,
|  kernel   | *basic* hardware access, etc.
+-----------+ 0x600
| BIOS data | BIOS data area, contained simple hardware descriptions, etc.
+-----------+ 0x400
| interrupt | the interrupt vector table, starting from 0 and going to 1k, contained 
|  vector   | the addresses of routines called when interrupts occurred.  e.g.
|  table    | interrupt 0x21 checked the address at 0x21*4 and far-jumped to that 
|           | location to service the interrupt.
+-----------+ 0x0

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

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

При загрузке программы и необходимых общих libs в память и распределении частей программы в нужные области ОС начинает выполнение вашего процесса, где бы ни находился его основной метод, и ваша программа берет на себя оттуда, делая системные вызовы когда это необходимо.

Различные системы (встраиваемые, независимо) могут иметь очень разные архитектуры, такие как системы без стеков, системы архитектуры Гарварда (с кодом и данными, хранящимися в отдельной физической памяти), системы, которые фактически поддерживают BSS в постоянной памяти (изначально заданный программистом) и т.д. Но это общий смысл.


Ты сказал:

Я также знаю, что компьютерная программа использует два типа памяти: стек и кучу, которые также являются частью первичной памяти компьютера.

"Стек" и "куча" - это просто абстрактные понятия, а не (обязательно) физически разные "виды" памяти.

A stack - это просто последняя структура данных. В архитектуре x86 на самом деле его можно направить случайно, используя смещение от конца, но наиболее распространенными функциями являются PUSH и POP для добавления и удаления элементов из него, соответственно. Он обычно используется для локальных переменных-функций (так называемое "автоматическое хранилище" ), аргументов функции, обратных адресов и т.д. (Более подробно)

A "heap" - это просто псевдоним для фрагмента памяти, который может быть выделен по требованию, и рассматривается случайным образом (что означает, вы можете получить доступ к любому местоположению в нем напрямую). Он обычно используется для структур данных, которые вы выделяете во время выполнения (на С++, используя new и delete, и malloc и друзей на C и т.д.).

Стек и куча на архитектуре x86 физически находятся в вашей системной памяти (RAM) и отображаются через распределение виртуальной памяти в адресное пространство процесса, как описано выше.

регистрирует (все еще на x86), физически находится внутри процессора (в отличие от ОЗУ) и загружается процессором, из области TEXT (и также может быть загружен из другого места в памяти или в других местах в зависимости от исполняемых команд ЦП). Они по сути являются очень маленькими, очень быстрыми ячейками памяти на кристалле, которые используются для различных целей.

Макет регистра сильно зависит от архитектуры (на самом деле, регистры, набор команд и макет/дизайн памяти - это именно то, что подразумевается под "архитектурой" ), и поэтому я не буду расширять его, но рекомендую вы берете курс ассемблера, чтобы лучше понять их.


Ваш вопрос:

В какой момент используется стек для выполнения инструкций? Инструкции переходят из ОЗУ в стек, в регистры?

Стек (в системах/языках, которые имеют и используют их) чаще всего используется следующим образом:

int mul( int x, int y ) {
    return x * y;       // this stores the result of MULtiplying the two variables 
                        // from the stack into the return value address previously 
                        // allocated, then issues a RET, which resets the stack frame
                        // based on the arg list, and returns to the address set by
                        // the CALLer.
}

int main() {
    int x = 2, y = 3;   // these variables are stored on the stack
    mul( x, y );        // this pushes y onto the stack, then x, then a return address,
                        // allocates space on the stack for a return value, 
                        // then issues an assembly CALL instruction.
}

Напишите простую программу, подобную этой, и затем скомпилируйте ее в сборку (gcc -S foo.c, если у вас есть доступ к GCC) и посмотрите. Сборка довольно проста. Вы можете видеть, что стек используется для локальных переменных функции и для вызовов функций, сохраняя их аргументы и возвращаемые значения. Вот почему, когда вы делаете что-то вроде:

f( g( h( i ) ) ); 

Все они вызываются поочередно. Он буквально создает стек вызовов функций и их аргументов, выполняет их, а затем выталкивает их, когда он откидывается назад (или вверх;). Однако, как упоминалось выше, стек (на x86) фактически находится в вашем пространстве памяти процесса (в виртуальной памяти), и поэтому его можно напрямую манипулировать; это не отдельный шаг во время выполнения (или, по крайней мере, ортогонален процессу).

FYI, выше приведено соглашение о вызове C, также используемое С++. Другие языки/системы могут подталкивать аргументы в стек в другом порядке, а некоторые языки/платформы даже не используют стеки и обходятся по-разному.

Также обратите внимание: это не фактические строки выполнения кода C. Компилятор преобразовал их в инструкции машинного языка в ваш исполняемый файл. Затем они (обычно) копируются из области TEXT в конвейер CPU, затем в регистры процессора и выполняются оттуда. [Это неверно. См. коррекция Ben Voigt ниже.]

Ответ 2

В течение очень короткого времени Sdaz получил замечательное количество upvotes, но, к сожалению, увековечивает неправильное представление о том, как инструкции перемещаются через CPU.

Вопрос:

Инструкции переходят из ОЗУ в стек, в регистры?

Сдаз сказал:

Также обратите внимание: это не фактические строки выполнения кода C. Компилятор преобразовал их в инструкции машинного языка в ваш исполняемый файл. Затем они (как правило) копируются из области TEXT в конвейер CPU, затем в регистры процессора и выполняются оттуда.

Но это неправильно. За исключением специального случая самомодифицирующего кода, инструкции никогда не вводятся в дататате. И они не могут быть выполнены из датапата.

x86 регистры процессора:

  • Общие регистры EAX EBX ECX EDX

  • Сегментные регистры CS DS ES FS GS SS

  • Указатели и указатели ESI EDI EBP EIP ESP

  • Индикатор EFLAGS

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

Ни один из этих регистров не используется для исполняемого кода. EIP содержит адрес исполняющей команды, а не сама инструкция.

Инструкции проходят через совершенно другой путь в ЦП от данных (архитектура Гарварда). Все текущие машины - это архитектура Гарварда внутри CPU. Большинство этих дней также являются архитектурой Гарварда в кеше. x86 (ваш обычный настольный компьютер) - это архитектура Von Neumann в основной памяти, то есть данные и код перемежаются в ОЗУ. Это не имеет значения, поскольку мы говорим о том, что происходит внутри CPU.

Классическая последовательность, преподаваемая в компьютерной архитектуре, - fetch-decode-execute. Контроллер памяти ищет инструкцию, хранящуюся по адресу EIP. Биты инструкции проходят через некоторую комбинационную логику для создания всех сигналов управления для разных мультиплексоров в процессоре. И после некоторых циклов блок арифметической логики приходит к результату, который синхронизируется в пункт назначения. Затем выбирается следующая команда.

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

В довершение всего, терминология немного запуталась, потому что регистр - это термин электротехники для коллекции D-триггеров. И инструкции (или особенно микроинструкции) могут очень хорошо храниться временно в такой коллекции D-триггеров. Но это не то, что имеется в виду, когда компьютерный ученый или инженер-программист или программист-разработчик использует регистр терминов. Они означают регистры данных, перечисленные выше, и они не используются для транспортировки кода.

Имена и количество регистров datapath различаются для других архитектур ЦП, таких как ARM, MIPS, Alpha, PowerPC, но все они выполняют инструкции, не передавая их через ALU.

Ответ 3

Точная компоновка памяти во время выполнения процесса полностью зависит от используемой вами платформы. Рассмотрим следующую тестовую программу:

#include <stdlib.h>
#include <stdio.h>

int main()
{
    int stackValue = 0;
    int *addressOnStack = &stackValue;
    int *addressOnHeap = malloc(sizeof(int));
    if (addressOnStack > addressOnHeap)
    {
        puts("The stack is above the heap.");
    }
    else
    {
        puts("The heap is above the stack.");
    }
}

В Windows NT (и это дети) эта программа будет в целом производить:

Куча находится выше стека

В POSIX-блоках он скажет:

Стек выше кучи

Модель памяти UNIX довольно хорошо объясняется здесь @Sdaz MacSkibbons, поэтому я не буду повторять это здесь. Но это не единственная модель памяти. Причина, по которой POSIX требует, чтобы эта модель была системным вызовом sbrk. В принципе, в поле POSIX, чтобы получить больше памяти, процесс просто говорит ядру переместить делитель между "дырой" и "кучей" дальше в область "дыры". Невозможно вернуть память в операционную систему, а сама операционная система не справляется с вашей кучей. Ваша библиотека времени выполнения C должна обеспечить это (через malloc).

Это также имеет последствия для типа кода, фактически используемого в двоичных файлах POSIX. Ящики POSIX (почти повсеместно) используют формат файла ELF. В этом формате операционная система отвечает за связь между библиотеками в разных файлах ELF. Поэтому во всех библиотеках используется независимый от положения код (то есть сам код может быть загружен в разные адреса памяти и по-прежнему работать), и все вызовы между библиотеками передаются через таблицу поиска, чтобы выяснить, где нужно переходить на перекресток вызовы библиотечных функций. Это добавляет некоторые накладные расходы и может быть использовано, если одна из библиотек изменит таблицу поиска.

Модель памяти Windows отличается от того, какой тип используемого кода отличается. Windows использует формат файла PE, который оставляет код в формате, зависящем от позиции. То есть, код зависит от того, где именно в виртуальной памяти загружается код. В спецификации PE есть флаг, который сообщает ОС, где именно в памяти библиотека или исполняемый файл должны отображаться при запуске вашей программы. Если программа или библиотека не могут быть загружены на предпочтительный адрес, загрузчик Windows должен перезагрузить библиотеку/исполняемый файл - в основном, он перемещает код, зависящий от позиции, для указания на новые позиции, который не требует таблиц поиска и не может быть использованным, потому что нет таблицы поиска для перезаписи. К сожалению, это требует очень сложной реализации в загрузчике Windows и имеет значительные накладные расходы на запуск, если изображение необходимо переупаковать. Большие коммерческие пакеты программного обеспечения часто изменяют свои библиотеки, чтобы начать преднамеренно на разных адресах, чтобы избежать перезагрузки; сами окна делают это с его собственными библиотеками (например, ntdll.dll, kernel32.dll, psapi.dll и т.д. - все имеют разные начальные адреса по умолчанию)

В Windows виртуальная память получается из системы посредством вызова VirtualAlloc, и она возвращается в систему через VirtualFree (Хорошо, технически VirtualAlloc переходит в NtAllocateVirtualMemory, но это деталь реализации) (Сравните это с POSIX, где память не может быть восстановлена). Этот процесс медленный (и IIRC, требуется, чтобы вы размещали в физических размерах страницы, обычно 4 кбайта или более). Windows также предоставляет собственные функции кучи (HeapAlloc, HeapFree и т.д.) Как часть библиотеки, известной как RtlHeap, которая включена как часть самой Windows, на которой время выполнения C (т.е. malloc и друзей) как правило, реализованы.

В Windows также имеется довольно много устаревших API-интерфейсов распределения памяти с тех пор, когда им пришлось иметь дело со старыми 80386, и теперь эти функции теперь построены поверх RtlHeap. Для получения дополнительной информации о различных API-интерфейсах, которые управляют управлением памятью в Windows, см. Эту статью MSDN: http://msdn.microsoft.com/en-us/library/ms810627.

Обратите внимание, что это означает, что в Windows один процесс (и обычно имеет) имеет более одной кучи. (Как правило, каждая разделяемая библиотека создает свою собственную кучу.)

(Большая часть этой информации взята из "Безопасного кодирования на C и С++" Роберта Сиакда)

Ответ 4

Стек

В архитекторе X86 CPU выполняет операции с регистрами. Стек используется только по соображениям удобства. Вы можете сохранить содержимое своих регистров в стек до вызова подпрограммы или системной функции, а затем загрузить их обратно, чтобы продолжить свою работу, когда вы ушли. (Вы могли бы сделать это вручную без стека, но это часто используемая функция, поэтому она имеет поддержку ЦП). Но вы можете сделать почти что угодно без стека на ПК.

Например, целочисленное умножение:

MUL BX

Умножает регистр AX на регистр BX. (Результат будет в DX и AX, DX, содержащих более высокие биты).

Машины на основе стека (например, JAVA VM) используют стек для их основных операций. Вышеприведенное умножение:

DMUL

Это выдает два значения из верхней части стека и умножает tem, а затем возвращает результат в стек. Стек необходим для таких машин.

Некоторые языки программирования более высокого уровня (например, C и Pascal) используют этот более поздний метод для передачи параметров функциям: параметры переносятся в стек в порядке слева направо и выталкиваются телом функции, а возвращаемые значения отбрасываются. (Это выбор, который производители компиляторов делают и вид злоупотреблений, как X86 использует стек).

Куча

Куча - это другая концепция, которая существует только в области компиляторов. Требуется боль от работы с памятью за вашими переменными, но это не функция процессора или ОС, а просто выбор домашнего хозяйства для блока памяти, который выдается ОС. Вы можете сделать это по-другому, если хотите.

Доступ к системным ресурсам

У операционной системы есть открытый интерфейс, как вы можете получить доступ к его функциям. В DOS параметры передаются в регистры ЦП. Windows использует стек для передачи параметров для функций ОС (Windows API).