Теперь два микрофона, поэтому без теста микрофона ни пуха, ни пирак.
Что же сразу без теста-то, ладно.
Сильно более солнечный, чем Москва с Кулиягродом.
Меня зовут Константин, прямо сейчас я работаю в браузере, совмещая с товарной вертикалью, так вот получается.
И расскажу я про такую довольно-таки олдскульную вещь, у меня 15 лет работу, я должен что-то олдскульное принести в эту модную конференцию.
А именно устройство для отладки у нас гнуди-баггер, он же к ГДБ.
Итак, маленький, короткий план о том, что я расскажу, небольшое введение, почему иногда нужно использовать ГДБ.
Дальше три просто чумовых истории про то, как можно отладить с помощью него просто шикарные ошибки.
И небольшое какое-нибудь заключение чисто по фану.
Вообще, про отладку и ошибки рассказывают на конференциях как-то слабо, мало, потому что, ну, как-то ты выходишь такой, мы сделали багу, мы ее исправляли, ну, как-то выглядит непрезентабельно.
Тем не менее, разработчики занимаются отладкой приблизительно 18% своего времени, как известно, поэтому...
желательно провести время с пользой и с фаном.
Итак, как мы будем проводить время с пользой?
Вообще, атлаточных инструментов — чудовищное количество, и они разведены по самым-самым разным областям.
То есть у нас имеется атлаточный инструмент для операционных систем, компиляторов, приложений, даже админы пилят свои атлаточные тлзы, чтобы понимать, как у них сервис работает, вообще, он живой или он уже умер.
И, естественно, у них у всех какое-то просто чудовищное пересечение по функционалу.
Вы можете сделать это в одном инструменте.
Вы можете сделать это в другом, третьем, пятом, десятом.
Вы всегда можете выбрать самые новомодные, самые клёвые, самые популярные, самые любимые.
Но иногда возникают такие забавные ситуации, когда...
Это можно сделать чуть ли не одним инструментом или желательно использоваться один конкретный инструмент.
И вот в таких ситуациях иногда оказывается этим инструментом ГДБ.
Собственно, посмотрим это на историях.
Первая история случилась очень давно, но она моя самая любимая, потому что я тогда был джуном, солнце было ярким, трава зеленый, а баги были легко объясняемыми на конференции.
И, собственно, наша сцена преступления начинается с того, что имеется какое-то бинарное дерево поиска.
Каждый разработчик когда-нибудь пытался написать какой-нибудь контейнер, и в продовом коде никогда не нужно это сделать, потому что у вас есть стандартные контейнеры, и не занимайтесь ерундой.
Но если вы хотите что-то специфическое от этого контейнера, а в данном случае там требовалось какая-то копия апдейт, синхронизация, VL-балансировка, бла-бла-бла и прочее, вот это вот все мы опустим детали, просто какое-то дерево.
Что, собственно, из себя представляет бинарные дерева, как, например, STD-мап, внутри себя устроен.
Это у нас какие-то ноды дерева бинарного.
И главное правило — это то, что ноды, которые меньше ключа в текущей ноде, они относятся к левому по дереву, которые больше в правом по дереве.
И когда мы пытаемся искать ноды, мы с этим правилом легко быстро находим нужную ноду или, наоборот, там добавляем, удаляем, всё, что хотим делаем.
Если мы будем добавлять их в отсортированном порядке, то тогда у нас вот этот дерево красивее будет вырождаться в кишку.
И чтобы у нас сложность от логорифма не превращалась в линейную, нужно делать операции балансировки дерева.
А операция балансировки дерева достаточно простой.
Они заменяют вот левую картинку на правую.
Вот если мы заменяем левую картинку на правую, то это называется правым вращением.
Если мы заменяем правую картинку на левую, то это у нас называется левым вращением.
И вот здесь вот я конкретно рисовал операцию правого вращения.
Она, по сути своей, вот я выделил красным-зеленым синим указатель, вот эти указатели, они у нас переставляются фактически по циклу.
Из-за счета того, что они переставляются по циклу, у нас происходит равно то, что нам нужно.
можно было бы, если бы это были бы обычные указатели, просто завести какую-то временную перемену и через четыре присваивания всё сделать.
Но у нас это смарт-пойнтеры какие-то, там это какое-то сложное дерево, и в случае смарт-пойнтеров лучше всё это делать через операции своп, потому что они не трогают рифкаунтеров, которые есть у смарт-пойнтеров.
И вообще не делать никаких лишних проверок.
В этом плане сделать через два свопа проще.
И так приблизить через два свопа и было всё написано.
Если у нас имеется какое-то место преступления, у нас должен быть какой-нибудь свидетель, который зафиксирует, что у нас что-то плохое происходит в данном случае.
Был написан шикарный, рандомизированный тест.
Там, для того, чтобы тест был детерминирован, использовался вихрь мерсэна с фиксированным седом.
В общем, красивый, замечательный тест.
Случайные ноды вставляют, случайные ноды удаляют, случайные ноды ищут.
Мы же не просто так код пишем, мы пишем код, чтобы он работал.
Все замечательно, он работает на ноутбуке.
А на тестовом кластере он падает.
И тест сообщает, что вы потеряли ноды.
Нам нужны какие-то улики, нам нужно понять, найти, кто же преступник.
Очевидно, что машинки у нас не из палаты мерафельсов, на них что-то отличается.
В данном случае отличался компилятор.
Как я говорю, история очень старая, поэтому здесь был JCC 4.1.1 и 4.2.
И вместе с ними еще было замечено, что все ошибки в тесте начинают происходить после первой операции вращения.
То есть что-то с ней не то, весь остальной код более-менее работает, а вот что-то с этим вращением неправильно.
Как с помощью ГДБ отладить?
Берём, запускаем ГДБ, указываем его через аргументы нашего бинарника, и он выводит нам какое-то стандартное приветствие со своей лицензией, как он читал наш бинарник, какие у него при этом проблемы или не возникли, и всё замечательно прочитал.
И он, главное, выводит свое приветствие.
Вот этот красивый ГДБ в круглоскобочках.
Я его здесь специально на слайдах выделил.
В норме вам придется его искать длинным-длинным-длинным выводе ГДБ, потому что он очень-очень вербозный, к сожалению.
Что нам нужно сделать, чтобы понять, что с этой функцией не так?
Ну, наверное, нам нужно на не остановиться для того, чтобы на не остановиться.
Мы делаем брикпоинт, мы говорим брикпоинт на какой-то функции.
Мы можем указать на mspace, классы, надклассы, шаблонные параметры, много чего навернуть.
И в данном случае он нам говорит, да, да, брикпоинт на одной функции в двух местах.
Потому что функция у нас вообще, говоря, inline-ница и иногда компилятором.
И в результате один и тот же код формально написанный в одной функции может оказаться сразу в двух местах assembler.
Поэтому он делает сразу два breakpoint.
Хорошо, запускаем программу.
Так я ему сразу заранее все через аргументы перекинул.
какой процесс с какими аргументами я хотел отлаживать, то он это все отлично прямо сразу запускает, выводит какие-то свои ворринги, сама программа что-то выводит, все это опустим.
Главное, что он остановился в каком-то там треде на нашей замечательной функции writeRotate.
У него есть какие-то аргументы, мы их тоже можем увидеть, мы можем увидеть строчку, на которой он остановился, даже номер этой строчки, файл, в котором все произошло, шикарно.
Мы можем с помощью команды List посмотреть какую-то окрестность этого плюсового кода, посмотреть, что же там вообще рядом происходит и распечатать какие-нибудь перемены.
Можно указательно Mi распечатать, указательно левый, направый или сразу вообще весь зыз.
Я здесь удалил все лишние перемены, поэтому здесь только Left и Right, а так, конечно же, было полезная нагрузка с нодами и всем остальным.
Для нашей истории это не очень интересно.
Для того, чтобы работать с ГДБ, желательно хорошо понимать Assembler, поэтому максимально ультракраткое видение, как вообще выглядит Assembler.
Сейчас про Assembler очень редко, кто рассказывает, редко где его учить.
Считается, что это прям безумно старая, ненужная информация, но тем не менее.
Для примера разъемем максимально простую тупую функцию, которая просто возводит число в квадрат.
Просто целое число просто возводит в квадрат.
Если мы ее скомпилируем в Debug режиме, то там будет вот приблизительно вот такой вот листинг.
У всех ассамблеров синтаксис разный, на разных архитектурах разный, здесь конкретно взят x8664, но в принципе у всех архитектур, у армы, мипса, практически у всех есть некоторый общий паттерн, у них имеется работа со стеком через push pop, вы хотите положить что-то на стек, достать что-то со стека,
муфлооооооооооооооооооооооооооооооооооооооооооооооооооооооооо
А также всякая арифметика, это субмулдив.
В общем, там все понятно по названию.
Плюс к этому у них обычно бывают суфиксы-прификсы, которые обозначают какие-нибудь данные про то, какие у нас, какого размера аргументы, какие-то дополнительные трюки, которые мы хотим сделать, или, может быть, флажочки, при которых мы хотим это сделать.
И в данном случае наша функция, по сути своей, делает следующий.
Она запоминает состояние регистр RBP, который указывает на фрейм функции на стеке.
Она туда сохраняет текущий стек-пойнтер.
Дальше она на стек кладет значение регистры EDI, в котором приходит у нас первый аргумент по
И видно, что она вычитает из текущего стекпонтера, это потому что в x86-ом ассэмблере стек растет сверху вниз.
То есть он от больших адресов идет к меньшим адресам.
И вот мы вычитаем 4 байта, потому что, видимо, это у нас 32-битное число.
Вот мы можем посмотреть в ассэмблер, мы сразу понимаем, что вот этот вот компилятором проинтерпретировался как 32-битный.
Дальше мы его тут же достаём, чтобы положить в регистре ix.
ix используется обычно для возврата из функции.
И его умножаем на самого себя, кладём результат в него же и всё, восстанавливаем значение rbp и возвращаемся.
Если мы посмотрим на тот же листинг в релизе, ничего этого нету.
Компилятор умный понимает, зачем нам запоминать это, потому что мы его не трогаем.
Зачем, по большому счету, он нам не нужен, если мы все посчитаем на регистрах.
Просто умножаем регистр сам на себя и возвращаем через EX.
Если вы будете смотреть в релизе в свой бедар, то вы скорее всего саму функцию-то не найдете, потому что компилятор, посмотрев на то, что это какие-то триосемблярных...
Операция, он, естественно, вставит ее вместо вызова кол на эту функцию, он просто возьмет и сразу сделает а ему на соответствующих регистрах.
И вообще не будет вашей функции.
При этом она, опять же, может продублироваться, поэтому если вы попытаетесь на нее брикпоинт, то у вас будет очень-очень много разных мест.
Из этого, кстати, можно сделать вывод, что если у вас функция коротенькая типа какого-нибудь сетера-геттера, ее лучше класть в хидр, чтобы она онлайнерлась и у вас перфоманс был соответствующий быстрый, а если функция длинная и жирная, то ее желательно класть в cpp-шник, чтобы она не компилировалась несколько раз для разных единиц трансляции.
Хорошо, мы теперь понимаем более-менее, что такое сэндер.
Давайте попробуем посмотреть, как наша функция устроена внутри.
Для того, чтобы работать со сэндером, желательно смотреть на человека, читаемые имена.
Они вот те самые манглинные, которые сохраняются где-то там внутри файлов объектных.
и который не содержит этих бет символов треугольных скобочек, пробел в круглоскобочек, как мы любим с описаниями функций.
Они все это превращают в неудобоваримую кашу, которая очень похожа на B64, но не является.
Им она просто является какой-то комбинацией всех этих всего этого текста.
Поэтому мы включаем демангл на наш ассамблер.
И дальше мы можем продезиссемблировать текущую функцию с помощью функции Disass.
Мы можем продезиссемблировать с ставкой в промежутке сеплясового кода, из которого это все получилось, тогда мы сможем более-менее понимать
что к чему в этом assembler коде, потому что мы будем видеть, что ага, вот это перемещение переменах превратилось в эту move, а вот здесь я что-то вычисляю, это превратилось вот это, а вот здесь я вставил if, и здесь у нас test и jmp.
Если у нас интересует не та функция, в которой мы находимся, какая-то другая, мы можем просто сделать дизас и имя соответствующей функции вместе с name space и всем всем всем, чтобы ее можно было найти.
Но есть еще одна очень чумовая возможность, а именно мы можем посмотреть прямо от текущей позиции, вот где у нас находится выполнение.
у процессора имеется специальный отдельный регистр PC, программ-каунтер, который показывает, какую инструкцию он прямо сейчас выполняет, и мы можем сказать, окей, вот прямо вот отсюда, и еще приблизительно 32 байта вперед.
Вот, давай вот это все продезинсимблируем, и он выбитит нам соответствующий кусочек функций.
Это удобно в случае, если функция заинланилась в какую-то большого длинного монстра на несколько тысяч строк, и у нас не хватает сил просмотреть его глазками, тогда мы можем какую-нибудь конкретную окрестность этой функции смотреть вот того места, в котором мы сейчас выполняемся.
вот приблизительно как выглядит функция в Assembler-е с компилированным каким-то современным клангом.
Естественно, надо из-за Inline с внутрь какой-то другой большой функции, поэтому я здесь вырезал кусок с помощью DesignPC и PCPlus 0x20.
Дальше, если у нас имеется какой-то Assembler, мы, наверное, хотим как-то посмотреть, как же он выполняется.
Мы можем выполнять конкретные шаги именно самого Assembler-а, то есть конкретно Assembler на инструкцию.
команда SI – Single Instruction.
Но есть также у ГДВ две других замечательных команды, а именно Step и Next, которые позволяют выполнить одну строчку плюсового кода.
Они обе пытаются выполнить строчку плюсового кода, но по-разному реагируют на вызов функций.
Next перешагивает через вызовы функций, пытаясь полностью выполнить весь вызов функции, а Step проваливается вызовы функций, и вы можете посмотреть, что же происходит внутри них.
Мы же можем с помощью всяких принтов и прочих понять, какие же у нас теперь переменные и что же происходит внутри этого райт-ротейта.
А происходит в нём вот такая вот чудовищная штука.
Но да бы почему-то отвалилась от всего дерева, сначала сама на себя ссылаться, а её правая под дерево стала её левым под деревом.
В общем, полная ерунда какая-то.
При этом первый своп, такое ощущение, что ты работал правильно, потому что нода А стала теперь корнем, на нее теперь ссылается ми, все замечательно.
Но такое ощущение, что что-то неправильно со вторым свопом.
И если немножечко посмотреть на то, как регистры в друг дружку перемещаются, видно, что компилятор почему-то вычислил ми-стрелочка write раньше, чем мы присвоили в ми-новое значение.
То есть у него где-то какая-то оптимизация сработала некорректно.
И мы видим то, что мы видим.
Получается ошибка в компиляторе, а как мы это исправим?
Мы же программисты на плюсах, мы не программисты этого компилятора.
Ну, достаточно простая, на самом деле, ошибка и простое исправление.
Мы можем взять и в DEF на соответствующую версию компилятора и сказать, что компилятор вот здесь вот стоит страшный ужасный барьер, через него инструкция не перетаскивай, ты можешь запутаться.
Вот это по-разному реализовывается для разных компиляторов, я не буду здесь этот макрос раскрывать, я просто скажу, что вот такая вот возможность есть.
И после этого всё начинает работать.
На старых компиляторах, у которых есть ошибка работает барьера, на новых компиляторах уже этой ошибки нет, всё замечательно.
Вообще, ошибка компилятора чрезвычайно редкая штука.
Вы с ней можете не столкнуться за десятки лет работы программистам на C++.
Но если она у вас попадается, в этот момент вы готовы рвать у себя последние оставшиеся волосы.
И если вы попытаетесь это всё отлаживать с помощью каких-нибудь принтов или логирования, у вас ничего не выйдет.
Потому что в тот момент, когда вы в writer.tape составляете принты или логирования, компилятор такой, ага.
пытаешься распечатать промежуточные перемены, я не буду делать мою злобную оптимизацию.
И вы ничего не видите, у вас всё отлично работает.
Вы убираете ваш прин... Вы у вас всё сломалось.
Вы вставляете, всё работает.
Вы убираете, всё сломалось.
Вот такой вот замечательный гизнбак.
Вот, АГДБ это позволяет найти.
Он позволяет найти, причём как бы не очень сложными операциями, вам придётся, конечно, прочитать assembler-код, но это не очень страшная штука на самом деле.
Более того, сами компиляторы тоже иногда падают в корку.
В моей практике было, когда я смог уронить GCC, я смог уронить кланк.
И если бы я с другими работал, я думаю, их бы уронил.
Иногда бывает даже более злобная штука, что компилятор оптимизировал ваш код и сделал его хуже.
То есть бывает так, что вот мне коллеги рассказывали, что одна из ранних версий кланга, когда ей достался в каком-то тест-кейсе цикл, она, когда этот цикл попыталась оптимизировать, у него получился вложенный цикл.
И в результате вместо линейной сложности тест стал выполняться квадратичной сложности и не укладываться в таймаут после оптимизации в релизе.
В дебаге выполнялся нормально, а в релизе сломался.
Вот, и казалось бы, в общем, редкие баги, но как бы бактрекер и уже ЦЦУ-кланга никогда не пуст, поэтому надо быть во все оружии.
И ГДБ — очень полезный инструмент.
Это у нас была первая чумовая история.
Или почему ГДБ позволяет вам увидеть правду, которую не позволяет увидеть пюсовый код?
Наши иностранные коллеги написали два микросервиса.
Прекрасных замечательных микросервис и отлично все замечательно работают.
Потом коллеги в Москве решили два этих микросервиса объединить в одну программу.
Ну, в один бинарник для того, чтобы там выполнять какие-то бизнес-цели.
И сразу после этого программа жвакается на том же коде, который до этого отлично работал в этих отдельных микросервисах вместе взятых.
Классическая корка, которую большинство разработчиков увидит, если попытаются запустить ГДБ, выглядит бы следующим образом.
Мы говорим ГДБ, бинарник, корка, он там выводит свое приветствие.
И после этого мы видим, и вы обратились по некорректному адресу.
Поэтому у вас SegmentationFault в такой-то функции, в такой-то строчке кода.
Вы видите, что у вас имеется какое-то использование указателя.
Вы сразу думаете, а кто-то забыл этот указатель проиненциализировать.
Вы смотрите backtrace на всякий пожарный.
Вы распечатываете этот указатель.
Вы понимаете, что он действительно нулевой.
Отлично, сразу понятно, кто-то ошибся в коде.
А кот выглядит очень неожиданно.
прямо двумя строчками выше мы присваиваем нинолевое значение.
И более того, с помощью принтов лагирования мы можем вывести сразу после этой строчки импл нинолевой.
А перед вот этой вот последней строчкой он уже нолевой, а в промежутке между ними только одна функция, которая инициализирует вообще совершенно другую структуру, она инициализирует метрики.
Наверное, в этой функции какая-то бага.
Давайте посмотрим, когда функция устроена.
Пробежались по массиву метрики и всем присвоили ноль.
Вообще, говоря, нормальный, корректный код.
Там массив, граница массива неправильно указана.
Здесь вот граница массива, ну, метрикс.
А там, собственно, массив как раз размера, ну, метрикс.
Это какой-то там макрос, который где-то определен, и в общем, у него имеется какое-то значение, все замечательно.
Мы не должны были никак вообще заехать на этот импл.
Да, в соседнем область памяти.
Но импл, то мы трогать-то не должны.
Но это очень подозрительный сосед.
Очень подозрительный сосед.
Давайте посмотрим на него в ГДБ.
Теперь уже на живом процессе.
Останавливаемся на функции initmetrics.
Да, вот эта функция initmetrics запускается в нашем методе init.
И мы можем посмотреть, собственно, куда происходит присвоение.
Мы можем взять и сказать, хорошо.
А вот куда последняя метрика кладется?
Для этого берем массив метрик, берем последний ее индекс последнего элемента, то есть, ну, метрик с минус один.
Дальше возьмем его адресом персантиком, и скоснуем к чарзвелочке, чтобы просто и удобнее было сравнивать адреса в памяти, не заботясь о том, какие-то типы.
Аналогичную штуку сделаем с вычитаем и получаем, что эта метрика от начала объекта находится где-то на 184-м байте.
А теперь посмотрим, а где же находится сам импл.
Мы можем сделать это находясь в другом фрейме.
Для этого мы должны передвинуться в фрейм функции выше.
Мы можем сделать это с помощью номера фрейма через команду фрейм и номер фрейма.
Либо просто Up, Down, перемещаясь по стеклу нашего трэда.
И что мы видим, у импла то же самое смещение 184.
В этот момент можно было бы усомниться в том, что ваши глаза вообще видят, но потому что и там 184, и здесь 184, а в коде у вас написано, что это лежит после.
Вообще, а какой размер этот объект?
Ну, зададим простой вопрос.
А какой размер у этого объекта?
Он 192, рапортует нам функцию выше по стеку.
Спрашиваем, функции внизу, а у тебя какой размер этого объекта?
А у меня 200, говорит функция снизу.
Вы думаете, что здесь происходит, что это?
Ну, в принципе, понятно было бы, что если это все попало в одну единицу трансляцию компилятора, то он бы, наверное, проверил, что у нас все варианты хорошие, что все правильно, что все замечательно, и нигде ничего не сломалось.
Наверное, это просто разные единицы трансляции.
И если посмотреть на backtrace, который был на предыдущем слайде, видно, что эти две функции пришли из разных cpp-файлов.
То есть кто-то у нас перевоёживался с компиляцией.
Как же можно перевоёживаться с компиляцией?
Вот у нас имеется одна библиотека пришедшая из одного микросервиса и другая библиотека пришедшая из другого микросервиса.
Они обе используют общую библиотеку «Метрик».
А люди были очень экзотические и любили развлекаться C++.
Они сделали локальный инклют на файл, которого нет в этой библиотеке.
В случае, если локально этого файла нет в этой библиотеке, то включается тот инклют, который есть в библиотеке, которая заинклюдила эту.
Метрик-конфик H для одного сервиса лежит в одной библиотеке, метрик-конфик H для другого сервиса лежит в другой библиотеке.
И когда мы компилируем через один путь, мы получаем список из четырех метрик.
Когда мы компилируем через другой путь, мы получаем список из пяти метрик.
И у нас получается, что размер объекта разный в зависимости от того, какой из унитов трансляции вы компилируете.
Вы обманули компилятор, вы получили проблемы.
Мисс, не обманывайте компилятор, и жизнь вам будет проще.
В одном конкретном юнизе-трансляции компилятор всё проверяет, но между разными он может иногда не проверять, и это линкер тоже не проверяет, в результате вы можете напороться вот такую глупую ошибку.
Не используйте злобный лойку с инклюдами, и жизнь ваша будет проще и приятнее.
И если вы глядите на плюсовый код, вы не можете понять, где ошибку, а ГДБ это находит просто шелком пальцев.
Третья история или корка после форка.
Как скомбинировать несколько методов отладки и диагностики в один?
ГДБ тут будет не самый решающий ролик играть, но очень хорошую вспомогательную роль.
Итак, сцена преступления.
Две команды шарили общий код.
Ну, в общем, нормальная ситуация, история.
И у них разошлось понимание о том, что же с этим кодом делать.
Одна команда говорит, давайте мы этот код сделаем проще, тоньше, чтобы был хороший перформанс, а другая, нужно нагрузить больше логики, чтобы было все очень
Ну, в общем, они разошлись с пониманием, и маленькая команда сказала, всё, мы форкаем свою часть и перепишем её по-правильному.
Они даже ещё переконфигуировали её под свои потребности, чтобы всё было замечательно у них, так как они давно хотели, но им мешали.
Вот то, что у них до этого всё отлично, замечательно работало, после форка у них всё отлично падает.
Хорошо, если у нас имеется сцена преступления, давайте попытаемся собрать какие-нибудь улики.
Какая у нас первая улика?
Если мы перекомпилируем бинарник с другими опциями, как в другой момент времени, с минорными изменениями, то вероятность ошибки меняется.
Возможно, мы где-то надругались над компилятором.
Улика вторая, если мы посылаем тестовых запросов больше количества, то вероятность увеличивается, причем в параллеле, если посылаем.
То есть это похоже на гонку тредов.
Большинство корок где-то в потрахах аллокатор, а аллокаторы на что-то жалуются, что какая-то база у него побита.
Возможно, мы как-то некорректно используем аллокатор.
Хорошо, давайте попробуем посмотреть на всех подозреваемых по очереди.
И так как у нас корка после форка, то первый подозреваемый, естественно, что форк где-то нам что-то нагадил, и мы каким-то образом из-за этого обманули компилятор.
Что может сделать форк плохого?
Форк может породить полный дубль класса.
Если он порождает полный дубль класса, то, если компилятор, как мы видели, видит это в разных единицах трансляции, он не всегда проверяет корректность.
И у нас линкер потом из этого шевает какой-то трифкин, в общем, этот трифкин кафтан.
И, в общем, получается какая-то ерунда, которая не работает.
Там может возникнуть дубликаты функции, дубликаты классов, много чего разного.
Для того, чтобы это все отладить, перехватываем вызов линкера, узнаем у него какие библиотеки ему приходят на вход, узнаем, какие из этих библиотек пришли из левой части форка, которые пришли из правой части форка.
Понятно, что там кто-нибудь с зависимостью невозможно косячил, поэтому у нас, возможно, из обоих частей форка пришли какие-то библиотеки, и форк был сделан, возможно, не совсем корректно.
Для каждой из библиотек вызываем NameMangling команду, которая позволяет посмотреть, какие символы определены в этих объектных файлах.
Мы даем дополнительный ключик, чтобы она у нас именно деманглила.
Она нам показывает все соцветующие символы.
Причем меня интересует только секция текста, где хранятся функции.
И я отрезаю оттуда все адреса и прочую информацию.
Меня интересует только само название функции со всеми шаблонными параметрами и прочим.
Сортируем, уникализируем, смотрим пересечения, находим несколько сот функций пересечения.
Это на самом деле не всегда означает, что это проблема, потому что у вас может быть общий хитр, который попался и в левую библиотеку, и в правую библиотеку, и там и там скампелировался, и поэтому вы имеете дубль.
А иногда это может быть реальный дубль, когда у вас функция определена сразу в нескольких местах, причем по-разному.
Хорошо, как исправить это?
Очень просто, заносим функции с одной стороны в namespace или с другой стороны в namespace или с обоих сторон, но заносим по-разному именованной namespace.
И вообще, гельный был бы вариант.
И у нас еще больше корок стала.
Казалось бы, я же что-то исправил, должно стать лучше.
Давайте попробуем понять вообще, а проблем это на самом деле вообще есть.
Возьмём реальный binargin, который у нас падает, вызываем для него функцию dwarfdump.
Команду dwarfdump, она распечатает нам всю соответствующую информацию про то, где какие функции, с какого файла они пришли.
Находим, что у нас две функции пришли из левой части форка, они заменяются на свои части...
Они должны были бы прийти из правой части форка, но взялись из левой.
И, в принципе, они там буквально бинарно по коду совпадают.
То есть, цифовая замена, в общем, не страшна.
Мы не корректно используем компилятор, возможно, это плохо будет в будущем, когда эти функции поменяются, но прямо сейчас он не виноват, этот конкретный подозреваемый.
То есть у нас остается либо гонка тредов, либо не корректно использование локатора.
Искать гонка тредов сложно, поэтому будем искать под фонарем.
А именно будем искать некорректное использование локатора.
Как вообще надо локатором можно другаться?
У него вообще две функции, две функции.
Вот что здесь можно натворить?
Очень много чего можно натворить.
Во-первых, одна из вещей, которую можно плохо сделать с локатором, это использовать ненетелизированную память.
Большинство байтов в памяти процесса это просто тупо нули.
В результате, если вы память освободили, то в локатор пришли нули, потом он кому-то другому отдал эти же нули к какому-то следующему потребителю.
И если вы предполагаете, что у вас там какой-нибудь инициализированный счетчик равный нулю, то у вас неожиданно код может работать.
Но не потому, что вы его хорошо написали, а просто потому, что локатор вам отдал нули.
И на это можно очень неплохо напороться.
Если память вдруг начинает замусриваться.
Какое ещё может быть некорректно использовано?
Мы сказали аллокатору, вот, держи память и продолжаем на неё ссылаться и что-то с ней делать.
В этот момент эта память вообще, говоря, может к другому треду относиться и он там с какой-то свои полезные данные хранит безобразие.
Ещё мы можем обмануть алокатер тем, что скажем ему, освободил память, а потом снова освободил память.
А он имеет какие-то интересы в каундре, о том, сколько у него структура, золотсированного прилимом в диапазоне, и у него база данных начинает быть некорректной.
У него там какие-то инварианты пересадятся ходиться.
Есть еще много всяких экзотики, мы можем попытаться освободить указатель на память, которую мы никогда вообще не алацировали, или пытаться из серединки алацированного блока вызвать фри.
Это действительно очень безумно редкие варианты.
Мы хотим поймать наши виновные трэды в том, чтобы поймать их за ручку, когда они это всё сделают.
Нам желательно промодифицировать аллокатор.
Если вы статически линкуете с аллокатором, то это достаточно простая операция.
Вы просто поменяете код перед тем, как статически линкуете.
Если там динамически линковать, то
Там придется находить исходники, компилировать, менять и линковаться с новой версией.
Но в моем случае все было просто.
У нас была статическая линковка с аллокатором.
Поэтому я просто промодифицировал код и перекомпилировал.
Что же мы можем сделать с аллокатором?
В случае, если у нас идёт использование неинетилизированной памяти, то мы можем, перед тем, как вернуть из аллокации память клиенту, мы можем её заполнить каким-то мусором, желательно с большим количеством битов и какой-нибудь редкий паттер, чтобы он, маловероятно, встречался в памяти.
Что мы можем сделать с использованием после Free?
Вот когда нам сказали, что память освобождает, мы ее передворительно засыпаем к нинч-мусорам и после этого только вызываем Free внутри аллокатора.
Ну, то есть фактически мы делаем что-то вроде интерсептора.
И как поймать Double Free?
Мы можем попытаться посмотреть, если на Free приходит нам память, в которой сейчас находится тот паттерн мусора, который мы записали перед Free.
Если он встречается в той памяти, которая нам пришла, скорее всего, это попытка Double Free.
Мы можем это запустить в тесте в Проде и желательно такого не делать, потому что там реально может такой паттерн встретиться, потому что мы можем не предсказать какого-то паттерна, о которой там встречается.
Для теста вполне неплохо.
Какие у нас есть сложности?
Free приходит только указатель.
Мы не знаем вообще до какого места он залоцирован.
Мы не знаем вообще, до куда этим мусором добивать.
К счастью, в большинстве алкаторов есть где-то внутри закопанная функция, которая по сути своей означает узнать по указателю, сколько мы залоцировали.
Как я уже сказал, в паттерн мусора желательно много заполненных битов, потому что большинство битов в памяти нулевые.
И есть еще одна неприятная вещь, что MEMSET, если его использовать на блоках там больше нескольких килобайт, он начинает настолько сильно затормаживать программу, что ошибка прячется.
Если вы пытаетесь поймать два трэда, которые сталкиваются в очень-очень короткий момент времени, то существенное изменение времени выполнения функций приводит к тому, что они просто не сталкиваются, и вы не находите ошибку, хотя она есть.
Значит, нам нужно по-хорошему какой-то более простой, более менее инвазивный способ поймать нашего виновника.
Мы можем заполнять, прямо вот пишем сам указатель, когда мы делаем алок, инвертированный указатель в момент, когда мы перед фри.
И при этом только в том случае, если GetSize сказал, что вот этот вот указатель нам вообще туда можно уместить, что этот блок соответствующего размера.
И мы нашли, мы нашли кого-то, кто попался на нашу ловушку на Double Free.
Это случайный прохожий, который очень похож на виновника.
Существуют классы в плюсах, которые не виртуальные, не задержат никаких членов с данными.
Там вообще нечего унициализировать.
Там просто какие-то функции.
Тем не менее, такой класс можно салатсировать.
И стандарт говорит, у каждого объекта должен быть уникальный адрес памяти.
Значит, у этого должен быть адрес памяти, значит, у него должен быть какой-то размер.
И по стандарту он должен быть хотя бы один байт, и обычно он так один байт и есть.
И вот там одно байтовый такой класс.
В аллог приходит один байт, аллог говорит, в один байт я не впихну свой указатель, значит я здесь не буду заполнять мусором.
Free говорит, а я-то знаю, что на самом деле локатор-то выделил большой блок размером с указатель, я там свой мусор запишу.
После этого следующий аллог говорит, а я не буду писать, а Free находит тот же самый паттерн, который мы записали на предыдущем Free.
Вот такой вот случайный прохожий.
Окей, значит, нам нужно использовать GetSize и на аллоке, чтобы точно знать, что действительно там после всех преобразования аллокатора там умещается наш указатель.
То есть мы храним указатель на аллоке, инвертируемый указатель на free, только если GetSize у нас больше размера указателя, и у нас новый подозреваемый.
У нас имеется корочка, мы заглядываем в эту корочку, мы осмотрим Backtrace.
Отлично, ловушка сработала.
У нас имеется Race, Abort, вот наша обёрточка MyFreeDebug внутри моего аллокатора.
И это происходит где-то внутри Realloc.
Realloc у довольно-таки большого числа аллокаторов реализован очень просто.
Мы аллацируем новый блок, значит мы вызываем внутри себя функцию Alloc.
И после этого делаем free на старый блок.
Знаешь, вот это вот самый фри, кто-то сделал ещё кроме нас.
Главное, что нам нужно запомнить, что это происходит в каком-то буфере, который мы пытаемся увеличить.
И вот этот адрес надо куда-нибудь себе на бумажечку выписать.
Теперь надо найти подельника.
Если мы нашли виновного, это не означает, что мы поняли картину преступления, нам нужно еще найти подельника.
Нам нужно посмотреть, чем занимаются другие трэды.
Для этого запускаем команду Infosreds.
Можно было бы запускать какую-нибудь команду thread.apply.all.backtrace, но это будет слишком жирно.
У меня там было почти тысяча трэдов.
И для тысячи трэдов посмотреть стекл в несколько десятков функций мои глазки устанут.
Поэтому я просто смотрю поверхам.
Большинство трэдов — это какие-то случайные рабочие трэды, которые где-то висят внутри своих пулов и ждут, когда же им принесут какую-нибудь полезную работу, которую они будут выполнять.
Обычно у этих трэдов наверху стека находится либо PthRedConVate, либо EpoLVate, либо какой-нибудь еще Vate.
Остальных трэдов, которые занимаются в этот момент чем-то существенным, не очень много.
И вероятность того, что подельник ушел далеко, а по стеку невелика.
Поэтому у нас есть большая вероятность, что мы его поймаем.
Поэтому я смотрю на оставшиеся трэды, а там где-то 6 трэдов занимались полезной работой.
чтобы переключиться на него, я говорю, Сред и номер Среда, и 15 минут спустя в различных очень сложных принтов, я здесь немножечко удалил эндоинформации, там была строчка чуть-чуть подлиннее, но, грубо говоря, это гетер внутри, гетер внутри, гетер внутри, гетер внутри.
Возьмём контексту, контексты возьмём под контексту, под контексты возьмём контекст, соответственно, из-за вот тот самую часть контекста, а внутри него возьмём логер.
И вот этого логера возьмём соответствующий буфер, и в этот буфер что-нибудь запишем.
И там встречается ровно тот же самый номер, который у нас был, ровно тот же самый.
То есть картина преступления уже ясна два трэда почему-то одновременно пишут в буфер.
Посмотрим по коду, чем они в этот момент занимаются.
А выясняется, что там библиотека лагирования.
И для того, чтобы упростить задачу лагирования перед тем, как сбрасывать данные в общий лог,
Библиотека их накапливает в промежученном буфере, чтобы все сообщения, относящиеся к обработке одного запроса, легли рядом.
Это удобно для обработки.
Это удобно для диагностики и для многих других вещей.
И вот, собственно, этот буфер они отлично заполняли.
два-две замечательных идеи сработали.
Первая идея, кто-то решил, что надо отвечать пользователям максимально быстро, поэтому мы сначала ответим пользователю, а потом будем писать какую диагностику, вот это вот всё сложный влог, чтобы ответ пользователь получил максимально быстро.
Хорошо, замечательная идея, то есть у нас всё-то в отдельном треде происходит.
Таких тредов оказалось два, которые что-то дописывают в фоне.
И вот эти два фоновых трэда, которые что-то дописывали в лок, они столкнулись на этом буфере, и он отрисайзился, и отрисайзили, они их одновременно.
У библиотеки имеется опциональный синг, в случае, если там много трэдов, предполагается в использовании включаем, больше никаких корок, ловушка в коде, плюс ГДБ, и, пожалуйста, отладили.
Ну и какое-то маленькое, простое заключение.
Классная тула с духом улдскула.
Находить ошибки это весело, особенно с хорошими инструментами.
Она увеличивает вашу продуктивность.
Вы меньше тратите на дебак, а значит, больше тратите на написание полезного кода.
Зарабатывайте больше денег.
И это, в принципе, несложно, как можно посмотреть из предыдущих слайдов.
Это, в принципе, делается даже в очень-очень-очень садистских ситуациях.
Ну, а если вас интересует еще большее количество интересных историй, я иногда их публикую внутри Яндекса в Летушке, и вообще Яндекса не мает.
Времени буквально задам пару вопросов и опущу.
Вопрос, как автор относится к ллдб, рр и гуи-фронтент для гдб и ллдб.
Гуи обычно гдб настраивается очень-очень-очень плохо.
Поэтому да, через них можно обычно сделать все то же самое.
Если автор связки GUIS с самим, ГДБ сделал все хорошо.
Что не всегда правда, к сожалению.
Поэтому я с духом Oskula предпочитаю консоль.
Вторая вещь, есть очень полезная штука, как ГДБ-сервер.
То есть вы можете запустить ГДБ-сервер на продовой машинке у себя локальный ГДБ, подсоединиться по сети и отлаживать по сети, это очень удобно.
По поводу... Сколько там ещё было букв каких-то?
Много букв давайте пропустим.
Про ГДБ сервер нужно ли тащить ГДБ в контейнер с сервисами?
Ну вот, ГДБ, этот самый сервер достаточно.
Ну, в принципе, ГДБ сервер сам по себе внутри включает, по сути, свои ГДБ.
Факт то, что вам не нужно туда тащить весь код, который вы компилируете.
Вы компилируете на одной машинке.
Вы аккуратненько его выкладываете, там у вас кода нет, но у вас там есть ГДБ, который может подключиться к этому процессу и посмотреть на него вживую.
А если корка, то, наоборот, корку притаскиваете к себе и смотрите на него локально припорируйте.
Почему нужно было оборачивать аллокатор вместо использования асанта сан?
Адрес санитайзер – шикарная штука.
В том случае, если бага воспроизводится при любой скорости кода.
Если у вас хоть немножечко где-нибудь вставили миллисекундную задержочку и ошибочка ушла, адрес санитайзер сделает применную такую штуку.
Он запустит вашу программу в 10 раз медленнее.
Вы отлично увидите, что у вас никаких ошибок не происходит и вы не нашли ошибку.
Все замечательно, все хорошо, но это не то, что вы хотите.
Вы хотите найти ошибку, а ошибка прячется при любом изменении таймингов.
Вот, к сожалению, такая у нее неприятная особенность.
Есть более какие-то мудрые всякие санитайзеры, которые пытаются найти трэдовые гонки, но у них ровно та же самая болячка.
Поэтому вот такие вот менее инвазивные методы очень часто находят ошибку быстрее и требуют меньше ресурсов.
Это немаловажная штука, потому что
Тот сервис, который я пытался отлашивать, у него было 130 или даже 150 гигов оперативной памяти, занято под различные базы данных.
В результате, если я попытался бы использовать адрес санитайзер, адрес санитайзер выделил бы еще приблизительно столько же на свои структуры и еще какой-то там свой аэродирхет.
И в этот момент мне пришлось бы искать машинку, на которую вмещается приблизительно 300 гигов памяти, не включая все остальные процессы, которые еще что-то будут делать.
И это все бы работал очень медленно, и не факт, что я бы нашел ошибку вообще.
Какой дебагер использует для отладки мульти-трейт-кода?
Вот я здесь специально говорю, что это вот похоже на детективные истории, потому что когда вы пытаетесь отладить ошибку, у них как у любого преступника свои собственные маркеры, свои собственные фишечки, по которым их можно найти.
Если у нас имеется гонка тредов,
то вы замечаете, что я увеличиваюсь тредов здесь, я уменьшаю тредов здесь, и у меня ошибка становится чуть чаще, чуть реже воспроизводимый.
И, сходя из этого, можете понять приблизительно, где это может располагаться и попытаться найти какие-то инварианты, которые нарушаются в программе.
Вы смотрите в ГДБ-корке, что у вас.
какие из инвариантов в коде нарушены и пытаетесь посмотреть, а действительно ли они нарушаются.
Обычно вставить какой-нибудь простой ассерт на то, что какой-то инвариант выполняется достаточно быстро и не очень сильно замедляет код, поэтому ошибка не успевает спрятаться.
И вы можете проверить, ведет ли нарушение инварианта к ошибке, не ведет ли это нарушение инварианта к ошибке.
Много разных методов, они вот все под какие-то конкретные кейсы, когда вы используете триды.
Чаще всего у вас ошибка в том, что вы только что редактировали, поэтому вы просто перечитываете свой код и проверяете его инварианты.
Но если это у вас ошибка от переконфигуирования и у вас имеются конкретные шаги, вы можете бинарным делением найти то конкретное изменение, которое к этому привело.
В данном случае этого просто не получилось сделать из-за того, что
Людям в процессе форка пришлось сделать с тысячи разных коммитов тысячи переписываний, всякие конфигурации и искать бинарным поискам было слишком сложно.
Я же дал мое короткий ответ на этот вопрос.
Подобные детективные расследования реально автоматизировать?
Например, написать скрипты на каких-то отдельных этапах фразового корка?
Да, на самом деле, можно очень много чего из этого все автоматизировать.
Я в свое время развлекался подобным роду вещами.
Если у вас имеется очень-очень-очень редко воспроизводимая корка, которая происходит на каком-то большом кластере вычислений,
И у вас эти корки происходят в принципе часто, но на каждый конкретный кейс вы не можете ее воспроизвести, то возможно вам придется дебажить на каком-то большом кластере, собирать много корок и пытаться анализировать какие-то общие паттерны в этих корках.
В частности, вы можете взять, посмотреть,
стектрейсы, вы можете распечатать стектрейсы каждого треда, которые происходят в этих процессах и найти те паттерны, которые чаще всего повторяют.
Вы просто берете там 3-4 функции сверху.
Ну, там 5-6, если паттерны очень частотные.
И среди них выделяете те, которые повторяются и отделяя те, которые в мусорной типа тех же вейтов.
Итак, у меня здесь в чате еще примерно 10 миллионов вопросов.
Я думаю, что ответить на них уже в чате.
Итак, еще раз аплодисменты.
С нами был Константин Обыков.