Хранятся ли в оперативной памяти объявления типов данных, такие как «int» и «char», когда выполняется программа на C?


74

Когда выполняется программа на C, данные сохраняются в куче или стеке. Значения хранятся в адресах RAM. Но как насчет индикаторов типа (например, intили char)? Они тоже хранятся?

Рассмотрим следующий код:

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

Я прочитал, что A и 4 хранятся в адресах RAM здесь. Но как насчет aи x? Наиболее запутанно, как выполнение узнает, что aэто char и xint? Я имею в виду, intи charупоминается ли где-нибудь в оперативной памяти?

Допустим, значение хранится где-то в ОЗУ как 10011001; если я являюсь программой, которая выполняет код, как я узнаю, является ли это 10011001 charили int?

Что я не понимаю, так это то, как компьютер знает, когда он читает значение переменной с адреса, такого как 10001, является ли он intили char. Представьте, что я нажимаю на программу под названием anyprog.exe. Сразу же начинает выполняться код. Включает ли этот исполняемый файл информацию о том, являются ли хранимые переменные типа intили char?


24
Эта информация полностью теряется во время выполнения. Вы (и ваш компилятор) должны заранее убедиться, что память будет интерпретирована правильно. Это ответ, который вы получили после?
5gon12eder

4
Это не так. Поскольку он предполагает, что вы знаете, что делаете, он берет все, что находит по указанному вами адресу памяти, и записывает это в стандартный вывод. Если то, что было написано, соответствует читаемому символу, оно в конечном итоге будет отображаться на чьей-то консоли как читаемый символ. Если это не так, это будет выглядеть как бред или, возможно, случайный читаемый символ.
Роберт Харви

22
@ user16307 Короткий ответ заключается в том, что в статически типизированных языках всякий раз, когда вы печатаете символ, компилятор будет генерировать код, отличный от того, который был бы для вывода целого числа. Во время выполнения больше не известно, что xтакое char, но запускается код печати символов, потому что это то, что выбрал компилятор.
Ixrec

13
@ user16307 Он всегда хранится как двоичное представление числа 65. Будет ли оно напечатано как 65 или как A, зависит от кода, который ваш компилятор создал для его распечатки. Рядом с 65 нет метаданных, которые говорят, что это на самом деле char или int (по крайней мере, не в статически типизированных языках, таких как C).
Ixrec

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расположению в памяти и прочитать его:

0x00000004: 0x00000061

Шестнадцатеричное значение для aASCII равно 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-файле), которые делают этот переход к тому, что мы видим на экране?
user16307

2
@ user16307 да, есть дополнительные инструкции. Каждая строка кода, которую вы пишете, потенциально может быть превращена во множество инструкций. Есть инструкции, чтобы выяснить, какой символ использовать, есть инструкции, какие пиксели нужно изменить и на какой цвет они меняют, и т. Д. Есть также код, который вы на самом деле не видите. Например, использование std :: cout означает, что вы используете библиотеку. Ваш код для записи в консоль может состоять только из одной строки, но функции, которые вы вызываете, будут содержать больше строк, и каждая строка может превратиться во множество машинных инструкций.
Shaz

8
@ user16307 Otherwise how can console or text file outputs a character instead of int Поскольку существует другая последовательность инструкций для вывода содержимого ячейки памяти в виде целого числа или буквенно-цифровых символов. Компилятор знает о типах переменных, выбирает соответствующую последовательность инструкций во время компиляции и записывает ее в EXE.
Чарльз И. Грант

2
Я бы нашел другую фразу для «самого байтового кода», поскольку байт-код (или байт-код) обычно относится к промежуточному языку (например, Java-байт-код или MSIL), который может фактически хранить эти данные для использования во время выполнения. К тому же, не совсем понятно, на что подразумевается «байт-код» в этом контексте. В противном случае, хороший ответ.
jpmc26

