Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Языки и методы программирования - лекция-11: ди...

Anton
December 09, 2024

Языки и методы программирования - лекция-11: динамическая память

Лекция курса "Языки и методы программирования"
Лекция-11: динамическая память
- Хотим загрузить содержимое файла в память компьютера
- Вариант: использовать простой массив, который создаём внутри подпрограммы
- Проблема размера статического массива
- Массив переменной длины VLA (variable length array)
- Проблема вернуть статический массив из подпрограммы
- Сегменты памяти: динамическая память (куча)
- stdlib, malloc, free, sizeof
- Документация: GCC (GNU Compiler Collection)
- Документация: Язык Си (реализация GNU C)
- Документация: Стандартная библиотека Си
- Внутреннее устройство malloc
- Замечание: в С++ - new и delete
- Нехватка памяти: стек vs куча
- Проблема фрагментации памяти
- Утечка памяти
- Просмотр сегментов памяти запущенного приложения в ОС GNU/Linux
- VLA vs malloc
- Давайте всё-таки загрузим файл
- Задания для самостоятельной работы

Anton

December 09, 2024
Tweet

More Decks by Anton

Other Decks in Education

Transcript

  1. Допустим, мы хотим • Загрузить содержимое файла в память компьютера

    • Или получить другую информацию из внешнего источника • Например, поток данных через сеть или текст, введенный пользователем с клавиатуры
  2. Условия • Содержимое файла поместим в массив • Мы заранее

    не знаем размер файла, поэтому до запуска программы нам не известен размер массива, который необходимо создать • Мы хотим выполнить загрузку файла внутри специальной подпрограммы
  3. Вспомним про массивы • Мы не можем возвращать копию (значение)

    массива в качестве возвращаемого значения функции (через return) • Но мы можем возвращать указатель на массив (т. е. можем вернуть его адрес)
  4. #include <stdio.h> int* array_stack() { int arr1[3]; arr1[0] = 23;

    arr1[1] = 43; arr1[2] = 34; return arr1; } int main(void) { int* parr1 = array_stack(); // Ошибка сегментирования (сделан дамп памяти) parr1[0] = 115; printf("parr1: %d, %d, %d\n", parr1[0], parr1[1], parr1[2]); }
  5. > gcc prog-1-return-local-pointer.c prog-1-return-local-pointer.c: In function ‘array_stack’: prog-1-return-local-pointer.c:10:12: warning: function

    returns address of local variable [-Wreturn-local-addr] return arr1; ^ > ./a.out Ошибка сегментирования (сделан дамп памяти) • Предупреждение на этапе компиляции: функция возвращает адрес локальной переменной • На этапе запуска это влечёт логичное последствие: ошибка работы с сегментами памяти, программу убивает операционная система
  6. Таким образом • Массив имеет фиксированный размер • И мы

    не можем вернуть его из подпрограммы • Не выполнено ни одно из исходных требований
  7. Проблема размера • Мы создали массив с известным заранее размером

    — количество элементов указали в коде • Но мы сейчас не знаем заранее (на этапе компиляции), сколько места для хранения содержимого файла нам потребуется • До тех пор, пока не обратимся к файлу во время выполнения программы
  8. Мы бы могли • Создать достаточно большой массив, чтобы в

    него гарантированно уместился достаточно большой файл • Это может быть ок для некоторых ситуаций (например, ввод с клавиатуры не слишком длинных строк, для которых можно ограничить максимальный размер) • Но для загрузки достаточно больших файлов (картинки или видео) этот подход не целесообразен
  9. Попробуем еще один вариант • Заведем переменную, целое число —

    размер массива • Вычислим её значение во время выполнения программы (например, запросим размер файла из файловой системы или попросим ввести с клавиатуры) • Создадим массив, указав в качестве размера эту переменную • Доверимся интуиции — вдруг сработает
  10. #include <stdio.h> int* array_stack_vla() { int size; printf("size="); scanf("%d", &size);

    int arr2[size]; arr2[0] = 123; arr2[1] = 143; arr2[2] = 134; return arr2; } int main(void) { int* parr2 = array_stack_vla(); // Ошибка сегментирования (сделан дамп памяти) parr2[0] = 225; printf("parr2: %d, %d, %d\n", parr2[0], parr2[1], parr2[2]); }
  11. > gcc prog-2-return-local-pointer-vla.c prog-2-return-local-pointer-vla.c: In function ‘array_stack_vla’: prog-2-return-local-pointer-vla.c:13:12: warning: function

    returns address of local variable [-Wreturn-local-addr] return arr2; ^ > ./a.out size=3 Ошибка сегментирования (сделан дамп памяти) • Размер массива ввели с клавиатуры на этапе запуска (size=3), компилятор не знал про него заранее • Предупреждение на этапе компиляции: функция возвращает адрес локальной переменной (как в прошлый раз) • На этапе запуска это влечёт логичное последствие: ошибка работы с сегментами памяти, программу убивает операционная система (как в прошлый раз)
  12. Замечание • Мы создали массив, при этом в качестве размера

    массива указали переменную size, которая получает значение во время выполнения программы • Обычно компилятор знает заранее, сколько для каждой подпрограммы необходимо выделить места на стеке, и генерирует такой код, который выделяет сразу всё необходимое место перед передачей управления в подпрограмму • Но в этом случае он должен будет сгенерировать код, который будет выделять необходимое количество памяти на стеке на лету — по запросу на создание массива, после того, как станет известен необходимый размер массива внутри переменной
  13. Замечание • Такая технология выделения памяти на стеке под массив

    неизвестного заранее размера называется VLA (variable length array) — массив переменной длины • Она реализована как функция-расширение компилятора, • не является обязательной функцией языка (и в любом случае её можно отключить) • Её не обязаны поддерживать все компиляторы
  14. Замечание • Вы можете легко столкнуться с ситуацией, когда попытка

    создать массив переменной длины, использовав при этом в качестве размера массива переменную, • вызовет несогласие компилятора: он выдаст ошибку, не скомпилирует код, не создаст исполняемый файл • Обычно такое сообщение с непривычки может ввести в ступор • В gcc функция VLA по умолчанию включена (поэтому код выше скомпилируется с gcc просто так) • Можно выключить, запустив gcc с параметром «-Werror=vla»
  15. > gcc -Werror=vla prog-2-return-local-pointer-vla.c prog-2-return-local-pointer-vla.c: In function ‘array_stack_vla’: prog-2-return-local-pointer-vla.c:7:5: error:

    ISO C90 forbids variable length array ‘arr2’ [-Werror=vla] int arr2[size]; ^ prog-2-return-local-pointer-vla.c:13:12: warning: function returns address of local variable [-Wreturn-local-addr] return arr2; ^ cc1: some warnings being treated as errors • Ошибка компиляции: невозможно создать массив переменной длины (variable length array — vla) • Исполняемый файл не создан
  16. Таким образом • Размер массива определяется в процессе выполнения программы

    (при условии, что включена VLA) • Первое из исходных требований выполнено • Но: мы всё равно не можем вернуть его из подпрограммы, т. к. он размещен на стеке, • память на стеке в любом случае освобождается после выхода из подпрограммы
  17. Сегменты памяти • Инструкции — фиксированные адреса • Данные —

    строки-константы, только чтение, фиксированные адреса • Глобальные переменные — фиксированные адреса • Стек (stack) — динамические адреса • Динамическая память (куча — heap) — динамические адреса
  18. • Инструкции — фиксированные адреса • Данные — строки-константы, только

    чтение, фиксированные адреса • Глобальные переменные — фиксированные адреса • Стек (stack) — динамические адреса • Динамическая память (куча — heap) — динамические адреса Сегменты памяти
  19. #include <stdio.h> #include <stdlib.h> int* array_heap() { // https://www.gnu.org/software/libc/manual/html_node/Basic-Allocation.html //

    This function returns a pointer to a newly allocated block size bytes long, // or a null pointer (setting errno) if the block could not be allocated. int size; printf("size="); scanf("%d", &size); int* arr3 = malloc(sizeof(int) * size); arr3[0] = 323; arr3[1] = 343; arr3[2] = 334; return arr3; } int main(void) { int* parr3 = array_heap(); parr3[0] = 914; printf("parr3: %d, %d, %d\n", parr3[0], parr3[1], parr3[2]); // освободить динамическую память free(parr3); // *** Error in `./a.out': double free or corruption (fasttop): 0x0000000001e50010 *** free(parr3); }
  20. > gcc prog-3-return-heap-pointer.c > ./a.out size=3 parr3: 914, 343, 334

    *** Error in `./a.out': double free or corruption (fasttop): 0x000000000097a830 *** • Всё скомпилировалось без предупреждений • Размер массива ввели с клавиатуры на этапе запуска (size=3), компилятор не знал про него заранее • Запустили код, всё прошло хорошо • (до тех пор, пока не попробовали освободить память повторно)
  21. Итого • Подключили библиотеку stdlib.h Внутри нее определены две новые

    функции: • malloc — выделить память • free — освободить память Дополнительно (базовая конструкция языка): • Оператор sizeof: получить размер типа данных или переменной, в байтах
  22. GCC (GNU Compiler Collection) gcc.gnu.org/onlinedocs/gcc/ • Описание коллекции компиляторов GNU

    (в т.ч. компилятор Си) • (описания функции malloc здесь нет) • 6. Extensions to the C Language Family • VLA: 6.20. Arrays of Variable Length gcc.gnu.org/onlinedocs/gcc/Variable-Length.html#Variable- Length
  23. Язык Си (реализация GNU C) • The GNU C Reference

    Manual www.gnu.org/software/gnu-c-manual/gnu-c-manual.html • (описания функции malloc здесь тоже нет) • sizeof: 3.11. The sizeof Operator www.gnu.org/software/gnu-c-manual/gnu-c- manual.html#The-sizeof-Operator
  24. Стандартная библиотека Си • The GNU C Library (GNU libc)

    www.gnu.org/software/libc/manual/html_node/index.html • Динамическая память на куче: 3.2.3. Unconstrained Allocation www.gnu.org/software/libc/manual/html_node/Unconstrained- Allocation.html • malloc: 3.2.3.1. Basic Memory Allocation www.gnu.org/software/libc/manual/html_node/Basic-Allocation.html • free: 3.2.3.3. Freeing Memory Allocated with malloc www.gnu.org/software/libc/manual/html_node/Freeing-after-Malloc.html
  25. Внутреннее устройство malloc sourceware.org/glibc/wiki/MallocInternals • The GNU C library's (glibc's)

    malloc library contains a handful of functions that manage allocated memory in the application's address space. The glibc malloc is derived from ptmalloc (pthreads malloc), which is derived from dlmalloc (Doug Lea malloc). • This malloc is a "heap" style malloc, which means that chunks of various sizes exist within a larger region of memory (a "heap") as opposed to, for example, an implementation that uses bitmaps and arrays, or regions of same-sized blocks, etc. • In ancient times, there was one heap per application, but glibc's malloc allows for multiple heaps, each of which grows within its address space.
  26. • Arena A structure that is shared among one or

    more threads which contains references to one or more heaps, as well as linked lists of chunks within those heaps which are "free". Threads assigned to each arena will allocate memory from that arena's free lists. • Heap A contiguous region of memory that is subdivided into chunks to be allocated. Each heap belongs to exactly one arena. • Chunk A small range of memory that can be allocated (owned by the application), freed (owned by glibc), or combined with adjacent chunks into larger ranges. Note that a chunk is a wrapper around the block of memory that is given to the application. Each chunk exists in one heap and belongs to one arena. • Memory A portion of the application's address space which is typically backed by RAM or swap.
  27. • Память разделена на арены (arena), кучи (heap) и блоки

    (chunk) • Арены содержат кучи, • Кучи делятся на блоки • Указатель на первую арену объявлен как статическая переменная в коде malloc (должна попасть в область глобальных переменных?) • Остальные структуры расположены (судя по всему) в динамической области памяти • И так далее
  28. • Инструкции — фиксированные адреса • Данные — строки-константы, только

    чтение, фиксированные адреса • Глобальные переменные — фиксированные адреса • Стек (stack) — динамические адреса • Динамическая память (куча — heap) — динамические адреса Сегменты памяти
  29. There's a static variable in the malloc code that points

    to this arena, and each arena has a next pointer to link additional arenas.
  30. Нехватка памяти • Стек: переполнение стека («stack overflow»), приложение вылетает

    • Куча: malloc возвращает нулевой указатель, приложение продолжает работать штатно (если не забудете его проверить на NULL, т. е. на 0) • Стек (VLA): переполнение стека
  31. Проблема фрагментации • Выделяем подряд много небольших блоков памяти (покрывают

    значительную часть всей доступной памяти) • Потом в произвольном порядке освобождаем • Теперь хотим выделить относительно большой блок памяти • В общей сумме свободных блоков памяти достаточно, чтобы хватило уместить большой блок • Но т. к. они расположены не подряд (зубья расчески), уместить среди занятых блоков большой блок целиком никак не получится • В этом случае память фрагментирована
  32. • Как решить: (бог его знает) смотрите документацию malloc •

    Можно представить разные алгоритмы • В том числе запуск процедуры дефрагментации в специальном менеджере памяти (сборщик мусора) (это уже не для Си) • Кстати, в Си (с gcc) стандартную реализацию malloc из стандартной библиотеки stdlib можно заменить на свою
  33. #include <stdlib.h> int main(void) { int* parr1 = malloc(sizeof(int) *

    3); int* parr2 = parr1; int* parr3 = parr2; parr1 = 0; parr2 = 0; parr3 = 0; parr1 = malloc(sizeof(int) * 2); free(parr1); }
  34. Утечка памяти • Указатель указывает на выделенную динамическую память •

    Стираем значение указателя (переназначим на новую динамическую область, на локальную переменную или просто в ноль), не освободив предварительно целевую область • Область памяти останется помеченной как используемая до конца работы приложения • У нас в программе к этой области больше не ведет ни один указатель (ни одна «ниточка»), мы не знаем её адрес, у нас больше нет способа её освободить • («ниточки исчезают, а шарик остаётся»)
  35. Сегменты памяти для запущенного приложения • Запустить приложение в режиме

    отладки или зациклить или поставить на ввод (чтобы не завершилось) • Узнать pid (process id — идентификатор процесса) > ps -A | grep a.out 15149 pts/4 00:00:23 a.out • Вывести сегменты памяти для приложения > cat /proc/15149/maps
  36. > ps -A | grep a.out 15149 pts/4 00:00:23 a.out

    > cat /proc/15149/maps 00400000-00401000 r-xp 00000000 103:05 4758650 /home/user/prog-test/a.out 00600000-00601000 r--p 00000000 103:05 4758650 /home/user/prog-test/a.out 00601000-00602000 rw-p 00001000 103:05 4758650 /home/user/prog-test/a.out 01545000-01566000 rw-p 00000000 00:00 0 [heap] 7f285b120000-7f285b2e0000 r-xp 00000000 103:02 2113484 /lib/x86_64-linux-gnu/libc-2.23.so 7f285b2e0000-7f285b4e0000 ---p 001c0000 103:02 2113484 /lib/x86_64-linux-gnu/libc-2.23.so 7f285b4e0000-7f285b4e4000 r--p 001c0000 103:02 2113484 /lib/x86_64-linux-gnu/libc-2.23.so 7f285b4e4000-7f285b4e6000 rw-p 001c4000 103:02 2113484 /lib/x86_64-linux-gnu/libc-2.23.so 7f285b4e6000-7f285b4ea000 rw-p 00000000 00:00 0 7f285b4ea000-7f285b510000 r-xp 00000000 103:02 2113469 /lib/x86_64-linux-gnu/ld-2.23.so 7f285b6ba000-7f285b6bd000 rw-p 00000000 00:00 0 7f285b70f000-7f285b710000 r--p 00025000 103:02 2113469 /lib/x86_64-linux-gnu/ld-2.23.so 7f285b710000-7f285b711000 rw-p 00026000 103:02 2113469 /lib/x86_64-linux-gnu/ld-2.23.so 7f285b711000-7f285b712000 rw-p 00000000 00:00 0 7ffef09dc000-7ffef09fd000 rw-p 00000000 00:00 0 [stack] 7ffef0a21000-7ffef0a24000 r--p 00000000 00:00 0 [vvar] 7ffef0a24000-7ffef0a26000 r-xp 00000000 00:00 0 [vdso] ffffffffff600000-ffffffffff601000 r-xp 00000000 00:00 0 [vsyscall]
  37. #include <stdio.h> #include <stdlib.h> int main(void) { // массив на

    куче char* str_malloc = malloc(sizeof(char) * 18); printf("addr(char* str_malloc): 0x%lx\n", (unsigned long)str_malloc); // зациклить, чтобы приложение повисело while(1) { } }
  38. > gcc prog-heap-mem-addr.c > ./a.out addr(char* str_malloc): 0x1545420 • Обратим

    внимание на адреса • Соотнесем их с диапазонами адресов сегментов памяти в таблице maps • и посмотрим на права доступа к сегментам
  39. VLA vs malloc • VLA удобнее с точки зрения лаконичности

    кода: - массив объявляется естественным интуитивным образом, - память освобождается автоматически (программа выходит из области видимости, внутри которой объявлен массив) • В случае с динамической памятью malloc необходимо самостоятельно освобождать память: - с одной стороны, не допускать утечек, - с другой — следить за тем, чтобы она не была удалена раньше времени или не удалена повторно - Стратегию управления памятью требуется внедрять в логику приложения.
  40. VLA vs malloc • VLA не позволяет обработать ситуацию с

    нехваткой памяти: если памяти на стеке не достаточно, то приложение вылетит с ошибкой переполнения стека («stack overflow») • Для механизма динамического выделения памяти с malloc нехватка памяти — это штатная ситуация: в этом случае malloc вернет нулевой адрес (одна из важных рекомендаций хорошего кода — после выделения динамической памяти всегда его проверять)
  41. #include <stdio.h> int main(void) { unsigned long size; printf("size="); scanf("%lu",

    &size); unsigned char arr[size]; arr[0] = 123; arr[1] = 143; arr[size-1] = 234; printf("arr[%lu]=%d\n", size-1, arr[size-1]); }
  42. > gcc prog-5-vla-long-stack-overflow.c > ./a.out size=500 arr[499]=234 > ./a.out size=999999999

    Ошибка сегментирования (сделан дамп памяти) • На 999999999 (9 девяток) переполнение стека
  43. #include <stdio.h> #include <stdlib.h> int main(void) { unsigned long size;

    printf("size="); scanf("%lu", &size); unsigned char* parr = malloc(size); if(parr) { parr[0] = 123; parr[1] = 143; parr[size-1] = 234; printf("parr[%lu]=%d\n", size-1, parr[size-1]); free(parr); } else { printf("not enough memory for %lu bytes\n", size); } }
  44. > gcc prog-6-heap-long-avoid-overflow.c > ./a.out size=500 parr[499]=234 > ./a.out size=999999999

    parr[999999998]=234 > ./a.out size=999999999999 not enough memory for 999999999999 bytes • На 999999999 (9 девяток) ок • На 12 девяток недостаточно памяти (поймали и обработали ок)
  45. #include <stdio.h> #include <stdlib.h> char* load_file(const char* name, int* size)

    { FILE *fp; fp = fopen(name, "r"); if (!fp) { *size = 0; return 0; } else { // узнать размер файла fseek(fp, 0L, SEEK_END); int fsize = ftell(fp); // вернуться обратно к началу файла fseek(fp, 0L, SEEK_SET); char* file_bytes = malloc(fsize); if (!file_bytes) { fclose(fp); *size = 0; return 0; } else { fgets(file_bytes, fsize, fp); fclose(fp); *size = fsize; return file_bytes; } } }
  46. int main(void) { const char* name = "file2load.txt"; int size;

    char* file_bytes = load_file(name, &size); if(!file_bytes) { printf("failed to loaded file: %s\n", name); } else { printf("loaded file: %s\n", name); printf("size=%d\n", size); printf("contents:\n%s\n", file_bytes); printf("file_bytes[%d]=%d\n", size-1, file_bytes[size-1]); // освободить динамическую память free(file_bytes); } }
  47. > gcc prog-7-load-file-heap.c > ./a.out loaded file: file2load.txt size=203 contents:

    Never imagine yourself not to be otherwise than what it might appear to others that what you were or might have been was not otherwise than what you had been would have appeared to them to be otherwise. file_bytes[202]=0 • Файл загрузили целиком в буфер • В конце буфера-строки — 0
  48. Ввод-вывод: stdlib • printf (stdio.h): 12.12 Formatted Output www.gnu.org/software/libc/manual/html_node/F ormatted-Output.html

    • scanf (stdio.h): 12.14 Formatted Input www.gnu.org/software/libc/manual/html_node/F ormatted-Input.html
  49. Работа со строками: stdlib • string.h: 5 String and Array

    Utilities www.gnu.org/software/libc/manual/html_node/S tring-and-Array-Utilities.html
  50. Работа с файлами: stdlib • fopen, fclose, fseek, fgets (stdio.h):

    12 Input/Output on Streams www.gnu.org/software/libc/manual/html_node/I_002fO-on-Streams.html http://www.gnu.org/software/libc/manual/html_node/Opening- Streams.html http://www.gnu.org/software/libc/manual/html_node/Closing- Streams.html http://www.gnu.org/software/libc/manual/html_node/File- Positioning.html http://www.gnu.org/software/libc/manual/html_node/Line-Input.html
  51. Самостоятельно: VLA и порядок размещения переменных на стеке • Если

    помните, на прошлой лекции (про массивы) мы пытались повредить переменную, обращаясь к массиву по индексу за пределами его размера • У нас не получилось повредить обычные переменные, т. к. компилятор (gcc) размещал массив на стеке ниже, чем обычные переменные • Подумайте, изменится ли ситуация, если при компиляции будет задействована технология VLA • Проверьте, получится ли повредить значение обычной переменной через обращение за пределы массива в таком случае
  52. Задание-1 • Загрузите содержимое текстового файла в массив (предложение в

    несколько слов) • Разбейте текст на отдельные слова • Выведите результат — отдельные слова: одна строка — одно слово • Два варианта: без использования стандартных функций для работы со строками, с использованием стандартных функций для работы со строками
  53. Задание-2 • Зашифровать фразу методом «Жюля Верна» • Исходный алфавит:

    латиница • Количество перемешиваний алфавита: одно • Количество частей разбиения алфавита (n — ключ шифра) задаётся в коде (или ввод с клавиатуры) • Фраза для шифрования задаётся в коде (или ввод с клавиатуры)
  54. Задание-2 (советы) • Динамический 2-д массив: «char** arr» (массив указателей

    на строки) • Сначала выделяется место под сами указатели, потом каждый из указателей настраивается на новую динамическую строку • Целочисленное деление («a / b») с округлением вверх можно сделать следующим образом: int res = (a + b - 1) / b; // a / b • Длина строки: strlen (string.h) • Склеить строки: strncat (string.h) • Найти символ внутри строки: strchr (string.h) (вернет указатель на символ) • Вычислить порядковый индекс найденного символа (из адреса символа вычесть адрес строки): int char_index = char_addr - str;
  55. Задание-3 (бонус) • Расшифровать текст, зашифрованный методом «Жюля Верна» •

    Исходный алфавит — латиница • Количество перемешиваний алфавита — одно • Код шифра (количество частей разбиения алфавита) задаётся в коде или с клавиатуры • Зашифрованная фраза для расшифровки задаётся в коде или с клавиатуры