Чи зберігаються декларатори типу даних типу "int" та "char" в оперативній пам'яті під час виконання програми C?


74

Коли програма C працює, дані зберігаються у купі або стеку. Значення зберігаються в RAM-адресах. А як щодо типових індикаторів (наприклад, intабо char)? Чи вони також зберігаються?

Розглянемо наступний код:

char a = 'A';
int x = 4;

Я читав, що A і 4 зберігаються тут у RAM-адресах. Але про що aі x? Найбільш заплутано, як екзекуція знає, що aце знак, і xце int? Я маю на увазі, є intі charзгадується десь в оперативній пам’яті?

Скажімо, значення зберігається десь у оперативній пам’яті як 10011001; якщо я програма, яка виконує код, то як я дізнаюся, чи це 10011001 - це charчи int?

Те , що я не розумію, як комп'ютер знає, коли він зчитує значення змінної з адреси , такі як 10001, чи є це intабо char. Уявіть, я натискаю програму під назвою anyprog.exe. Відразу код починає виконуватись. Чи містить цей виконуваний файл інформацію про те, чи зберігаються змінні типу intчи char?


24
Ця інформація повністю втрачається під час виконання. Ви (і ваш компілятор) повинні заздалегідь переконатися, що пам'ять буде інтерпретована правильно. Це відповідь, яку ви шукали?
5gon12eder

4
Це не так. Оскільки він передбачає, що ви знаєте, що ви робите, він бере все, що знайде, за вказаною вами адресою пам'яті, і записує це в stdout. Якщо все, що було написано, відповідає читаному символу, воно з часом з’явиться на чиїйсь консолі як читабельний символ. Якщо це не відповідає, воно буде відображатися як химерність або, можливо, випадковий читабельний символ.
Роберт Харві

22
@ user16307 Коротка відповідь полягає в тому, що в статично набраних мовах кожен раз, коли ви роздруковуєте char, компілятор видасть інший код, ніж для друку int. Під час виконання більше не існує знань, що xє знаком, але запускається код друку char, оскільки саме це обрав компілятор.
Іксрек,

13
@ user16307 Він завжди зберігається як двійкове представлення числа 65. Буде роздруковано як 65 або як A, залежить від коду, який створив ваш компілятор для його друку. Поруч із 65 не існує жодних метаданих, які б говорили, що це насправді char чи int (принаймні, не на статично набраних мовах, таких як C).
Іксрек,

2
Повністю зрозуміти концепції , які ви задаєте про тут і реалізувати їх самостійно, ви можете пройти курс компілятора, наприклад , Coursera один
mucaho

Відповіді:


122

Щоб вирішити питання, яке ви опублікували в кількох коментарях (які, я думаю, вам слід відредагувати у своєму дописі):

Що я не розумію, це те, як комп'ютер знає, коли він читає значення змінної з адреси та адреси, наприклад 10001, якщо це int або char. Уявіть, що я натискаю програму під назвою anyprog.exe. Відразу код починає виконуватись. Чи містить цей файл EXE інформацію про те, чи зберігаються змінні як у або char?

Тож давайте додамо до нього якийсь код. Скажімо, ви пишете:

int x = 4;

І припустимо, що він зберігається в оперативній пам’яті:

0x00010004: 0x00000004

Перша частина - адреса, друга частина - значення. Коли ваша програма (яка виконується як машинний код) працює, все, на що вона бачить, 0x00010004- це значення 0x000000004. Він не "знає" тип цих даних, і не знає, як він повинен "використовуватись".

Отже, як у вашій програмі з'ясовується правильна справа? Розглянемо цей код:

int x = 4;
x = x + 5;

У нас тут є читання та запис. Коли ваша програма читає xз пам'яті, вона знаходить 0x00000004там. І ваша програма знає, що додати 0x00000005до неї. А причина, по якій ваша програма «знає», це дійсна операція, полягає в тому, що компілятор гарантує, що операція є дійсною через безпеку типу. Ваш компілятор уже перевірив, що ви можете додавати 4та 5разом. Отже, коли ваш бінарний код працює (exe), це підтвердження не повинно проводити. Він просто виконує кожен крок наосліп, припускаючи, що все в порядку (погані речі трапляються, коли вони насправді не є нормальними).

Ще один спосіб подумати про це такий. Я надаю вам цю інформацію:

0x00000004: 0x12345678

Той самий формат, що і раніше - адреса зліва, значення праворуч. Якого типу є значення? На даний момент ви знаєте стільки ж інформації про це значення, скільки і ваш комп’ютер, коли він виконує код. Якби я сказав вам додати до цього значення 12743, ви могли б це зробити. Ви не маєте поняття, якими будуть наслідки цієї операції для всієї системи, але додавання двох чисел - це те, що вам справді добре, тож ви могли це зробити. Це робить значення int? Не обов’язково - все, що ви бачите, це два 32-бітні значення та оператор додавання.

Можливо, деяка плутанина - це повернення даних назад. Якщо у нас є:

char A = 'a';

Як комп'ютер знає відображатись aу консолі? Ну, є багато кроків до цього. Перший - це перейти до Aпам'яті s в пам'яті та прочитати:

0x00000004: 0x00000061

aШестнадцяте значення для ASCII становить 0x61, тому вищезгадане може бути чимось, що ви побачите в пам'яті. Тож тепер наш машинний код знає ціле значення. Звідки це знати, щоб перетворити ціле значення в символ для його відображення? Простіше кажучи, компілятор обов'язково здійснив усі необхідні кроки для здійснення цього переходу. Але сам ваш комп'ютер (або програма / exe) не має уявлення про тип цих даних. Це 32-бітове значення може бути що завгодно - int, char, половина double, покажчик, частина масиву, частина string, частина інструкції і т.д.


Ось коротка взаємодія, яку може мати ваша програма (exe) з комп'ютером / операційною системою.

