Что такое действительный указатель в gcc linux x86-64 C++?

Я программирую C++, используя gcc в малоизвестной системе под названием linux x86-64. Я надеялся, что, возможно, есть несколько человек, которые использовали эту же конкретную систему (и, возможно, они также смогут помочь мне понять, что является допустимым указателем в этой системе). Мне не нужен доступ к местоположению, на которое указывает указатель, я просто хочу вычислить его с помощью арифметики указателя.

Согласно разделу 3.9.2 стандарта:

A valid value of an object pointer type represents either the address of a byte in memory (1.7) or a null pointer.

И согласно [расшир.доб.]/4:

When an expression that has integral type is added to or subtracted from a pointer, the result has the type of the pointer operand. If the expression P points to element x[i] of an array object x with n elements, the expressions P + J and J + P (where J has the value j) point to the (possibly-hypothetical) element x[i + j] if 0 ≤ i + j ≤ n; otherwise, the behavior is undefined. Likewise, the expression P - J points to the (possibly-hypothetical) element x[i − j] if 0 ≤ i − j ≤ n; otherwise, the behavior is undefined.

И согласно вопрос stackoverflow о действительных указателях C++ в целом:

Is 0x1 a valid memory address on your system? Well, for some embedded systems it is. For most OSes using virtual memory, the page beginning at zero is reserved as invalid.

Ну, это совершенно ясно! Итак, помимо NULL, допустимый указатель — это байт в памяти, нет, подождите, это элемент массива, включающий элемент сразу после массива, нет, подождите, это страница виртуальной памяти, нет, подождите, это Супермен!

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

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

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

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

#include <iostream>
#include <inttypes.h>
#include <assert.h>
using namespace std;

extern const char largish[1000000000000000000L];
asm("largish = 0");

int main()
{
  char* smallish = new char[1000000000];
  cout << "largish base = " << (long)largish << "\n"
       << "largish length = " << sizeof(largish) << "\n"
       << "smallish base = " << (long)smallish << "\n";
}

Результат:

largish base = 0
largish length = 1000000000000000000
smallish base = 23173885579280

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

Учитывая количество способов управления памятью и объединения программных модулей, которые поддерживаются в linux x86-64, компилятор C++ действительно не могу знает обо всех массивах и различных стилях отображения страниц.

Наконец, почему я специально упоминаю gcc? Потому что часто кажется, что указатель Любые считается допустимым указателем... Возьмем, например:

char* super_tricky_add_operation(char* a, long b) {return a + b;}

Хотя после прочтения всех спецификаций языка вы можете ожидать, что реализация super_tricky_add_operation(a, b) изобилует неопределённым поведением, на самом деле это очень скучно, просто инструкция add или lea. Это так здорово, потому что я могу использовать его для очень удобных и практичных вещей, таких как массивы с отсчетом от нуля, если никто не будет возиться с моими add инструкциями только для того, чтобы указать на недопустимые указатели. Я любовьgcc.

Таким образом, кажется, что любой компилятор C++, поддерживающий стандартные инструменты компоновки в linux x86-64, почти должен рассматривать указатель Любые как действительный указатель, и gcc, похоже, является членом этого клуба. Но я не совсем уверен на 100% (то есть, учитывая достаточную дробную точность).

Итак... кто-нибудь может привести надежный пример указателя неверный в gcc linux x86-64? Под твердым я подразумеваю ведущее к неопределенному поведению. И объясните, что порождает неопределенное поведение, разрешенное спецификациями языка?

(или предоставьте gcc документацию, доказывающую обратное: все указатели действительны).

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

Cody Gray 03.03.2019 11:52

@ Коди Грей Отличная идея! Я опубликовал ответ на основе расширенного обсуждения (недавно преобразованного в чат).

personal_cloud 03.03.2019 11:59

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

Galik 03.03.2019 12:11

Вы знаете, что такое неопределенное поведение? Это не авария. Это не поджигает ваш компьютер. Это не звонить в полицию, не красть твою девушку, не начинать ядерную войну. Или все эти вещи. Это просто поведение, о котором стандарт отказывается говорить, не более того. Почему вы снова ожидаете найти особенно забавный ассемблерный код в super_tricky_add_operation?

