Подробности реализации указателя в C - PullRequest
16 голосов
/ 30 августа 2009

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

  1. sizeof (int *) == sizeof (char *) == sizeof (void *) == sizeof (func_ptr *)

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

  3. Представление указателя в памяти совпадает с целым числом той же длины в битах, что и архитектура.

  4. Умножение и деление типов данных указателя запрещено только компилятором. ПРИМЕЧАНИЕ: Да, я знаю, что это бессмысленно. Я имею в виду, есть ли аппаратная поддержка, запрещающая такое неправильное использование?

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

  6. Увеличение указателя эквивалентно добавлению sizeof(the pointed data type) к адресу памяти, сохраненному указателем. Если p является int32*, тогда p+1 равен адресу памяти через 4 байта после p.

Я больше всего привык к указателям, которые используются в непрерывном виртуальном пространстве памяти. Для такого использования я обычно могу считать их адресами в числовой строке. См. Вопрос переполнения стека Сравнение указателей .

Ответы [ 11 ]

10 голосов
/ 30 августа 2009

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

sizeof(int *) == sizeof(char *) == sizeof(void *) == sizeof(func_ptr *)

Я не знаю ни одной системы, в которой я знаю , что это неверно, но рассмотрим:

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

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

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

Подумайте о членских указателях против обычных указателей. Они не имеют одинаковое представление (или размер). Указатель члена состоит из указателя this и смещения.

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

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

Зависит от того, как определяется эта длина в битах. :) int на многих 64-битных платформах по-прежнему 32 бит. Но указатель составляет 64 бита. Как уже было сказано, процессоры с моделью сегментированной памяти будут иметь указатели, состоящие из пары чисел. Точно так же указатели членов состоят из пары чисел.

Умножение и деление типов данных указателя запрещено только компилятором.

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

Однако, чтобы ответить на вопрос, что вы имеете в виду , найдите процессоры Motorola 68000. Я считаю, что у них есть отдельные регистры для целых чисел и адресов памяти. Это означает, что они могут легко запретить такие бессмысленные операции.

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

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

Увеличение указателя эквивалентно добавлению sizeof (указанного типа данных) к адресу памяти, сохраняемому указателем.

Опять же, это гарантировано. Если вы считаете, что концептуально указатель не указывает на адрес, он указывает на объект , тогда это имеет смысл. Добавление одного к указателю, очевидно, сделает его указателем на объект next . Если длина объекта составляет 20 байт, то при увеличении указателя он будет перемещен на 20 байт, чтобы он переместился к следующему объекту .

.

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

Наконец, как я уже упоминал в комментарии к вашему вопросу, имейте в виду, что C ++ - это просто язык. Неважно, к какой архитектуре она компилируется. Многие из этих ограничений могут показаться неясными на современных процессорах. Но что, если вы ориентируетесь на ЦП прошлых лет? Что, если вы ориентируетесь на ЦП следующего десятилетия? Вы даже не знаете, как они будут работать, поэтому вы не можете много о них думать. Что если вы ориентируетесь на виртуальную машину? Уже существуют компиляторы, которые генерируют байт-код для Flash и готовы к запуску с веб-сайта. Что если вы хотите скомпилировать исходный код C ++ в Python?

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

8 голосов
/ 30 августа 2009

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

  1. Стандарт не требуется ( см. Этот вопрос ). Например, sizeof(int*) может быть не равно size(double*). void* гарантированно сможет хранить любое значение указателя.
  2. Стандарт не требуется. По определению, размер является частью представления. Если размер может быть другим, представление также может быть другим.
  3. Не обязательно. На самом деле «длина битов архитектуры» - это смутное утверждение. Что такое 64-битный процессор? Это адресная шина? Размер регистров? Шина данных? Что?
  4. Нет смысла «умножать» или «делить» указатель. Это запрещено компилятором, но вы, конечно, можете умножить или разделить базовое представление (что на самом деле не имеет смысла для меня), что приводит к неопределенному поведению.
  5. Может быть, я не понимаю вашу точку зрения, но все в цифровом компьютере - это просто какое-то двоичное число.
  6. Да; вид. Это гарантированно указывает на местоположение, которое sizeof(pointer_type) дальше. Это не обязательно эквивалентно арифметическому добавлению числа (т. Е. дальше - логическая концепция. Фактическое представление зависит от архитектуры)
