Как исторически люди могли использовать целые числа для хранения указателей C?

Сейчас я читаю «Компьютерные системы: взгляд программиста» Брайанта и О'Халларона. Они отмечают, что

Например, многие программисты исторически предполагали, что объект, объявленный как тип int, может использоваться для хранения указателя. Это прекрасно работает для большинства 32-битных программ, но приводит к проблемам для 64-битных программ.

Я пытаюсь понять, как программисты вообще могли сделать такое. Обычно int подписывается, поэтому не приведет ли сохранение указателя со значением больше 2^31 к приведению типов и ошибкам? Полагаю, это в основном исторический курьез, но я решил, что все же спрошу.

C был более небрежным в 1970-х и 1980-х годах; тогда это позволяло подобные вещи. Поскольку язык C был стандартизирован в 1989/1990 годах, совершать такие злоупотребления стало труднее. В этом тысячелетии для достижения сомнительных эффектов требуется серьезная борьба с компилятором посредством приведения типов.

Jonathan Leffler 16.07.2024 17:09

Найдите книгу Lions о ядре Unix V6, чтобы узнать, как выглядел ранний C.

Jonathan Leffler 16.07.2024 17:11

Понял, спасибо! Просто чтобы подтвердить, правильно ли мое подозрение: такая практика могла привести к проблемам с указателями со значениями > 2^31-1 (верхняя граница положительных значений для [подписанных] целых чисел)? @ДжонатанЛеффлер

EE18 16.07.2024 17:12

Когда люди хранили указатели в int, компьютеры обычно имели 16 МБ оперативной памяти, и никто не мог себе представить, что в ближайшее время появятся 64-битные указатели.

Jabberwocky 16.07.2024 17:15

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

Jonathan Leffler 16.07.2024 17:15

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

Thomas Jager 16.07.2024 17:15

Другие сказали «как», я скажу «почему». В то время C считался «на шаг выше языка ассемблера». Кроме того, процессоры были намного медленнее, чем сейчас, память была дорогой, а оптимизаторы компиляторов не были такими сложными. Таким образом, программисты C проделали большую часть «ручной оптимизации» кода (i++; мог генерировать меньше инструкций на ассемблере, чем i = i + 1;), чтобы выжать максимум из того, что у них было. Также имейте в виду, что сам язык C был более примитивным; например не было никакого "союза". Эквивалентность «int/pointer» иногда использовалась для получения степени абстрактного хранения данных.

Streve Ford 16.07.2024 17:17

У вас всего полмегабайта оперативной памяти. Почему указатель должен быть больше 2 ^ 31?

Stack Exchange Supports Israel 17.07.2024 08:00

Возможно, вам стоит спросить об этом на SE Retrocomputing.

Fe2O3 17.07.2024 08:06

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

the busybee 17.07.2024 09:22
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
0
10
136
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Ответ принят как подходящий

Во-первых, это не вызвало бы особых проблем, если бы люди не сделали что-нибудь смешное с этими целыми числами. Например. скажем, у меня есть указатели 0xFFFF0000 и 0xFFFF0010, и я привожу их к целым числам. Я получу -65536 и -65520. Если я смогу вернуть их к указателям, я верну те же указатели. Если я вычту второе из первого, я получу 16 для указателей и для int. Если я сравню их, я получу первое меньше второго для указателей и для целых чисел - однако этот хрупкий; 0x7FFF0000 и 0x81000000 будут сравниваться неправильно. Короче говоря, по крайней мере круговой переход от указателя к int и к указателю работает нормально, когда указатели и int имеют одинаковую ширину.

Во-вторых, почти наверняка они имели в виду как int, так и unsigned int (определяющей характеристикой является ширина, а не знак), и часто указатели хранились как беззнаковые целые числа, которые безупречно работали на 32-битных системах.

Я понимаю вашу точку зрения, что сравнение не работает, спасибо! Кстати, прежде чем я приму предложение, можете ли вы объяснить, как приведение приводит к получению этих значений? Я просто наивно воспринимаю эти битовые комбинации как представления целых чисел, дополняемые до 2, но получаю совершенно другие числа, чем вы. Например, 0xFFFF000 представляет -2^32 после инвертирования и добавления 1 для получения величины. Требует ли стандарт C чего-то еще?

EE18 16.07.2024 18:08