6
@ user16307 Постарайтесь не беспокоиться о C ++ и C #. То, что говорят эти люди, намного выше вашего текущего понимания работы компьютеров и компиляторов. В целях того, что вы пытаетесь понять, аппаратное обеспечение НЕ знает ничего о типах, char, int или чем-либо еще. Когда вы сказали компилятору, что некоторая переменная была int, он генерировал исполняемый код для обработки области памяти, КАК ЕСЛИ это был int. Сама ячейка памяти не содержит информации о типах; просто ваша программа решила рассматривать его как int. Забудьте все остальное, что вы слышали об информации типа времени выполнения.
Андрес Ф.

43

Я думаю, что ваш главный вопрос выглядит так: «Если тип стирается во время компиляции и не сохраняется во время выполнения, то как компьютер узнает, выполнять ли intкод, который интерпретирует его как код, или выполнять код, который интерпретирует его как char? "

И ответ ... компьютер не делает. Однако компилятор действительно знает, и это будет просто поставить правильный код в двоичном в первую очередь. Если бы переменная была напечатана как char, то компилятор не поместил бы код для обработки ее как a intв программу, он поместил бы код для обработки ее как a char.

Там являются причины сохраняющего типа во время выполнения:

  • Динамическая типизация: в динамической типизации проверка типов происходит во время выполнения, поэтому, очевидно, тип должен быть известен во время выполнения. Но C не динамически типизирован, поэтому типы могут быть безопасно удалены. (Обратите внимание, что это совсем другой сценарий. Динамические типы и статические типы на самом деле не одно и то же, и в языке смешанного типа вы все равно можете стереть статические типы и сохранить только динамические типы.)
  • Динамический полиморфизм: если вы выполняете другой код, основанный на типе времени выполнения, то вам необходимо сохранить тип времени выполнения. C не имеет динамического полиморфизма (на самом деле он вообще не имеет никакого полиморфизма, за исключением некоторых специальных жестко закодированных случаев, например, +оператора), поэтому по этой причине ему не требуется тип времени выполнения. Однако, опять же, тип времени выполнения в любом случае отличается от статического типа, например, в Java вы можете теоретически стереть статические типы и при этом сохранить тип времени выполнения для полиморфизма. Также обратите внимание, что если вы децентрализуете и специализируете код поиска типов и помещаете его в объект (или класс), то вам также не обязательно нужен тип времени выполнения, например C ++ vtables.
  • Отражение во время выполнения: если вы позволяете программе отражать свои типы во время выполнения, тогда вам, очевидно, нужно сохранять типы во время выполнения. Вы можете легко увидеть это с Java, который сохраняет типы первого порядка во время выполнения, но стирает аргументы типа в универсальные типы во время компиляции, так что вы можете отражать только конструктор типа («необработанный тип»), но не аргумент типа. Опять же, C не имеет отражения во время выполнения, поэтому ему не нужно хранить тип во время выполнения.

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

Тип Erasure вполне нормальный. Это не влияет на безопасность типов: типы проверяются во время компиляции, как только компилятор убедится, что программа безопасна для типов, типы больше не нужны (по этой причине). Это не влияет на статический полиморфизм (он же перегрузка): когда разрешение перегрузки завершено, и компилятор выбрал правильную перегрузку, ему больше не нужны типы. Типы также могут направлять оптимизацию, но, опять же, после того, как оптимизатор выбрал свои оптимизации на основе типов, они больше не нужны.

Сохранение типов во время выполнения требуется только тогда, когда вы хотите что-то сделать с типами во время выполнения.

Haskell - один из самых строгих, самых строгих, типобезопасных статически типизированных языков, и компиляторы Haskell обычно стирают все типы. (Исключением является передача словарей методов для классов типов, я считаю.)


3
Нет! Почему? Для чего нужна эта информация? Компилятор выводит код для чтения charв скомпилированный двоичный файл. Он не выводит код для int, он не выводит код для byte, он не выводит код для указателя, он просто выводит только код для char. В зависимости от типа не принимаются решения во время выполнения. Вам не нужен тип. Это совершенно и совершенно не имеет значения. Все соответствующие решения уже были приняты во время компиляции.
Йорг Миттаг

2
Нет Компилятор просто помещает код для печати char в двоичный файл. Период. Компилятор знает, что по этому адресу памяти есть char, поэтому он помещает код для печати char в двоичный файл. Если значение по этому адресу памяти по какой-то странной причине оказалось не символом, то, ну, все, черт возьми, вырвалось на свободу В основном так работает целый класс эксплойтов.
Йорг Миттаг

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

9
Нет. Компилятор знает, какой код он должен поместить в двоичный файл. Нет смысла хранить эту информацию. Если вы печатаете int, компилятор поместит код для печати int. Если вы печатаете символ, компилятор поместит код для печати символа. Период. Но это всего лишь небольшая картина. Код для печати символа будет интерпретировать битовый шаблон определенным образом, код для печати int будет интерпретировать бит другим способом, но нет способа отличить битовый шаблон, который является int, от битового шаблона, который это символ, это строка битов.
Йорг Миттаг

2
@ user16307: «Не содержит ли exe-файл информацию о том, какой адрес и какой тип данных?» Может быть. Если вы компилируете данные отладки, данные отладки будут включать информацию об именах переменных, адресах и типах. И иногда эти данные отладки хранятся в файле .exe (в виде двоичного потока). Но он не является частью исполняемого кода, и он не используется самим приложением, только отладчиком.
Бен Фойгт

12

Компьютер не «знает», что это за адреса, но знает, что есть, в соответствии с инструкциями вашей программы.

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

Когда приходит время читать, в инструкциях не говорится «посмотрите, какой тип данных есть», а просто говорится что-то вроде «загрузить эту память как число с плавающей запятой». Если адрес для чтения был изменен или что-то перезаписало эту память чем-то, отличным от плавающего числа, то ЦПУ просто в любом случае с радостью загрузит эту память как число с плавающей запятой, и в результате могут произойти любые странные вещи.

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


как бы вы объяснили, если процессор находит 0x00000061 в регистре и выбирает его; и представьте, что консольная программа должна выводить это как символ, а не как int. Вы имеете в виду, что в этом exe-файле есть некоторые коды команд, которые знают, что адрес 0x00000061 является символом и преобразуется в символ с использованием таблицы ASCII?
user16307

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

@ 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? -
user16307

@ user16307 Нет, нет никакой дополнительной информации о том, является ли что-то int или char. Позже я добавлю некоторые примеры, предполагая, что никто не побьет меня.
8bittree

1
@ user16307: EXE-файл содержит эту информацию косвенно. Процессор, выполняющий программу, не заботится о типах, используемых при написании программы, но большая часть этого может быть выведена из инструкций, используемых для доступа к различным ячейкам памяти.
Барт ван Инген Шенау

@ user16307 там на самом деле немного дополнительной информации. Exe-файлы знают, что целое число составляет 4 байта, поэтому, когда вы пишете «int a», компилятор резервирует 4 байта для переменной a и, таким образом, может вычислить адрес 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, но они не имеют отношения к обсуждению.

%eax32-битный регистр данных общего назначения %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

Исторически C считал память состоящей из нескольких групп пронумерованных слотов типа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 RULES!" или делать что-либо еще, исходя из теории, что было бы законно, чтобы в ячейках памяти содержалось aоборудование, которое записывает, какой тип использовался для их записи, и чтобы такое оборудование реагировало на недопустимую попытку чтения любым способом, в том числе путем "ФРИНК ПРАВИЛА!" быть выведенным.

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

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

Однако в 2009 году авторы некоторых компиляторов, таких как CLANG, определили, что, поскольку стандарт позволяет компиляторам делать все, что им нравится, в случаях, когда память записывается с использованием одного типа и читается как другой, компиляторы должны сделать вывод, что программы никогда не получат ввод, который мог бы вызвать такую ​​вещь, чтобы произойти. Поскольку в стандарте говорится, что компилятору разрешено делать все, что ему нравится, когда получен такой недопустимый ввод, код, который будет иметь эффект только в тех случаях, когда стандарт не предъявляет никаких требований, может (и, по мнению некоторых авторов компилятора, должен) быть опущен как неактуально Это изменяет поведение нарушений псевдонимов по сравнению с тем, что память, которая, учитывая запрос на чтение, может произвольно возвращать последнее значение, записанное с использованием того же типа, что и запрос на чтение, или любое более недавнее значение, записанное с использованием какого-либо другого типа,


