В предыдущем видео мы узнали про, так называемое, отладочное макроопределение, дебажные дефайны, которые на этапе препроцессинга включали специальные проверки в стандартные библиотеки, которые не давали вам выйти за границы массива, передать в алгоритм сеть диапазонов итераторов и так далее, и так далее. Но здесь это можно считать некоторым везением. Разработчики стандартной библиотеки о вас позаботились и написали специальный код, который можно включить препроцессором и который найдет вам ошибки в процессе выполнения программы. Но что делать, если вы сами пишете свою библиотеку? Или, если вы непосредственно работаете с памятью, вызываете "New", вызываете "Delete" и все еще нуждаетесь в помощи. Например, вы забываете очистить память и у вас происходит утечка, или вы дважды удаляете одну и ту же память, или вы используете инвалидированные ссылки. Кто вам поможет, если это код не привязаный к стандартной библиотеке? Вам понадобятся санитайзеры. Санитайзеры — это некоторый способ собрать программу еще на уровень ниже, который позволяет найти, обнаружить использование чужой памяти и использование освобожденной памяти, обнаружить повторное освобождение памяти, найти утечки памяти и другие подобные проблемы. Давайте, рассмотрим простой пример, но рассмотрим мы его на Linux-машине, потому что под Linux-ом с санитайзерами все в порядке, а под Windows они реализованы не до конца. Про Windows будет отдельная статья, а показывать я буду все на Linux-машине. Итак, я беру консоль и покажу вам простой пример. В этом простом примере я напишу функцию, в которой я верну ссылку на строчку, которая будет невалидной. Функция "MakeString", здесь внутри, допустим, у меня будет вектор строк, состоящий из двух строк в С++ и Python. И верну я нулевой элемент этого вектора. А функция "Main" я напишу const auto ссылка ref равно "MakeString" и попробую вывести содержимое этой ссылки. Давайте, я скомпилирую эту программу с помощью компилятора Clang ++ 6.0. main.cpp минус о main. Компилирую, все компилируется. Я ее запускаю и получаю Segmentation fault. Мне никто, как в предыдущем видео, не сказал, где у меня проблема. И это меня огорчает и поэтому я включаю санитайзер, Address санитайзер с помощью флага минус f sanitize равно adrress. Я компилирую с этим флагом. Опять же, у меня программа теперь будет работать медленнее, но она будет обо мне больше заботиться, я ее запускаю и вижу, что что-то пошло не так. Adrress санитайзер ругается на использование неизвестного адреса 0х060600с000001. Это мне ни о чем не говорит, как и то, где конкретно произошла проблема. Тут опять же какие-то адреса и что-то не так, а что непонятно, тут даже есть ссылка на мой файл, но непонятно, где конкретно в нем проблема, а хотелось бы увидеть конкретную строчку, в которой я что-то сделал не так. И здесь не поможет символайзер — программа, которая конвертирует адреса инструкции моей программы в конкретные строчки кода. Итак, что я должен сделать, чтобы сообщение об ошибке от санитайзера было более понятным? Во-первых, я должен компилировать со специальными флажками, а именно с флажком минус g, с флажком минус fno-omit-frame-pointer. Это уже флажки, которые скорее в некоторых ситуациях покажут вам более понятный вывод санитайзера. Fno-omit-frame-pointer с одним словом frame и минус fno-optimize-sibling-calls. Вот так. Компилирую. Пробую запустить, у меня все еще какие-то адреса, мне еще нужно указать специальную переменную окружения с путем к символайзеру. Как этот путь найти? Я обращаюсь к файлам пакета llvm 6.0 с утилитами llvm и грепаю там, ищу строчку symbolize. Вижу, что у меня есть путь в usr/lib/llvm-6.0/bin/llvm/symbolizer и именно его я указываю в переменной ASAN.SYMBOLIZER.PATH при запуске программы, не при компиляции, при запуске. Вот этот путь, я его указал и с этой переменной запускаю программу. И теперь я вижу ту же ошибку, но наряду с адресами инструкций у меня есть и пояснение на тему того, какие это функции, в частности я вижу, что в main.сpp, в строчке 17 у меня вызвало что-то про поток и дальше все пошло не так. Что у меня в main.cpp в строчке 17? У меня здесь обращение к этой самой ссылке. Значит нужно обратить внимание на эту конкретную ссылку. Что не так со ссылкой? Да то, что то, на что она ссылалась освободилось при выходе из функции "MakeString". И вот мне санитайзер на конкретную строчку вот здесь указал. Давайте попробуем с помощью санитайзера найти проблему в коде большего объема. Опять же, это будет задача "Демографические показатели" с глубоко запрятанной ошибкой. Давайте, я посмотрю на код, увижу, что он примерно тот же. Вот тут есть пол, есть человек, есть какие-то операторы, есть статистика и так далее, и так далее, и так далее. Давайте, я скомпелирую той же строчкой компиляции. Все компилируется. Теперь я запускаю программу, опять же, с переменной про Symbolizer Path. Я должен ввести информацию про людей, я, опять же, поленюсь и введу одного человека с параметрами 0 0 0 и у меня что-то упало. Давайте, почитаем текст ошибки. Adrress санитайзер говорит, что мы обратились по неизвестному адресу 0 0 0 0 0 0 0 0 0. Говоря человеческим языком, мы обратились по нулевому указателю. Так делать нельзя. Тут даже написано что "Аdrress points to the zero page", то есть это нулевая страница. Там, конечно, ничего искать не нужно. И это случилось в функции "ComputeMedianAge", в файле main.cpp, в строчке 78. Давайте, я посмотрю строчку 78, middle стрелочка age, то middle, итератор middle оказался нулевым указателем. И тут сходу не очень понятно, что происходит и в чем конкретно проблема, поэтому пойдем в отладчик, классический отладчик gdb. Gdb.main запускаем, вводим 1 0 0 0 и сразу видно, где у меня случился Segmentation fault — в return middle age. Переключаемся в код — и на что мы здесь можем посмотреть? Мы можем посмотреть на middle, наверное, и он говорит, что "Cannot acsess memory at adrress 0x0". Ну, да. Middle нулевой указатель. Почему? Откуда взялся middle? Middle — это begin от range_copy плюс range_copy.size пополам. Давайте посмотрим на range_copy и увидим, что range_copy это пустой вектор, вектор длины ноль и капесити ноль. Понятно, что обращаться по итератору, который получен с пустого вектора, бесполезно, ну и он оказался нулевым указателем. Проблема, собственно, вот в этом закомментированном коде, который проверял пустой ли диапазон. Если его раскомментировать, все будет хорошо. Давайте для порядка его раскомментируем. Раскомментировали, нужно перекомпилировать программу, давайте, под санитайзером, чтобы убедиться, что у нас правда всё-всё-всё хорошо. Запускаем программу, вводим 1 0 0 0 и всё в порядке, все медианы и возраста ноль, что довольно логично. Отлично. Давайте обсудим ещё такой интересный вопрос, который возникает у опытных пользователей С++. Почему нельзя было просто, когда у меня программа упала, пойти в отладчик и там точно также увидеть строчку, к которой мы обратились по нулевому указателю? Дело в том, что, во-первых, санитайзер позволяет искать утечки памяти, а отладчик вам такого не найдет, потому что он к этому не приспособлен. Это первое. А второе, санитайзер разработан таким образом, что, конечно же, он замедляет программу из-за дополнительных проверок в недрах компилятора, но замедляет он ее не так сильно, как отладчик, то есть, если вы даже зайдете в документацию по санитайзеру, то там будет написано, что ваша программа замедлится раза в два, но если вы запустите под отладчиком вашу программу, она будет работать еще дольше, потому что ответчик отслеживает вообще все-все-все инструкции. Ну и, наконец, программа собраная под санитайзером, это та же самая программа и запускаемая точно таким же образом, что довольно удобно встраивается в любую инфраструктуру запуска каких-либо бинарников. Запустить же программу под отладчиком это все-таки некоторое приключение. Давайте, покажу что санитайзер умеет ловить утечки. Я вернусь в консоль, вернусь в мой проект "Example", открою там main.cpp и потеряю там какую-нибудь память. Это довольно легко сделать в С++. Int*P равно new_Int, ну и давайте попробуем больше ничего не делать, прямо так скомпилировать, запустить. Мне Leak санитайзер говорит: "Detected memory leaks", память утекла. Давайте, я запущу с символайзером и увижу, что утекли четыре байта из одного объекта, ну там Int был, логично, которые выделились в операторе New, которые вызвали в восьмой строчке файла main.cpp. Правда, вот в восьмой строчке я правда вызвал New и Delete для него не вызвал. Санитайзер нашел утечку и сказал, где конкретно была выделена эта память. Итак, подведем итоги. Мы изучили санитайзеры и дебажные дефайны. Это инструменты, которые позволяют легко скомпилировать вашу программу в режиме усиленного поиска ошибок и недоверия разработчику. Программа из-за этого замедляется, но зато в процессе выполнения программы, не при компиляции, находятся всевозможные ошибки. При этом, все-таки не гарантируется, что эти инструменты найдут все возможные ошибки, в некоторых экспериментальных режимах работы санитайзеры, еще написано, что случаются ложно-положительные срабатывания и об этом нужно куда-то писать. Хотя на практике, конечно, это случается редко благодаря тому, что санитайзерами пользуются многие и многие разработчики. К этому блоку вам будут даны некоторые задачи, во многом вам известные, в которых вам нужно будет найти ошибки с помощью санитайзеров и отладочных макроопределений. Более того, во всем этом курсе, во многих задачах в тестирующей системе на некоторых тестах ваша программа будет запускаться с включенными санитайзерами и, возможно, отладочными макроопределениями. И поэтому вам нужно будет усиленно следить за утечками памяти и прочими нехорошими вещами и у себя также проверять ваши программы с использованием тех самых инструментов, которые мы рассмотрели в этих видео.