@EE18: FFFF0000 — это не 2^−32. Чтобы убедиться в этом, обратите внимание, что FFFFFFFF равен -1, FFFFFFFE равен -2, FFFFFFFC равен -4, FFFFFFF8 равен -8, FFFFFFF0 равен -16 и так далее. Когда вы дойдете до FFFF0000, вы увидите, что оно равно -65 536.

Eric Postpischil 16.07.2024 20:37

@EE18 Стандарт C, до C23, я думаю, на самом деле не определял конкретное представление отрицательных чисел (именно поэтому переполнение целых чисел со знаком было UB). Конечно, на практике оно всегда бинарно полно. При двоичном дополнении вы можете инвертировать число, инвертировав и добавив 1, вы правы. Итак, 0xFFFF0000 (32 бит) с отрицанием равно 0xFFFF, прибавьте 1 и получите 0x10000 == 65536.

Andrey Turkin 16.07.2024 22:43

Приношу извинения вам обоим — я не знаю, почему я выбрал 2^32 вместо 2^16. Думаю, мозговой пердеж подсчитывает количество битов в четырех шестнадцатеричных цифрах. Но в примере, который вы упомянули, сравнение неверно, можете ли вы объяснить, почему? Кажется, вы указали только 7, а не 8 шестнадцатеричных цифр, поэтому я не уверен, что имеется в виду.

EE18 17.07.2024 00:04

конечно. Намерение состояло в том, чтобы противопоставить положительные и отрицательные части целочисленного пространства: 0x7FFF0000 и 0x81000000. Первое сравнивает меньше второго в беззнаковом формате, но при преобразовании в знаковое второе становится отрицательным, поэтому первое сравнивается больше второго. На самом деле это может привести к некоторым проблемам, если кто-то будет настолько глуп, чтобы сказать: попробуйте перебрать некоторый массив, охватывающий 0x80000000, используя целые числа вместо указателей (потому что обычный способ for (i = begin; i < end; +=i) не будет работать правильно из-за i < end, дающего неправильные результаты.

Andrey Turkin 17.07.2024 00:36

Ох, ужасная ошибка с моей стороны. Я вижу сейчас. Еще раз спасибо!

EE18 17.07.2024 02:30

Когда я впервые начал изучать C, мне было трудно понять, как он работает внутри. Итак, я решил изучить ассемблер x86, и внезапно все стало иметь смысл. Ключевой момент заключается в том, что с точки зрения ЦП и указатели, и целые числа (со знаком или без знака) представляют собой просто биты, находящиеся в памяти или регистрах. Указатель — это, по сути, беззнаковое число, содержащее адрес памяти (или «серийный номер») определенного байта. Это означает, что вы можете без проблем сохранить указатель в регистре и использовать его как целое число со знаком в следующей команде (и наоборот). В C различие между числами со знаком и без знака становится существенным, когда компилятор генерирует код для арифметических операций. Однако если вы сохраните указатель в 32-битном int в 32-битной системе, его биты останутся неизменными. Вот несколько примеров, иллюстрирующих эту концепцию:

// Assume that the system uses 32-bit addressing:
#include <stdlib.h>
#include <stdio.h>

void main() {
  void * ptr = malloc(32);
  printf("pointer value %p\n", ptr);
  int as_int = (int)ptr; // ok, retains information
  unsigned int as_uint = (unsigned int)ptr; // same
  // You cannot compare as_int and as_uint becase that won't make sense,
  // but after a cast (that, again, does nothing to the bits) it's ok:
  int is_same = (int)as_uint == as_int;
  printf("as_int %d  as_uint %u\n", as_int, as_uint);
  printf("as_int and as_uint are equal? %u\n", is_same);

  // let's write something using the address
  *(int*)as_int = 0xdeadbeef;
  // ... and read it back
  printf("0x%x\n", *(int*)ptr);

  // loses information by throwing away 2 'most significant' bytes
  // (similar to casting a pointer to an `int` on a 64-bit system)
  short as_short = (int)ptr;
  printf("as_short %d\n", as_short);
}

Чтобы скомпилировать приведенный выше код в 64-битной системе с GCC, используйте флаг -m32, например:
gcc -m32 test.c -o test

Возможный результат:

pointer value 0x585151a0
as_int 1481724320  as_uint 1481724320
as_int and as_uint are equal? 1
0xdeadbeef
as_short 20896

«Итак, я решил изучить ассемблер x86, и внезапно все стало иметь смысл». Точно. Назовите меня экстремистом, но ИМХО должен быть закон, запрещающий обучать людей C, прежде чем обучать их ассемблере.

Ruud Helderman 18.07.2024 17:10

Другие вопросы по теме