Вопрос об утечке памяти в C после перемещения указателя (что именно освобождается?)

#c #memory-management #memory-leaks #malloc

#c #управление памятью #утечки памяти #malloc

Вопрос:

Я понимаю, что приведенный ниже пример кода — это то, чего вы никогда не должны делать. Мой вопрос представляет лишь один интерес. Если вы выделяете блок памяти, а затем перемещаете указатель (нет-нет), когда вы освобождаете память, каков размер освобождаемого блока и где он находится в памяти? Вот надуманный фрагмент кода:

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

int main(void) {
    char* s = malloc(1024);
    strcpy(s, "Some string");
    // Advance the pointer...
    s  = 5;
    // Prints "string"
    printf("%sn", s);
    /*
     * What exactly are the beginning and end points of the memory 
     * block now being deallocated?
     */
    free(s);
    return 0;
}
  

Вот что, я думаю, у меня происходит. Освобождаемый блок памяти начинается с байта, который содержит букву «s» в «строке». 5 байтов, которые содержали «Some», теперь потеряны.

Мне интересно: освобождены ли также 5 байт, местоположение которых в памяти сразу после окончания исходных 1024 байт, или они просто оставлены в покое?

Кто-нибудь точно знает, что делает компилятор? Это не определено?

Спасибо.

Комментарии:

1. 1, потому что я узнал о чем-то, о чем не подумал.

Ответ №1:

Вы не можете передать указатель, который не был получен из malloc , calloc или realloc в free (за исключением NULL ).

Вопрос 7.19 в C FAQ имеет отношение к вашему вопросу.

Последствия вызова неопределенного поведения объясняются здесь.

Комментарии:

1. Спасибо. Я пересматриваю C сейчас, после многих лет, когда не смотрел на него. Я никогда ничего с этим не делал, кроме педагогических, игрушечных программ. Когда-то я более или менее понимал указатели, и, похоже, это возвращается ко мне 😉 Ваш комментарий напоминает мне, что переменная pointer — это не то же самое, что сам указатель. После изменения адреса, сохраненного в «s», это все та же переменная, но уже не тот указатель.

Ответ №2:

Это неопределенное поведение в стандарте, поэтому вы не можете ни на что полагаться.

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

В последний раз, когда я смотрел, было две основные практики реализации.

Один из них заключается в том, чтобы вести отдельную запись о выделенных блоках вместе с выделенным адресом. Функция free() просматривает блок, чтобы увидеть, что нужно освободить. В этом случае он, скорее всего, просто не найдет его и вполне может просто ничего не делать. Утечка памяти. Однако никаких гарантий нет.

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

Итак, я бы ожидал либо утечки памяти (ничего не освобождается), либо повреждения кучи (слишком многое помечено как свободное, а затем перераспределено).

Ответ №3:

Да, это неопределенное поведение. По сути, вы освобождаете указатель, которого не делали malloc .

Комментарии:

1. что еще хуже, я бы ожидал, что это приведет к сбою в большинстве библиотек malloc.

2. Неопределенное поведение означает «может случиться все, что угодно». От взрыва вашей машины вместе с вами до корректной работы.

3. Распространенная идиома, которую мы используем на моей работе для обозначения неопределенного поведения, — «напишите своей матери»

Ответ №4:

Вы не можете передать указатель, который вы не получили от malloc (или calloc или realloc …) free . Это включает смещения в блоки, которые вы получили из malloc . Нарушение этого правила может привести к чему угодно. Обычно это заканчивается наихудшей мыслимой возможностью в самый неподходящий момент.

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

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

int main() {
    char *new_s;
    char *s = malloc(1024);
    strcpy(s, "Some string");

    new_s = realloc(s, 5);
    if (!new_s) {
        printf("Out of memory! How did this happen when we were freeing memory? What a cruel world!n");
        abort();
    }
    s = new_s;

    s[4] = 0; // put the null terminator back on
    printf("%sn", s); // prints Some

    free(s);
    return 0;
}
  

realloc работает как для увеличения, так и для сжатия блоков памяти, но может (или не может) перемещать память для этого.

Ответ №5:

Это делает не компилятор, а стандартная библиотека. Поведение не определено. Библиотека знает, что она выделила вам оригинал s . s 5 Не назначается ни одному блоку памяти, известному библиотеке, даже если он находится внутри известного блока. Итак, это не сработает.

Ответ №6:

Мне интересно: освобождены ли также 5 байт, местоположение которых в памяти сразу после окончания исходных 1024 байт, или они просто оставлены в покое?

Оба. Результат не определен, поэтому компилятор волен делать либо то, либо другое, что ему действительно хотелось бы. Конечно (как и во всех случаях «неопределенного поведения») для конкретной платформы и компилятора существует конкретный ответ, но любой код, который полагается на такое поведение, является плохой идеей.

Ответ №7:

