Поиск…


Вступление

Отладка от разработчиков C ++ отнимает много времени. Эта тема призвана помочь в решении этой задачи и вдохновить техник. Не ожидайте обширного списка проблем и решений, установленных инструментами или руководством по указанным инструментам.

замечания

Эта тема еще не завершена, примеры из следующих методов / инструментов были бы полезны:

  • Упоминайте больше инструментов статического анализа
  • Инструменты двоичных инструментов (например, UBSan, TSan, MSan, ESan ...)
  • Закалка (CFI ...)
  • Fuzzing

Моя программа на C ++ заканчивается segfault - valgrind

Давайте попробуем выполнить базовую неудачную программу:

#include <iostream>

void fail() {
    int *p1;
    int *p2(NULL);
    int *p3 = p1;
    if (p3) {
        std::cout << *p3 << std::endl;
    } 
}

int main() { 
    fail();
}

Постройте его (добавьте -g для включения информации об отладке):

g++ -g -o main main.cpp

Бежать:

$ ./main
Segmentation fault (core dumped)
$

Давайте отлаживаем его с valgrind:

$ valgrind ./main
==8515== Memcheck, a memory error detector
==8515== Copyright (C) 2002-2015, and GNU GPL'd, by Julian Seward et al.
==8515== Using Valgrind-3.11.0 and LibVEX; rerun with -h for copyright info
==8515== Command: ./main
==8515==
==8515== Conditional jump or move depends on uninitialised value(s)
==8515==    at 0x400813: fail() (main.cpp:7)
==8515==    by 0x40083F: main (main.cpp:13)
==8515==
==8515== Invalid read of size 4
==8515==    at 0x400819: fail() (main.cpp:8)
==8515==    by 0x40083F: main (main.cpp:13)
==8515==  Address 0x0 is not stack'd, malloc'd or (recently) free'd
==8515==
==8515==
==8515== Process terminating with default action of signal 11 (SIGSEGV): dumping core
==8515==  Access not within mapped region at address 0x0
==8515==    at 0x400819: fail() (main.cpp:8)
==8515==    by 0x40083F: main (main.cpp:13)
==8515==  If you believe this happened as a result of a stack
==8515==  overflow in your program's main thread (unlikely but
==8515==  possible), you can try to increase the size of the
==8515==  main thread stack using the --main-stacksize= flag.
==8515==  The main thread stack size used in this run was 8388608.
==8515==
==8515== HEAP SUMMARY:
==8515==     in use at exit: 72,704 bytes in 1 blocks
==8515==   total heap usage: 1 allocs, 0 frees, 72,704 bytes allocated
==8515==
==8515== LEAK SUMMARY:
==8515==    definitely lost: 0 bytes in 0 blocks
==8515==    indirectly lost: 0 bytes in 0 blocks
==8515==      possibly lost: 0 bytes in 0 blocks
==8515==    still reachable: 72,704 bytes in 1 blocks
==8515==         suppressed: 0 bytes in 0 blocks
==8515== Rerun with --leak-check=full to see details of leaked memory
==8515==
==8515== For counts of detected and suppressed errors, rerun with: -v
==8515== Use --track-origins=yes to see where uninitialised values come from
==8515== ERROR SUMMARY: 2 errors from 2 contexts (suppressed: 0 from 0)
$

Сначала мы сосредоточимся на этом блоке:

==8515== Invalid read of size 4
==8515==    at 0x400819: fail() (main.cpp:8)
==8515==    by 0x40083F: main (main.cpp:13)
==8515==  Address 0x0 is not stack'd, malloc'd or (recently) free'd

В первой строке указывается, что segfault вызвано чтением 4 байтов. Вторая и третья строки - стек вызовов. Это означает, что недопустимое чтение выполняется в функции fail() , строка 8 main.cpp, которая вызывается основной строкой 13 main.cpp.

Рассматривая строку 8 main.cpp, мы видим

std::cout << *p3 << std::endl;

Но сначала проверяем указатель, так что что случилось? Позволяет проверить другой блок:

==8515== Conditional jump or move depends on uninitialised value(s)
==8515==    at 0x400813: fail() (main.cpp:7)
==8515==    by 0x40083F: main (main.cpp:13)

Это говорит нам, что в строке 7 есть униализированная переменная, и мы ее читаем:

