메모리 손상 디버깅


23

우선, 이것이 절대적인 답을 가진 완벽한 Q & A 스타일의 질문이 아니라는 것을 알고 있지만 더 잘 작동하는 표현을 생각할 수는 없습니다. 나는 이것에 대한 절대적인 해결책이 없다고 생각하며 이것이 Stack Overflow 대신 여기에 게시하는 이유 중 하나입니다.

지난 달 동안 나는 더 현대적이고 확장하기 쉽고 수정하기 위해 상당히 오래된 서버 코드 (mmorpg)를 다시 작성했습니다. 나는 네트워크 부분으로 시작하여 나를 위해 물건을 처리하기 위해 타사 라이브러리 (libevent)를 구현했습니다. 모든 리팩토링 및 코드 변경으로 어딘가에 메모리 손상이 발생했으며 어디에서 발생하는지 찾기 위해 고심하고 있습니다.

일부 봇을 시뮬레이션하기 위해 기본 봇을 구현할 때도 더 이상 충돌이 발생하지 않습니다 (일부 문제를 일으키는 libevent 문제를 수정했습니다).

나는 지금까지 시도했다 :

지옥에서 벗어나기-실제로 충돌 할 때까지는 잘못된 쓰기가 없습니다 (생산에서 1 + 하루가 걸리거나 1 시간이 걸릴 수 있습니다). 기회? (주소 범위를 "확산"하는 방법이 있습니까?)

코드 분석 도구, 즉 적용 범위 및 cppcheck 그들이 코드에서 지적하고 혐의와 우연한 사례를 지적하는 동안 심각한 것은 없었습니다.

undodb를 통해 gdb와 충돌 할 때까지 프로세스를 기록 한 다음 거꾸로 작동합니다. 이 / sounds /는 할 수 있어야하지만 자동 완성 기능을 사용하여 gdb를 충돌 시키거나 가능한 많은 분기가 있기 때문에 손실되는 내부 libevent 구조로 이어집니다 (하나의 손상으로 인해 다른 것이 발생하므로 에). 포인터가 원래 할당 된 위치 / 할당 된 위치를 볼 수 있다면 대부분의 분기 문제를 제거 할 수 있다면 좋을 것 같습니다. 나는 undodb로 valgrind를 실행할 수 없으며 정상적인 gdb 레코드는 사용할 수 없을 정도로 느립니다 (valgrind와 결합하여 작동하는 경우).

코드 검토! 나 자신에 의해 (완전히) 친구들이 내 코드를 살펴 보도록했지만, 그것이 충분하다는 것은 의심 스럽다. 나는 코드 검토 / 디버깅을하기 위해 개발자를 고용하려고 생각했지만 너무 많은 돈을 넣을 여유가 없으며 작은 일을 할 사람을 찾을 곳을 모를 것입니다. 그가 문제를 찾지 못하거나 자격을 갖춘 사람이 없다면 돈을 들이지 않아도됩니다.

나는 또한주의해야한다 : 나는 보통 일관된 역 추적을 얻는다. 크래시가 발생하는 곳은 몇 가지가 있는데, 대부분 소켓 클래스가 어떻게 든 손상되는 것과 관련이 있습니다. 소켓이 아닌 것을 가리키는 잘못된 포인터이거나 소켓 클래스 자체가 횡설수설로 덮어 씌워집니다 (부분적으로?). 가장 많이 사용되는 부분 중 하나이기 때문에 가장 많이 충돌하는 것으로 생각되지만 사용되는 첫 번째 손상된 메모리입니다.

이 문제는 모두 거의 2 개월 동안 (바람직한 취미 프로젝트로) 바빠서 내가 심술 I은 IRL이되어 포기하는 것에 대해 정말 실망하고 있습니다. 나는 그 문제를 찾기 위해 내가 무엇을해야한다고 생각할 수 없다.

내가 놓친 유용한 기술이 있습니까? 어떻게 처리합니까? (이것에 대한 정보가 많지 않기 때문에 일반적이지 않을 수 있습니다 .. 또는 나는 정말 장님입니까?)

편집하다:

중요한 경우 일부 사양 :

gcc 4.7을 통한 C ++ (11) 사용 (데비안 Wheezy에서 제공하는 버전)

코드베이스는 약 150k 라인입니다

david.pfx post에 대한 응답으로 편집 : (느린 응답에 대해 죄송합니다)

패턴을 찾기 위해 충돌에 대한주의 깊은 기록을 유지하고 있습니까?