7 голосов
/ 30 августа 2009

Для 6 .: указатель не обязательно является адресом памяти. См., Например, « Великий заговор указателей » от пользователя переполнения стека jalf :

Да, я использовал слово «адрес» в комментарии выше. Важно понять, что я имею в виду под этим. Я имею в виду не «адрес памяти, по которому физически хранятся данные», а просто абстракцию «все, что нам нужно, чтобы найти значение. Адрес i может быть любым, но если он у нас есть, мы всегда можем найти и изменить i. "

И

Указатель не является адресом памяти! Я упоминал об этом выше, но давайте скажем это снова. Указатели обычно реализуются компилятором просто как адреса памяти, да, но это не обязательно. "

6 голосов
/ 30 августа 2009

Дополнительная информация об указателях из стандарта C99:

  • 6.2.5 §27 гарантирует, что void* и char* имеют идентичные представления, то есть их можно использовать взаимозаменяемо без преобразования, то есть один и тот же адрес обозначается одним и тем же битовым шаблоном (что не обязательно должно быть истинным) для других типов указателей)
  • 6.3.2.3 §1 гласит, что любой указатель на неполный или тип объекта может быть приведен к (и из) void* и обратно и все еще быть действительным; сюда не входят указатели функций!
  • 6.3.2.3 §6 гласит, что void* может быть приведен к (и из) целым числам, а 7.18.1.4 §1 предоставляет соответствующие типы intptr_t и uintptr_t; проблема: эти типы являются необязательными - в стандарте прямо указывается, что необязательно должен быть целочисленный тип, достаточно большой, чтобы фактически содержать значение указателя!
3 голосов
/ 01 сентября 2009