Програма: Я хочу запустити. Мені потрібно 20 Мб пам'яті.

Операційна система: знаходить 20 вільних МБ пам'яті, які не використовуються, і передає їх

(Важлива примітка полягає в тому, що це може повернути будь-які 20 вільних МБ пам'яті, вони навіть не повинні бути суміжними. На даний момент програма тепер може працювати в пам'яті, яку вона має, не розмовляючи з ОС)

Програма: Я припускаю, що перше місце в пам'яті - це 32-розрядна ціла змінна x.

(Компілятор гарантує, що доступ до інших змінних ніколи не торкнеться цього місця в пам'яті. У системі немає нічого, що говорить, що перший байт є змінною x, або що ця змінна xціла. Аналогія: у вас є мішок. Ви кажете людям, що Ви будете класти в цю сумку лише кульки жовтого кольору. Коли хтось пізніше щось витягне з сумки, то було б шокуюче, що вони витягнуть щось синє чи кубик - щось жахливо пішло не так. Те саме стосується комп’ютерів: ваш Програма тепер припускає, що перше місце пам’яті є змінною x, і це ціле число. Якщо що-небудь ще написано над цим байтом пам'яті або вважається, що це щось інше - трапилося щось жахливе. Компілятор забезпечує такі речі речей не трапиться)

Програма: Тепер я напишу 2на перші чотири байти, де я вважаю, що xзнаходиться.

Програма: Я хочу додати 5 до x.

  • Читає значення X у тимчасовому регістрі

  • Додає 5 до тимчасового реєстру

  • Зберігає значення тимчасового регістра назад у перший байт, який досі вважається таким x.

Програма: Я припускаю, що наступний доступний байт - це змінна char y.

Програма: Я напишу aна змінну y.

  • Для пошуку значення байтів використовується бібліотека a

  • Байт записується на адресу, яку передбачає програма y.

Програма: Я хочу відобразити вміст y

  • Читає значення у другому місці пам'яті

  • Використовує бібліотеку для перетворення з байта в символ

  • Використовує графічні бібліотеки для зміни екрана консолі (встановлення пікселів від чорного до білого, прокрутка однієї лінії тощо)

(І продовжується звідси)

Напевно, ви зациклювались на тому, що відбувається, коли першого місця в пам'яті вже немає x? або другого вже немає y? Що відбувається, коли хтось читає xяк вказівник charчи yяк? Словом, трапляються погані речі. Деякі з цих речей мають чітко визначену поведінку, а деякі - невизначену поведінку. Невизначена поведінка - це саме те - все може статися, від нічого зовсім, до збоїв програми чи операційної системи. Навіть чітко визначена поведінка може бути шкідливою. Якщо я можу змінити xвказівник на свою програму і дозволити вашій програмі використовувати її як покажчик, то я можу змусити вашу програму почати виконувати мою програму - саме це і роблять хакери. Компілятор є там, щоб переконатися, що ми не використовуємо його int xякstringта речі такого характеру. Машинний код сам по собі не знає типів, і він буде робити лише те, що вказує інструкція. Існує також велика кількість інформації, виявленої під час виконання: якими байтами пам'яті програма дозволяє використовувати? Починається xз першого байту чи 12-го?

Але ви можете собі уявити, як жахливо було б насправді писати такі програми (а можна, мовою асемблера). Ви починаєте з "декларування" своїх змінних - ви кажете собі, що байт 1 є x, байт 2 є y, і, коли ви пишете кожен рядок коду, завантажуючи і зберігаючи регістри, ви (як людина) повинні пам'ятати, що це таке, xа яке один з них є y, тому що система не має уявлення. І ви (як людина) повинні пам'ятати , які типи xі yє, тому що ще раз - система не має ні найменшого уявлення.


Дивовижне пояснення. Тільки частина, яку ви написали: "Як це зробити, щоб перетворити ціле значення в символ для його відображення? Простіше кажучи, компілятор обов'язково ввів усі необхідні кроки для здійснення цього переходу". для мене все ще туманно. Скажімо, процесор отримав 0x00000061 з регістра оперативної пам'яті. З цього моменту ви говорите, що є інші вказівки (у файлі EXE), які роблять цей перехід до того, що ми бачимо на екрані?
користувач16307,

2
@ user16307 так, є додаткові інструкції. Кожен рядок коду, який ви пишете, потенційно може бути перетворений на багато інструкцій. Є інструкції, щоб визначити, який символ використовувати, є вказівки, які пікселі видозмінювати та в який колір вони змінюються тощо. Також є код, який ви насправді не бачите. Наприклад, використання std :: cout означає, що ви використовуєте бібліотеку. Ваш код для запису на консоль може бути лише одним рядком, але функція (и), яку ви викликаєте, буде більше рядків, і кожен рядок може перетворитися на багато інструкцій на машині.
Шаз

8
@ user16307 Otherwise how can console or text file outputs a character instead of int Оскільки існує інша послідовність інструкцій щодо виведення вмісту місця пам'яті у вигляді цілого чи буквено-цифрового символу. Компілятор знає про типи змінних і вибирає відповідну послідовність інструкцій під час компіляції та записує її в EXE.
Чарльз Е. Грант

2
Я знайшов би іншу фразу для "самого байтового коду", оскільки байт-код (або байт-код) зазвичай посилається на проміжну мову (наприклад, Java Bytecode або MSIL), яка може насправді зберігати ці дані під час виконання. Плюс не зовсім зрозуміло, на який "байт-код" слід посилатися в цьому контексті. Інакше приємна відповідь.
jpmc26