1
Упоминание неопределенного поведения при обрезке шрифтов тому, кто не понимает, почему RTTI отсутствует, кажется нелогичным
Коул Джонсон,

@ColeJohnson: Жаль, что нет никакого формального названия или стандарта для диалекта C, поддерживаемого 99% компиляторов до 2009 года, поскольку как с точки зрения обучения, так и с практической точки зрения их следует рассматривать как принципиально разные языки. Поскольку одно и то же название дано как диалекту, который развил ряд предсказуемых и оптимизируемых поведений за 35 лет, так и диалекту, который выбрасывает такое поведение для предполагаемой цели оптимизации, трудно избежать путаницы, говоря о вещах, которые в них работают по-разному. ,
суперкат

Исторически C работал на машинах Lisp, которые не позволяли так свободно играть с типами. Я почти уверен, что многие из «предсказуемых и оптимизируемых поведений», замеченных 30 лет назад, просто нигде не работали, кроме BSD Unix на VAX.
Просфилаес

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

1
... где unsigned charзначения, которые используются для создания типа "пришли". Если программа должна была разложить указатель на unsigned char[], быстро отобразить его шестнадцатеричное содержимое на экране, а затем стереть указатель unsigned char[], а затем принять некоторые шестнадцатеричные числа с клавиатуры, скопировать их обратно в указатель и затем разыменовать этот указатель Поведение будет четко определено в случае, когда набранное число совпадает с отображаемым числом.
Суперкат

3

В С этого нет. Другие языки (например, Lisp, Python) имеют динамические типы, но C статически типизирован. Это означает, что ваша программа должна знать, какой тип данных должен интерпретироваться как символ, целое число и т. Д.

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


Чего я не понимаю, так это как компьютер знает, когда он читает значение переменной и адрес, такой как 10001, если это int или char. Представьте, что я нажимаю на программу anyprog.exe. Сразу же начинает выполняться код. Включает ли этот exe-файл информацию о том, хранятся ли переменные как в или как char? -
user16307

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

1
Обратите внимание, что динамическая типизация - не единственная причина для сохранения типов. Java статически типизирована, но она все равно должна сохранять типы, потому что она позволяет динамически отражать тип. Кроме того, он имеет полиморфизм во время выполнения, то есть диспетчеризацию метода на основе типа времени выполнения, для которого ему также необходим тип. C ++ помещает код диспетчеризации метода в сам объект (или, скорее, в класс), поэтому ему не нужен тип в некотором смысле (хотя, конечно, в некотором смысле vtable является частью типа, поэтому, по крайней мере, частью тип будет сохранен), но в Java, код способа доставки централизован.
Йорг Миттаг

посмотрите на мой вопрос, который я написал "когда выполняется программа на C?" разве они косвенно хранятся в exe-файле среди кодов команд и в конечном итоге размещаются в памяти? Я пишу это снова для вас: если процессор находит 0x00000061 в регистре и извлекает его; и представьте, что консольная программа должна выводить это как символ, а не как int. есть ли в этом exe-файле (машинный / двоичный код) некоторые коды команд, которые знают, что адрес 0x00000061 является символом и преобразуется в символ с использованием таблицы ASCII? Если это так, то это означает, что идентификаторы типа int косвенно находятся в двоичном формате ???
user16307

Если значение равно 0x61 и объявлено как символ (т. Е. «A»), и вы вызываете подпрограмму для его отображения, [в конце концов] будет системный вызов для отображения этого символа. Если вы объявили его как int и вызвали подпрограмму display, компилятор будет знать, как сгенерировать код для преобразования 0x61 (десятичное 97) в последовательность ASCII 0x39, 0x37 ('9', '7'). Итог: сгенерированный код отличается, потому что компилятор знает, как обращаться с ними по-разному.
Майк Харрис