n. 1.8e9-where's-my-share m. 03.03.2019 22:10

«Под твердым я подразумеваю ведущее к неопределенному поведению». Как вы планируете идентифицировать неопределенное поведение? Глядя на свой компьютер и наблюдая сбой? Ты не можешь это делать. Глядя на свой компьютер и наблюдая, как он загорелся? Ты не можешь это делать. Не наблюдая за тем, как твой дом подвергается спецназу, не наблюдая, как уходит твоя девушка, не наблюдая, как конец света наступает в результате ядерного апокалипсиса. Вы можете идентифицировать только UB прочитав стандарт. Если стандарт говорит, что ваша программа имеет UB, значит, она имеет UB (см. определение UB в предыдущем комментарии).

n. 1.8e9-where's-my-share m. 03.03.2019 22:16

@н.м. Моя цель — понять, как GCC интерпретирует (расплывчатый) стандарт языка в отношении валидности указателя. Если мы увидим, как он использует допущения языка в генерируемом им ассемблерном коде, это будет очень хорошей подсказкой. Неопределенный стандарт не означает автоматически, что GCC что-то не поддерживает.

personal_cloud 03.03.2019 23:09

В достоверности указателя нет абсолютно ничего неопределенного. [basic.compound] Каждое значение типа указателя является одним из следующих: (3.1) — указатель на объект или функцию (говорят, что указатель указывает на объект или функцию), или (3.2) — указатель после конца объекта ( 8.7), или (3.3) — нулевое значение указателя (7.11) для этого типа, или (3.4) — недопустимое значение указателя. Компилятору не нужно интерпретировать это каким-то особым образом. Он может предполагать, что все указатели, с которыми вы что-либо делаете, действительны.

n. 1.8e9-where's-my-share m. 03.03.2019 23:50

@н.м. OK. Но разве мы не установили, что существует множество способов создать «объект»? И C++ не предоставляет единую конструкцию или фасадный интерфейс для обнаружения всех этих различных типов объектов (кроме попытки доступа к ним), а только общий диапазон адресного пространства. Если я создаю новый распределитель объектов, обязан ли я как-то «рассказывать» языку об этом?

personal_cloud 04.03.2019 00:10

Никаких "мы" не имеем. Вы можете объявить и определить объект или создать его с помощью оператора new. Получается, давайте посчитаем их по пальцам, раз, два, это два способа создания объектов. Вы не «открываете» объекты. Вы знаете, где они. В целом у меня сложилось впечатление, что вы не знаете, о чем спрашиваете. Это о симптомах УБ? Речь идет о создании объектов? Это о достоверности указателя? Это слишком широко. Пожалуйста, по одному вопросу.

n. 1.8e9-where's-my-share m. 04.03.2019 06:46

@н.м. А как насчет mmap, malloc, ввода-вывода, общих страниц, перехваченных страниц и т. д.... Это все действительные массивы! Нет, я не знаю, откуда все это у простого API, и компилятор тоже. Да, мой вопрос о симптомах UB. Как объясняется в ответах, GCC делает знает общий диапазон виртуального адресного пространства и использует его при оптимизации сравнения. Так проявляется УБ на практике. (Или всех UB можно избежать, используя uintptr_t, хотя тогда вам нужно настроить его кратно sizeof(elem) и вернуть его обратно к указателю перед доступом к назначенной памяти)

personal_cloud 04.03.2019 07:00

"Это все допустимые массивы!" Говорит кто? Только стандарт определяет, что является допустимым указателем, а что нет. Можете ли вы процитировать соответствующий стандартный язык? Существует отчет о дефекте, который показывает доступ к памяти с помощью malloc без предварительного размещения нового объекта в ней (общая идиома, пришедшая из C) — это UB. Это прискорбно, но это то, что в настоящее время говорит стандарт.

n. 1.8e9-where's-my-share m. 04.03.2019 07:03

