Как реализовать calloc

Я пытаюсь переписать malloc и calloc, мой вопрос о реализации calloc, а не о том, как его использовать.

Всегда следует использовать calloc() вместо malloc()+memset(), потому что это может использовать преимущества copy-on-write (COW).

Некоторые calloc реализованы так:

void * calloc(size_t nelem, size_t elsize)
{
    void *p;

    p = malloc (nelem * elsize);
    if (p == 0)
        return (p);

    bzero (p, nelem * elsize);
    return (p);
}

Но они вообще не используют COW (и не проверяют переполнение).

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

Вместо использования MAP_ANON мы могли бы mmap из /dev/zero:

fd = open("/dev/zero", O_RDWR); 
a = mmap (0, 4096e4, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_FILE, fd, 0);

Но /dev/zero не требуется POSIX, и можно было бы легко сделать sudo mv /dev/zero /dev/foo, нарушив мою реализацию.

Как правильно переписать calloc(), соблюдая принцип копирования при записи?


person Bilow    schedule 20.10.2017    source источник
comment
Правильный способ реализации calloc() и malloc(), вероятно, включает в себя выполнение соответствующего системного вызова. Детали, поневоле, зависят от системы.   -  person John Bollinger    schedule 20.10.2017
comment
@JohnBollinger Какой системный вызов, например?   -  person Bilow    schedule 20.10.2017
comment
mmap, например, если выделение достаточно велико, а mmap имеет параметр, гарантирующий нулевое заполнение (например, MAP_ANONYMOUS в Linux). Хотя я не уверен, что это обычная оптимизация.   -  person rici    schedule 20.10.2017
comment
В ответе, который вы связали, есть примечание, в котором говорится, что с mmap ядро ​​всегда очищает память, так как вы не оставляете память от других процессов в нем (однако, я думаю, это, вероятно, гарантировано только в Linux). Так что в этом случае память уже должна быть обнулена. Вам нужно будет только обнулить память, если она поступает из одного из ваших пулов.   -  person Cassandra Fox    schedule 20.10.2017
comment
обратите внимание, что ваш nelem * elsize опасен и часто является источником дыр в безопасности. Вы должны проверить наличие переполнения там (ключевое слово: __builtin_mul_overflow())   -  person ensc    schedule 21.10.2017
comment
Вот аналогичный вопрос   -  person Bilow    schedule 22.10.2017


Ответы (1)


Чистый POSIX не поддерживает анонимные сопоставления памяти, и нет интерфейсов более низкого уровня, чем calloc, для выделения обнуленной памяти.

Существующие реализации POSIX поддерживают сопоставление анонимной частной памяти в качестве расширения с помощью флага MAP_ANON или MAP_ANONYMOUS (или исторически, путем сопоставления с /dev/zero). Ядро следит за тем, чтобы приложение видело только обнуленную память. (Существуют и более старые интерфейсы, такие как brk и sbrk, но их сложно использовать, поскольку они не являются потокобезопасными.)

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

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

person Florian Weimer    schedule 21.10.2017
comment
В противном случае он возвращается напрямую, потому что известно, что он свежий и, следовательно, был очищен ядром. Это предположение может не сработать, если в программе есть ошибка переполнения буфера или случайный указатель, который еще не записывает в this неиспользуемое пространство. Неопределенное поведение может возникнуть позже в совершенно не связанной части программы, потому что блок, возвращаемый calloc(), не будет полностью установлен на все биты нулевыми. Программист, скорее всего, потратит долго время на отладку и может ошибочно заключить, что calloc() нельзя доверять, пока не будет найдена настоящая ошибка, если вообще будет обнаружена. - person chqrlie; 21.10.2017
comment
Если есть переполнение буфера, все ставки в любом случае сняты. Кроме того, переполнение буфера в куче на самом деле легко обнаруживается с помощью valgrind или Address Sanitizer (а также есть проприетарные инструменты). Переполнения на основе стека гораздо сложнее обнаружить (если они не повреждают адрес возврата или канарейку стека). - person Florian Weimer; 21.10.2017
comment
Вы абсолютно правы, я просто указал на пример из реальной жизни, который быстро решился бы с помощью valgrind, если бы программист разрабатывал на приличной системе. Вместо этого эта ошибка вызвала недоверие всей компании к библиотеке C, и calloc() была запрещена. - person chqrlie; 21.10.2017
comment
Вы подтверждаете, что если MAP_ANON существует, я могу быть уверен, что ядро ​​даст мне обнуленную память? - person Bilow; 21.10.2017
comment
Вам нужно читать документацию. Я не удивлюсь, если есть какая-то непонятная платформа, которая имеет MAP_ANON и возвращает неочищенную память. В Linux, если вам нужна неочищенная память с поддерживающим ее ядром, вы также должны указать MAP_UNINITIALIZED. - person Florian Weimer; 21.10.2017
comment
Спасибо. Если я правильно понимаю, все реализации calloc(), использующие преимущества копирования при записи, полагаются на наличие свежей памяти из ядра, в противном случае они вызывают bzero(). Память также очищается, когда часть блока mmap уже использовалась. - person Bilow; 22.10.2017