3

Вы должны различать compiletimeи runtimeс одной стороны , и , codeи dataс другой стороны.

С точки зрения машины нет разницы между тем, что вы называете codeили instructionsтем, что вы называете data. Все сводится к цифрам. Но некоторые последовательности - то, что мы бы назвали code- делают то, что мы считаем полезным, другие - просто crashмашина.

Работа, выполняемая ЦП, представляет собой простой 4-х шаговый цикл:

  • Получить "данные" с заданного адреса
  • Расшифруйте инструкцию (то есть "интерпретируйте" число как instruction)
  • Прочитайте эффективный адрес
  • Выполнить и сохранить результаты

Это называется инструктивным циклом .

Я прочитал, что A и 4 хранятся в адресах RAM здесь. Но как насчет а и х?

aи xявляются переменными, которые являются заполнителями для адресов, где программа могла бы найти «содержимое» переменных. Таким образом, каждый раз, когда используется переменная a, фактически существует адрес используемого содержимого a.

Наиболее запутанно, как выполнение узнает, что a - это символ, а x - это int?

Казнь не знает ничего. Из того, что было сказано во введении, процессор только выбирает данные и интерпретирует эти данные как инструкции.

Функция printf предназначена для того, чтобы «знать», какой тип ввода вы вводите в нее, то есть ее результирующий код дает правильные инструкции, как обращаться с особым сегментом памяти. Конечно, есть возможность скопировать бессмысленный вывод: использование адреса, в котором не хранится ни одной строки вместе с «% s» printf(), приведет к тому, что бессмысленный вывод будет остановлен только случайным местом в памяти, где 0 ( \0).

То же самое касается точки входа в программу. Под C64 можно было помещать ваши программы в (почти) каждый известный адрес. Ассемблер-программы были запущены с инструкции, которая называлась sysадресом: это sys 49152было обычное место для написания кода вашего ассемблера. Но ничто не помешало вам загрузить, например, графические данные 49152, что привело к падению машины после «запуска» с этой точки. В этом случае цикл инструкций начинался со считывания «графических данных» и попытки интерпретировать их как «код» (что, конечно, не имело смысла); эффекты были иногда поразительными;)

Допустим, значение хранится где-то в ОЗУ как 10011001; если я - программа, которая выполняет код, как я узнаю, является ли этот 10011001 символом или целым числом?

Как сказано: «контекст» - то есть предыдущие и последующие инструкции - помогают обрабатывать данные так, как мы хотим. С точки зрения машины, нет разницы ни в каком месте памяти. 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' в ячейке памяти aи 4 в ячейке памяти x.

Таким образом, идентификаторы типа данных int / char используются только компилятором, а не микропроцессором во время выполнения программы. Следовательно, они не хранятся в памяти.


Хорошо, идентификаторы типа данных int / char не хранятся непосредственно в памяти как переменные, но не косвенно ли они хранятся в exe-файле среди кодов команд и в конечном итоге размещаются в памяти? Я пишу это снова для вас: если процессор находит 0x00000061 в регистре и извлекает его; и представьте, что консольная программа должна выводить это как символ, а не как int. есть ли в этом exe-файле (машинный / двоичный код) некоторые коды команд, которые знают, что адрес 0x00000061 является символом и преобразуется в символ с использованием таблицы ASCII? Если это так, то это означает, что идентификаторы типа int косвенно находятся в двоичном формате ???
user16307

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

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и в оперативной памяти intимеют одинаковые значения .

Теперь, если вам интересно, как printfудается выводить 65для intи Aдля char, то это потому, что вы должны указать в «строке формата», как printfследует обрабатывать значение .
(Например, %cозначает, что значение рассматривается как a char, и %dозначает, что значение рассматривается как целое число; в любом случае, то же значение.)


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