@н.м. Размещение new является необязательным для типов C, таких как int, поскольку C++ обратно совместим с C. Я предполагаю, что это включает «mmap, malloc, ввод-вывод, общие страницы, захваченные страницы» и т. д. Я не понимаю, как будет работать размещение new с теми вещами, когда другой процесс/библиотека и т.д. создал данные. И даже для размещения new я не думаю, что компилятору разрешено создавать для него внешнюю структуру отслеживания (где для этого ресурс памяти?). Размещение new должно просто вызывать конструктор класса, который обычно только обновляет значения в самом классе и, возможно, выделяет некоторые члены.

personal_cloud 04.03.2019 07:07

В любом случае, если вы предполагаете, что malloc создает допустимый массив символов, это еще один способ создания объекта. C++ не имеет mmap или любого другого способа выделения памяти. Если указатель исходит от функции, неизвестной реализации, например, написанной на другом языке, реализация должна предполагать, что указатель действителен, иначе было бы довольно сложно взаимодействовать с другими языками. Но тогда вы создаете объекты вне программы на C++. Рассказывать, как это делается, не входит в рамки стандарта С++.

n. 1.8e9-where's-my-share m. 04.03.2019 07:16

«Размещение new является необязательным для типов C, таких как int». Нет, это не так, «поскольку C++ обратно совместим с C». Нет, это не так.

n. 1.8e9-where's-my-share m. 04.03.2019 07:17

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

n. 1.8e9-where's-my-share m. 04.03.2019 07:32
Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать 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
15
537
3
Перейти к ответу Данный вопрос помечен как решенный

Ответы 3

Следующие примеры показывают, что GCC конкретно предполагает, по крайней мере, следующее:

  • Глобальный массив не может находиться по адресу 0.
  • Массив не может обернуться вокруг адреса 0.

Примеры неожиданного поведения, возникающего из-за арифметики недопустимых указателей в gcc linux x86-64 C++ (спасибо, melpomene):

  • largish == NULL оценивается как false в программе в вопросе.
  • unsigned n = ...; if (ptr + n < ptr) { /*overflow */ } можно оптимизировать до if (false).
  • int arr[123]; int n = ...; if (arr + n < arr || arr + n > arr + 123) можно оптимизировать до if (false).

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

Спасибо всем в чате за помощь в решении вопроса.

GCC знает, что он (и компоновщик) никогда не поместит статические данные по адресу 0, поэтому largish == NULL даже не нужно проверять во время выполнения, это известно как ложное. Нарушение предположений компилятора с помощью asm("largish=0"); в основном является неопределенным поведением.

Peter Cordes 03.03.2019 22:32

@Питер Кордес Верно. Я подозреваю, что в основном все разрывы составляют около 0. В принципе, если предположить, что «действительный массив» не начинается с 0 и не обертывается вокруг 0. На это указывает этот ответ. ... Хотя можно было бы немного уточнить.

personal_cloud 03.03.2019 22:37
Ответ принят как подходящий

Обычно математика указателей делает именно то, что вы ожидаете, независимо от того, указывают ли указатели на объекты или нет.

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

Интересные угловые случаи включают в себя массив в самом верху виртуального адресного пространства: указатель на единицу за концом будет преобразован в ноль, поэтому start < end будет ложным?!? Но сравнение указателей не должно обрабатывать этот случай, потому что ядро ​​​​Linux никогда не будет отображать верхнюю страницу, поэтому указатели на нее не могут указывать на объекты или просто проходить мимо них. См. Почему я не могу отобразить (MAP_FIXED) самую высокую виртуальную страницу в 32-битном процессе Linux на 64-битном ядре?


Связанный:

GCC делает имеет максимальный размер объекта PTRDIFF_MAX (это знаковый тип). Так, например, на 32-разрядной платформе x86 массив размером более 2 ГБ не полностью поддерживается для всех случаев генерации кода, хотя вы можете mmap один.

См. мой комментарий к Каков максимальный размер массива в C? — это ограничение позволяет gcc реализовать вычитание указателя (для получения размера), не сохраняя перенос из старшего бита, для типов шире, чем char, где результат вычитания C находится в объектах, а не в байтах, поэтому в asm это (a - b) / sizeof(T).


Don't ask how I knew that the default memory manager would allocate something inside of the other array. It's an obscure system setting. The point is I went through weeks of debugging torment to make this example work, just to prove to you that different allocation techniques can be oblivious to one another).

