C/C++로 프로그램을 개발하는 과정에서 아주 난감해지는 경우 중 하나는, 바로 Debug 빌드와 Release 빌드의 실행 결과가 서로 다를 때이다. 개발 중이던 Debug 빌드 스냅샷에서는 잘만 돌아가는 프로그램이 정작 최적화된 Release 빌드에서는 이따금씩(항상도 아니고!) 에러가 난다면?

이런 버그는 문제를 찾아내려고 정작 디버거를 붙여서 실행할 때는 재연되지 않는 경우가 태반이어서 프로그래머를 더욱 애먹인다. 특히 복잡한 멀티스레드와 관련된 버그라면 그저 묵념뿐..;; 하지만 그런 특수한 경우가 아니라면, Debug와 Release의 실행 결과가 다른 이유는 본인의 경험상 거의 대부분이 초기화되지 않은 변수 때문이었다.

비주얼 C++은 Debug 빌드에서는 초기화되지 않은(공간 확보만 해 놓고 프로그램이 아직 건드리지는 않은) 메모리의 영역을 티가 나는 값으로 미리 표시도 해 놓고 아주 특수하게 취급해 준다. 메모리를 할당해도 좌우에 여분을 두고 좀 넉넉하게 할당하며, 때로는 그 넉넉한 여분 공간의 값이 바뀐 것을 감지하여(바뀌어서는 안 되는데) 배열 첨자 초과 같은 에러를 알려 주기도 한다. 프로그래머의 입장에서야 이건 꽤 유용한 기능이다.

그러나 Release 빌드에는 이런 거추장스러운 작업이 물론 전혀 없다. 그러니 메모리 범위를 초과한다거나, 읽어서는 안 되는 엉뚱한 주소의 메모리로부터 값을 읽거나, 올바른 영역이더라도 초기화되지 않은 쓰레기 값을 얻었을 때의 결과는 두 빌드가 서로 극과 극으로 달라질 수밖에 없다.

이렇게, 빌드 configuration에 따라 동작이 달라지는 코드는 두말 할 나위도 없이 결함이 들어있는 faulty 코드이다. 이런 코드에서 문제의 원인을 찾는 건 극도로 어려운 일이다. 서울에서 김 서방 찾기, 모래사장에서 바늘 찾기, 사격장에서 흘린 탄피 찾기가 따로 없다. ㅜㅜ 자기가 짠 코드에서 결함을 찾는 것도 어려워 죽겠는데 하물며 회사 같은 데서 남이 짠 faulty 코드를 인수인계 받았다면... -_-;;;

(본인이 다니던 모 병특 회사에서 본인의 직속 상사는 이렇게 말했다. “그런 코드를 짜는 건 프로그래밍을 하는 게 아니라 똥을 싸는 거다.” 공감한다. -_-)

C/C++은 물론 간단한 지역 변수에 대해서야 ‘이 변수를 초기화하지 않고 사용했습니다’ 같은 지적을 컴파일 시점에서 해 준다. 그러나 복잡한 포인터나 배열로 가면 일일이 그 용법이 올바른지 컴파일 시점에서 판단하지는 못한다. 그저 프로그래머가 조심해서 코드를 작성하는 수밖에 없다.

이와 관련된 본인의 경험을 소개하겠다.
꽤 옛날에 짜 놓은 비주얼 C++ MFC 기반 GUI 프로그램 소스의 내부에서, 핵심 알고리즘만 떼어내서 다른 콘솔 프로그램에다 붙여야 할 일이 있었다.
그 당시에는 나름 구조적으로 프로그램을 만든 것이지만, 지금 관점에서 모듈간의 cohesion은 여전히 개판오분전이었던지라 상당수의 코드를 리팩터링해야 했다.

그래서 코드를 붙였는데, 원래의 GUI 프로그램에서는 잘 돌아가던 코드가 새로운 프로젝트에서는 얼마 못 가서 뻗어 버렸다. Debug 빌드와 Release 빌드의 실행 결과가 다른 건 두말 할 나위도 없거니와, 심지어 같은 Release 빌드도 F5 디버거를 붙여서 실행하면 별 탈이 없는데 그냥 실행하면 뻗었다! 이건 스레드 쓰는 프로그램도 아닌데! 이거야말로 제일 골치 아픈 경우가 아닐 수 없었다.

Debug 빌드는 Release 빌드보다 워낙 느리게 돌아가고, Release 빌드도 디버거를 붙였을 때와 그렇지 않았을 때 성능이 살짝 달라진다. 그러니 앞에서 언급했듯이 스레드 관련 race condition은 영향을 받을 수 있다. 하지만 그런 것도 아니라면? 의심스러운 배열은 무조건 다 0으로 초기화하고, 혹시 내가 리팩터링을 하면서 실수를 하지는 않았는지 몇 번이나 꼼꼼이 살펴봤지만 문제는 눈에 띄지 않았다.

