소스 변수와 저장 공간이 항상 일치하지는 않는다
디버거에서 변수가 optimized out으로 보이면 메모리가 사라졌거나 디버거가 고장 났다고 생각하기 쉽습니다. 실제로는 컴파일러가 계산을 단순화하면서 그 변수의 독립적인 저장 공간을 만들지 않았을 수 있습니다. 상수로 대체된 값, 다른 계산에 합쳐진 값, 특정 구간에서만 레지스터에 있던 값은 소스 이름으로 항상 조회할 수 없습니다. 프로그램이 실행하는 것은 소스 자체가 아니라 최적화된 명령어라는 점을 기억해야 합니다. 변수 하나가 보이지 않는 것만으로 초기화가 안 됐다고 결론 내리기보다 현재 빌드에 어떤 최적화가 적용됐는지 먼저 확인합니다.
관찰용 빌드와 재현용 빌드를 구분하기
처음 어셈블리를 배우는 작은 예제에서는 -g -O0 조합이 소스와 명령어를 대응시키기 편합니다. 하지만 실제 배포 빌드에서만 발생하는 문제를 조사할 때 무조건 최적화를 끄면 오류 자체가 사라질 수 있습니다. 디버그 정보와 최적화는 서로 배타적인 설정이 아니므로 실제 문제가 나는 빌드 조건을 남겨 두는 것이 중요합니다. GCC는 디버깅을 고려한 -Og 옵션도 제공합니다. 어느 옵션이 적합한지는 학습용 관찰인지 실제 실패의 재현인지에 따라 달라집니다. 빌드 설정을 바꿨다면 기존 실행과 달라진 조건을 기록하고 이전 결과와 섞어서 해석하지 마세요.
줄 번호가 건너뛰는 이유 찾기
최적화된 프로그램에서는 소스의 계산이 합쳐지거나 반복문 밖으로 이동할 수 있습니다. 그 결과 줄 단위 실행이 예상과 다르게 보이고 함수 호출이 인라인으로 바뀌어 별도의 프레임이 나타나지 않을 수도 있습니다. 이런 상황에서 같은 줄을 여러 번 보거나 다음 줄을 건너뛴다고 바로 제어 흐름 오류를 의심하면 조사 방향이 틀어집니다. DBG Desk는 명령어 단위로 실행하므로 실제 분기와 값의 이동을 확인할 수 있습니다. 다만 특정 소스 표현이 어느 명령어에 대응하는지는 디버그 정보의 품질과 최적화 정도에 영향을 받습니다. 주소 이동과 호출 스택을 함께 읽으세요.
보이지 않는 값 대신 확인 가능한 경계를 잡기
특정 지역 변수를 조회할 수 없다면 함수에 들어오는 입력과 나가는 결과처럼 관찰 가능한 경계를 찾습니다. 함수 호출 직전과 직후의 레지스터, 출력 버퍼의 내용, 외부에 기록된 결과를 비교하면 중간 변수 없이도 계산이 틀어지는 구간을 좁힐 수 있습니다. 필요하다면 작은 재현 프로그램으로 계산을 분리하되 타입과 컴파일 옵션을 유지하세요. 단순히 변수를 보려고 volatile을 붙이는 방법은 프로그램의 최적화와 동작 조건을 바꾸므로 일반적인 수정으로 삼지 않는 편이 좋습니다. 원래 실패 조건과 관찰을 쉽게 만든 조건에서 각각 무엇을 확인했는지 구분해 남깁니다.
두 빌드를 비교할 때 남길 항목
관찰용 파일과 실패를 재현하는 파일을 같은 이름으로 덮어쓰지 말고 별도 폴더에 보관해 보세요. 각각의 컴파일 옵션과 실행 입력을 기록하면 최적화가 없는 버전에서만 보이는 변수 값을 배포 버전에도 그대로 적용하는 실수를 줄일 수 있습니다. 한쪽에서 함수가 인라인 처리되었다면 스택 모양도 달라질 수 있으므로 함수 이름만 비교하지 말고 최종 출력과 실패 조건을 함께 봅니다. 빌드를 바꿔 문제가 사라진 현상은 원인을 찾았다는 결론이 아닙니다. 변경된 최적화가 어떤 계산이나 메모리 배치에 영향을 줬는지 추가 가설을 세울 근거로 사용하는 것이 적절합니다.
gcc -g -O0 example.c -o example-debug.exe
gcc -g -O2 example.c -o example-optimized.exe