sizeof(char*) != sizeof(void(*)(void)? - Не на x86 в режиме 36-битной адресации (поддерживается практически на каждом процессоре Intel начиная с Pentium 1)

«Представление указателя в памяти совпадает с целым числом одинаковой длины в битах» - в любой современной архитектуре нет представления в памяти; помеченная память никогда не завоевывала популярность и уже устарела до стандартизации C. В действительности память даже не содержит целых чисел, только биты и, возможно, слова (не байты; большая часть физической памяти не позволяет читать только 8 бит).

«Умножение указателей невозможно» - 68000 семейства; регистры адресов (те, которые содержат указатели) не поддерживали этот IIRC.

«Все указатели можно приводить к целым числам» - не для PIC.

«Увеличение T * эквивалентно добавлению sizeof (T) к адресу памяти» - верно по определению. Также эквивалентно &pointer[1].

2 голосов
/ 01 сентября 2009

В 1950-х, 1960-х и 1970-х годах было много «адресно-ориентированных» архитектур. Но я не могу вспомнить какие-либо основные примеры, которые имели компилятор Си. Я вспоминаю машины ICL / Three Rivers PERQ , которые в 1980-х годах были адресованы по словам и имели доступное для записи хранилище управления (микрокод). В одном из его примеров был компилятор C и разновидность Unix под названием PNX , но компилятору C требовался специальный микрокод.

Основная проблема в том, что типы char * на машинах с адресацией слов неудобны, однако вы их реализуете. Вы часто с sizeof(int *) != sizeof(char *) ...

Интересно, что до C существовал язык под названием BCPL , в котором основным типом указателя был адрес слова; то есть, увеличение указателя дало вам адрес следующего слова, а ptr!1 дало вам слово в ptr + 1. Был другой оператор для адресации байта: ptr%42, если я помню.

2 голосов
/ 30 августа 2009

В общем, ответ на все вопросы " да ", и это потому, что только те машины, которые реализуют популярные языки, непосредственно увидели свет и сохранились в текущем столетии. Хотя языковые стандарты оставляют за собой право изменять эти «инварианты» или утверждения, в реальных продуктах это никогда не случалось, за исключением возможных пунктов 3 и 4, которые требуют некоторого пересмотра, чтобы быть универсально верным.

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

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

В частности:

  1. Представление в памяти всех указателей для данной архитектуры одинаково независимо от типа данных, на который указывает. Верно, за исключением крайне дурацких прошлых разработок, в которых пытались реализовать защиту не на языках со строгой типизацией, а на аппаратном.
  2. Представление указателя в памяти совпадает с целым числом той же длины в битах, что и архитектура. Может быть, конечно, какой-то тип интеграла тот же, см. LP64 против LLP64 .
  3. Умножение и деление типов данных указателя запрещено только компилятором. правый .
  4. Все значения указателя могут быть приведены к единственному целому числу. Другими словами, какие архитектуры все еще используют сегменты и смещения? Сегодня ничто не использует сегменты и смещения, но C int часто недостаточно велик, вам может понадобиться long или long long для удержания указателя.
  5. Увеличение указателя эквивалентно добавлению sizeof (указанного типа данных) к адресу памяти, сохраняемому указателем. Если p равно int32 *, то p + 1 равно адресу памяти через 4 байта после p. Да.

Интересно отметить, что каждый ЦП архитектуры Intel, т. Е. Каждый PeeCee, содержит сложную единицу сегментации эпической, легендарной, сложности. Тем не менее, он эффективно отключен. Всякий раз, когда загружается операционная система ПК, она устанавливает базы сегментов равными 0, а длины сегментов - ~ 0, обнуляя сегменты и давая модель плоской памяти.

2 голосов
/ 30 августа 2009

Я не знаю о других, но для DOS предположение №3 неверно. DOS - 16-битная система, использующая различные приемы для отображения памяти объемом более 16 бит.

2 голосов
/ 30 августа 2009

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

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

Умножение и деление типов данных указателя запрещено только компилятором.

Вы не можете умножать или делить типы. ; Р

Я не уверен, почему вы хотите умножить или разделить указатель.

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

Стандарт C99 позволяет хранить указатели в intptr_t, который является целочисленным типом. Так что да.

Увеличение указателя эквивалентно добавлению sizeof (указанного типа данных) к адресу памяти, сохраняемому указателем. Если p равно int32 *, то p + 1 равно адресу памяти через 4 байта после p.

x + y, где x - это T *, а y - это целое число, эквивалентное (T *)((intptr_t)x + y * sizeof(T)), насколько я знаю. Выравнивание может быть проблемой, но заполнение может быть предоставлено в sizeof. Я не совсем уверен.

0 голосов
/ 21 января 2015

Я хотел бы знать архитектуры, которые нарушают предположения, которые я перечислены ниже.

Я вижу, что Стивен С. упомянул машины PERQ, а MSalters упомянул 68000 и PIC.

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

sizeof (int *) == sizeof (char *) == sizeof (void *) == sizeof (func_ptr *)?

Не обязательно. Некоторые примеры:

Большинство компиляторов для 8-разрядных процессоров Гарвардской архитектуры - PIC и 8051 и M8C - делают sizeof (int *) == sizeof (char *), но отличается от sizeof (func_ptr *).

Некоторые из очень маленьких чипов в этих семействах имеют 256 байтов ОЗУ (или меньше), но несколько килобайт PROGMEM (Flash или ROM), поэтому компиляторы часто делают sizeof (int *) == sizeof (char *) равным 1 (один 8-битный байт), но sizeof (func_ptr *) равен 2 (два 8-битных байта).

Компиляторы для многих более крупных чипов в тех семействах с несколькими килобайтами оперативной памяти и 128 или около того килобайтами PROGMEM делают sizeof (int *) == sizeof (char *) равным 2 (два 8-битных байта), но sizeof (func_ptr *) равен 3 (три 8-битных байта).

Несколько микросхем Гарвардской архитектуры могут хранить ровно 2 ^ 16 ("64 КБ") PROGMEM (флэш-памяти или ПЗУ) и еще 2 ^ 16 ("64 КБ") ОЗУ с вводом-выводом в ОЗУ. Компиляторы для такого чипа делают sizeof (func_ptr *) всегда равным 2 (два байта); но часто есть способ сделать другие типы указателей sizeof (int *) == sizeof (char *) == sizeof (void *) в "long ptr" 3-байтовый универсальный указатель , который имеет дополнительный магический бит, который указывает, указывает ли этот указатель на RAM или PROGMEM. (Это тот тип указателя, который вам нужно передать в функцию "print_text_to_the_LCD ()", когда вы вызываете эту функцию из множества различных подпрограмм, иногда с адресом переменной строки в буфере, которая может быть где угодно в ОЗУ, а иногда с одним из многих постоянных строк, которые могут быть где угодно в PROGMEM). Такие компиляторы часто имеют специальные ключевые слова («short» или «near», «long» или «far»), чтобы позволить программистам специально указывать три разных типа указателей на символы в одной и той же программе - строки констант, которым требуется всего 2 байта, чтобы указать, где в PROGMEM они расположены, неконстантные строки, которым требуется только 2 байта, чтобы указать, где в ОЗУ они расположены, и тип 3-байтовых указателей, которые принимает print_text_to_the_LCD ().

Большинство компьютеров, построенных в 1950-х и 1960-х годах, используют 36-битную длину слова или 18-битную длину слова с 18-битной (или меньшей) адресной шиной. Я слышал, что компиляторы C для таких компьютеров часто используют 9-битные байты , с sizeof (int *) == sizeof (func_ptr *) = 2, что дает 18 битов, поскольку все целые числа и функции должны быть выровнены по словам; но sizeof (char *) == sizeof (void *) == 4, чтобы воспользоваться специальными инструкциями PDP-10 , которые хранят такие указатели в полном 36-битном слове. Это полное 36-разрядное слово включает в себя адрес 18-разрядного слова и еще несколько битов в других 18-разрядных словах, которые (среди прочего) указывают битовую позицию указательного символа в этом слове.

Представление в памяти всех указателей для данной архитектуры одинаково независимо от типа данных, на который указывает?

Не обязательно. Некоторые примеры:

На любой из упомянутых выше архитектур указатели бывают разных размеров. Так как же они могли иметь «одинаковое» представление?

Некоторые компиляторы в некоторых системах используют "дескрипторы" для реализации указателей символов и других видов указателей.Такой дескриптор отличается для указателя, указывающего на первый "char" в "char big_array[4000]", чем для указателя, указывающего на первый "char" в "char small_array[10]", что, вероятно, различные типы данных, даже если небольшой массив начинается с точно такого же места в памяти, которое ранее занимал большой массив. Дескрипторы позволяют таким машинам перехватывать и перехватывать переполнения буфера, которые вызывают такие проблемы на других машинах.

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

Представление указателя в памяти совпадает с целым числом той же длины в битах, что и архитектура?

Не обязательно. Некоторые примеры:

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

Я слышал, что миникомпьютеры Nova имеют «бит косвенности» в каждом слове, которое вдохновило «косвенный код с резьбой» . Звучит так, как будто целое число очищает этот бит, а указатель устанавливает этот бит.

Умножение и деление типов данных указателя запрещено только компилятором. ПРИМЕЧАНИЕ: Да, я знаю, что это бессмысленно. Я имею в виду - есть ли аппаратная поддержка, чтобы запретить это неправильное использование?

Да, некоторые устройства не поддерживают такие операции напрямую.

Как уже упоминали другие, команда «умножения» в 68000 и 6809 работает только с (некоторыми) «регистрами данных»; их нельзя напрямую применять к значениям в «адресных регистрах». (Компилятору было бы довольно легко обойти такие ограничения - переместить эти значения из регистра адресов в соответствующий регистр данных и затем использовать MUL).

Все значения указателя могут быть приведены к одному типу данных?

Да.

Для правильной работы memcpy () стандарт C требует, чтобы каждое значение любого указателя можно было привести к пустому указателю ("void *").

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

Все значения указателя можно привести к единственному целому числу? Другими словами, какие архитектуры все еще используют сегменты и смещения?

Я не уверен.

Я подозреваю, что все значения указателей могут быть преобразованы в интегральные типы данных "size_t" и "ptrdiff_t", определенные в "<stddef.h>".

Увеличение указателя эквивалентно добавлению sizeof (указанные данные тип) на адрес памяти, сохраненный указателем. Если p является int32 * тогда p + 1 равен адресу памяти 4 байта после p.

Неясно, о чем вы здесь спрашиваете.

В: Если у меня есть массив какой-то структуры или примитивного типа данных (например, «#include <stdint.h> ... int32_t example_array[1000]; ...»), и я увеличиваю указатель, который указывает на этот массив (например, «int32_t p = & example_array» [99]; ... p ++; ... "), теперь указатель указывает на следующий следующий по порядку член этого массива, который является байтом sizeof (указанным типом данных) дальше в памяти?

A: Да, компилятор должен сделать указатель, после его однократного приращения, указывать на следующий независимый последовательный int32_t в массиве, размер байтов (указанный тип данных) которого будет дальше в памяти, чтобы соответствовать стандартам.

Q: Итак, если p - это int32 *, то p + 1 равно адресу памяти через 4 байта после p?

A: Когда sizeof (int32_t) фактически равен 4, да. В противном случае, например, для некоторых машин, адресуемых по словам, включая некоторые современные DSP, где sizeof (int32_t) может равняться 2 или даже 1, тогда p + 1 равно адресу памяти 2 или даже 1 "C байтов" после p.

Q: Так что, если я возьму указатель и приведу его к "int" ...

A: Один тип "всего мира - ересь VAX".

Q: ... а затем приведите это "int" обратно в указатель ...

A: Еще один тип "всего мира - ересь VAX".

Q: Итак, если я возьму указатель p, который является указателем на int32_t, и приведу его к некоторому целочисленному типу, который достаточно большой, чтобы содержать указатель, а затем добавлю sizeof( int32_t ) к этому целочисленному типу, а затем позже приведем этот целочисленный тип обратно к указателю - когда я все это сделаю, результирующий указатель будет равен p + 1?

Не обязательно.

Многие DSP и несколько других современных чипов имеют адресную адресацию, а не байтовую обработку, используемую 8-битными чипами.

Некоторые из компиляторов C для таких чипов вмещают по 2 символа в каждое слово, но для хранения int32_t требуется 2 таких слова - поэтому они сообщают, что sizeof( int32_t ) равно 4. (Я слышал слухи, что есть компилятор C для 24-битного Motorola 56000, который делает это).

Компилятор должен упорядочить такие вещи, чтобы выполнение «p ++» с указателем на int32_t увеличивало указатель на следующее значение int32_t. Для компилятора есть несколько способов сделать это.

Один совместимый со стандартами способ заключается в сохранении каждого указателя на int32_t как «адрес собственного слова». Поскольку для хранения одного значения int32_t требуется 2 слова, компилятор C компилирует «int32_t * p; ... p++» в некоторый язык ассемблера, который увеличивает это значение указателя на 2. С другой стороны, если он выполняет «int32_t * p; ... int x = (int)p; x += sizeof( int32_t ); p = (int32_t *)x;», этот компилятор C для 56000, скорее всего, скомпилирует его на ассемблер, который увеличивает значение указателя на 4.

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

Некоторые PIC и 8086 и другие системы имеют несмежные ОЗУ - несколько блоков оперативной памяти по адресам, которые «сделали аппаратное обеспечение проще». С отображенным в память вводом / выводом или вообще без привязки к промежуткам в адресном пространстве между этими блоками.

Это даже более неловко, чем кажется.

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...