Что такое правильный указатель в gcc linux x86-64 C ++? - PullRequest
0 голосов
/ 03 марта 2019

Я программирую на 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, действительный указатель - это байт в памяти, нет, подождите, это элемент массива, включающий элемент сразу после массива, нет, подождите, это страница виртуальной памяти, нет, подождите, это Супермен!

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

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

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

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

#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 документацию, доказывающую обратное: все указатели действительны).

Ответы [ 3 ]

0 голосов
/ 03 марта 2019

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

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

Second, 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, с моделью кода по умолчанию (-mcmodel=small, где предполагается, что весь статический код + данные помещается в 2GB )

    $ 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 является неправильным типом для указателей в целом (потому что Windowsx64 - это LLP64 ABI, где long - 32 бита).Вы хотите intptr_t или uintptr_t, или что-то эквивалентное printf("%p"), которое печатает необработанный void*.

0 голосов
/ 06 марта 2019

Стандарт не предусматривает существования какого-либо хранилища, кроме того, которое обеспечивает реализация через объекты статической, автоматической или длительности потока, или использования функций стандартной библиотеки, таких как 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.

0 голосов
/ 03 марта 2019

Следующие примеры показывают, что 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).

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

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

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