1. 개요
소프트웨어 개발자가 임의의 소프트웨어에서 버그를 쉽게 찾도록 디버깅을 도와주는 개발자 도구.개발 중인 소스의 버그를 찾는 용도 외에도 이미 존재하는 프로그램의 분석 및 리버스 엔지니어링을 위해 사용되기도 하며, 따라서 컴파일 언어의 경우 디컴파일러의 용도와 겹치거나 디컴파일러 기능을 내장하고 있기도 한다.
2. 구조
2.1. 기능
- step-by-step execution: 디버거의 핵심 기능 중 하나. 보통 명령 또는 tree단위로 프로그램을 수행시키고, 개발자가 설정한 브레이크 포인트(break point, 중단점) 앞에서 실행을 멈춘다. step-in은 하위 프레임 호출 안으로 들어가기(=스택 위로 프레임을 쌓기)를 의미하고, step-out은 주로 현재 실행중인 프레임을 반환하기(=스택에서 프레임 pop하기)를 의미한다.
- Hardware breakpoint: 하드웨어 의존적인 브레이크 포인트, 프로세서 내의 디버깅 관련 레지스터에 중단 지점을 설정한 후 프로세서가 EIP에 도달하면 프로세서 차원에서 실행을 멈춘다. 오버헤드가 가장 적고 임베디드 환경 같은 경우 JTAG와 같은 물리적인 방법으로 디버깅을 하는 경우에도 제어가 가능하다. 다만 프로세서의 레지스터는 한정적이므로 하드웨어 중단점은 무한정 생성할 수 없다. x86 프로세서를 예로 들면 DR0-3 레지스터가 하드웨어 중단점을 관리하며, 이에 따라 최대 4개의 하드웨어 중단점을 설정할 수 있다.
- Software breakpoint: 보통 디버거의 중단점은 이 방식을 기본으로 한다. 중단점이 걸린 코드는 인터럽트 명령어로 대체되며, 이 인터럽트에 도달하면 도치된 명령어를 원 명령어로 변경한 후, 실행을 계속한다. Step-by-step 디버거도 이 방법을 응용한 것으로, 프로그램 코드를 모두 인터럽트 명령어로 대체해 한 명령어 단위로 실행하는 효과를 낸다.
- Watch: 특정 메모리 주소의 접근을 감지한다. 변수와 같은 메모리 내의 주소에 CPU가 접근하는 경우 인터럽트를 내며 중단시키거나 횟수를 센다. 보통 읽기, 쓰기 조건을 별도로 설정하는 것이 가능하다.
- Stack trace: 프로그램의 스택을 추적한다. 프로그램이 segfault 와 같은 치명적인 오류를 내면서 중단되는 경우 EIP가 위치한 함수를 거슬러 어떤 경로로 실행되었는지 추적하는 방법으로도 사용된다.
- Memory Dump: 실행중인 프로세스의 메모리를 덤프한다.
- Injection: 원격 코드를 실행중인 프로세스에 주입시키거나, 변경한다.
- Debug Server / Client: 원격으로 실행중인 프로세스에 네트워크 또는 시리얼 통신 등을 통하여 다른 컴퓨터 등에서 디버깅 작업을 할 수 있다. 주로 사용되는 영역은 커널 드라이버로 커널에서 도는 디바이스 드라이버 특성상 segfault와 같이 더이상 진행할 수 없는 오류가 생기는 경우 시스템 자체가 터지게 되는데 원격으로 연결된 상태라면 커널 내에서 중단이 발생한 상태여도 디버깅을 진행할 수 있다.
- Static/Dynamic Analysis: 구문 분석을 통한 로직 탐색을 수행하는 정적 분석 기능과 프로그램의 런타임에서 분석을 시도하는 동적 분석으로 나뉜다. 프로그램을 디버거와 함께 실행시키며 오류가 발생하는 지점에 도달하면 해당 부분부터 사람이 직접 개입한다. 또 다른 목적은 리버스 엔지니어링 분야로, 특히 컴퓨터 바이러스등과 같이 코드 바이너리가 패킹 되어 있는 경우 단순 정적 분석을 통한 동작 원리 분석은 불가능하다. 하지만 동적 분석은 프로그램의 코드를 실제로 실행시켜 가면서 분석하게 되므로 로직 분석을 가능하게 한다.
3. 종류
3.1. 백엔드와 프런트엔드
디버거라는 것이 언어마다 구현 방법이 천차만별이지만, 안 그래도 복잡한 디버깅 효율을 최대한으로 끌어올리려면 GUI로 현재 중단점, 컨텍스트 트리, 호출 스택, 메모리 뷰 등 다양한 정보를 IDE에 시각적으로 통합(integration)해 보여 줄 필요가 있다. 이렇게 되면 [math(N)]개의 IDE마다 [math(M)]개의 언어별로 일일히 디버깅 지원을 추가해야 하는데, 총 [math(N\times M)]개의 디버깅 지원을 구현한다는 것은 개발자의 관점에서 결코 좋은 설계가 아니다.따라서 보통의 경우 컴파일러나 언어 서버의 백엔드와 프런트엔드를 분리하듯이, 개별 언어별로 추상적인 디버거 백엔드를 한 번만 만들고 개별 IDE 제조사들은 Debug Adapter Protocol 등으로 이 둘을 통합해 재사용성을 높이는 방식의 설계를 사용한다. 실제로 대부분의 IDE가 자체 디버거를 내장한 것 같아도 백엔드로 gdb 등이나 다른 외부 디버거를 라이브러리 형태로 링크하고 있는 경우가 많다.
디버거 백엔드를 단지 라이브러리로 링크하는 것뿐 아니라 디버거를 별도 프로세스로 실행하고 프런트엔드(IDE) 역시 실행해 프로세스간 통신 또는 OSI L7 수준의 프로토콜로 통신하는 식의 구현도 늘어나자 디버거 백엔드-프런트엔드라는 용어 대신 디버거 서버-클라이언트 등의 용어도 사용되는 편.
이러한 구조적 분리로 인해 얻는 부가적 혜택 중 또 하나는 remote debugging 지원으로, SSH 등을 통해 서버에 원격 개발 환경을 구축한 경우 서버에서 빌드된 아티팩트를 유저 환경까지 매번 복사해 가져와서 디버거를 실행할 필요가 없어진다. 굳이 필요한가 의문이 들 수 있지만, 예를 들어 Windows에서 UNIX API를 사용하는 프로그램을 개발한다면 당연히 관련 디버깅 환경을 구축하는 데에도 많은 시간이 소모될 것이고, 원격 개발 환경을 사용한다면 이러한 문제를 피할 수 있다.
4. 목록
4.1. 백엔드
아래 항목들은 디버거 구현의 사실상 핵심을 담당하는 만큼 간단하지만 독립적인 프런트엔드를 내장하고 있어, 완제품(complete) 디버거를 겸하기도 한다. 따라서 흔히 디버거라고만 하면 아래 항목들만을 의미한다.- gdb
- lldb
- 브라우저 개발자 도구 - 브라우저 개발자 도구는 주로 인패널 JavaScript 디버거로 쓰이는 경우가 많긴 하지만, Chrome의 경우 CDP(chrome devtool protocol)를 통해 헤드리스 원격 디버깅을 지원하는 방식으로 Visual Studio Code 등 다른 IDE의 디버깅 백엔드로 사용될 수 있다.
- IDA
4.2. 프런트엔드
개별 IDE에 내장 및 플러그-인 되는 방식으로 구현된 프런트엔드. 흔히 '디버깅 지원', '디버거 지원', '디버거 플러그인' 등으로 불린다.==# 기타 #==