6
@ user16307 Намагайтеся не турбуватися про C ++ та C #. Що ці люди говорять, це набагато вище вашого теперішнього розуміння того, як працюють комп’ютери та компілятори. Для цілей того, що ви намагаєтеся зрозуміти, апаратне забезпечення НЕ знає нічого про типи, char чи int чи що завгодно. Коли ви сказали компілятору, що деяка змінна була цілою, вона генерувала виконуваний код для обробки місця в пам'яті. ЯКЩО це був int. Місце самої пам'яті не містить інформації про типи; це просто те, що ваша програма вирішила трактувати це як int. Забудьте про все, що ви чули про інформацію про тип виконання.
Андрес Ф.

43

Я думаю, що ваше головне питання виглядає так: "Якщо тип стирається під час компіляції і не зберігається під час виконання, то як комп'ютер знає, чи потрібно виконувати код, який інтерпретує його як intчи виконати код, який інтерпретує його як char? "

І відповідь… комп’ютер ні. Однак компілятор це знає, і він просто поставить правильний код у двійковий на перше місце. Якби змінна була charвведена як , тоді компілятор не став би код для трактування її як intв програмі, він поставив би код, щоб розглянути її як char.

Там є причини зберігає типу під час виконання:

  • Динамічне введення тексту: при динамічному введенні перевірка типу відбувається під час виконання, тому, очевидно, тип повинен бути відомий під час виконання. Але C не динамічно набирається, тому типи можна безпечно стирати. (Однак зауважте, що це зовсім інший сценарій. Динамічні типи та статичні типи насправді не одне і те ж, і мовою змішаного введення тексту ви все ще можете стирати статичні типи і зберігати лише динамічні типи.)
  • Динамічний поліморфізм: якщо ви виконуєте інший код на основі типу часу виконання, вам потрібно тримати тип виконання. C не має динамічного поліморфізму (насправді у нього немає поліморфізму, за винятком випадків, за винятком деяких спеціальних жорстко кодованих випадків, наприклад, +оператора), тому він не потребує типу виконання з цієї причини. Однак, знову ж таки, тип виконання в будь-якому випадку відрізняється від статичного типу, наприклад, на Java, ви можете теоретично стерти статичні типи і все одно зберегти тип виконання для поліморфізму. Зауважте також, що якщо ви децентралізуєте і спеціалізуєте код пошуку типу і помістите його всередині об'єкта (або класу), вам також не обов'язково потрібен тип виконання, наприклад, V + Vtables.
  • Відображення часу виконання: якщо ви дозволяєте програмі відображати її типи під час виконання, то вам, очевидно, потрібно тримати типи під час виконання. Ви можете легко побачити це за допомогою Java, яка зберігає типи першого порядку під час виконання, але стирає аргументи типу до загальних типів під час компіляції, тому ви можете роздумувати лише про конструктор типів ("необроблений тип"), але не аргумент типу. Знову ж, C не має відображення під час виконання, тому не потрібно зберігати тип під час виконання.

Єдиною причиною збереження типу під час виконання в C було б налагодження, однак, налагодження зазвичай виконується з наявного джерела, і тоді ви можете просто шукати тип у вихідному файлі.

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

Збереження типів під час виконання потрібно лише тоді, коли ви хочете щось зробити з типами під час виконання.

Haskell є однією з найсуворіших, найжорсткіших, безпечних для статичних мов, а компілятори Haskell зазвичай стирають усі типи. (Виняток становить пропуск методологічних словників для типів класів, я вважаю.)


3
Немає! Чому? Для чого потрібна ця інформація? Компілятор виводить код для читання a charу складений бінарний файл. Він не виводить код для int, він не виводить код для a byte, він не виводить код для вказівника, він просто виводить тільки код для a char. Немає рішень часу виконання, які приймаються залежно від типу. Вам не потрібен тип. Це абсолютно і зовсім не має значення. Усі відповідні рішення вже прийняті під час складання.
Йорг W Міттаг

2
Немає. Компілятор просто кладе код для друку знаків у двійковій. Період. Компілятор знає, що в цій пам'яті є char, тому він ставить код для друку знаку у двійковій. Якщо значення цієї адреси пам'яті з якихось дивних причин не є символом, то, ну, все пекло розривається. Це в основному, як працює цілий клас безпеки.
Йорг W Міттаг

2
Подумайте над цим: якби процесор якось знав про типи даних програм, то всім на планеті довелося б купувати новий процесор щоразу, коли хтось винайде новий тип. public class JoergsAwesomeNewType {};Побачити? Я щойно винайшов новий тип! Вам потрібно придбати новий процесор!
Йорг W Міттаг

9
Ні. Це не так. Компілятор знає, який код він повинен поставити у двійковий код. Немає сенсу зберігати цю інформацію навколо. Якщо ви друкуєте int, компілятор поставить код для друку int. Якщо ви друкуєте char, компілятор поставить код для друку char. Період. Але це просто трохи візерунка. Код для друку char буде певним чином інтерпретувати бітовий візерунок, код для друку int інтерпретує біт по-іншому, але немає способу відрізнити бітовий візерунок, який є int від бітового шаблону, який це char, це рядок біт.
Йорг W Міттаг

2
@ user16307: "Чи не містить файл exe інформацію про адресу, який тип даних?" Може бути. Якщо ви компілюєте дані налагодження, дані налагодження включатимуть інформацію про імена змінних, адреси та типи. І іноді дані налагодження зберігаються у файлі .exe (як двійковий потік). Але він не є частиною виконуваного коду, і його не використовує сама програма, а лише відладчик.
Бен Войгт

12

Комп'ютер не "знає", що таке адреси, але це знання того, що передбачено інструкціями вашої програми.

Коли ви пишете програму C, яка записує та читає змінну char, компілятор створює код складання, який записує цей фрагмент даних десь як char, а ще десь є інший код, який читає адресу пам'яті та інтерпретує її як char. Єдине, що пов’язує ці дві операції разом - це розташування цієї адреси пам'яті.

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