но почему некоторые говорят здесь, в случае C #, это не история? Я прочитал некоторые другие комментарии, и они говорят, что в C # и C ++ история (информация о типах данных) отличается, и даже процессор не выполняет вычисления. Есть идеи по этому поводу?
user16307

@ user16307 Если процессор не выполняет вычисления, программа не работает. :) Что касается C #, я не знаю, но я думаю, что мой ответ применим и там. Что касается C ++, я знаю, что мой ответ применим там.
BenjiWiebe

0

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

Это все, что делает процессор, все, что он может сделать. Нет такой вещи как int или char.

x = 4 + 5

Будет выполнять как:

  1. Загрузите 00000100 в регистр 1
  2. Загрузите 00000101 в регистр 2
  3. IДобавьте регистр 1 в регистр 2 и сохраните в регистр 1

Инструкция iadd запускает аппаратное обеспечение, которое ведет себя так, как будто регистры 1 и 2 являются целыми числами. Если они на самом деле не представляют собой целые числа, позже все может пойти не так. Лучший результат обычно терпит крах.

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

редактирование: обратите внимание, что фактический машинный код фактически не упоминает 4, 5 или целое число где-либо. это просто два набора битов и инструкция, которая принимает два набора битов, предполагает, что они являются целочисленными, и складывает их вместе.


0

Короткий ответ, тип закодирован в инструкциях процессора, которые генерирует компилятор.

Хотя информация о типе или размере информации не сохраняется напрямую, компилятор отслеживает эту информацию при доступе, изменении и хранении значений в этих переменных.

как выполнение знает, что a - это символ, а x - это int?

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

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

Обратите внимание, что регистры могут быть разных размеров. На процессорах Intel x86 EAX имеет ширину 32 бита, половину составляет AX, то есть 16, а AX делится на AH и AL, оба на 8 бит.

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

То же самое происходит с другими операциями. Существует много инструкций для выполнения сложения, в зависимости от размера операндов, и даже если они подписаны или не подписаны. См. Https://en.wikipedia.org/wiki/ADD_(x86_instruction), где перечислены различные возможные дополнения.

Допустим, значение хранится где-то в ОЗУ как 10011001; если я - программа, которая выполняет код, как я узнаю, является ли это 10011001 символом или целым

Во-первых, char будет 10011001, но int будет 00000000 00000000 00000000 10011001, потому что они имеют разные размеры (на компьютере с такими же размерами, как указано выше). Но давайте рассмотрим случай для signed charпротив unsigned char.

То, что хранится в ячейке памяти, можно интерпретировать как угодно. Часть обязанностей компилятора C состоит в том, чтобы гарантировать, что то, что хранится и читается из переменной, выполняется согласованным образом. Таким образом, дело не в том, что программа знает, что хранится в ячейке памяти, а в том, что она заранее согласна с тем, что она всегда будет читать и записывать одни и те же вещи. (не считая таких вещей, как типы кастинга).


но почему некоторые говорят здесь, в случае C #, это не история? Я прочитал некоторые другие комментарии, и они говорят, что в C # и C ++ история (информация о типах данных) отличается, и даже процессор не выполняет вычисления. Есть идеи по этому поводу?
user16307

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 для машины на Лиспе, но никто не использует машины на Лиспе в наше время и в целом. Кстати , объектно-ориентированная архитектура была рекурсивной .
Натан Ринго

2
Я думаю, что этот ответ не поможет. Это усложняет ситуацию далеко за пределы текущего уровня понимания ОП. Ясно, что OP не понимает базовую модель выполнения CPU + RAM и как компилятор переводит символический источник высокого уровня в исполняемый двоичный файл. По моему мнению, помеченная память, RTTI, Lisp и т. Д. - это далеко за пределы того, что спрашивающий должен знать, и это только смущает его / ее.
Андрес Ф.

но почему некоторые говорят здесь, в случае C #, это не история? Я прочитал некоторые другие комментарии, и они говорят, что в C # и C ++ история (информация о типах данных) отличается, и даже процессор не выполняет вычисления. Есть идеи по этому поводу?
user16307
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.