Прежде всего, вы никогда не используете выделенный место для large[]. Вы использовали встроенный ассемблер, чтобы он начинался с адреса 0, но ничего не сделали для фактического сопоставления этих страниц.

Ядро не будет перекрывать существующие сопоставленные страницы, когда new использует brk или mmap для получения новой памяти от ядра, поэтому на самом деле статическое и динамическое выделение не может перекрываться.

Во-вторых, char[1000000000000000000L] ~= 2^59 байт. Текущее аппаратное и программное обеспечение x86-64 поддерживает только канонические 48-битные виртуальные адреса (со знаком, расширенным до 64-битных). Это изменится с будущим поколением оборудования Intel, которое добавит еще один уровень таблиц страниц, доведя нас до 48+9 = 57-битных адресов. (Все еще с верхней половиной, используемой ядром, и большой дырой посередине.)

Ваше нераспределенное пространство от 0 до ~ 2 ^ 59 покрывает все адреса виртуальной памяти пользовательского пространства, которые возможны в Linux x86-64, поэтому, конечно, все, что вы выделяете (включая другие статические массивы), будет где-то «внутри» этого поддельного массива.


Удаление extern const из объявления (таким образом, фактически выделенный массив является, https://godbolt.org/z/Hp2Exc) сталкивается со следующими проблемами:

//extern const 
char largish[1000000000000000000L];
//asm("largish = 0");

/* rest of the code unchanged */
  • Относительная RIP или 32-битная абсолютная (-fno-pie -no-pie) адресация не может достичь статических данных, которые связываются после large[] в BSS, с моделью кода по умолчанию (-mcmodel=small где предполагается, что весь статический код + данные помещаются в 2 ГБ)

    $ g++ -O2 large.cpp
    /usr/bin/ld: /tmp/cc876exP.o: in function `_GLOBAL__sub_I_largish':
    large.cpp:(.text.startup+0xd7): relocation truncated to fit: R_X86_64_PC32 against `.bss'
    /usr/bin/ld: large.cpp:(.text.startup+0xf5): relocation truncated to fit: R_X86_64_PC32 against `.bss'
    collect2: error: ld returned 1 exit status
    
  • компиляция с -mcmodel=medium помещает large[] в раздел больших данных, где он не мешает адресации других статических данных, но сам адресуется с использованием 64-битной абсолютной адресации. (Или -mcmodel=large делает это для всего статического кода/данных, поэтому каждый вызов является косвенным movabs reg,imm64/call reg вместо call rel32.)

    Это позволяет нам скомпилировать и связать, но тогда исполняемый файл не запустится., потому что ядро ​​​​знает, что поддерживаются только 48-битные виртуальные адреса, и не будет отображать программу в своем загрузчике ELF перед ее запуском или для PIE перед запуском ld.so на ней.

    peter@volta:/tmp$ g++ -fno-pie -no-pie -mcmodel=medium -O2 large.cpp
    peter@volta:/tmp$ strace ./a.out 
    execve("./a.out", ["./a.out"], 0x7ffd788a4b60 /* 52 vars */) = -1 EINVAL (Invalid argument)
    +++ killed by SIGSEGV +++
    Segmentation fault (core dumped)
    peter@volta:/tmp$ g++ -mcmodel=medium -O2 large.cpp
    peter@volta:/tmp$ strace ./a.out 
    execve("./a.out", ["./a.out"], 0x7ffdd3bbad00 /* 52 vars */) = -1 ENOMEM (Cannot allocate memory)
    +++ killed by SIGSEGV +++
    Segmentation fault (core dumped)
    

(Интересно, что мы получаем разные коды ошибок для исполняемых файлов PIE и не-PIE, но еще до того, как execve() завершится.)


Обманывать компилятор + компоновщик + среду выполнения с помощью asm("largish = 0"); не очень интересно и создает очевидное неопределенное поведение.

Забавный факт №2: x64 MSVC не поддерживает статические объекты размером более 2^31-1 байт. IDK, если у него есть эквивалент -mcmodel=medium. В основном GCC терпит неудачу для предупреждения об объектах, слишком больших для выбранной модели памяти.

<source>(7): error C2148: total size of array must not exceed 0x7fffffff bytes

<source>(13): warning C4311: 'type cast': pointer truncation from 'char *' to 'long'
<source>(14): error C2070: 'char [-1486618624]': illegal sizeof operand
<source>(15): warning C4311: 'type cast': pointer truncation from 'char *' to 'long'

Кроме того, он указывает, что long — неправильный тип для указателей в целом (поскольку Windows x64 — это LLP64 ABI, где long — 32 бита). Вы хотите intptr_t или uintptr_t, или что-то эквивалентное printf("%p"), которое печатает необработанный void*.

Спасибо за эту перспективу; Я согласен с тем, что ядро ​​не будет выделять largish, и что попытка задействовать линкер в largish вызывает гораздо большие проблемы. Но цель largish — удовлетворить требования языка к арифметике указателей, а не заставить ядро ​​что-то делать. Где в спецификации языка сказано, что «массив» (для целей [expr.add]/4) должен быть выделен ядром? (Я имею в виду, да, люди интерпретировали это именно так, при определенных предположениях, но это не единственно возможная интерпретация)

personal_cloud 03.03.2019 22:13

Если уж на то пошло, как элементарная арифметика вообще взаимодействует с ядром? Разве это не было бы очевидно в файле .o? Но если я добавлю указатели, все, что я увижу, это инструкция lea или add, ни одна из которых не касается ядра.

personal_cloud 03.03.2019 22:15

@personal_cloud: да, ядро ​​вообще ни при чем. UB не означает, что это должен сбой, это означает, что разрешается не сработает и/или будет супер-странным. Ваши хаки с largish[] создали указатель, который на самом деле не указывает на объект. Но в любом случае, этот ответ был просто попыткой устранить недостаток в вашей предпосылке и той части вопроса, которую я процитировал. Я не слишком углубился в выяснение того, что еще вы на самом деле спрашиваете.

Peter Cordes 03.03.2019 22:29

C++ предъявляет расплывчатые требования к допустимости указателя для целей арифметики указателей. Я спрашиваю, как GCC интерпретирует это требование. Он явно может обрабатывать многие случаи, которые не связаны с выделением ядра, включая различные настраиваемые распределители, аппаратные драйверы внутри самого ядра, схемы ленивого сопоставления, mmaps для поврежденных файлов, настраиваемые массивы, которые частично или полностью помещаются в компоновщик. позже... так много примеров. Существует ли общий принцип или всего несколько исключений вокруг null (включая сравнения, обтекающие 0).

personal_cloud 03.03.2019 22:32

@personal_cloud: обычно математика указателей делает именно то, что вы ожидаете, независимо от того, указывают ли указатели на объекты или нет. Как я уже сказал, UB не означает, что имеет потерпит неудачу. Интересные угловые случаи включают в себя массив в самом верху виртуального адресного пространства: указатель на единицу за концом будет преобразован в ноль, поэтому start < end будет ложным?!? Но сравнение указателей не должно обрабатывать этот случай, потому что ядро ​​​​Linux никогда не будет отображать верхнюю страницу, поэтому указатели на нее не могут указывать на объекты или просто проходить мимо них. См. эти вопросы и ответы.

Peter Cordes 03.03.2019 22:36

Да, когда дело доходит до арифметики указателей, я все время думал, что GCC просто делает предположения об общем диапазоне виртуального адресного пространства. Ответ должен подчеркнуть это. Хорошо, вы как бы прикрываете это PTRDIFF_MAX. Я приму.

personal_cloud 03.03.2019 22:44

@personal_cloud: увидев ваши комментарии, я понял, что это важная часть ответа, и переместил его выше.

Peter Cordes 03.03.2019 22:46

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

personal_cloud 03.03.2019 22:58

@personal_cloud: нет, mmap не может по-настоящему «обойти» это. Это системный вызов Unix, и он не заботится об ограничениях реализации C, поэтому он не ограничивает искусственно размеры выделения. (И внутренне ядро ​​использует целочисленную математику без знака для работы с размерами. Кроме того, в 64-битном ядре 3 ГБ — это тривиальный размер. Однако 32-битное ядро ​​все еще может справиться с этим, если скомпилировано с пользователем 3: 1: ядро было разделено так, чтобы было доступно много виртуального адресного пространства пользовательского пространства). Но если вы передадите указатели на начало и конец области mmap 2,5G в size_t sz(int *end, int*start) {return end-start;}, это будет UB.

Peter Cordes 03.03.2019 23:26

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

Это не означает, что люди в Комитете не были хорошо осведомлены о том, что многие среды выполнения предоставляют формы хранения, о которых реализации C могут ничего не знать. Однако ожидается, что люди, которые действительно работали с различными платформами, будут лучше, чем Комитет, определять, какие вещи программисты должны будут делать с такими «внешними» адресами и как лучше всего поддерживать такие потребности. Стандарту незачем заниматься такими вещами.

Как оказалось, существуют некоторые среды выполнения, в которых компилятору удобнее обрабатывать арифметику указателей как целочисленную математику, чем делать что-либо еще, и многие компиляторы для таких платформ с пользой обрабатывают арифметику указателей даже в тех случаях, когда от них не требуется Сделай так. Для 32-битных и 64-битных x86 и x64 я не думаю, что существуют какие-либо битовые шаблоны для недопустимых ненулевых адресов, но может быть возможно сформировать указатели, которые не ведут себя как действительные указатели на объекты, к которым они обращаются. .

Например, учитывая что-то вроде:

char x=1,y=2;
ptrdiff_t delta = (uintptr_t)&y - (uintptr_t)&x;
char *p = &x+delta;
*p = 3;

даже если представление указателя определено таким образом, что использование целочисленной арифметики для добавления delta к адресу x даст y, это никоим образом не гарантирует, что компилятор распознает, что операции над *p могут повлиять на y, даже если p имеет значение y адрес. Указатель p фактически будет вести себя так, как будто его адрес недействителен, даже если битовый шаблон будет соответствовать адресу y.

x86-64 имеет только 48-битные виртуальные адреса (или 57-битные с 5-уровневыми таблицами страниц на будущих аппаратных средствах). Канонические адреса — это те, которые правильно расширены по знаку до 64-бит, поэтому используемые диапазоны — это младшие и старшие 47-битные диапазоны вверху и внизу виртуального адресного пространства. Вы можете называть неканонические указатели «недопустимыми ненулевыми адресами», но они по-прежнему работают как целые числа, если вы никогда не разыменовываете их. См. также Должны ли сравнения указателей быть знаковыми или беззнаковыми в 64-разрядной версии x86?

Peter Cordes 06.03.2019 00:08

Это отличный пример с ptrdiff_t delta = (uintptr_t)&y - (uintptr_t)&x;, потому что и &x, и &x+delta допустимы, но они не указывают на один и тот же объект и, следовательно, слегка нарушают [expr.add]/4. Также отличное объяснение того, как оптимизация алиасинга может привести к неожиданному изменению результатов программы позже. Спасибо.

personal_cloud 06.03.2019 04:05

По какой-то причине кажется, что ведутся серьезные споры о том, должна ли (char*)(delta+(uintptr_t)&x); иметь доступ к y, но я задаюсь вопросом, почему любая реализация, которая не желает соблюдать такую ​​семантику, должна определять uintptr_t в первую очередь [это чисто необязательно] . ИМХО, преобразования целых чисел в указатели имеют большие неоновые вывески, которые должны заставить любой компилятор, который не является преднамеренно слепым, распознать, что результирующий указатель может иметь доступ практически к любому объекту, адрес которого был преобразован в целочисленный тип, и я действительно не могу думать...

supercat 06.03.2019 07:33

... из многих необдуманных ситуаций, когда это серьезно помешало бы тому, что в противном случае было бы полезной оптимизацией. Безусловно, Стандарт допускает такую ​​оптимизацию, но только потому, что стандарт никогда требует, чтобы указатель, созданный приведением uintptr_t, действительно можно было использовать для доступа к любому объекту (он просто требует, чтобы (char*)(uintptr_t)&x сравнивается с &x — не то чтобы это было бы можно использовать для доступа x). Авторы Стандарта наивно полагали, что нет необходимости говорить, что разработчики компиляторов не должны делать глупостей.

supercat 06.03.2019 07:39

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