Поганий час аналогії: уявіть складний складський склад, де на складі є пам'ять, а люди, які збирають речі, - це процесор. Одна частина складу 'програма' розміщує на полиці різні предмети. Інша програма іде і хапає предмети зі складу і складає їх у коробки. Коли їх витягують, їх не перевіряють, вони просто йдуть у відро. Весь склад функціонує, працюючи синхронно, при цьому правильні елементи завжди знаходяться в потрібному місці в потрібний час, інакше все виходить з ладу, як у фактичній програмі.


як би ви пояснили, якщо процесор знаходить 0x00000061 у реєстрі та отримує його; і уявіть, що програма консолі повинна виводити це як символ не int. ви маєте на увазі, що в цьому файлі EXE є деякі інструкційні коди, які знають, що адреса 0x00000061 є символом і перетворюється на символ за допомогою таблиці ASCII?
користувач16307

7
Зауважте, що "все виходить з ладу" - це насправді найкращий сценарій. "Дивні речі трапляються" - це другий найкращий сценарій, "тонко дивні речі трапляються" ще гірше, а найгірший - "речі трапляються за вашою спиною, щоб хтось навмисно маніпулював, щоб відбуватися так, як вони хочуть", він же подвиг безпеки.
Йорг W Міттаг

@ user16307: Код у програмі скаже комп'ютеру отримати цю адресу, а потім відобразити її відповідно до того, яке кодування використовується. Незалежно від того, чи є ці дані в пам’яті символом ASCII або повним сміттям, комп'ютер не турбується. Щось інше було відповідальним за встановлення цієї адреси пам'яті, щоб мати в ній очікувані значення. Я думаю, що вам може бути корисно спробувати деякі програми складання.
whatsisname

1
@ JörgWMittag: дійсно. Я думав згадати про переповнення буфера як приклад, але вирішив, що це просто зробить речі більш заплутаними.
whatsisname

@ user16307: Відображення даних на екрані - це програма. У традиційному unixen це термінал (програмне забезпечення, що імітує серійний термінал DEC VT100 - апаратний пристрій з монітором і клавіатурою, що відображає все, що входить у його модем, на монітор і надсилає все, що набрано на його клавіатурі, до його модему). У DOS це DOS (фактично текстовий режим вашої VGA карти, але дозволяє ігнорувати це), а в Windows - це command.com. Ваша програма не знає, що вона насправді роздруковує рядки, це просто друк послідовності байтів (цифр).
slebetman

8

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

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


Що я не розумію, це те, як комп'ютер знає, коли він читає значення змінної з адреси та адреси, наприклад 10001, якщо це int або char. Уявіть, що я натискаю програму під назвою anyprog.exe. Відразу код починає виконуватись. Чи містить цей файл EXE інформацію про те, чи зберігаються змінні як у або char? -
користувач16307

@ user16307 Ні, додаткової інформації про те, чи є щось цілим чи знаковим, немає. Додаю кілька прикладів матеріалів пізніше, припускаючи, що мене ніхто не б’є.
8bittree

1
@ user16307: Файл exe містить цю інформацію опосередковано. Процесор, який виконує програму, не дбає про типи, які використовуються під час написання програми, але значна частина цього може бути виведена з інструкцій, що використовуються для доступу до різних місць пам'яті.
Барт ван Інген Шенау

@ user16307 дійсно небагато додаткової інформації. Файли exe знають, що ціле число - 4 байти, тому коли ви пишете "int a", резервуар компілятора 4 байти для змінної і, таким чином, може обчислити адресу a та інших змінних після.
Есбен Сков Педерсен

1
@ user16307 практично немає різниці (окрім розміру типу) різниці між кодом int a = 65і char b = 'A'після його складання.

6

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

Деякі конкретні приклади можуть допомогти. Машинний код, наведений нижче, був сформований за допомогою gcc 4.1.2 у системі x86_64 під управлінням SuSE Linux Enterprise Server (SLES) 10.

Припустимо наступний вихідний код:

int main( void )
{
  int x, y, z;

  x = 1;
  y = 2;

  z = x + y;

  return 0;
}

Ось м'ясо згенерованого коду збірки, що відповідає вищевказаному джерелу (використовуючи gcc -S), із доданими мною коментарями:

main:
.LFB2:
        pushq   %rbp               ;; save the current frame pointer value
.LCFI0:
        movq    %rsp, %rbp         ;; make the current stack pointer value the new frame pointer value
.LCFI1:                            
        movl    $1, -12(%rbp)      ;; x = 1
        movl    $2, -8(%rbp)       ;; y = 2
        movl    -8(%rbp), %eax     ;; copy the value of y to the eax register
        addl    -12(%rbp), %eax    ;; add the value of x to the eax register
        movl    %eax, -4(%rbp)     ;; copy the value in eax to z
        movl    $0, %eax           ;; eax gets the return value of the function
        leave                      ;; exit and restore the stack
        ret

З цього випливає кілька додаткових матеріалів ret, але це не стосується обговорення.

%eaxце 32-розрядний реєстр даних загального призначення. %rspце 64-розрядний реєстр, зарезервований для збереження покажчика стека , який містить адресу останнього, що висунуто на стек. %rbp- це 64-бітний регістр, зарезервований для збереження покажчика кадру , який містить адресу поточного кадру стека . Кадр стека створюється на стеку під час введення функції, і він залишає простір для аргументів функції та локальних змінних. Доступ до аргументів і змінних здійснюється за допомогою зрушень з покажчика кадру. У цьому випадку пам'ять для змінної xна 12 байт "нижче" адреси, що зберігається в %rbp.

У наведеному вище коді ми копіюємо ціле значення x(1, що зберігається в -12(%rbp)) в регістр %eaxза допомогою movlінструкції, яка використовується для копіювання 32-бітних слів з одного місця в інше. Потім ми дзвонимо addl, що додає ціле значення y(зберігається у -8(%rbp)) до значення, яке вже є %eax. Потім ми зберігаємо результат -4(%rbp), який є z.

