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

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

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

Допустимое значение типа указателя объекта представляет либо адрес байта в памяти (1.7), либо нулевой указатель.

И согласно [expr.add] / 4:

Когда выражение, имеющее целочисленный тип, добавляется к указателю или вычитается из него, результат имеет тип операнда указателя. Если выражение P указывает на элемент x [i] объекта массива x с n элементами, выражения P + J и J + P (где J имеет значение j) указывают на (возможно, гипотетический) элемент x [i + j], если 0 ≤ i + j ≤ n; в противном случае поведение не определено. Точно так же выражение P — J указывает на (возможно, гипотетический) элемент x [i — j], если 0 ≤ i — j ≤ n; в противном случае поведение не определено.

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

Является ли 0x1 допустимым адресом памяти в вашей системе? Что ж, для некоторых встроенных систем это так. Для большинства операционных систем, использующих виртуальную память, страница, начинающаяся с нуля, зарезервирована как недействительная.

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

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

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

Более того, компилятору практически невозможно узнать о многих из этих определений. Есть просто очень много способов создания действительного байта памяти (подумайте о микрокоде ленивого прерывания 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

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

См. также:  что означает Execution_date и backfill в воздушном потоке

Учитывая количество способов управления памятью и комбинирования программных модулей, которые поддерживаются в 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, похоже, является членом этого клуба. Но я не совсем уверен (то есть при достаточной дробной точности).

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

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

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

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

Вы изучали возможность создания абстрактного типа данных массива, отличного от нуля?   —  person personal_cloud    schedule 03.03.2019

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

См. также:  Как добавить тестовые примеры в Microsoft Test Manager через программу

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

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

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

@ n.m. В ПОРЯДКЕ. Но разве мы не установили, что существует множество способов создать объект? И C ++ не предоставляет единую конструкцию или интерфейс фасада для обнаружения всех этих различных типов объектов (кроме попыток доступа к ним), а только общий диапазон адресного пространства. Если я создаю новый распределитель объектов, обязан ли я каким-то образом сообщить об этом языку?   —  person personal_cloud    schedule 04.03.2019

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

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

См. также:  Невозможно отправить сигнал с помощью pthread_cond_signal другому процессу в C

Все это действительные массивы! Кто говорит? Только стандарт определяет, какой указатель является допустимым, а какой — нет. Вы можете процитировать соответствующий стандартный язык? Существует отчет о дефектах, который показывает доступ к памяти malloc’d без размещения в ней нового объекта (обычная идиома, которая исходит от C) — UB. Это прискорбно, но это то, что в настоящее время говорится в стандарте.   —  person personal_cloud    schedule 04.03.2019

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

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

Размещение new необязательно для типов C, таких как int. Нет, это не так, поскольку C ++ обратно совместим с C. Нет, это не так.   —  person personal_cloud    schedule 04.03.2019

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

Понравилась статья? Поделиться с друзьями:
IT Шеф
Комментарии: 3
  1. personal_cloud

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

    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).


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

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

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

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

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


    Удаление 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, с моделью кода по умолчанию (_ 16_, где предполагается, что весь статический код + данные умещаются в 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 на нем.

      [email protected]:/tmp$ g++ -fno-pie -no-pie -mcmodel=medium -O2 large.cpp
      [email protected]:/tmp$ strace ./a.out 
      execve("./a.out", ["./a.out"], 0x7ffd788a4b60 /* 52 vars */) = -1 EINVAL (Invalid argument)
      +++ killed by SIGSEGV +++
      Segmentation fault (core dumped)
      [email protected]:/tmp$ g++ -mcmodel=medium -O2 large.cpp
      [email protected]:/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 — это ABI LLP64, где long — 32 бита). Вам нужно intptr_t или uintptr_t, или что-то эквивалентное printf("%p"), которое печатает необработанное void*.

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

    В таком случае, как элементарная арифметика вообще взаимодействует с ядром? Разве это не было бы очевидно в файле .o? Но если я добавляю указатели, все, что я вижу, — это инструкции lea или add, ни одна из которых не касается ядра. person personal_cloud; 04.03.2019

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

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

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

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

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

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

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

  2. personal_cloud

    Следующие примеры показывают, что 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");, по сути, является неопределенным поведением. person personal_cloud; 04.03.2019

    @ Питер Кордес Правильно. Я подозреваю, что в основном все неоднородности составляют около 0. В основном, если предположить, что действительный массив не начинается с 0 и не оборачивается вокруг 0. Это то, на что указывает этот ответ. … Хотя это можно было бы немного уточнить. person personal_cloud; 04.03.2019

  3. personal_cloud

    Стандарт не предполагает наличия какого-либо хранилища сверх того, которое реализация предоставляет через объекты статической, автоматической или продолжительности потока, или использование функций стандартной библиотеки, таких как 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-уровневыми таблицами страниц в будущем HW). Канонические адреса — это те, которые правильно подписаны до 64-битных, поэтому используемые диапазоны — это низкие и высокие 47-битные диапазоны вверху и внизу виртуального адресного пространства. Вы можете назвать неканонические указатели недопустимыми ненулевыми адресами, но они по-прежнему работают как целые числа, если вы никогда не разыменовываете их. См. Также Следует ли сравнение указателей быть подписанным или беззнаковым в 64-разрядной системе x86? person personal_cloud; 06.03.2019

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

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

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

Добавить комментарий

;-) :| :x :twisted: :smile: :shock: :sad: :roll: :razz: :oops: :o :mrgreen: :lol: :idea: :grin: :evil: :cry: :cool: :arrow: :???: :?: :!: