Що відбувається, коли працює комп'ютерна програма?


180

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

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

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

Я не можу зрозуміти, як ці дві речі з’єднуються. У який момент стек використовується для виконання інструкцій? Інструкції йдуть від оперативної пам'яті, до стека, до регістрів?


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

FYI, вищезгадана умова C виклику , яка також використовується C ++. Інші мови / системи можуть висунути аргументи на стек в іншому порядку, а деякі мови / платформи навіть не використовують стеки і розглядають це по-різному.

Також зауважте, це не фактичні рядки виконання коду С. Компілятор перетворив їх на інструкції машинної мови у вашому виконуваному файлі. Потім вони (як правило) копіюються з області TEXT в конвеєр процесора, потім в регістри процесора і виконуються звідти. [Це було неправильно. Див . Виправлення Бена Войта нижче.]


4
вибачте, але найкраща відповідь на книгу стане кращою відповіддю, ІМО
Андрій

13
Так, "RTFM" завжди краще.
Sdaz MacSkibbons

56
@Andrey: можливо, ви повинні змінити цей коментар на "також, ви можете прочитати свою-добру книгу-рекомендацію " Я розумію, що подібне питання потребує більшого розслідування, але щоразу, коли вам доведеться починати коментар із "вибачте, але. .. ", можливо, ви дійсно повинні розглянути можливість позначення посади для уваги модератора або принаймні запропонувати пояснення, чому ваша думка має стосуватися когось у будь-якому випадку.
mkelley33

2
Відмінна відповідь. Це, безумовно, очистило деякі речі для мене!
Maxpm

2
@Mikael: Залежно від реалізації, у вас може бути обов’язкове кешування, і в цьому випадку будь-який час зчитуються дані з пам'яті, зчитується цілий рядок кешу і заповнюється кеш. Або, можливо, можна дати підказку менеджеру кешу, що дані знадобляться лише один раз, тому копіювання їх у кеш не корисне. Це для читання. Для запису є кеш-запис і кеш-запис, який впливає, коли контролери DMA можуть зчитувати дані, а потім існує ціла низка протоколів когерентності кешу для роботи з декількома процесорами, кожен з яких має свій кеш-пам'ять. Це дійсно заслуговує на власне питання.
Бен Войгт

61

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

Задане запитання:

Інструкції йдуть від оперативної пам'яті, до стека, до регістрів?

Сдаз сказав:

Також зауважте, це не фактичні рядки виконання коду С. Компілятор перетворив їх на інструкції машинної мови у вашому виконуваному файлі. Потім вони (як правило) копіюються з області TEXT в конвеєр процесора, потім в регістри процесора і виконуються звідти.

Але це неправильно. За винятком особливого випадку коду, що самозмінюється, інструкції ніколи не надходять на шлях даних. І вони не можуть бути, не можуть бути виконані з потоку даних.

У регістрах процесора x86 є:

  • Загальні регістри EAX EBX ECX EDX

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

  • Індекс та покажчики ESI EDI EBP EIP ESP

  • Індикатор EFLAGS

Існують також деякі регістри з плаваючою комою та SIMD, але для цілей цієї дискусії ми класифікуємо їх як частина копроцесора, а не CPU. Блок управління пам’яттю всередині процесора також має деякі власні регістри, ми знову розглянемо це як окремий процесорний блок.

Жоден із цих регістрів не використовується для виконуваного коду. EIPмістить адресу інструкції, що виконує, а не саму інструкцію.

Інструкції проходять зовсім інший шлях в ЦП від даних (Гарвардська архітектура). Всі сучасні машини є гарвардською архітектурою всередині процесора. Більшість цих днів також є архітектура Гарварду в кеші. x86 (ваша звичайна настільна машина) - це архітектура Von Neumann в основній пам'яті, тобто дані та код переплітаються в оперативній пам'яті. Це зовсім не до речі, оскільки ми говоримо про те, що відбувається всередині процесора.

Класична послідовність, що викладається в комп'ютерній архітектурі, - це вибор-декодування-виконання. Контролер пам'яті шукає інструкцію, що зберігається за адресою EIP. Біти інструкції проходять через деяку комбінаційну логіку для створення всіх керуючих сигналів для різних мультиплексорів в процесорі. А після деяких циклів арифметична логічна одиниця приходить до результату, який тактований у пункт призначення. Тоді дістається наступна інструкція.

На сучасному процесорі все працює трохи інакше. Кожна вхідна інструкція перекладається на цілу низку інструкцій з мікрокоду. Це дає можливість конвеєрного керування, оскільки ресурси, використані першою мікроінструкцією, пізніше не потрібні, тому вони можуть розпочати роботу над першою мікроінструкцією з наступної інструкції.

На додаток, термінологія трохи плутається, оскільки регістр є терміном електротехніки для колекції D-flipflops. І вказівки (або особливо мікроінструкції) дуже добре можуть тимчасово зберігатися в такій колекції D-flipflops. Але це не те, що мається на увазі, коли вчений-комп'ютер або інженер програмного забезпечення або розробник заводу використовує термін « регістр» . Вони означають регістри даних, як зазначено вище, і вони не використовуються для транспортування коду.

Імена та кількість реєстрів шляхів передачі даних різняться для інших архітектур процесора, таких як ARM, MIPS, Alpha, PowerPC, але всі вони виконують інструкції, не пропускаючи їх через ALU.


Дякуємо за роз’яснення. Я вагався, щоб додати, що я не дуже знайомий з цим, але це робив на чуже прохання.
Sdaz MacSkibbons

s / ARM / RAM / в "значенні даних та коду переплутані в ARM". Правильно?
Bjarke Freund-Hansen

@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 використовує стек для передачі параметрів для функцій ОС (API API).

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.