예, 여전히 최근에 발생한 충돌에 대한 덤프가 있습니다.

몇 곳이 정말 비슷합니까? 어떤 방법으로?

글쎄, 가장 최신 버전 (코드를 추가 / 제거하거나 관련 구조를 변경할 때마다 변경되는 것처럼 보입니다)은 항상 항목 타이머 방법에 잡힐 것입니다. 기본적으로 항목은 특정 시간이 지난 후 만료되며 업데이트 된 정보를 클라이언트에 보냅니다. 유효하지 않은 소켓 포인터는 플레이어 클래스에 있습니다 (주로 말할 수있는 한 유효합니다). 또한 정상적인 종료 후 명시 적으로 파괴되지 않은 모든 정적 클래스 ( __run_exit_handlers백 트레이스에서)를 파괴하는 정리 단계에서 많은 충돌이 발생합니다 . 대부분 std::map하나의 클래스를 포함 하며, 그것이 첫 번째로 올 것이라고 추측합니다.

손상된 데이터는 어떻게 생겼습니까? 제로? 아스키? 패턴?

나는 아직 패턴을 찾지 못했습니다. 부패가 시작된 곳을 모르기 때문에 말하기가 어렵습니다.

힙 관련입니까?

그것은 전적으로 힙 관련입니다 (gcc의 스택 가드를 활성화했으며 아무것도 잡지 못했습니다).

부패 이후에 부패가 발생합니까 free()?

당신은 그것에 대해 조금 더 자세히 설명해야 할 것입니다. 이미 자유로 워진 물건에 대한 포인터를 가지고 있다는 의미입니까? 일단 객체가 파괴되면 모든 참조를 null로 설정합니다. 그것은 valgrind에 나타나야하지만 그렇지 않았습니다.

네트워크 트래픽 (버퍼 크기, 복구주기)에 특별한 것이 있습니까?

네트워크 트래픽은 원시 데이터로 구성됩니다. 따라서 (u) intX_t 또는 압축 (패딩을 제거하기 위해) char 배열은보다 복잡한 것들을 위해 구조화됩니다. 각 패킷에는 예상 크기에 대해 유효성이 검사되는 id와 패킷 크기 자체로 구성된 헤더가 있습니다. 그것들은 크기가 몇 Mb 인 가장 큰 (내부 '부팅'패킷, 시작시 한 번 발생) 10-60 바이트 정도입니다.

많은 생산 주장. 피해가 전파되기 전에 조기에 예상대로 충돌합니다.

한때 std::map부패 와 관련된 충돌이 있었으며 , 각 엔터티에는 "보기"맵이 있습니다. 각 엔터티는이를 볼 수 있으며 그 반대도 마찬가지입니다. 나는 앞뒤에 200 바이트 버퍼를 추가하고 0x33으로 채우고 각 액세스 전에 확인했습니다. 부패가 마술처럼 사라졌고, 나는 무언가를 움직여서 다른 것을 부패시켰다.

전략적 로깅을 통해 이전에 발생한 상황을 정확하게 알 수 있습니다. 답변에 가까워 질수록 로깅에 추가하십시오.

그것은 .. 연장합니다.

필사적으로 상태를 저장하고 자동 다시 시작할 수 있습니까? 그렇게하는 몇 가지 프로덕션 소프트웨어를 생각할 수 있습니다.

나는 다소 그렇게한다. 이 소프트웨어는 기본 "캐시"프로세스와 캐시에 액세스하여 물건을 가져오고 저장하는 다른 작업자 프로세스로 구성됩니다. 따라서 충돌 당별로 많은 진전을 잃지 않고 여전히 모든 사용자를 연결 해제하는 등 솔루션이 아닙니다.

동시성 : 스레딩, 경쟁 조건 등

"비동기"쿼리를 수행하는 mysql 스레드가 있습니다. 이는 모두 손대지 않고 모든 잠금 기능을 통해 데이터베이스 클래스와 정보를 공유합니다.

인터럽트

30 초 동안주기를 완료하지 않으면 중단되는 중단 타이머를 막을 수 있습니다.이 코드는 안전해야합니다.

if (!tics) {
    abort();
} else
    tics = 0;

틱은 volatile int tics = 0;사이클이 완료 될 때마다 증가합니다. 오래된 코드도 있습니다.

이벤트 / 콜백 / 예외 : 상태 또는 스택 손상이 예기치 않게

많은 콜백 (비동기 네트워크 I / O, 타이머)이 사용되고 있지만 나쁜 일을해서는 안됩니다.

비정상적인 데이터 : 비정상적인 입력 데이터 / 타이밍 / 상태

나는 그와 관련된 몇 가지 우연한 사례가있었습니다. 패킷이 처리되는 동안 소켓을 연결 해제하면 nullptr 등에 액세스 할 수 있었지만 클래스 자체에 완료된 후에 모든 참조가 정리되므로 지금까지 쉽게 파악할 수있었습니다. (파괴 자체는 매 사이클마다 파괴 된 객체를 모두 삭제하는 루프에 의해 처리됩니다)

비동기 외부 프로세스에 대한 종속성

정교하게 관리? 위에서 언급 한 캐시 프로세스와 다소 차이가 있습니다. 내 머리 꼭대기에서 상상할 수있는 유일한 것은 충분히 빨리 끝내지 않고 가비지 데이터를 사용하는 것입니다.하지만 네트워크를 사용하고 있기 때문에 그렇지 않습니다. 동일한 패킷 모델.


7
안타깝게도 이것은 사소한 C ++ 앱에서 일반적입니다. 소스 제어를 사용하는 경우 다양한 변경 세트를 테스트하여 문제를 유발 한 코드 변경을 좁히는 것이 가능하지만이 경우에는 불가능할 수 있습니다.
Telastyn

예, 제 경우에는 실제로 실현 가능하지 않습니다. 나는 기본적으로 작업에서 완전히 2 개월 동안 완전히 부서진 다음 약간의 작업 코드가있는 디버깅 단계로 갔다. 구식 시스템은 실제로 모든 것을 손상시키지 않고 새로운 종류의 유연한 네트워크 코드를 구현할 수 없었습니다.
Robin

2
이 시점에서 각 부품을 분리하고 분리해야 할 수도 있습니다. 솔루션의 각 클래스 / 서브 세트를 가져 와서 기능을 수행 할 수 있도록 모의 객체를 만들고 실패한 섹션을 찾을 때까지 살아있는 지옥을 테스트하십시오.
Ampt

더 이상 충돌이 발생하지 않을 때까지 코드 부분을 주석 처리하여 시작하십시오.
cpp81

1
Valgrind, Coverity 및 cppcheck 외에도 Asan 및 UBsan을 테스트 체제에 추가해야합니다. 코드가 corss-platofrm 인 경우 Microsoft의 엔터프라이즈 분석 ( /analyze) 및 Apple의 Malloc 및 Scribble 가드도 추가하십시오. 컴파일러 경고는 진단적이고 시간이 지남에 따라 더 좋아지기 때문에 가능한 한 많은 표준을 사용하여 가능한 많은 컴파일러를 사용해야합니다. 은 총알이 없으며 하나의 크기가 모든 것에 맞지는 않습니다. 툴과 컴파일러를 많이 사용할수록 각 툴의 장단점이 있으므로 적용 범위가 더 넓어집니다.

답변:


21

어려운 문제이지만 이미 본 충돌에서 더 많은 단서가 발견 될 것으로 보입니다.

  • 패턴을 찾기 위해 충돌에 대한주의 깊은 기록을 유지하고 있습니까?
  • 몇 곳이 정말 비슷합니까? 어떤 방법으로?
  • 손상된 데이터는 어떻게 생겼습니까? 제로? 아스키? 패턴?
  • 멀티 스레딩이 있습니까? 경쟁 조건이 될 수 있습니까?
  • 힙 관련입니까? free () 후에 부패가 발생합니까?
  • 스택 관련입니까? 스택이 손상 되었습니까?
  • 매달려있는 참조가 가능합니까? 신비하게 변한 데이터 가치?
  • 네트워크 트래픽 (버퍼 크기, 복구주기)에 특별한 것이 있습니까?

우리가 비슷한 상황에서 사용한 것들.

  • 많은 생산 주장. 피해가 전파되기 전에 조기에 예상대로 충돌합니다.
  • 많은 경비원. 로컬 변수 전후의 추가 데이터 항목, 객체 및 mallocs ()는 값으로 설정되어 자주 확인됩니다.
  • 전략적 로깅을 통해 이전에 발생한 상황을 정확하게 알 수 있습니다. 답변에 가까워 질수록 로깅에 추가하십시오.

필사적으로 상태를 저장하고 자동 다시 시작할 수 있습니까? 그렇게하는 몇 가지 프로덕션 소프트웨어를 생각할 수 있습니다.

