C++
C ++ 디버깅 및 디버그 방지 도구 및 기법
수색…
소개
C ++ 개발자들로부터 많은 시간을 보냈다. 이 주제는이 태스크를 지원하고 기술에 대한 영감을주기위한 것입니다. 언급 한 도구에 대한 도구 또는 설명서로 해결할 수있는 광범위한 문제 및 솔루션 목록은 기대하지 마십시오.
비고
이 주제는 아직 완전하지 않지만 다음 기술 / 도구에 대한 예가 유용합니다.
- 더 많은 정적 분석 도구 언급
- 이진 계측 도구 (예 : UBSan, TSan, MSan, ESan ...)
- 경화 (CFI ...)
- 퍼지기
나의 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 바이트를 읽음으로써 발생한다고 설명합니다. 두 번째 및 세 번째 줄은 호출 스택입니다. 이는 잘못된 읽기가 main.cpp의 main, line 13에서 호출되는 main.cpp의 8 행 인 fail()
함수에서 수행됨을 의미합니다.
main.cpp의 8 번째 줄을 보면
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) {
어떤 점에서 우리는 p2 대신에 p3를 점검 할 것인지를 가리 킵니다. 그러나 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;
초기화되지 않은 포인터로 안내합니다.
GDB를 이용한 Segfault 분석
이 예제에서 위와 동일한 코드를 사용할 수 있습니다.
#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 쉘에있게 될 것입니다. 실행을 입력하십시오.
(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가 NULL을 나타내는 0x0으로 초기화되었음을 알 수 있습니다. 이 줄에서 우리는 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;
}
선호하는 코딩 스타일과 관계없이 일관된 코딩 (및 서식 지정) 스타일을 사용하면 코드를 이해하는 데 도움이됩니다.
위의 코드를 보면 가독성과 디버깅 성을 향상시키기위한 몇 가지 개선 사항을 확인할 수 있습니다.
별도의 작업을위한 별도의 기능 사용
개별 기능을 사용하면 세부 사항에 관심이 없으면 디버거의 일부 기능을 건너 뛸 수 있습니다. 이 특별한 경우에는 데이터 작성 또는 인쇄에 관심이 없으며 정렬에만 들어가기를 원할 것입니다.
또 다른 장점은 코드를 단계별로 실행하면서 적은 코드를 읽고 암기해야한다는 것입니다. 이제 main()
에서 3 줄의 코드를 읽는 것만으로 전체 기능 대신 이해할 수 있습니다.
세 번째 이점은 간단히 살펴볼 코드가 적기 때문에 초안에서이 버그를 발견하는 데 숙련 된 눈을 확보하는 데 도움이됩니다.
일관된 형식 / 구조 사용
일관된 서식 및 구문을 사용하면 코드에서 혼란을 제거하여 텍스트가 아닌 코드에 더 쉽게 집중할 수 있습니다. '올바른'형식 지정 스타일에 대한 많은 토론이있었습니다. 스타일에 관계없이 코드에서 일관된 단일 스타일을 사용하면 친숙 함을 개선하고 코드에 더 쉽게 집중할 수 있습니다.
형식 지정 코드는 시간이 많이 걸리는 작업이므로이를 위해 전용 도구를 사용하는 것이 좋습니다. 대부분의 IDE는 이것을 지원하기 위해 적어도 일종의 지원을하고 있으며, 인간보다 더 일관된 포맷을 할 수 있습니다.
컨테이너의 시작 / 끝을 얻기 위해 자유 스타일과 멤버 함수를 더 이상 섞지 않으므로 스타일은 공백과 줄 바꿈에 국한되지 않습니다. ( 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 사용
- 성능 점검
- 불필요한 사본 찾기
- 비효율적 인 알고리즘 호출 찾기
Clang은 이미 많은 컴파일러 경고를 가지고 있기 때문에 목록이 그다지 크지 않을 수도 있지만 고품질 코드 기반에 한 걸음 더 가까이 다가 갈 수 있습니다.
기타 도구
유사한 목적을 가진 다른 도구들이 있습니다 :
- 외부 도구로서의 Visual Studio 정적 분석기
- clazy , Qt 코드 확인을위한 Clang 컴파일러 플러그인
결론
C ++에는 많은 정적 분석 도구가 존재합니다. 둘 모두 컴파일러에서 외부 도구로 빌드됩니다. 그것들을 시험해보기 만해도 쉬운 셋업을 위해서는 시간이 많이 걸리지 않으며, 코드 리뷰에서 놓칠 수있는 버그를 발견하게됩니다.
안전 스택 (스택 손상)
스택 손상은보기에 까다로운 버그입니다. 스택이 손상됨에 따라 디버거는 흔히 현재 위치와 도착 방법에 대한 스택 추적을 제공 할 수 없습니다.
이것은 안전한 스택이 작동하는 곳입니다. 스레드에 단일 스택을 사용하는 대신 안전 스택과 위험한 스택을 사용합니다. 안전 스택은 일부 부품이 위험한 스택으로 이동한다는 점을 제외하고는 이전과 똑같이 작동합니다.
스택의 어느 부분이 이동합니까?
스택을 손상시킬 잠재 성이있는 모든 부품이 안전 스택 밖으로 이동합니다. 스택의 변수가 참조로 전달되거나이 변수의 주소를 취하는 즉시 컴파일러는 안전한 스택 대신 두 번째 스택에이 변수를 할당하기로 결정합니다.
결과적으로 해당 포인터로 수행하는 모든 조작, 해당 포인터 / 참조를 기반으로 한 메모리의 수정 사항은 두 번째 스택의 메모리에만 영향을 미칩니다. 안전 스택에 가까운 포인터를 얻지 못하면 스택은 스택을 손상시킬 수 없으며 디버거는 여전히 스택의 모든 함수를 읽어 좋은 트레이스를 제공 할 수 있습니다.
실제로 사용되는 것은 무엇입니까?
안전 스택은 더 나은 디버깅 경험을 제공하기 위해 발명되지는 않았지만, 불쾌한 버그에 대한 좋은 부작용입니다. 원래 목적은 코드 삽입을 방지하기 위해 반송 주소를 무시하는 것을 방지하기 위해 코드 포인터 무결성 (CPI) 프로젝트의 일부분입니다. 즉 해커 코드를 실행하지 못하도록합니다.
이러한 이유로이 기능은 크롬으로 활성화되었으며 1 % 미만의 CPU 오버 헤드가있는 것으로 보고 되었습니다.
그것을 가능하게하는 방법?
-fsanitize=safe-stack
옵션은 컴파일러 에서 -fsanitize=safe-stack
을 전달할 수있는 clang 컴파일러 에서만 사용할 수 있습니다. GCC에서 같은 기능을 구현하기위한 제안 이있었습니다.
결론
스택 손상은 안전 스택을 사용할 때 디버그하기가 더 쉬워 질 수 있습니다. 성능 오버 헤드가 낮기 때문에 빌드 구성에서 기본적으로 활성화 할 수도 있습니다.