Висячие указатели realloc() и неопределенное поведение

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

Конечно, обычный случай, когда указатель становится недействительным, а затем снова становится «действительным», — это когда какой-то другой объект выделяется в память, которая использовалась ранее, и если вы используете указатель для доступа к памяти, это очевидно. неопределенное поведение. Висячая память указателя перезаписывает урок 1, в значительной степени.

Но что, если память снова станет доступной для того же выделения? Для этого существует только один стандартный способ: realloc(). Если у вас есть указатель где-то внутри malloc() блока памяти со смещением > 1, а затем используйте realloc(), чтобы сжать блок до меньшего, чем ваше смещение, ваш указатель, очевидно, станет недействительным. Если вы затем снова используете realloc(), чтобы снова увеличить блок, чтобы, по крайней мере, покрыть тип объекта, на который указывает висячий указатель, и ни в одном случае realloc() не переместил блок памяти, будет ли висячий указатель снова действительным?

Это такой угловой случай, что я действительно не знаю, как интерпретировать стандарты C или C++, чтобы понять это. Ниже приведена программа, которая это показывает.

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int main(void)
{
    static const char s_message[] = "hello there";
    static const char s_kitty[] = "kitty";

    char *string = malloc(sizeof(s_message));
    if (!string)
    {
        fprintf(stderr, "malloc failed\n");
        return 1;
    }

    memcpy(string, s_message, sizeof(s_message));
    printf("%p %s\n", string, string);

    char *overwrite = string + 6;
    *overwrite = '\0';
    printf("%p %s\n", string, string);

    string[4] = '\0';
    char *new_string = realloc(string, 5);
    if (new_string != string)
    {
        fprintf(stderr, "realloc #1 failed or moved the string\n");
        free(new_string ? new_string : string);
        return 1;
    }
    string = new_string;
    printf("%p %s\n", string, string);

    new_string = realloc(string, 6 + sizeof(s_kitty));
    if (new_string != string)
    {
        fprintf(stderr, "realloc #2 failed or moved the string\n");
        free(new_string ? new_string : string);
        return 1;
    }

    // Is this defined behavior, even though at one point,
    // "overwrite" was a dangling pointer?
    memcpy(overwrite, s_kitty, sizeof(s_kitty));
    string[4] = s_message[4];
    printf("%p %s\n", string, string);
    free(string);
    return 0;
}

person Myria    schedule 27.09.2014    source источник
comment
Ну, вы также можете спросить: «Что произойдет, если я напишу ошибки в своей программе». Указатели недействительны, но их разыменование является UB, даже если тот же блок памяти снова выделяется после другого malloc.   -  person Martin James    schedule 27.09.2014
comment
Указатель на освобожденную память может быть недопустимым, но он все еще может работать. Это зависит от того, изменилась ли память. Если он был освобожден, но все еще содержит те же значения (обычно это так), тогда код будет работать до тех пор, пока эта память не изменится, и в этом случае ваша программа, вероятно, выйдет из строя... что приведет к трудно отслеживаемым ошибкам, потому что это не детерминировано. Запустите программу, она падает при выполнении X, запустите ее снова, и она никогда не падает... все потому, что ваш указатель не был обновлен.   -  person AbstractDissonance    schedule 15.07.2016


Ответы (3)


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

Определенно да. Из раздела 6.2.4 стандарта C:

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

И из раздела 7.22.3.5:

Функция realloc освобождает старый объект, на который указывает ptr, и возвращает указатель на новый объект, размер которого определяется параметром size. Содержимое нового объекта должно быть таким же, как у старого объекта до освобождения, вплоть до меньшего из нового и старого размеров. Любые байты в новом объекте, превышающие размер старого объекта, имеют неопределенное значение.

Обратите внимание на ссылку на старый объект и новый объект... по стандарту, то, что вы возвращаете из realloc, является другим объектом, чем то, что вы было раньше; это ничем не отличается от выполнения free, а затем malloc, и нет никакой гарантии, что два объекта имеют один и тот же адрес, даже если новый размер равен ‹= старому размеру... и в реальных реализациях они часто не будут, потому что объекты разных размеров рисуются из разных свободных списков.

Что произойдет, если позже они снова станут действительными?

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

Но что, если память снова станет доступной для того же выделения? Для этого есть только один стандартный способ: realloc()

Извините, нет, стандарт C не содержит языка на этот счет.

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

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

висячий указатель снова действителен?

См. выше; инвалид недействителен; нет обратного пути.

Это такой угловой случай, что я действительно не знаю, как интерпретировать стандарты C или C++, чтобы понять это.

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

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

person Jim Balter    schedule 27.09.2014
comment
Я знаю обо всех случаях, когда память может перемещаться в другое место, поэтому мой код проверяет значения изменения указателя и прерывает работу, если это происходит. Он также заполняет испорченные байты обратно. Меня интересовал только ваш последний абзац. Я полагаю, однако, что даже проверки того, перемещено ли выделение, сами по себе недействительны, потому что сравнения указателей на разные блоки памяти возвращаются неуказанными ( но не undefined) результатов, что означает, что операторы if не могут иметь значения в теоретических реализациях. - person Myria; 29.09.2014

Если вы затем используете realloc() снова, чтобы увеличить блок обратно, по крайней мере, чтобы покрыть тип объекта, на который указывает висячий указатель, и ни в одном случае realloc() не переместил блок памяти, будет ли висячий указатель снова действительным?

