Perl 디버거 내부

Perl 디버거 내부 (perldebguts)

이 문서는 디버거 사용법을 알려주는 perldebug가 아니에요. 여기서는 디버거 내부의 낮은 수준 세부 사항을 다뤄요. Perl 내부에 아주 밝지 않은 사람에겐 어렵거나 이해 불가능한 내용이 많다는 점을 미리 말씀드릴게요(caveat lector).

출처: perldoc - perldebguts

본문

디버거 내부 (Debugger Internals)

Perl은 디버깅 환경을 만드는 데 쓰는 컴파일-타임·런-타임 특수 훅을 갖고 있어요. 이 훅을 perlrun에 설명된 perl -Dxxx 명령과 혼동하면 안 돼요(그건 특별히 빌드한 Perl에서만 쓸 수 있어요).

예를 들어 DB 패키지에서 내장 caller 함수를 부르면, 해당 스택 프레임이 호출될 때 받은 인자가 @DB::args 배열로 복사돼요. 이런 메커니즘은 -d 스위치로 Perl을 호출할 때 활성화돼요. 구체적으로 다음 기능들이 켜져요:

  • $ENV{PERL5DB}의 내용(없으면 BEGIN {require 'perl5db.pl'})이 프로그램 첫 줄 앞에 삽입돼요.
  • -dt 스위치를 주면 PERL5DB_THREADED=1이 환경에 설정돼요.
  • 각 배열 @{"_<$filename"}은 Perl이 컴파일한 파일 $filename의 줄들을 담아요. eval 문자열(서브루틴 포함)도 마찬가지고, 그 $filename(eval 34)처럼 보여요. 이 배열의 값은 숫자 컨텍스트에서 마법이에요: 줄이 중단 불가능할 때만 0과 같다고 비교돼요.
  • 각 해시 %{"_<$filename"}은 줄 번호를 키로 하는 중단점·액션을 담아요. 개별 항목은 설정 가능해요. Perl은 불리언 참만 신경 쓰며, perl5db.pl이 쓰는 값은 "$break_condition\0$action" 형태예요.
  • 각 스칼라 $ {"_<$filename"}$filename을 담아요.
  • require된 파일이 컴파일된 후 실행 전에, DB::postponed 서브루틴이 있으면 DB::postponed(*{"_<$filename"})이 호출돼요.
  • 각 서브루틴 subname 컴파일 후 $DB::postponed{subname} 존재를 검사하고, 있으면 DB::postponed(subname) 호출.
  • %DB::sub 해시는 키가 서브루틴 이름이고 값이 filename:startline-endline 형태인 역추적용 해시예요.
  • 실행이 중단점을 둘 수 있는 지점에 도달하면, $DB::trace·$DB::single·$DB::signal 중 하나라도 참이면 DB::DB() 서브루틴이 호출돼요. 이 변수들은 local 불가능해요. DB::DB() 내부(그리고 그 안에서 부른 함수)에서는 비활성화돼요(단, $^D & (1<<30)이 참이면 제외).
  • 실행이 서브루틴 호출에 도달하면 &DB::sub(args)가 대신 호출되고 $DB::sub에 호출된 서브루틴이 설정돼요.
  • goto로 non-XS 서브루틴에 들어가고 $^P의 0x80 비트가 설정돼 있으면 &DB::goto 호출.
  • 표준 디버거의 &DB::sub$DB::deep 변수에 의존해요(디버거 재귀 깊이 제한). $DB::deep이 없으면 서브루틴 호출이 불가능해요.

나만의 디버거 작성하기 (Writing Your Own Debugger)

PERL5DB 환경 변수로 디버거를 정의할 수 있어요. 최소한의 "동작하는" 디버거는 한 줄이에요:

sub DB::DB {}
$ PERL5DB="sub DB::DB {}" perl -d your-script

조금 더 유용한 버전은 이러해요:

sub DB::DB {print ++$i; scalar <STDIN>}

실제로 유용한 디버거는 서브루틴 호출의 순번과 이름을 찍어요:

{
  package DB;
  sub DB  {}
  sub sub {print ++$i, " $sub\n"; &$sub}
}

시작 시 디버거는 rc 파일(./.perldb 또는 ~/.perldb)을 읽고 옵션을 설정해요. &afterinit 서브루틴도 여기 정의할 수 있고, 디버거 초기화가 끝난 뒤 실행돼요. 그다음 PERLDB_OPTS 환경 변수를 읽어 o ... 명령의 인자처럼 처리해요.