Вызов free() для ptr, который не был выделен malloc или его собратьями, не определен.

Большинство реализаций malloc выделяют небольшую (обычно 4 байта) область заголовка непосредственно перед возвращением ptr. Это означает, что когда вы выделили 1024 байта, malloc фактически зарезервировал 1028 байт. При вызове free( ptr ), если ptr не равен 0, он проверяет данные в ptr — sizeof(заголовок). Некоторые распределители реализуют проверку работоспособности, чтобы убедиться, что это допустимый заголовок, и который может обнаружить неправильный ptr и подтвердить или завершить работу. Если проверка работоспособности отсутствует или она проходит ошибочно, бесплатная процедура будет воздействовать на любые данные, находящиеся в заголовке.

Ответ №8:

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

Возможно, вы найдете библиотекаря (реализация malloc / free library), который заберет такую книгу обратно, но во многих случаях я ожидаю, что вы заплатите штраф за небрежное обращение.

В черновике C99 (у меня нет под рукой окончательного варианта C99) есть что сказать по этой теме:

Функция free освобождает пространство, на которое указывает ptr , то есть делает его доступным для дальнейшего выделения. Если ptr указатель равен нулю, никаких действий не происходит. В противном случае, если аргумент не соответствует указателю, ранее возвращенному calloc , malloc или realloc функцией, или если пространство было освобождено вызовом free or realloc , поведение не определено.

По моему опыту, двойное освобождение или отсутствие «указателя», который не был возвращен через malloc , приведет к повреждению памяти и / или сбою, в зависимости от вашей malloc реализации. Специалисты по безопасности с обеих сторон забора не раз использовали это поведение, чтобы делать различные интересные вещи, по крайней мере, в ранних версиях широко используемого malloc пакета Doug Lea’s.

Ответ №9:

Реализация библиотеки может поместить некоторую структуру данных перед указателем, который она возвращает вам. Затем в free() он уменьшает указатель, чтобы получить доступ к структуре данных, сообщающей ему, как поместить память обратно в свободный пул. Таким образом, 5 байт в начале вашей строки «Some» интерпретируются как конец, struct используемый malloc() алгоритмом. Возможно, конец 32-битного значения, такого как размер выделенной памяти, или ссылка в связанном списке. Это зависит от реализации. Какими бы ни были детали, это просто приведет к сбою вашей программы. Как указывает Синан, если вам повезет!

Комментарии:

1. Неопределенный означает неопределенный . Вам повезет, если это просто приведет к сбою вашей программы.

Ответ №10:

Давайте будем умными здесь… free() это не черная дыра. По крайней мере, у вас есть исходный код CRT. Помимо этого, вам нужен исходный код ядра.

Конечно, поведение не определено в том смысле, что CRT / OS решает, что делать. Но это не мешает вам узнать, что на самом деле делает ваша платформа.

Быстрый просмотр CRT Windows показывает, что это free() приводит прямо к HeapFree() использованию кучи, специфичной для CRT. Кроме того, что вы используете RtlHeapFree() , а затем в системное пространство (NTOSKRN.EXE) с помощью диспетчера памяти Mm*() .

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

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

Это возможно в вашем случае освобождения памяти на несколько байтов в ваш блок (или перезаписи вашего выделенного размера). Конечно, вы можете обмануть это и самостоятельно записать маркер конца блока в правильном месте. Это позволит вам пройти проверку CRT, но по мере дальнейшего изменения пути к коду происходит более неопределенное поведение. Могут произойти три вещи: 1) абсолютно никакого вреда, 2) повреждение памяти в куче CRT или 3) исключение, вызванное любой из функций управления памятью.

Комментарии:

1. securecoding.cert.org/confluence/display/seccode/…

Ответ №11:

Короткая версия: это неопределенное поведение.

Длинная версия: я проверил сайт CWE и обнаружил, что, хотя это плохая идея, ни у кого, похоже, нет четкого ответа. Вероятно, потому, что оно не определено.

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

В любом случае, это явно не очень хорошая идея. 🙂

Комментарии:

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

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

3. Извините, забыл отметить сбой. Наверное, я забыл, поскольку другие люди упоминали об этом, и я пытался установить, что произойдет, если сбой не произойдет. Тому, кто проголосовал против меня, если у вас нет другой проблемы с моим ответом, пожалуйста, пересмотрите, поскольку вероятность сбоя на самом деле достаточно очевидна, так что даже я знаю об этом. Спасибо 🙂

4. Это не отрицательный результат для вас, это отрицательный результат для комментария. Дело не в том, что вы знаете, а в качестве того, что вы на самом деле записываете. Кроме того, я думаю, что остальная часть вашего ответа также неверна; как вы ожидаете, что реализация malloc будет искать либо длину, либо конечный адрес для s, если вы дадите ей другой указатель на s 5, который она никогда не выделяла?