Тепер давайте змінимо це, щоб ми мали справу зі doubleзначеннями замість intзначень:

int main( void )
{
  double x, y, z;

  x = 1;
  y = 2;

  z = x + y;

  return 0;
}

Біг gcc -Sзнову дає нам:

main:
.LFB2:
        pushq   %rbp                              
.LCFI0:
        movq    %rsp, %rbp
.LCFI1:
        movabsq $4607182418800017408, %rax ;; copy literal 64-bit floating-point representation of 1.00 to rax
        movq    %rax, -24(%rbp)            ;; save rax to x
        movabsq $4611686018427387904, %rax ;; copy literal 64-bit floating-point representation of 2.00 to rax
        movq    %rax, -16(%rbp)            ;; save rax to y
        movsd   -24(%rbp), %xmm0           ;; copy value of x to xmm0 register
        addsd   -16(%rbp), %xmm0           ;; add value of y to xmm0 register
        movsd   %xmm0, -8(%rbp)            ;; save result to z
        movl    $0, %eax                   ;; eax gets return value of function
        leave                              ;; exit and restore the stack
        ret

Кілька відмінностей. Замість movlі addlми використовуємо movsdта addsd(призначаємо та додаємо поплавці подвійної точності). Замість того, щоб зберігати проміжні значення в %eax, ми використовуємо %xmm0.

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


4