디버거 내부 변수 (Debugger Internal Variables)

  • @DB::dbline : @{"::_<current_file"}의 별명. 현재 선택된 파일의 줄들을 담아요.
  • %DB::dbline : %{"::_<current_file"}의 별명. 현재 파일의 줄 번호별 중단점·액션.

디버거 커스터마이즈 함수 (Debugger Customization Functions)

  • DB::parse_options(string) : perldebug의 옵션을 파싱.
  • DB::dump_trace(skip[,count]) : 지정 프레임을 건너뛰고 호출 프레임 목록 반환. 각 항목은 context·sub·args·file·line 키의 해시 참조.
  • DB::print_trace(FH, skip[, count[, short]]) : 호출자 프레임 정보를 형식화해 출력.

이 문서(perldebug 포함)에 문서화되지 않은 변수·함수는 내부 전용으로 간주하며 통보 없이 바뀔 수 있어요.

프레임 나열 출력 예시 (Frame Listing Output Examples)

frame 옵션으로 프레임 정보 출력을 제어할 수 있어요. 예를 들어 이 표현식 트레이스를:

$ perl -de 42
...
main::(-e:1):   0
  DB<1> sub foo { 14 }
  DB<2> sub bar { 3 }
  DB<3> t print foo() * bar()
main::((eval 172):3):   print foo() + bar();
main::foo((eval 168):2):
main::bar((eval 170):2):
42

option frame=2를 설정한 결과와 비교해 보세요:

  DB<5> t print foo() * bar()
3:      foo() * bar()
entering main::foo
 2:     sub foo { 14 };
exited main::foo
entering main::bar
 2:     sub bar { 3 };
exited main::bar
42

급할 때 frame의 비트 의미: 비트 2가 설정되면 서브루틴 퇴장 시에도 줄 출력. 비트 4는 인자·호출자 정보, 비트 8은 tied·참조여도 인자 출력. 비트 16은 반환 값까지 출력. 패키지 컴파일 시 Package lib/Carp.pm. 같은 줄이 들여쓰기와 함께 출력돼요.

정규식 디버깅 (Debugging Regular Expressions)

정규식 디버깅 출력을 켜는 두 가지 방법이 있어요.

-DDEBUGGING으로 빌드된 Perl이라면 명령줄 -Dr 플래그(더 상세히는 -Drv)를 써요. 그렇지 않으면 use re 'debug'를 써요(컴파일·실행 타임 모두에 영향). Perl 5.9.5부터 이 프래그마는 렉시컬 스코프예요.

컴파일-타임 출력 (Compile-time Output)

Compiling REx '[bc]d(ef*g)+h[ij]k$'
size 45 Got 364 bytes for offset annotations.
first at 1
rarest char g at 0
   1: ANYOF[bc](12)
  12: EXACT <d>(14)
  14: CURLYX[0] {1,32767}(28)
  ...
anchored 'de' at 1 floating 'gh' at 3..2147483647 (checking floating)
      stclass 'ANYOF[bc]' minlen 7

첫 줄은 정규식의 컴파일 전 형태, 두 번째는 컴파일된 크기와 오프셋/길이 테이블 바이트 수, 그다음은 첫 매칭 노드의 라벨·id를 보여줘요. 최적화 정보 줄에는 anchored·floating·stclass·minlen·noscan·isall·GPOS·plus·implicit·with eval·anchored(TYPE) 같은 필드가 나올 수 있어요. 이 최적화 정보는 확실히 매칭되지 않을 문자열에 대해 (느린) 정규식 엔진 진입을 피하는 데 쓰여요. isall 플래그가 설정되면 엔진 호출 자체를 피할 수 있어요.

노드 목록은 id: TYPE OPTIONAL-INFO (next-id) 형식이에요. 전체 노드 타입(END, SUCCEED, SBOL/MBOL/SEOL/MEOL/EOS/GPOS, BOUND 계열, REG_ANY, ANYOF 계열, POSIX 클래스, BRANCH, EXACT 계열, TRIE/AHOCORASICK, NOTHING/TAIL, STAR/PLUS/CURLY 계열, WHILEM, OPEN/CLOSE/REF, LONGJMP/BRANCHJ, IFMATCH/UNLESSM/SUSPEND/IFTHEN/GROUPP, EVAL, MINMOD/LOGICAL, GOSUB, 백트래킹 동사들, OPTIMIZED/PSEUDO 등)의 자세한 설명이 원문에 이어져요.