도움이 필요하시면 언제든지 세부 정보를 추가하십시오.


이와 같이 심각하게 결정되지 않은 버그가 그다지 일반적인 것은 아니며 추가 할 수있는 것들이 많지 않다고 덧붙일 수 있습니까? 그들은 다음을 포함합니다 :

  • 동시성 : 스레딩, 경쟁 조건 등
  • 인터럽트 / 이벤트 / 콜백 / 예외 : 예기치 않은 상태 또는 스택 손상
  • 비정상적인 데이터 : 비정상적인 입력 데이터 / 타이밍 / 상태
  • 비동기 외부 프로세스에 대한 종속성

이것들은 집중할 코드의 일부입니다.


+1 모든 좋은 제안, 특히 어설 션, 가드 및 로깅.
andy256

귀하의 답변에 대한 답변으로 내 질문에 대한 추가 정보를 편집했습니다. 그것은 실제로 내가 아직 광범위하게 보지 않은 종료 할 때의 충돌을 생각하게 만들었습니다. 그래서 지금은 그것에 대해 생각할 것입니다.
Robin

5

malloc / free의 디버깅 버전을 사용하십시오. 그것들을 포장하고 필요한 경우 직접 작성하십시오. 많은 재미!

내가 사용하는 버전은 모든 할당 전후에 가드 바이트를 추가하고 해제 된 청크를 자유롭게 검사하는 "할당 된"목록을 유지 관리합니다. 이것은 대부분의 버퍼 오버런과 다중 또는 불량 "무료"오류를 포착합니다.

가장 교활한 부패의 원인 중 하나는 청크가 해제 된 후에도 계속 덩어리를 사용하는 것입니다. Free는 해제 된 메모리를 알려진 패턴 (전통적으로 0xDEADBEEF)으로 채워야합니다. 할당 된 구조에 "마법 번호"요소가 포함되어 있고 구조를 사용하기 전에 적절한 마법 번호에 대한 검사를 자유롭게 포함 할 수 있습니다.


1
Valgrind는 두 번의 free / free 데이터 사용을 잡아야합니까?
로빈

새로운 종류의 삭제 / 삭제를 위해 이런 종류의 과부하를 작성하면 수많은 메모리 손상 문제를 찾는 데 도움이되었습니다. 특히 삭제시 확인되는 가드 바이트는 프로그램 트리거 중단 점을 유발하여 자동으로 디버거에 빠지게합니다.
Emily L.

3

당신의 질문에서 당신이 말한 것을 역설하기 위해, 당신에게 확실한 대답을 줄 수는 없습니다. 우리가 할 수있는 최선은 찾아야 할 것들과 도구와 기법을 제안하는 것입니다.

일부 제안은 순진하게 보이며 다른 제안은 더 적용 가능할 수도 있지만 희망적으로 후속 조치를 취할 수 있기를 바랍니다. david.pfx답변 에는 올바른 조언과 제안이 있습니다.

증상에서

  • 나에게 그것은 버퍼 오버런처럼 들린다.

  • 관련 문제는 확인되지 않은 소켓 데이터를 첨자 또는 키 등으로 사용하는 것입니다.

  • 전역 변수를 사용하고 있거나 같은 이름의 전역 및 로컬을 사용하거나 한 플레이어의 데이터가 다른 플레이어를 방해하는 것일 수 있습니까?

많은 버그와 마찬가지로 어딘가에 잘못된 가정을하고있을 것입니다. 아니면 둘 이상일 수도 있습니다. 여러 상호 작용 오류는 감지하기 어렵습니다.

  • 모든 변수에 설명이 있습니까? 그리고 당신은 유효성 주장을 정의 할 수 있습니까?
    추가하지 않으면 코드를 스캔하여 각 변수가 올바르게 사용 된 것으로 보입니다. 의미가있는 곳에 어설 션을 추가하십시오.

  • 로트 어설 션을 추가하라는 제안은 좋은 것입니다. 첫 번째는 모든 함수 진입 점입니다. 인수 및 관련 글로벌 상태를 확인하십시오.

  • 장기 실행 / 비동기 / 실시간 코드를 디버깅하기 위해 많은 로깅을 사용합니다.
    모든 함수 호출에 대해 로그 쓰기를 다시 삽입하십시오.
    로그 파일이 너무 커지면 로깅 함수가 파일을 랩핑 / 전환 할 수 있습니다
    . 로그 메시지가 함수 호출 깊이로 들여 쓰기되는 경우 가장 유용합니다.
    로그 파일은 버그가 어떻게 전파되는지 보여줄 수 있습니다. 하나의 코드가 지연된 행동 폭탄의 역할을하는 옳지 않은 일을 할 때 유용합니다.

