Простая программа для выделения и освобождения динамической памяти:
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 поддерживает указатели, что позволяет более гибко управлять данными и структурами.
Предполагая, что вы работаете с 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().