그다음은 오프셋/길이 테이블 덤프가 나와요. offset[length] 쌍의 목록으로, 각 노드가 컴파일 전 정규식의 어느 문자 위치에서 시작·얼마나 길게 있는지 보여줘요. 0[0] 항목은 대응 노드가 없다는 뜻이에요.

실행-타임 출력 (Run-time Output)

매칭 중에 디버깅이 켜져 있어도 실행-타임 출력이 없을 수 있어요. 이는 정규식 엔진에 진입하지 않았고 최적화기가 전부 처리했다는 뜻이에요. 엔진에 진입했다면:

  Matching '[bc]d(ef*g)+h[ij]k$' against 'abcdefg__gh__'
    Setting an EVAL scope, savestack=3
     2 <ab> <cdefg__gh_>    |  1: ANYOF
     3 <abc> <defg__gh_>    | 11: EXACT <d>
     ...

가장 중요한 정보는 현재 테스트 중인 컴파일된 정규식의 노드예요. 줄 형식은 STRING-OFFSET <PRE-STRING> <POST-STRING> |ID: TYPE이에요. TYPE 정보는 백트래킹 레벨에 따라 들여쓰기돼요.

Perl 메모리 사용 디버깅 (Debugging Perl Memory Usage)

Perl은 메모리 사용에 있어 꽤 낭비적이에요. 정수는 최소 20바이트, float은 24바이트, 문자열은 32바이트(경험치, 32비트 기준)를 넘지 못해요. 두 세 가지 방식으로 접근하는 변수는 20바이트가 더 늘 수 있어요. 반대로 sub foo; 한 줄이 버전에 따라 최대 500바이트를 차지할 수도 있어요. 소스→컴파일 코드는 대략 8배 bloating된다는 경험적 추정이 있어요.

-DL 커맨드라인 스위치는 Perl 5.6.0 경에 폐기됐어요(-DDEBUGGING 빌드에서만 가능했음). 요즘엔 Purifyvalgrind 같은 malloc 디버깅 도구를 쓰는 걸 권장해요(PERL_MEM_LOG 참고). 데이터 구조가 쓰는 메모리를 알려면 CPAN의 Devel::Size를 설치해요(size()total_size()의 차이에 주의). Perl의 malloc으로 빌드했다면 $ENV{PERL_DEBUG_MSTATS}를 설정해 분석할 수 있어요.

$ENV{PERL_DEBUG_MSTATS} 사용 (Using $ENV{PERL_DEBUG_MSTATS})

Perl의 malloc을 쓰고 필요한 스위치로 컴파일된 perl이라면, $ENV{PERL_DEBUG_MSTATS} > 1이면 컴파일 후, >= 1이면 프로그램 종료 전에 메모리 사용 통계를 출력해요:

 $ PERL_DEBUG_MSTATS=2 perl -e "require Carp"
 Memory allocation statistics after compilation: (buckets 4(4)..8188(8192)
    14216 free:   130   117    28     7     9   0   2     2   1 0 0
...

이 형식의 설명:

  • buckets SMALLEST(APPROX)..GREATEST(APPROX) : Perl의 malloc()은 버킷 단위 할당을 써요. 각 요청은 가장 가까운 버킷 크기로 올림돼요. 각 버킷은 메모리 점유량과 담을 수 있는 최대 사용자 데이터 크기 두 가지가 있어요.
  • Free/Used : SMALLESTGREATEST 사이 각 크기 버킷의 개수. 디버깅 빌드에선 일부 버킷이 음의 사용 가능 크기를 가질 수 있어요(쓸 수 없음).
  • Total sbrk() : perl이 sbrk(2)로 가져온 총 메모리와 sbrk(2) 호출 횟수, 그리고 반환 청크의 연속성에 대한 판단. 양수면 계속 연속 메모리가 공급될 가능성이 높다고 가정.
  • pad : 버킷 정렬에 필요한 sbrk 메모리. heads : 작은 버킷의 오버헤드 영역 총 크기. chain : 큰 버킷을 쪼갠 나머지 링크드리스트 총 크기. tail : sbrk했지만 아직 안 쓴 부분.

표준 Devel::Peek 모듈의 mstat() 함수로 실행 중 임의 지점에서 통계를 요청할 수 있어요.

함께 보기 (SEE ALSO)

perldebug, perl5db.pl, perlguts, perlrun, re, Devel::DProf.

더 알아보기 (Learn more)

더 깊이 보려면 다음 문서를 함께 읽어 보세요.