Нет. Если realloc() не возвращает нулевой указатель, вызов завершает время жизни выделенного объекта, подразумевая, что все указатели, указывающие на него, становятся недействительными. Если realloc() завершается успешно, возвращается адрес нового объекта.

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

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

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

person Christoph    schedule 27.09.2014
comment
новый будет работать в большинстве реализаций языка C - на самом деле нет, так как большинство реализаций модифицируют освобожденный раздел. Язык C несостоятелен. Действительно! - person Jim Balter; 27.09.2014
comment
@JimBalter: независимо от того, было ли изменено value объекта (и я не вижу причин для realloc(), а не free(), делать это в этом конкретном примере, и мы говорим о конечных байтах, тогда как метаданные обычно хранятся впереди), указатель будет указывать на правильное место внутри объекта - person Christoph; 27.09.2014
comment
Кхм. Когда вы перераспределяете блок на меньший размер (что освобождает хвост блока), теперь есть два блока, и передняя часть второго находится в середине исходного большего блока... перераспределение назад даст вам это метаданные. указатель укажет на правильное место внутри объекта — может быть, а может и нет; нет никакой гарантии, что пара realloc вернет вам ту же память. Даже перераспределение на меньший размер может вернуть совершенно другую часть памяти. - person Jim Balter; 27.09.2014
comment
@JimBalter: ах, я не подумал об этом; но учтите, что второй блок может оказаться слишком маленьким, чтобы его можно было вернуть в свободный список; в любом случае, моя главная мысль была о вычислении адреса, а не о значении - person Christoph; 27.09.2014
comment
Если второй блок слишком мал, чтобы его можно было вернуть в список свободных, тогда realloc не работает... иначе malloc приведет к утечке памяти. И валидность полностью зависит от содержания. И обратите внимание на мое редактирование моего комментария выше: вы понятия не имеете, где находятся данные после перераспределения. FWIW, раньше я писал код библиотеки C, включая malloc/realloc/free, зарабатывая на жизнь. - person Jim Balter; 27.09.2014
comment
Ваш комментарий - полная ерунда, причудливая соломенная чушь. Конечно, если старый и новый адреса равны, то an_element, который не изменился, по-прежнему указывает на блок, ну и что? Нет никакой гарантии, что они будут равными, и если вы читаете из указателя, а не записываете в него, невозможно узнать, что вы получу. Не надо пытаться донести до меня такие вещи, и мне такие нелепые дискуссии не интересны. - person Jim Balter; 27.09.2014
comment
Давайте продолжим обсуждение в чате. - person Christoph; 27.09.2014
comment
Нет, не будем. До свидания. (И вы должны удалить свой комментарий с моим именем из своего ответа ... это совершенно неуместно в SO.) - person Jim Balter; 27.09.2014
comment
@JimBalter: я не совсем понимаю, как мне удалось тебя разозлить, но все равно хороших выходных - person Christoph; 27.09.2014
comment
Вместо того, чтобы углубляться в это, я выскажу то же самое, что и в ответ на ответ cmaster: дело в том, что компилятор может повторно использовать переменную указателя для какой-либо другой цели после realloc, поэтому у вас нет гарантии, что он имеет то же значение, что и раньше. Поведение не определено, и точка. Говорить о том, что, вероятно, будет так, бессмысленно, особенно когда современные компиляторы все более агрессивно используют UB для оптимизации. - person Jim Balter; 27.09.2014
comment
@JimBalter: интересно; см. stackoverflow.com/questions/26073842/ - person Christoph; 27.09.2014
comment
@JimBalter: Учитывая int *p = malloc(4); intptr_t q=0,r=0; memcpy(&q, &p, sizeof p); free(p); memcpy(&r, &p, sizeof p);, есть ли в Стандарте что-нибудь, что позволило бы q == r быть ложным, если sizeof intptr_t и sizeof p равны? - person supercat; 29.04.2015

Это зависит от вашего определения «действительного». Вы прекрасно описали ситуацию. Если вы хотите считать это «действительным», то оно действительно. Если вы не хотите считать это «действительным», то это недействительно.

person David Schwartz    schedule 27.09.2014
comment
стандарт C считает его недействительным - person Christoph; 27.09.2014
comment
@Кристоф Ерунда. Рассмотрим: if (realloc(foo, 32) == foo) { /* HERE */ }. Конечно, foo там вполне допустимо, даже несмотря на то, что оно было передано в realloc. Помните, нам дано, что последующий вызов realloc вернул тот же указатель до того, как мы получили к нему доступ. - person David Schwartz; 27.09.2014
comment
опять же, не в соответствии со стандартом C: если realloc() не срабатывает, объект освобождается от выделения, заканчивая свое время жизни и делая все указатели недействительными с точки зрения семантики абстрактного языка; конечно, это будет работать на практике, но это не делает его действительным в отношении стандарта... - person Christoph; 27.09.2014
comment
В коде OP для overwrite установлено значение string + 6. После перераспределения строки до 5 байтов оптимизирующий компилятор может удалить значение overwrite, поскольку оно больше недействительно. Называйте это чепухой сколько угодно, но такое поведение недопустимо по стандарту и на практике может дать сбой. - person Jim Balter; 27.09.2014