Історично С розглядала пам'ять як такий, що складається з ряду груп пронумерованих слотів типуunsigned char(також називається "байт", хоча це не завжди повинно бути 8 біт). Будь-який код, який використовував що-небудь, що зберігається в пам'яті, повинен знати, в якому слоті або слотах зберігалася інформація, і знати, що слід робити з інформацією там [наприклад "інтерпретувати чотири байти, що починаються з адреси 123: 456, як 32-бітний значення з плаваючою комою "або" зберігають нижні 16 біт останньо обчисленої кількості на два байти, починаючи з адреси 345: 678]. Сама пам'ять не знала б і не хвилювалась, що значення, що зберігаються в слотах пам'яті, "означають". код намагався записати пам'ять, використовуючи один тип, і читати його як інший, бітові шаблони, що зберігаються записами, інтерпретуються відповідно до правил другого типу, з якими б наслідками не було наслідків.

Наприклад, якщо код слід зберігати 0x12345678до 32-бітного unsigned int, а потім спробувати прочитати два послідовних 16-бітових unsigned intзначення з його адреси та одне вище, то залежно від того, яка половина unsigned intзберігається де, код може прочитати значення 0x1234 і 0x5678, або 0x5678 і 0x1234.

Стандарт C99, однак, більше не вимагає, щоб пам'ять поводилася як купа пронумерованих слотів, які нічого не знають про те, що представляють їх бітові шаблони . Компілятору дозволено поводитись так, ніби слоти пам’яті знають про типи даних, що зберігаються в них, і дозволять лише ті дані, які записуються, використовуючи будь-який тип, крім того, unsigned charщоб читати, використовуючи той чи інший тип unsigned charабо той самий тип, як це було записано з; надалі компіляторам дозволено поводитись так, ніби слоти пам'яті мають силу та схильність довільно корумповувати поведінку будь-якої програми, яка намагається отримати доступ до пам'яті таким чином, що суперечить цим правилам.

Подано:

unsigned int a = 0x12345678;
unsigned short p = (unsigned short *)&a;
printf("0x%04X",*p);

деякі реалізації можуть надрукувати 0x1234, а інші можуть надрукувати 0x5678, але згідно зі стандартом C99 було б законним, щоб реалізація надрукувала "FRINK ПРАВИЛА!" або робити що-небудь інше, на теорії, що було б законним, щоб у місцях зберігання пам'яті aбуло включено обладнання, яке записує, який тип використовувався для їх запису, і щоб таке обладнання відповідало на недійсну спробу читання будь-яким способом, в тому числі шляхом викликання "ПРАВИЛА ФРІНКУ!" виводити.

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

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

Однак приблизно в 2009 році автори деяких компіляторів, таких як CLANG, визначили, що оскільки Стандарт дозволяє компіляторам робити все, що їм подобається, у випадках, коли пам'ять записується одним типом і читається як інший, компілятори повинні зробити висновок, що програми ніколи не отримуватимуть вхід, який міг би викликати таке. Оскільки Стандарт говорить, що компілятору дозволяється робити все, що йому подобається, коли надходить такий недійсний вхід, код, який матиме ефект лише у випадках, коли Стандарт не пред'являє жодних вимог, (а на думку деяких авторів-компіляторів, слід) опускати. як не має значення. Це змінює поведінку порушень згладжування на схожість з пам'яттю, яка, даючи запит на читання, може довільно повернути останнє значення, записане з використанням того ж типу, що і запит на читання, або будь-яке більш недавнє значення, записане за допомогою іншого типу,


1
Згадування невизначеної поведінки під час обрізки типу того, хто не розуміє, як немає RTTI, здається контрінтуїтивним
Коул Джонсон

@ColeJohnson: Це дуже погано, що немає формальної назви або стандарту для діалекту С, який підтримує 99% до 2009 р. Компіляторів, оскільки з точки зору викладання та практичної їх слід вважати принципово різними мовами. Оскільки однакова назва дається і тому діалекту, який розвинув ряд передбачуваних та оптимізуваних поведінок протягом 35 років, діалект, який викидає таку поведінку з передбачуваною метою оптимізації, важко уникнути плутанини, коли говорити про речі, які в них працюють інакше .
supercat

Історично C працював на машинах Lisp, які не дозволяли настільки вільно грати з типами. Я майже впевнений, що багато хто з "передбачуваних та оптимізуваних способів поведінки", які спостерігалися 30 років тому, просто не працювали ніде, крім BSD Unix на VAX.
профілі

@prosfilaes: Можливо, "99% компіляторів, які використовувались з 1999 по 2009 рік", були б точнішими? Навіть коли компілятори мали варіанти деяких агресивних цілочисельних оптимізацій, вони були саме такими. Я не знаю, що я коли-небудь бачив компілятор до 1999 року, у якому не було режиму, який би не гарантував, що даний int x,y,z;вираз x*y > zніколи не зробить нічого, крім повернення 1 або 0, або де згладжування порушень матиме будь-який ефект крім того, щоб дозволити компілятору довільно повернути старе або нове значення.
supercat

1
... де unsigned charзначення, які використовуються для побудови типу "прийшов". Якщо програма повинна розкласти вказівник на an unsigned char[], коротко покажіть його шістнадцятковий вміст на екрані, а потім стерти вказівник, the unsigned char[], а пізніше прийняти кілька шістнадцяткових чисел з клавіатури, скопіювати їх назад у покажчик, а потім перенаправити цей покажчик , поведінка буде чітко визначена у випадку, коли число, яке було введено у відповідність до відображеного числа.
supercat

3

В С це не так. Інші мови (наприклад, Lisp, Python) мають динамічні типи, але C - статично набраний. Це означає, що ваша програма повинна знати, який тип даних належним чином інтерпретувати - це символ, ціле число тощо.

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


Що я не розумію, це те, як комп'ютер знає, коли він читає значення змінної з адреси та адреси, наприклад 10001, якщо це int або char. Уявіть, що я натискаю програму під назвою anyprog.exe. Відразу код починає виконуватись. Чи містить цей файл EXE інформацію про те, чи зберігаються змінні як у або char? -
користувач16307

1
@ user16307 По суті, ні, вся ця інформація повністю втрачена. Машиновий код повинен бути розроблений досить добре, щоб зробити свою роботу правильно навіть без цієї інформації. Все, що турбує комп’ютер, - це те, що за адресою є вісім біт підряд 10001. Під час написання машини або асемблерного коду ви можете вручну працювати з такими речами, як ваша робота, чи робота компілятора .
Panzercrisis

1
Зауважте, що динамічне введення тексту - не єдина причина збереження типів. Java введена статично, але вона все одно повинна зберігати типи, оскільки вона дозволяє динамічно відображати тип. Крім того, він має поліморфізм виконання, тобто метод відправки, заснований на типі виконання, для якого він також потребує цього типу. C ++ ставить код диспетчеризації методу в сам об’єкт (вірніше, клас), тому він не потребує типу в якомусь сенсі (хоча, звичайно, vtable в певному сенсі є частиною типу, так що, справді, принаймні, частина тип буде збережений), але в Java, код способу доставки централізований.
Йорг W Міттаг

подивіться на моє запитання, яке я написав "коли виконується програма С?" Хіба вони опосередковано не зберігаються у файлі EXE серед інструкційних кодів і врешті-решт займають місця в пам'яті? Я пишу це ще раз для вас: Якщо ЦП знайде 0x00000061 у реєстрі та витягне його; і уявіть, що програма консолі повинна виводити це як символ не int. чи є в цьому файлі EXE (машина / двійковий код) якісь коди інструкцій, які знають, що адреса 0x00000061 є символом та перетворюється на символ за допомогою таблиці ASCII? Якщо це так, це означає, що ідентифікатори char int опосередковано знаходяться у двійковій формі ???
користувач16307

Якщо значення дорівнює 0x61 і оголошується як знак char (тобто 'a'), і ви викликаєте процедуру для його відображення, [зрештою] буде системний виклик для відображення цього символу. Якщо ви оголосили це як int та зателефонували у програму відображення, компілятор буде знати код для перетворення 0x61 (десятковий 97) у послідовність ASCII 0x39, 0x37 ('9', '7'). Підсумок: генерується код відрізняється тим, що компілятор знає по-різному ставитися до них.
Майк Харріс

3

Ви повинні розрізняти compiletimeі runtimeз одного боку , і , codeі dataз іншого боку.

З точки зору машини не існує різниці між тим, що ви називаєте codeабо instructionsі те , що ви дзвоните data. Все зводиться до чисел. Але деякі послідовності - те, що ми би назвали code- роблять те, що нам здається корисним, інші - просто crashмашиною.

Робота, яку виконує процесор, - це простий 4-х ступінчастий цикл:

  • Отримати "дані" з заданої адреси
  • Розшифрувати інструкцію (тобто "інтерпретувати" число як an instruction)
  • Прочитайте ефективну адресу
  • Виконання та збереження результатів

Це називається циклом інструкцій .

Я читав, що A і 4 зберігаються тут у RAM-адресах. Але як щодо a і x?

aі xє змінними, які є заповнювачами адрес, де програма могла б знайти "вміст" змінних. Отже, щоразу, коли змінна aвикористовується, фактично є адреса вмісту aвикористовуваної.

Найбільш заплутано, як виконання знає, що a - це char, а x - int?

Виконання нічого не знає. З сказаного у вступі, процесор лише отримує дані та інтерпретує ці дані як інструкції.

Функція printf призначена для того, щоб "знати", який саме вхід ви вводите в нього, тобто отриманий в ньому код дає правильні вказівки, як діяти зі спеціальним сегментом пам'яті. Звичайно, можна генерувати дурницький вихід: використання адреси, де жодна рядок не зберігається разом із "% s" в printf(), призведе до того, що виведення дурниць зупиниться лише випадковим місцем пам'яті, де 0 ( \0).

Те саме стосується точки входу програми. Під C64 можна було розмістити свої програми (майже) кожну відому адресу. Асамблеї-програми розпочалися з інструкції, названої з sysнаступною адресою: sys 49152було загальним місцем для розміщення коду асемблера. Але ніщо не завадило вам завантажити, наприклад, графічні дані 49152, що призведе до аварії машини після "запуску" з цієї точки. У цьому випадку цикл інструкцій розпочався з читання "графічних даних" та спроби інтерпретувати його як "код" (що, звичайно, не мало сенсу); наслідки були приголомшливими;)