많은 사람들이 자체적으로 자란 로깅 코드를 가지고 있습니다. 어딘가에 오래된 C 매크로 로그 시스템이 있고 아마도 C ++ 버전이 있습니다 ...


3

다른 답변에서 언급 된 모든 것은 매우 관련이 있습니다. ddyer가 부분적으로 언급 한 중요한 것 중 하나는 malloc / free를 감싸는 것이 이점이 있다는 것입니다. 그는 몇 가지 언급하지만 매우 중요한 디버깅 도구를 추가하고 싶습니다. 모든 malloc / free를 몇 줄의 콜 스택 (또는 관심있는 경우 전체 콜 스택)과 함께 외부 파일에 기록 할 수 있습니다. 주의를 기울이면 쉽게 만들 수 있고 프로덕션 환경에서 사용할 수 있습니다.

당신이 묘사 한 바에 따르면, 개인적으로 추측 한 것은 메모리를 비우는 어딘가에 포인터에 대한 참조를 유지하고 더 이상 당신에게 속하지 않거나 포인터를 작성하지 않을 수 있다는 것입니다. 위의 기술로 모니터링 할 크기 범위를 유추 할 수 있으면 로깅 범위를 상당히 좁힐 수 있어야합니다. 그렇지 않으면, 어떤 메모리가 손상되었는지 알게되면, 로그에서 쉽게 만들었던 malloc / free 패턴을 알아낼 수 있습니다.

중요한 점은 언급했듯이 메모리 레이아웃을 변경하면 문제가 숨겨 질 수 있다는 것입니다. 따라서 로깅이 할당을하지 않으면 (가능한 경우!) 또는 가능한 한 적게하는 것이 매우 중요합니다. 메모리와 관련된 경우 재현성에 도움이됩니다. 또한 문제가 멀티 스레딩과 관련된 경우 가능한 한 빨리 도움이 될 것입니다.

또한 타사 라이브러리의 할당을 트랩하여 올바르게 기록 할 수 있도록하는 것도 중요합니다. 어디에서 왔는지 모릅니다.

마지막 대안으로 할당 할 때마다 최소 2 페이지를 할당하고 할당을 해제 할 때 매핑을 해제하는 사용자 지정 할당자를 만들 수도 있습니다 (할당을 페이지 경계에 정렬하고 페이지를 할당 한 후 액세스 할 수없는 것으로 표시). 페이지 끝에 할당하고 뒤에 페이지를 할당하고 표시 할 수없는 것으로 표시). 가상 메모리 주소를 적어도 한동안 새로운 할당에 재사용하지 않도록하십시오. 즉, 가상 메모리를 직접 관리해야합니다 (예비 및 원하는대로 사용). 이로 인해 성능이 저하되고 피드 할당량에 따라 상당한 양의 가상 메모리가 사용될 수 있습니다. 이를 완화하기 위해 64 비트로 실행하거나이를 필요로하는 할당 범위를 줄일 수 있다면 (크기에 따라) 도움이 될 것입니다. Valgrind는 이미이 작업을 잘 수행하고 있지만 문제를 해결하기에는 너무 느릴 수 있습니다. 몇 가지 크기 또는 객체에 대해서만이 작업을 수행하면 (해당 객체에 대해서만 특수 할당자를 사용할 수있는 경우) 성능에 미치는 영향이 최소화됩니다.


0

충돌하는 메모리 주소에 감시 점을 설정하십시오. GDB는 유효하지 않은 메모리를 유발 한 명령에서 중단됩니다. 그런 다음 역 추적으로 손상을 일으키는 코드를 볼 수 있습니다. 이는 손상의 원인이 아닐 수 있지만 각 손상의 감시 지점을 반복하면 문제의 원인이 될 수 있습니다.

그런데 질문에 C ++ 태그가 지정되어 있으므로 참조 카운트를 유지하여 소유권을 관리하고 포인터가 범위를 벗어난 후에 메모리를 안전하게 삭제하는 공유 포인터를 사용하는 것이 좋습니다. 그러나 순환 종속성을 거의 사용하지 않으면 교착 상태가 발생할 수 있으므로주의해서 사용하십시오.

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.