C free() на виртуальной машине Ubuntu, вопрос относительно кучи памяти

Простая программа для выделения и освобождения динамической памяти:

int main(int argc, char **argv) {
    char *b1, *b2, *b3, *b4, *b_large;
    b1 = malloc(8);
    memset(b1, 0xaa, 8);
    b2= malloc(16);
    memset(b2, 0xbb, 16);
    b3 = malloc(25);
    memset(b3, 0xcc, 25);
    b4= malloc(1000);
    memset(b4, 0xdd, 1000);
    free(b1);
    free(b2);
    free(b3);
    free(b4);

Перед первым free():

(gdb) x/20gx  0x555555559290 
0x555555559290: 0x0000000000000000  0x0000000000000021
0x5555555592a0: 0xaaaaaaaaaaaaaaaa  0x0000000000000000
0x5555555592b0: 0x0000000000000000  0x0000000000000021
0x5555555592c0: 0xbbbbbbbbbbbbbbbb  0xbbbbbbbbbbbbbbbb
0x5555555592d0: 0x0000000000000000  0x0000000000000031
0x5555555592e0: 0xcccccccccccccccc  0xcccccccccccccccc
0x5555555592f0: 0xcccccccccccccccc  0x00000000000000cc
0x555555559300: 0x0000000000000000  0x00000000000003f1
0x555555559310: 0xdddddddddddddddd  0xdddddddddddddddd
0x555555559320: 0xdddddddddddddddd  0xdddddddddddddddd

И после первого free():

(gdb) x/20gx  0x555555559290 
0x555555559290: 0x0000000000000000  0x0000000000000021
0x5555555592a0: 0x0000000555555559  0xd13e7903c502febc
0x5555555592b0: 0x0000000000000000  0x0000000000000021
0x5555555592c0: 0xbbbbbbbbbbbbbbbb  0xbbbbbbbbbbbbbbbb
0x5555555592d0: 0x0000000000000000  0x0000000000000031
0x5555555592e0: 0xcccccccccccccccc  0xcccccccccccccccc
0x5555555592f0: 0xcccccccccccccccc  0x00000000000000cc
0x555555559300: 0x0000000000000000  0x00000000000003f1
0x555555559310: 0xdddddddddddddddd  0xdddddddddddddddd
0x555555559320: 0xdddddddddddddddd  0xdddddddddddddddd

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

Может ли кто-нибудь объяснить, почему функция free() ведет себя таким образом?

🤔 А знаете ли вы, что...
C поддерживает указатели, что позволяет более гибко управлять данными и структурами.


1
168
2

Ответы:

Решено

Предполагая, что вы работаете с Ubuntu 22.04 и, следовательно, с glibc 2.36.

Современные распределители кучи в настоящее время... довольно сложны, и glibc является ярким примером этого. Не ожидайте, что так легко увидите такие приятные вещи, как простые указатели.

Случайное значение, которое вы видите (0xd13e7903c502febc), — это внутренний tcache_key. Это действительно случайное значение uintptr_t, которое инициализируется один раз при запуске программы с помощью getrandom(2) и позже вставляется в свободные фрагменты, которые хранятся в tcache.

Значение tcache_keyвставляется в фрагменты, которые помещаются в tcache, а затем проверяется бесплатно в качестве простого усиления защиты от двойного освобождения. Он удаляется при перераспределении фрагмента tcache. Это было реализовано в glibc 2.34. Раньше (glibc 2.29-2.33) вместо tcache_key использовалось фиксированное значение.

Если вам интересно, что такое «tcache», то это кэш каждого потока, состоящий из нескольких сегментов. Каждый сегмент зарезервирован для заданного размера распределения и содержит освобожденные фрагменты именно этого размера в односвязном списке, состоящем не более чем из 7 элементов в порядке LIFO (вновь освобожденные фрагменты вставляются в заголовок). Когда ведро заполнено, освобождение фрагмента такого размера не добавит его в tcache, а вместо этого будет следовать «обычной» процедуре освобождения и в конечном итоге окажется в одном из обычных «корзин» арены в соответствии с довольно запутанным алгоритмом glibc.

Указатели на следующий фрагмент в сегментах tcache также искажаются в свободном режиме и восстанавливаются в alloc (как вы можете видеть здесь ) с помощью специальных макросов, которые используют неявную случайность отображения (mmap_base), предоставляемую ядром через ASLR.. Вот почему вы также не видите четкого указателя в кусках.


После free(b1); free(b2); у меня следующее:

(gdb) x/20gx 0x5555555592a0 - 0x10
0x555555559290: 0x0000000000000000  0x0000000000000021  -- b1
0x5555555592a0: 0x0000000555555559  0xdefa7fb306dd6989
0x5555555592b0: 0x0000000000000000  0x0000000000000021  -- b2
0x5555555592c0: 0x000055500000c7f9  0xdefa7fb306dd6989
0x5555555592d0: 0x0000000000000000  0x0000000000000031  -- b3
0x5555555592e0: 0xcccccccccccccccc  0xcccccccccccccccc
0x5555555592f0: 0xcccccccccccccccc  0x00000000000000cc
0x555555559300: 0x0000000000000000  0x00000000000003f1  -- b4
0x555555559310: 0xdddddddddddddddd  0xdddddddddddddddd
0x555555559320: 0xdddddddddddddddd  0xdddddddddddddddd

В бесплатной версии и b1, и b2 попадают в одно и то же ведро tcache, самое маленькое, для размера 16 (malloc(8) округляется до 16). У нас b2 в качестве главы, так как список LIFO.

tcache выглядит так:

{
  counts = {2, 0, 0, 0, ...},
  entries = {0x5555555592c0, 0x0, 0x00, 0x00, ...}
}

Как вы можете видеть выше, 0xdefa7fb306dd6989 — это значение tcache_key. Глядя на свободный фрагмент 0x5555555592c0, его указатель ->next искажен на 0x000055500000c7f9. Реальную стоимость можно получить как:

(0x5555555592c0UL >> 12) ^ 0x55500000c7f9UL == 0x5555555592a0UL

->next->next — это просто NULL (искаженный до 0x0000000555555559).


Когда вы говорите: «Я ожидал увидеть читаемые указатели вперед и назад во второй строке памяти», вы описываете поведение больших фрагментов без кэширования. Даже игнорируя/отключая tcache, достаточно маленькие фрагменты также могут снова храниться в односвязных списках (fastbins) до определенного фиксированного числа (IIRC). Только для больших размеров у вас действительно есть двусвязные списки, где оба указателя используются так, как вы ожидаете.

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

Установив символы отладки из пакета libc6-dbg и используя плагин pwndbg GDB, вы получите очень полезные команды для проверки кучи glibc и различных контейнеров. Например:

  • heap показаны все фрагменты
  • bins показывает состояние корзин арены
  • arena показывает фактическую структуру арены
  • tcache показывает состояние tcache

и более...


Для получения дополнительной информации также прочтите эту интересную статью: Ключи Tcache. Примитивная двойная защита .


После ответа Марко я немного поигрался с GLIBC_TUNABLES следующим образом:

(gdb) set environment GLIBC_TUNABLES glibc.malloc.tcache_count=0
(gdb) set environment GLIBC_TUNABLES glibc.malloc.mxfast=0

Отключение tcache и быстрых контейнеров, как указано выше, дает поведение, которое я ожидал в своем исходном вопросе, - несортированный контейнер в ответ на вызов free().