Скажімо, значення зберігається десь у оперативній пам’яті як 10011001; якщо я програма, яка виконує код, то як я буду знати, чи цей 10011001 - це char чи int?

Як було сказано: "Контекст" - тобто попередні та наступні інструкції - допомагають обробляти дані таким чином, як ми цього хочемо. З машинної точки зору, немає різниці в будь-якому місці пам'яті. intі charце лише словниковий запас, який має сенс у compiletime; під час runtime(на рівні складання) немає charабо int.

Що я не розумію, це те, як комп'ютер знає, коли він зчитує значення змінної з такої адреси, як 10001, будь то int чи char.

Комп’ютер нічого не знає . Програміст робить. Скомпільований код породжує контекст , необхідний для створення значущих результатів для людини.

Чи містить цей виконуваний файл інформацію про те, чи зберігаються змінні типу int або char

Так і Ні . Інформація, чи є це, intчи charвтрачається. Але з іншого боку, контекст (інструкції, які розповідають, як поводитися з місцями пам'яті, де зберігаються дані) зберігається; так що неявно так, "інформація" неявно доступна.


Хороша відмінність між часом компіляції та часом виконання.
Майкл Блекберн

2

Давайте будемо тримати цю дискусію лише мовою С.

Програма, на яку ви посилаєтесь, написана мовою високого рівня, наприклад C. Комп'ютер розуміє лише машинну мову. Мови вищого рівня надають програмісту можливість виражати логіку більш людяним способом, який потім переводиться на машинний код, який мікропроцесор може декодувати і виконувати. Тепер обговоримо згаданий вами код:

char a = 'A';
int x = 4;

Спробуємо проаналізувати кожну частину:

char / int відомі як типи даних. Вони кажуть компілятору виділити пам'ять. У випадку з charним буде 1 байт і int2 байти. (Зверніть увагу, що розмір пам'яті знову залежить від мікропроцесора).

a / x відомі як ідентифікатори. Тепер це ви можете сказати "зручні для користувача" імена, присвоєні місцям пам'яті в оперативній пам'яті.

= вказує компілятору зберігати "A" у місці пам'яті aта 4 у пам'яті x.

Отже, ідентифікатори типу int / char використовуються лише компілятором, а не мікропроцесором під час виконання програми. Отже, вони не зберігаються в пам'яті.


нормально ідентифікатори типу int / char даних безпосередньо не зберігаються в пам'яті як змінні, але чи вони непрямо зберігаються у файлі EXE серед інструкційних кодів і врешті-решт займають місця в пам'яті? Я пишу це ще раз для вас: Якщо ЦП знайде 0x00000061 у реєстрі та витягне його; і уявіть, що програма консолі повинна виводити це як символ не int. чи є в цьому файлі EXE (машина / двійковий код) якісь інструкційні коди, які знають, що адреса 0x00000061 є символом та перетворюється на символ за допомогою таблиці ASCII? Якщо це так, це означає, що ідентифікатори char int опосередковано знаходяться у двійковій формі ???
користувач16307

Ні для процесора всі його номери. Для вашого конкретного прикладу друк на консолі не залежить від того, чи змінною є char чи int. Я оновлю свою відповідь детальним потоком того, як програма високого рівня перетворюється в машинну мову до виконання програми.
prasad

2

Моя відповідь тут дещо спрощена і стосуватиметься лише C.

Ні, інформація про тип не зберігається в програмі.

intабо charне є індикаторами типу ЦП; тільки до компілятора.

У EXE, створеному компілятором, будуть інструкції по маніпулюванню ints, якщо змінна була оголошена як int. Так само, якщо змінна була оголошена як a char, exe буде містити інструкції по маніпулюванню a char.

В:

int main()
{
    int a = 65;
    char b = 'A';
    if(a == b)
    {
        printf("Well, what do you know. A char can equal an int.\n");
    }
    return 0;
}

Ця програма надрукує своє повідомлення, оскільки у charі RAM intє однакові значення .

Тепер, якщо вам цікаво, як printfвдається вивести 65для intі Aдля a char, це тому, що вам потрібно вказати у "рядку формату", як printfслід ставитися до значення .
(Наприклад, %cозначає трактувати значення як a char, а %dозначає трактувати значення як ціле число; хоч і те саме значення в будь-якому випадку.)


2
Я сподівався, що хтось використає приклад, використовуючи printf. @OP: int a = 65; printf("%c", a)виведе 'A'. Чому? Тому що процесор не хвилює. Для цього все, що він бачить, - це біти. Ваша програма сказала процесору зберігати 65 (випадково значення 'A'ASCII) у, aа потім виводити символ, що з радістю робить. Чому? Тому що це не байдуже.
Коул Джонсон

але чому деякі кажуть тут у випадку C #, це не історія? я читаю деякі інші коментарі, і вони говорять, що в C # і C ++ історія (інформація про типи даних) відрізняється, і навіть процесор не робить обчислень. Будь-які ідеї з цього приводу?
користувач16307

@ user16307 Якщо процесор не виконує обчислення, програма не працює. :) Щодо C #, я не знаю, але я думаю, що моя відповідь стосується і цього. Що стосується C ++, я знаю, що моя відповідь застосовується саме там.
BenjiWiebe

0

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

Це все процесор коли-небудь, все, що він коли-небудь може зробити. Немає такого поняття, як int чи char.

x = 4 + 5

Виконується як:

  1. Завантажте 00000100 в регістр 1
  2. Завантажте 00000101 в регістр 2
  3. IAdd регістр 1, щоб зареєструвати 2, і зберігати в регістрі 1

