Позволяет ли стандарт C присваивать указателю произвольное значение и увеличивать его?


хорошо ли определено поведение этого кода?

#include <stdio.h>
#include <stdint.h>

int main(void)
{
    void *ptr = (char *)0x01;
    size_t val;

    ptr = (char *)ptr + 1;
    val = (size_t)(uintptr_t)ptr;

    printf("%zu\n", val);
    return 0;
}

Я имею в виду, можем ли мы назначить какое-то фиксированное число указателю и увеличить его, даже если он указывает на какой-то случайный адрес? (Я знаю, что вы не можете разыменовать его)

5   51  

5 ответов:

назначение:

void *ptr = (char *)0x01;

и реализация определенного поведения потому что он преобразует целое число в указатель. Это подробно описано в разделе 6.3.2.3 стандарт C по поводу указателей:

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

что касается последующей арифметики указателя:

ptr = (char *)ptr + 1;

это зависит от нескольких вещей.

во-первых, текущее значение ptrмая быть представлением ловушки в соответствии с пунктом 6.3.2.3 выше. Если это так, то поведение undefined.

Далее идет вопрос о том,0x1 указывает на допустимый объект. Добавление указателя и целого числа допустимо только в том случае, если и операнд указателя, и результат указывают на элементы объекта массива (один объект считается массивом размера 1) или один элемент за объектом массива. Это подробно описано в разделе 6.5.6:

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

8 когда выражение с целочисленным типом добавляется или вычитается из Указателя, результат имеет тип указателя операнд. Если операнд указателя указывает на элемент массива объект, и массив достаточно большой, результат указывает на элемент смещение от исходного элемента такое, что разность индексы результирующих и исходных элементов массива равны целочисленное выражение. Иначе говоря, если выражение P указывает на i-й элемент объекта массива, выражения (P)+N (что эквивалентно N+(P) ) и (P) - N (где N имеет значение n) указывает на, соответственно, i+n-й и i-n-й элементы объекта массива, если они существуют. Более того, если выражение P указывает на последний элемент объект массива, выражение (P)+1 указывает один за последним элемент объект массива, и если выражение Q указывает один мимо последний элемент объекта массива, выражение (Q) -1 указывает на последний элемент массива объекта. если оба указателя операнд и результат указывают на элементы одного массива объекта, или один после последнего элемента массива объекта, в оценка не должна приводить к переполнению; в противном случае поведение не определено. если результат указывает на один после последнего элемента объект массива, он не должен использоваться в качестве операнда унарного * оператор, который оценивается.

на размещенной реализации значение 0x1 почти наверняка делает не указывает на допустимый объект, в этом случае дополнение undefined. Однако встроенная реализация может поддерживать установку указателей на определенные значения, и если это так, то это может быть так 0x1 действительно указывает на допустимый объект. Если это так, то поведение хорошо определенными, иначе undefined.

нет, поведение программы не определено. Как только неопределенная конструкция достигается в программе, любое будущее поведение не определено. Парадоксально, но любое поведение в прошлом также не определено.

результат void *ptr = (char*)0x01; определяется реализацией, отчасти из-за того, что a char может иметь представление ловушку.

но поведение последующей арифметики указателя в заявлении ptr = (char *)ptr + 1; и undefined. Это потому, что указатель арифметика допустимо только в пределах массивов, включая один после конца массива. Для этого объект представляет собой массив длины один.

Да, код четко определен как определенный реализацией. Он не является неопределенным. См. ISO/IEC 9899:2011 [6.3.2.3] / 5 и примечание 67.

язык C изначально создавался как язык системного программирования. Системное программирование требовало манипулирования аппаратным обеспечением, отображенным в памяти, требуя, чтобы вы набивали жестко закодированные адреса в указатели, иногда увеличивали эти указатели, а также читали и записывали данные из и в результирующий адрес. С этой целью присвоение и целое число a указатель и манипулирование этим указателем с помощью арифметики хорошо определены языком. Делая его определяемым реализацией, язык позволяет, чтобы все виды вещей могли произойти: от классической остановки и возгорания до возникновения ошибки шины при попытке разыменовать нечетный адрес.

разница между неопределенным поведением и определяемым реализацией поведением в основном неопределенное поведение означает "не делайте этого, мы не знаем, что произойдет" и реализация-определенное поведение означает: "это нормально, чтобы идти вперед и делать это, это до вас, чтобы знать, что произойдет."

- это неопределенное поведение.

из N1570 (курсив добавлен):

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

Если значение является представлением ловушки, чтение его является неопределенным поведением:

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

и

идентификатор является первичным выражением, если он был объявлен как обозначающий объект (в этом случае это lvalue) или функция (в этом случае это обозначение функции).

следовательно, строка void *ptr = (char *)0x01; уже потенциально неопределенное поведение, на реализации, где (char*)0x01 или (void*)(char*)0x01 представление ловушку. С левой стороны-это выражение, именующее это не имеет символьного типа и считывает представление ловушки.

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

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

единственные причины, по которым стандарт вообще что-то говорит целочисленные преобразования в указатель таковы:

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

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

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

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

Я думаю, было бы проще всего просто сказать, что любая операция, связанная с преобразованием целого числа в указатель с помощью все, что отличается от значений intptr_t или uintptr_t, полученных из преобразований указателя на целое число, вызывает неопределенное поведение, но затем обратите внимание, что для качественных реализаций, предназначенных для низкоуровневого программирования, обычно обрабатывается неопределенное поведение "документированным образом, характерным для среды". Стандарт не указывает, когда реализации должны обрабатывать программы, которые вызывают UB таким образом, но вместо этого рассматривает его как проблему качества реализации.

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

char *p = (char*)1;
p++;

как эквивалент "char p = (char) 2;", то следует ожидать, что реализация будет работать именно так. С другой стороны, реализация может определить поведение преобразования целого числа в указатель таким образом, что даже:

char *p = (char*)1;
char *q = p;  // Not doing any arithmetic here--just a simple assignment

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