Позволяет ли стандарт 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;
}

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

Ответ 1

Назначение:

void *ptr = (char *)0x01;

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

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

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

ptr = (char *)ptr + 1;

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

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

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

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

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

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

Ответ 2

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

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

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

Ответ 3

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

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

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

Ответ 4

Это неопределенное поведение.

От N1570 (выделено мной):

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

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

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

А также

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

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

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

Ответ 5

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

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

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

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

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

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

Я думаю, было бы проще просто сказать, что любая операция, включающая преобразования целого-на-указатель, с любыми значениями, отличными от значений intptr_t или uintptr_t, полученных от конверсий с указателями на целые, вызывает Undefined Behavior, но затем обратите внимание, что это обычное явление для реализаций качества для низкоуровневого программирования для обработки неопределенного поведения "документированным образом, характерным для окружающей среды". В стандарте не указывается, когда реализации должны обрабатывать программы, которые вызывают 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

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