Інструкція iadd запускає апаратне забезпечення, яке поводиться так, ніби регістри 1 і 2 є цілими числами. Якщо вони насправді не представляють цілих чисел, пізніше всі речі можуть піти не так. Найкращий результат - як правило, крах.

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

редагувати: Зауважте, що фактичний код машини насправді ніде не згадує 4, 5 або ціле число. це лише два шаблони бітів, і інструкція, яка бере два бітні шаблони, припускає, що вони є ints, і додає їх разом.


0

Коротка відповідь, тип кодується в інструкціях щодо процесора, які створює компілятор.

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

як виконання знає, що a - це char, а x - int?

Це не так, але коли компілятор виробляє машинний код, він знає. А intі а charможуть бути різного розміру. В архітектурі, де char є розміром байта, а int - 4 байти, змінна xне в адресах 10001, а також у 10002, 10003 та 10004. Коли коду потрібно завантажити значення xв регістр процесора, він використовує інструкцію для завантаження 4 байтів. Завантажуючи char, він використовує інструкцію для завантаження 1 байта.

Як вибрати, яку з двох інструкцій? Компілятор вирішує під час компіляції, це не робиться під час виконання після перевірки значень у пам'яті.

Зауважте також, що регістри можуть бути різного розміру. У процесорах Intel x86 EAX шириною 32 біти, половина - AX, що становить 16, а AX розділено на AH та AL, обидва - 8 біт.

Отже, якщо ви хочете завантажити ціле число (на процесори x86), ви використовуєте інструкцію MOV для цілих чисел, щоб завантажити char, ви використовуєте інструкцію MOV для символів. Їх обох називають MOV, але вони мають різні коди. Ефективно - дві різні інструкції. Тип змінної закодований в інструкції до використання.

Те ж саме відбувається і з іншими операціями. Існує багато вказівок щодо виконання додавання, залежно від розміру операндів, і навіть якщо вони підписані або непідписані. Див. Https://en.wikipedia.org/wiki/ADD_(x86_instruction), де перераховано різні можливі доповнення.

Скажімо, значення зберігається десь у оперативній пам’яті як 10011001; якщо я програма, яка виконує код, то як я буду знати, чи цей 10011001 - це char чи int

По-перше, знаком буде 10011001, але інтом буде 00000000 00000000 00000000 10011001, тому що вони різного розміру (на комп'ютері з тими ж розмірами, як згадувалося вище). Але давайте розглянемо випадок для signed charVS unsigned char.

Те, що зберігається в пам'яті, можна інтерпретувати будь-коли. Частина обов'язків компілятора С полягає в тому, щоб те, що зберігається і читається зі змінної, виконується послідовно. Так що програма не знає, що зберігається в пам'яті, а в тому, що вона завжди погоджується, що вона завжди буде читати і записувати такі самі речі. (не рахуючи таких речей, як типи лиття).


але чому деякі кажуть тут у випадку C #, це не історія? я читаю деякі інші коментарі, і вони говорять, що в C # і C ++ історія (інформація про типи даних) відрізняється, і навіть процесор не робить обчислень. Будь-які ідеї з цього приводу?
користувач16307

0

але чому деякі кажуть тут у випадку C #, це не історія? я читаю деякі інші коментарі, і вони говорять, що в C # і C ++ історія (інформація про типи даних) відрізняється, і навіть процесор не робить обчислень. Будь-які ідеї з цього приводу?

У мовах, що перевіряються типом, як C #, перевірка типу здійснюється компілятором. У коді Бенджі написано:

int main()
{
    int a = 65;
    char b = 'A';
    if(a == b)
    {
        printf("Well, what do you know. A char can equal an int.\n");
    }
    return 0;
}

Просто відмовився б складати. Аналогічно, якщо ви намагалися множити рядок і ціле число (я збирався сказати додати, але оператор "+" перевантажений конкатенацією рядків, і це може просто працювати).

int a = 42;
string b = "Compilers are awesome.";
double[] c = a * b;

Компілятор просто відмовився б генерувати машинний код із цього C #, незалежно від того, наскільки ваша струна поцілувалася до нього.


-4

Інші відповіді правильні, оскільки фактично кожен споживчий пристрій, з яким ви стикаєтесь, не зберігає інформацію про тип. Однак у минулому (і сьогодні в дослідницькому контексті) було декілька апаратних конструкцій, які використовують теговану архітектуру - вони зберігають дані і тип (і, можливо, іншу інформацію). Сюди найбільше слід віднести машини Lisp .

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


3
У питанні конкретно зазначено, що він посилається на мову C (а не на Lisp), а мова C не зберігає змінних метаданих. Хоча це, безумовно, можливо, для реалізації C це зробити, оскільки стандарт цього не забороняє, але на практиці це ніколи не відбувається. Якщо у вас є приклади , що стосуються питання, надішліть листа з вказівкою конкретні цитати і надати посилання , які відносяться до мови C .

Ну, ви можете написати компілятор С для машини Lisp, але ніхто не використовує машини Lisp в цей день і вік взагалі. Об’єктно-орієнтована архітектура , до речі, була Рекурсів .
Натан Рінго

2
Я думаю, що ця відповідь не корисна. Це ускладнює речі, що перевищують сучасний рівень розуміння ОП. Зрозуміло, що ОП не розуміє основної моделі виконання CPU + оперативної пам'яті, і як компілятор переводить символічне джерело високого рівня у виконуваний двійковий файл. Помічена пам'ять, RTTI, Lisp тощо, на мою думку, є набагато вищим за те, що запитуючий повинен знати, і лише його більше заплутає.
Андрес Ф.

але чому деякі кажуть тут у випадку C #, це не історія? я читаю деякі інші коментарі, і вони говорять, що в C # і C ++ історія (інформація про типи даних) відрізняється, і навіть процесор не робить обчислень. Будь-які ідеї з цього приводу?
користувач16307
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.