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

180

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

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

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

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

gaijinco
источник
43
+1 за фундаментальный вопрос!
mkelley33
21
хм ... вы знаете, они пишут книги об этом. Вы действительно хотите изучить эту часть архитектуры ОС с помощью SO?
Андрей
1
Я добавил пару тегов, основываясь на характере вопроса, связанном с памятью, и на ссылке на C ++, хотя я думаю, что хорошим ответом мог бы также стать кто-то, знающий Java или C #!)
mkelley33
14
Голосование и одобрение. Я всегда боялся спросить ...
Maxpm
2
Термин «помещает их в регистры» не совсем прав. На большинстве процессоров регистры используются для хранения промежуточных значений, а не исполняемого кода.

Ответы:

161

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

+---------+
|  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.
+---------+

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

Обратите внимание, что в некоторых системах позиции, например, стека и кучи могут быть в другом порядке (см . Ответ Билли О'Нила ниже для более подробной информации о 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 разрешал прямой доступ к памяти операционной системы, без защиты, что означало, что программы пользовательского пространства могли, как правило, иметь прямой доступ или перезаписывать все, что им нравилось.

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

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

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


Ты сказал:

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

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

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

«Куча» это просто прозвище куска памяти , который может быть выделен по требованию, и адресована случайным образом (то есть, вы можете получить доступ в любом месте в нем непосредственно). Он обычно используется для структур данных, которые вы выделяете во время выполнения (в C ++, с использованием newи delete, и mallocи друзей в C и т. Д.).

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

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

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


Ваш вопрос:

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

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

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) фактически находится в пространстве памяти вашего процесса (в виртуальной памяти), и поэтому им можно манипулировать напрямую; это не отдельный шаг во время выполнения (или, по крайней мере, ортогональный процессу).

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

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

Сдаз МакСкиббонс
источник
4
извините, но хорошая рекомендация для книги была бы лучшим ответом, IMO
Андрей
13
Да, «RTFM» всегда лучше.
Sdaz MacSkibbons
56
@ Андрей: возможно, вам следует изменить этот комментарий на «также, возможно, вы захотите прочитать свою-хорошую книгу-рекомендацию ». Я понимаю, что этот вопрос заслуживает более тщательного изучения, но всякий раз, когда вам нужно начинать комментарий с «извините, но. .. "возможно, вам стоит подумать о том, чтобы пометить пост для внимания модератора или хотя бы предложить объяснение того, почему ваше мнение должно так или иначе иметь значение для всех.
mkelley33
2
Отличный ответ. Это определенно прояснило некоторые вещи для меня!
Maxpm
2
@Mikael: в зависимости от реализации у вас может быть обязательное кэширование, и в этом случае в любое время, когда данные читаются из памяти, читается вся строка кэша и заполняется кэш. Или же можно дать подсказку администратору кеша, что данные понадобятся только один раз, поэтому копирование в кеш бесполезно. Это для чтения. Для записи существуют кэши с обратной записью и сквозной записью, которые влияют на то, когда контроллеры DMA могут считывать данные, а затем существует целый ряд протоколов когерентности кэша для работы с несколькими процессорами, каждый из которых имеет свой собственный кэш. Это действительно заслуживает своего собственного Q.
Бен Фойгт
61

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

Заданный вопрос:

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

Сдаз сказал:

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

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

В регистрах процессора x86 являются:

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

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

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

  • Индикатор EFLAGS

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

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

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

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

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

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

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

Бен Фойгт
источник
Спасибо за разъяснения. Я не решался добавить это, поскольку я не очень хорошо знаком с этим, но сделал это по чьей-либо просьбе.
Sdaz MacSkibbons
s / ARM / RAM / в "означает, что данные и код смешаны в ARM". Правильно?
Бьярке Фрейнд-Хансен
@bjarkef: в первый раз да, но не во второй. Я исправлю это.
Бен Фойгт
17

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

#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 и требует значительных временных затрат при запуске, если требуется перезагружать образ. Крупные коммерческие программные пакеты часто модифицируют свои библиотеки так, чтобы целенаправленно запускаться по разным адресам, чтобы избежать перебазирования; Windows сама делает это с помощью своих собственных библиотек (например, ntdll.dll, kernel32.dll, psapi.dll и т. д. - все они имеют разные начальные адреса по умолчанию)

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

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

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

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

Билли ОНил
источник
Отличная информация, спасибо! Надеюсь, что "user487117" в конечном итоге действительно возвращается. :-)
Sdaz MacSkibbons
5

Стек

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

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

MUL BX

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

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

DMUL

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

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

Куча

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

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

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

vbence
источник