if (p3) {

Что указывает нам на прямую, где мы проверяем p3 вместо p2. Но как же возможно, что p3 неинициализируется? Мы инициализируем его:

int *p3 = p1;

Valgrind советует нам повторить с --track-origins=yes , давайте сделаем это:

valgrind --track-origins=yes ./main

Аргумент для valgrind сразу после valgrind. Если мы поместим его после нашей программы, он будет передан нашей программе.

Вывод почти такой же, есть только одно отличие:

==8517== Conditional jump or move depends on uninitialised value(s)
==8517==    at 0x400813: fail() (main.cpp:7)
==8517==    by 0x40083F: main (main.cpp:13)
==8517==  Uninitialised value was created by a stack allocation
==8517==    at 0x4007F6: fail() (main.cpp:3)

Что говорит нам, что неинициализированное значение, которое мы использовали в строке 7, было создано в строке 3:

int *p1;

который ведет нас к нашему неинициализированному указателю.

Анализ Segfault с GDB

Для этого примера используется тот же код, что и выше.

#include <iostream>

void fail() {
    int *p1;
    int *p2(NULL);
    int *p3 = p1;
    if (p3) {
        std::cout << *p2 << std::endl;
    } 
}

int main() { 
    fail();
}

Сначала скомпилируем его

g++ -g -o main main.cpp

Позволяет запустить его с помощью gdb

gdb ./main

Теперь мы будем в оболочке gdb. Введите run.

(gdb) run
The program being debugged has been started already.
Start it from the beginning? (y or n) y
Starting program: /home/opencog/code-snippets/stackoverflow/a.out 

Program received signal SIGSEGV, Segmentation fault.
0x0000000000400850 in fail () at debugging_with_gdb.cc:11
11            std::cout << *p2 << std::endl;

Мы видим, что ошибка сегментации происходит в строке 11. Таким образом, единственной переменной, используемой в этой строке, является указатель p2. Давайте рассмотрим его печатную печать содержимого.

(gdb) print p2
$1 = (int *) 0x0 

Теперь мы видим, что p2 был инициализирован равным 0x0, что означает NULL. В этой строке мы знаем, что мы пытаемся разыменовать указатель NULL. Поэтому мы и исправим это.

Чистый код

Отладка начинается с понимания кода, который вы пытаетесь отлаживать.

Плохой код:

int main() {
    int value;
    std::vector<int> vectorToSort;
    vectorToSort.push_back(42); vectorToSort.push_back(13);
    for (int i = 52; i; i = i - 1)
    {
    vectorToSort.push_back(i *2);
    }
    /// Optimized for sorting small vectors
    if (vectorToSort.size() == 1);
    else
        {
        if (vectorToSort.size() <= 2)
            std::sort(vectorToSort.begin(), std::end(vectorToSort));
        }
    for (value : vectorToSort) std::cout << value << ' ';
return 0; }

Лучший код:

std::vector<int> createSemiRandomData() {
    std::vector<int> data;
    data.push_back(42);
    data.push_back(13);
    for (int i = 52; i; --i)
        vectorToSort.push_back(i *2);
    return data;
}

/// Optimized for sorting small vectors
void sortVector(std::vector &v) {
    if (vectorToSort.size() == 1)
        return;
    if (vectorToSort.size() > 2)
        return;

    std::sort(vectorToSort.begin(), vectorToSort.end());
}

void printVector(const std::vector<int> &v) {
    for (auto i : v)
        std::cout << i << ' ';
}

int main() {
    auto vectorToSort = createSemiRandomData();
    sortVector(std::ref(vectorToSort));
    printVector(vectorToSort);

    return 0;
 }

Независимо от стилей кодирования, которые вы предпочитаете и используете, наличие последовательного стиля кодирования (и форматирования) поможет вам понять код.

Рассматривая приведенный выше код, можно выделить пару улучшений для улучшения удобочитаемости и отладки:

Использование отдельных функций для отдельных действий

Использование отдельных функций позволяет пропустить некоторые функции в отладчике, если вас не интересуют детали. В этом конкретном случае вам может быть не интересно создавать или распечатывать данные и только хотеть входить в сортировку.

Еще одно преимущество заключается в том, что вам нужно читать меньше кода (и запоминать его), выполняя код. Теперь вам нужно только прочитать 3 строки кода в main() , чтобы понять это, а не целую функцию.

Третье преимущество заключается в том, что у вас просто меньше кода для просмотра, что помогает обученному глазу обнаружить эту ошибку за считанные секунды.

Использование последовательного форматирования / построения

Использование последовательного форматирования и построения приведет к удалению помех из кода, что упростит фокусировку на коде вместо текста. Многие дискуссии обсуждались в стиле «правильного» форматирования. Независимо от этого стиля, наличие единого кода в коде улучшит знакомство и упростит фокусировку на коде.

Поскольку код форматирования занимает много времени, рекомендуется использовать для этого специальный инструмент. Большинство IDE имеют хотя бы некоторую поддержку для этого и могут сделать форматирование более последовательным, чем люди.

Вы можете заметить, что стиль не ограничивается пробелами и символами новой строки, так как мы больше не смешиваем функции free-style и member, чтобы получить начало / конец контейнера. ( v.begin() vs std::end(v) ).

Обратите внимание на важные части вашего кода.

Независимо от стиля, который вы определяете для выбора, приведенный выше код содержит пару маркеров, которые могут дать вам подсказку о том, что может быть важно:

  • Комментарий, который optimized , указывает на некоторые причудливые методы
  • Некоторые ранние возвращения в sortVector() показывают, что мы делаем что-то особенное
  • std::ref() указывает, что что-то происходит с sortVector()

Заключение

Чистый код поможет вам понять код и сократит время, необходимое для его отладки. Во втором примере анализатор кода может даже обнаружить ошибку на первый взгляд, в то время как ошибка может быть скрыта в деталях в первом. (PS: ошибка находится в сравнении с 2 ).

Статический анализ

Статический анализ - это метод, при котором проверяется код шаблонов, связанных с известными ошибками. Использование этого метода занимает меньше времени, чем анализ кода, однако его проверки ограничиваются только запрограммированными в инструменте.

Проверки могут включать неправильную полуточку за оператором if (var); ( if (var); ) до расширенных алгоритмов графа, которые определяют, не инициализирована ли переменная.

Предупреждения компилятора

Включение статического анализа легко, самая упрощенная версия уже встроена в ваш компилятор:

Если вы включите эти параметры, вы заметите, что каждый компилятор обнаружит ошибки, которые другие не делают, и что вы получите ошибки в тех методах, которые могут быть действительными или действительными в определенном контексте. while (staticAtomicBool); может быть приемлемым, даже если while (localBool); нет.

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

Поскольку приведенные выше опции включают все предупреждения, они могут включать предупреждения, которые вам не нужны. (Почему ваш код должен быть совместим с C ++ 98?) Если это так, вы можете просто отключить это конкретное предупреждение:

  • clang++ -Wall -Weverything -Werror -Wno-errortoaccept ...
  • g++ -Wall -Weverything -Werror -Wno-errortoaccept ...
  • cl.exe /W4 /WX /wd<no of warning>...

Когда предупреждения компилятора помогают вам во время разработки, они немного замедляют компиляцию. Вот почему вы не всегда можете включить их по умолчанию. Либо вы запускаете их по умолчанию, либо включаете некоторую непрерывную интеграцию с более дорогостоящими проверками (или всеми из них).

Внешние инструменты

Если вы решите провести некоторую непрерывную интеграцию, использование других инструментов не является таким растяжкой. У инструмента, такого как clang-tidy, есть список проверок, который охватывает широкий круг проблем, некоторые примеры:

  • Актуальные ошибки
    • Предотвращение нарезки
    • Утверждения с побочными эффектами
  • Проверка читаемости
    • Вводящий в заблуждение отступ
    • Идентификация идентификатора
  • Проверка модернизации
    • Использовать make_unique ()
    • Использовать nullptr
  • Проверка производительности
    • Найти ненужные копии
    • Поиск неэффективных вызовов алгоритмов

Список может быть не таким большим, поскольку у Клана уже много предупреждений компилятора, однако он приблизит вас на один шаг к высококачественной кодовой базе.

Другие инструменты

Существуют и другие инструменты с аналогичной целью:

Заключение

Для C ++ существует множество инструментов статического анализа, которые встроены в компилятор в качестве внешних инструментов. Попытка их не занимает много времени для простых настроек, и они найдут ошибки, которые могут упустить при просмотре кода.

Безопасный стек (повреждение стека)

Коррупция стека вызывает раздражающие ошибки. Когда стек поврежден, отладчик часто не может дать вам хорошую трассировку стека, где вы находитесь, и как вы туда попали.

Здесь вступает в действие безопасный стек. Вместо использования одного стека для ваших потоков он будет использовать два: безопасный стек и опасный стек. Безопасный стек работает так же, как и раньше, за исключением того, что некоторые части перемещаются в опасный стек.

Какие части стека перемещаются?

Каждая часть, которая имеет потенциал для разложения стека, будет выведена из безопасного стека. Как только переменная в стеке передается по ссылке или одна берет адрес этой переменной, компилятор решит выделить это во втором стеке, а не в безопасном.

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

Для чего он фактически используется?

Безопасный стек не был изобретен, чтобы дать вам лучший опыт отладки, однако это хороший побочный эффект для неприятных ошибок. Это первоначальная цель - это часть проекта Integrity (CPI) Code-Pointer Integrity (CPI) , в котором они пытаются предотвратить переопределение обратных адресов для предотвращения ввода кода. Другими словами, они пытаются предотвратить выполнение кода хакеров.

По этой причине эта функция активирована на хроме и, как сообщается, имеет накладные расходы <1% ЦП.

Как включить его?

Прямо сейчас этот параметр доступен только в компиляторе clang , где можно передать -fsanitize=safe-stack в компилятор. Было предложено реализовать ту же функцию в GCC.

Заключение

Повреждения стека могут стать более легкими для отладки при включении безопасного стека. Из-за низкой производительности, вы можете даже активировать по умолчанию в своей конфигурации сборки.



Modified text is an extract of the original Stack Overflow Documentation
Лицензировано согласно CC BY-SA 3.0
Не связан с Stack Overflow