별 수 있나. printf 로그를 곳곳에다 박아 넣어서 의심스러운 부분을 추적한 뒤 다행히 문제를 찾아냈다.
게임 같은 리얼타임 시스템에서는, 심지어 디버그 로그 찍는 코드만 추가해도 버그가 쏙 숨바꼭질을 해 버리는 막장 중의 막장 경우도 있다만 내 프로그램은 그런 정도는 아니어서리..;;

사실은 기존 GUI 프로그램에서 돌아가던 코드에서부터 문제가 있었다.
배열을 선언했는데, 0~1번 인덱스에 접근할 일이 없어서

ptrData = new char[100];
ptrData-=2;

같은 잔머리를 굴려 줬던 것이다. 요런 짓을 옛날에 Deap 자료구조를 구현할 때도 했던 것 같다.
그러니 이 포인터로는 0과 1번 인덱스를 건드리지 않아야 하는데...
그런데 그것이 실제로 일어났습니다. ㄲㄲㄲㄲㄲ

그 허용되지 않는 메모리의 상태가 GUI 프로그램과 콘솔 프로그램, 심지어 같은 프로그램도 Debug와 Release, 디버거 붙이냐 안 붙이냐 여부에 따라 싹 달라져서 나를 골탕먹였던 것이다. 예전에는 수 년째 아무 탈 없이 잘 돌아가던 코드가 말이다.
저런 간단하고 고전적인 배열 첨자 초과 문제가 이런 결과를 야기할 줄 누가 알았을까?

C/C++은 내가 짠 코드를 내가 완전히 책임질 수 있고 컴퓨터 관점에서의 성능· 능률· 최적화가 중요한 해커나 컴덕후에게는 가히 환상적인 언어이다. 이보다 더 좋을 수가 없다. 예전에 내가 비유했듯, 세벌식이 기계 능률과 인체 공학적인 특징을 잘 살린 것만큼이나 이 언어는 고급 언어의 특성과 기계적인 특성을 꽤-_- 잘 절충했다.

그러나 언어의 구조적으로 가능한 무질서도가 너무 높은 것도 사실. C/C++가 까이는 면모 자체가 크게 (1) 언어 자체의 복잡도 내지 결함 그리고 (2) unmanaged 환경이라는 여건 자체라는 두 갈래로 나뉘는 양상을 보인다. 오늘날의 소프트웨어 시스템에서 프로그래밍 언어는 모름지기 수십, 수백만 줄의 프로젝트에서 살인적인 복잡도를 제어 가능해야 하고, 작성한 코드의 최소한의 품질과 안전성이 보장되어야 하며, 또 무엇보다도 빨리빨리 빌드가 돼야 하는데 C/C++은 영 한계를 보이기도 한다.

뭐, 그래도 이미 C/C++로 작성된 코드가 너-_-무 많고 그것도 다들 중요한 저수준 계층에 있다 보니, 이 언어가 쉽게 없어지지는 않을 것이고 특히 C++은 몰라도 C는 절대 안 없어지리라.. ㅋㅋ 프로그래밍 언어의 라틴어급.

C/C++과는 전혀 다른 언어이다만, 과거엔 QuickBasic도 IDE에서 돌리는 프로그램과, 실제로 컴파일-링크를 한 EXE의 실행 모습이 대동소이하게 달라서 프로그래머를 애먹이기도 했다. 물론 이건 C/C++에서의 Debug/Release와는 다른 양상 때문에 차이가 나는 경우이다.
결론은, 프로그램 작성하다가도 틈틈이 Release 형태로 최종 결과물을 확인하는 게 필요하다. ^^

Posted by 사무엘

2011/06/22 08:23 2011/06/22 08:23
,
Response
No Trackback , 6 Comments
RSS :
http://moogi.new21.org/tc/rss/response/529

Trackback URL : http://moogi.new21.org/tc/trackback/529

Comments List

  1. 주의사신 2011/06/22 08:55 # M/D Reply Permalink

    1. 졸업 작품 만들 적에 변수 초기화를 하나 안 했더니, 디버거에서는 돌아가다가, 실제로 실행하면 안 되고, 또 어떻게 빌드하면 디버거에서는 안 돌아가고, 실제 실행하면 돌아가고....

    이 문제의 원인을 찾느라 한 나절 반 정도 썼던 것 같네요.

    그리고서 내린 결론.

    "버그는 운이나 우연에 의해 생기는 것이 아니라 잘못된 개발 습관에 의한 필연적인 결과이다."


    2. 들어보셨겠지만, printf 같은 것을 사용했을 때 버그가 사라지는 것을 '하이젠버그'라고 합니다. 원자 등을 다루는 세계에서는 빛을 쏘면 광자랑 입자가 충돌해서 그 성질이 변하기 때문에 잘 관찰하기 어렵다는 '하이젠베르그의 불확정성 원리'에서 출발한 용어이지요.


    3. 졸업 작품 프로젝트 할 적에는 Debug 빌드 자체를 없애 버렸답니다. Lua Dll의 최적화 레벨(3)과 프로그램의 최적화 레벨(2)가 충돌해서 버그를 만들어내더군요. 이거 해결하려면, 모든 Dll의 Debug버전을 구해야 하는데, 이건 아무리 생각해도 아닌지라....


    4. Release 버전 빌드 시에는 다음 프로그램을 사용해 주면 좋습니다.

    http://ospace.tistory.com/113

    DbgView라는 프로그램인데요. OutputDebugString의 내용을 보여 줍니다.

    1. 사무엘 2011/06/22 14:13 # M/D Permalink

      의견 감사합니다. ^^

      1. 초기화되지 않은 변수는 정말 만악의 근원입니다. 배열이나 구조체 멤버는 컴파일러가 제대로 체크도 못 하기 때문에 상황이 더욱 심각하죠. 초기화되지 않은 변수라는 개념 자체가 존재하는 언어가 C/C++말고 또 있을까 싶습니다.

      2. 하이젠버그 맞습니다. 뭔가 사고가 나면 일단 현장 보존이 최우선인데... 저건 read-only operation조차 상태를 바꾸는 operation으로 바꾸면서 개발자를 더욱 패닉에 빠뜨리는 상황이죠.

      3. 모듈간에 debug와 release configuration이 충돌하는 건 보통 C 라이브러리의 충돌 때문인 경우가 많습니다. debug CRT를 쓰는 DLL이 malloc해서 넘겨 준 포인터를 release CRT를 쓰는 EXE가 free하는 경우.. 더 말이 필요 없죠. 이것도 엄밀히 말해서 그리 좋은 디자인이 아니며, DLL은 자기가 쓰는 메모리 할당/해제 함수를 외부에도 공개해야 할 것입니다.

      4. 사실, 윈도우 NT 계열에서도 OutputDebugStringW는 내부적으로는 버퍼를 ansi로 바꿔서 A 함수를 호출한답니다. 따라서 디버깅 로그에는 원래 유니코드 문자가 표기되지 않습니다.

  2. 김 기윤 2011/06/22 16:47 # M/D Reply Permalink

    1. 초기화되지 않은 변수-_-로 피해를 입은 적인 한두번이 아니다보니 이쪽은 아예 도가 터버렸습니다.

    2. Debug모드와 Release 모드의 속도차이-_-로 발생한 버그는 저도 겪어본 적이 있습니다. 프로그램이 실행 된 뒤 경과 시간을 얻어내는 함수를 사용해서 그 값을 Rand의 seed 로 주는 코드에서 문제가 발생했었는데, debug모드는 잘 되는데 release모드에서 안 된 이유가 하필이면 그 seed 로 0이 들어갔는데, 0 에서 문제가 발생하는 rand 코드였던...

    1. 사무엘 2011/06/23 00:54 # M/D Permalink

      C/C++로 본격적인 프로그램 개발을 해 본 분이라면 본문 내용에 정말 공감할 겁니다. =_=
      이거 다음으로는, DLL과 EXE 사이에 함수 calling convention이 일치하지 않아서 괴상한 버그가 발생하던 게 저의 기억에 남습니다.

  3. 김 기윤 2011/06/23 16:28 # M/D Reply Permalink

    그러고보니 꽤 유니크한 경우로 Debug에서 빌드가 안되고 Release 에서 빌드가 되던(컴파일은 양쪽 다 되는데 Debug쪽에서 링킹 거부) 괴상한 상황이 있었는데, 나중에 알고보니 사용한 라이브러리는 Multithread-Debug 인데 현재 프로젝트에서는 Multithread-Debug DLL 의 불일치로 인해서 생긴 링킹거부였던 적도 있습니다. 다만, 원인을 알기 전까지는 1주일가량 삽질의 삽질을 거듭했었죠. orz

    1. 사무엘 2011/06/23 23:36 # M/D Permalink

      http://minjang.egloos.com/2146607 참조.
      말씀하신 것처럼, C 라이브러리의 사용 형태(특히 static이냐 DLL이냐)가 모듈간에 꼬여도 정말 무진장 골치 아프죠.
      링크 에러는 컴파일 에러와는 달리 행번호 정보도 안 나오니 더욱 난감합니다.
      소스 코드뿐만이 아니라 프로젝트 설정이나 컴파일 옵션이 잘못되어 나기도 하는 에러이니까요.

Leave a comment
« Previous : 1 : ... 1683 : 1684 : 1685 : 1686 : 1687 : 1688 : 1689 : 1690 : 1691 : ... 2139 : Next »

블로그 이미지

그런즉 이제 애호박, 단호박, 늙은호박 이 셋은 항상 있으나, 그 중에 제일은 늙은호박이니라.

- 사무엘

Archives

Authors

  1. 사무엘

Calendar

«   2024/04   »
  1 2 3 4 5 6
7 8 9 10 11 12 13
14 15 16 17 18 19 20
21 22 23 24 25 26 27
28 29 30        

Site Stats

Total hits:
2668340
Today:
61
Yesterday:
3517