Perl 핵심 C 코드 해킹 팁

Perl 핵심 C 코드 해킹 팁 (perlhacktips)

Perl 코어의 C 코드를 해킹하는 가장 좋은 방법을 알려 주는 문서예요. 흔한 문제들, 디버깅, 프로파일링 등을 다룬답니다. 아직 perlhackperlhacktut을 읽지 않으셨다면 먼저 읽어 보시길 권해요.

출처: perlhacktips - Tips for Perl core C code hacking

흔한 문제들 (COMMON PROBLEMS)

Perl 소스는 이제 모든 플랫폼이 지원한다고 알려진 몇 가지 C99 기능을 허용하지만, 대부분은 ANSI C89 규칙을 따르고 있어요. "특정 플랫폼이 Perl이 깨졌든 말든 상관없어"라고 생각하시나요? 그런데도 J2EE 프로그래머에 대한 수요는 아직도 많다고 들었어요.

Perl 환경 문제 (Perl environment problems)

  • 스레딩으로 컴파일하기 (-Duseithreads)

스레딩으로 컴파일하면 Perl의 함수 프로토타입이 완전히 다시 쓰여져요. 여러분의 변경 사항을 그 설정으로도 꼭 시험해 보시길 바래요. 이와 관련해 "Perl_ 없는" API와 "Perl_이 붙은" API의 차이를 알아 두시면 좋아요:

Perl_sv_setiv(aTHX_ ...);
sv_setiv(...);

전자는 컨텍스트를 명시적으로 전달하는데, 예를 들어 스레드 빌드에서 필요해요. 후자는 암묵적으로 처리하죠. 둘을 헷갈리지 마세요. aTHX_를 넘기지 않는다면, 함수 첫 줄에 dTHX를 넣어야 해요.

컨텍스트에 관한 더 자세한 논의는 perlguts의 "How multiple interpreters and concurrency are supported"를 참고하세요.

  • -DDEBUGGING 없이 컴파일하기

DEBUGGING 정의는 컴파일러에 더 많은 코드를 노출시키므로, 문제가 생길 가능성도 많아져요. 한번 시도해 보세요.

  • (읽기 전용이 아닌) 전역 변수 도입하기

수정 가능한 전역 변수는 진짜 전역이든 파일 내 static이든 도입하지 마세요. 이건 나쁜 습관이고, 멀티스레딩과 그 밖의 동시성을 복잡하게 만들 뿐이에요. 올바른 방법은 새 인터프리터 변수로 도입하는 것인데, intrpvar.h를 참고하세요. (이진 호환성을 위해 파일의 맨 끝에 추가하세요.)

읽기 전용(const) 전역 변수 도입은 괜찮아요. 다만 nm libperl.a|egrep -v ' [TURtr] ' 같은 명령으로 (nm이 BSD 스타일 출력이라면) 추가한 데이터가 정말 읽기 전용인지 확인하세요. (읽기 전용이라면 그 명령의 출력에 나타나지 않아야 해요.)

정적 문자열을 원한다면 상수로 만드세요:

static const char etc[] = "...";

상수 문자열의 배열을 원한다면 const의 올바른 조합을 주의 깊게 맞추세요:

static const char * const yippee[] =
    {"hi", "ho", "silver"};
  • 새 함수를 export하지 않기

어떤 플랫폼(Win32, AIX, VMS, OS/2 등)은 공개 API(공유 Perl 라이브러리)의 일부인 함수는 명시적으로 export로 표시해야 해요. perlguts의 embed.pl에 대한 논의를 참고하세요.

  • 새 함수를 export하기

진짜 새 기능이든 어려운 리팩토링이든, 그 반짝이는 새 결과물이 export까지 제대로 됐다면 이제 뭐가 잘못될 수 있을까요? 아마도 그 함수가 애초에 export될 필요가 없었다는 것뿐이에요. Perl은 export하지 말았어야 할 함수를 export해 온 길고 그리 영광스럽지 못한 역사를 지니고 있답니다.

함수가 단 하나의 소스 파일 안에서만 쓰인다면 static으로 만드세요. perlguts의 embed.pl 논의를 참고하세요.

함수가 여러 파일에서 쓰이지만 Perl 내부 전용으로만 쓰인다면(이게 흔한 경우예요) 공개 API로 export하지 마세요. 마찬가지로 perlguts의 embed.pl 논의를 참고하세요.

C99

5.35.5부터 코어 C 소스에서 일부 C99 기능을 허용하기 시작했어요. 하지만 dual life 확장의 코드는 여전히 C89만 써야 해요. 구형 플랫폼에서 돌아가는 이전 버전의 Perl에서도 컴파일돼야 하기 때문이죠. 또, 우리의 헤더는 C++로도 유효해야 해요. C++로 작성된 XS 확장이 그 헤더를 include해야 하기 때문이에요. 그래서 헤더에서는 *멤버 구조체 초기화(member structure initialisers)*를 쓸 수 없어요.

C99 지원은 우리가 지원하는 모든 플랫폼에서 아직 완전하지 않아요. 기준선으로는 아래에 설명된 특정 C99 기능이 있는 C89 의미론만 가정할 수 있고, 그 기능들은 어디서나 동작한다고 검증했어요. 추가 C99 기능을 프로브해서 사용 가능한 곳에서 쓰는 것은 괜찮지만, 그 기능을 지원하지 않는 컴파일러를 위한 폴백도 함께 있어야 해요. 예를 들어 C11 스레드 로컬 저장소를 사용 가능하면 쓰지만, 그 외에는 POSIX 스레드 전용 API로 폴백해요.

코드는 다음 C99 기능이 존재한다고 가정하고 사용할 수 있어요.

  • 선언과 코드의 혼합 (mixed declarations and code)
  • 64비트 정수 타입 (64 bit integer types)

기존 소스와의 일관성을 위해 long longunsigned long long을 직접 쓰지 말고 typedef인 I64U64를 사용하세요.

  • 가변 인자 매크로 (variadic macros)
void greet(char *file, unsigned int line, char *format, ...);
#define logged_greet(...) greet(__FILE__, __LINE__, __VA_ARGS__);

__VA_OPT__는 C23과 C++20에서 표준화됐음을 참고하세요. 그 전에는 gcc 확장이었어요.

  • for 루프의 선언 (declarations in for loops)
for (const char *p = message; *p; ++p) {
    putchar(*p);
}
  • 멤버 구조체 초기화 (member structure initialisers)

하지만 헤더에서는 안 돼요. C++에 비교적 최근에야 지원이 추가됐기 때문이에요.

따라서 C와 XS 코드에서는 괜찮지만 헤더에서는 안 되는 것:

struct message {
    char *action;
    char *target;
};

struct message mcguffin = {
    .target = "member structure initialisers",
    .action = "Built"
 };

복합 리터럴(compound literals)에 비슷한 문법을 쓸 수는 없어요. 우리가 Perl을 C++ 컴파일러로도 빌드하기 때문이에요:

/* this is fine */
struct message m = {
    .target = "some target",
    .action = "some action"
};
/* this is not valid in C++ */
m = (struct message){
    .target = "some target",
    .action = "some action"
};

구조체 지정자(designators)는 쓸 수 있지만, 배열 지정자는 C++가 전혀 지원하지 않으므로 쓸 수 없어요.

  • 유연한 배열 멤버 (flexible array members)

이건 표준에 부합해요:

struct greeting {
    unsigned int len;
    char message[];
};

하지만 소스 코드는 이미 많은 곳에서 "컴파일러와의 부당한 친분" 해킹을 쓰고 있어요:

struct greeting {
    unsigned int len;
    char message[1];
};

엄밀히 message[0] 너머에 접근하는 것은 정의되지 않은 동작이에요. 하지만 K&R 시절부터 흔히 쓰인 해킹이고, 어디서든(perl 소스든 다른 흔한 C 코드든) 실질적인 문제가 된 적이 없어요. 그래서 적극적으로 C99 방식으로 바꿔서 얻을 게 뭔지 불분명해요.

  • // 주석

우리가 테스트한 모든 컴파일러가 이 사용을 지원해요. 테스트한 모든 인간이 지원하는 건 아니지만요. 헤더 파일에서는 피해야 해요. 이걸 만나면 깨지는 CPAN 코드가 있기 때문이에요.

  • 불리언 (booleans)

<stdbool.h>가 제공하는 bool, true, false를 사용할 수 있어요 (C++에서는 네이티브로).

코드는 명시적으로 위에 나열되지 않은 다른 C99 기능을 쓰면 안 돼요. 예를 들어:

  • 가변 길이 배열 (variable length arrays)

어떤 MSVC도 지원하지 않으며, 앞으로도 바뀌지 않을 거예요. 변수가 상수 표현식인 "가변" 길이 배열조차 MSVC에서는 문법 오류예요.

  • <stdint.h>의 C99 타입

handy.h에 정의된 PERL_INT_FAST8_T 등을 사용하세요.

  • <inttypes.h>의 C99 포맷 문자열

VMS libc의 snprintfPRIdN 등을 아주 최근에야 지원을 추가했어요. 즉 이를 지원하지 않는 살아있는 설치본이 존재한다는 뜻이고, %zu 같은 포맷도 마찬가지예요.

(perl의 sv_catpvf 등은 sv.c의 파서 코드를 쓰는데, 여기서 z 한정자와 SVf 같은 perl 전용 포맷을 지원해요.)

위에 나열되지 않은 C99 기능을 쓰고 싶다면 다음 중 하나를 해야 해요.

  • Configure에서 프로브하고, config.sh에 변수를 설정하며, 그 기능이 없는 플랫폼을 위한 폴백 로직을 헤더에 추가하라.
  • 테스트 코드를 작성하고, 무조건적으로 의존하기 전에 우리가 지원해야 하는 플랫폼에서 동작하는지 검증하라.

지금의 C99 기능 세트를 얻기 위해 썼던 것과 같은 계획을 반복하고 싶을 거예요. 이전에 쓴 C99 프로브는 https://markmail.org/thread/odr4fjrn72u2fkpz의 메시지를 참고하세요. 가장 "까다로운" 컴파일러 두 개는 MSVC와 VMS의 벤더 컴파일러로 보여요. 지금까지 모든 *nix 컴파일러는 지원 측면에서 훨씬 유연했어요.

*nix 플랫폼에서 Configure는 컴파일러 플래그를 적절히 설정하려 해요. 우리가 테스트한 벤더 컴파일러 모두 기본적으로 C99(또는 C11)를 지원해요. 하지만 구버전 gcc는 기본이 C89이고, 대부분의 C99를(경고와 함께) 허용하지만 -std=gnu99를 추가하지 않으면 for 루프의 선언을 금지해요. 대안인 -std=c99 좋아 보일 수 있지만, 일부 플랫폼에서 <unistd.h>가 일부 프로토타입을 선언하지 못하게 해서 빌드를 깨뜨릴 수 있어요. gcc의 -ansi 플래그는 -std=c89를 의미하므로 더 이상 설정할 수 없고, 그래서 Configure 옵션 -gccansipedantic은 이제 -pedantic만 추가해요.

Perl 코어 소스 파일들(소스 배포의 최상위 레벨에 있는 것들)은 가능한 한 많은 -std=gnu99, -pedantic, 그리고 일부 -W 플래그로 자동 컴파일돼요 (cflags.SH 참고). ext/ dist/ cpan/ 등의 파일들은 설치된 perl이 XS 확장을 컴파일할 때 쓰는 것과 같은 플래그로 컴파일돼요.

기본적으로 Configurecflags.SH가 그 플랫폼의 gcc 버전에 가장 좋은 플래그 조합을 골랐다고 가정해도 안전해요. C 방언을 강제하는 플래그를 더 추가하려는 시도는 로컬이든 코드가 배포되는 다른 시스템이든 문제를 일으킬 거예요.

gcc 3.1의 C99 지원이 우리에겐 충분하다고 믿지만, 19년 된 gcc가 곁에 없어서 확인은 못 해요. ㅎㅎ. C99를 기본으로 하지 않는 고대 벤더 컴파일러가 있다면 시도해 볼 플래그는 다음과 같아요:

AIX: -qlanglvl=stdc99

HP/UX: -AC99

Solaris: -xc99

심볼 이름과 네임스페이스 오염 (Symbol Names and Namespace Pollution)

합법적인 심볼 이름 고르기

C는 밑줄로 시작하고 바로 대문자 [A-Z] 또는 또 다른 밑줄이 뒤따르는 심볼을 구현용으로 예약해요. C++는 두 개의 연속 밑줄을 포함하는 심볼을 예약하고, 전역 네임스페이스에서 밑줄로 시작하는 심볼(대문자가 뒤따르는 것만이 아니라)도 예약해요. 우리가 C++에 신경 쓰는 이유는 헤더 파일(.h)이 그것으로 컴파일 가능해야 하고, 일부 개발자는 모든 개발을 C++ 컴파일러로 하기 때문이에요.

이를 지키지 않는다고 해서 결과적으로 문제가 생길 가능성은 아마 없어요. 구현이 쓰는 이름과 우연히 마주치지만 않으면 동작하거든요. 실제로 perl 코어에는 구현 예약 심볼을 쓴 예가 꽤 있어요. (점차 바뀌고 있죠.) 하지만 구현이 여러분이 (어쩌면 수년 전에) 골랐던 이름을 쓰기로 결정하는 순간, 여러분의 코드는 동작을 멈출 수 있어요.

그러니 가장 좋은 방법은:

  • 파일 레벨 심볼 이름을 밑줄로 시작하지 마라 (예: _FOOBAR)

함수나 블록 안의 심볼이 밑줄 다음 소문자로 시작하는 것은 괜찮아요:

{
     int _ref;
     ...
}
  • 심볼 이름에 두 개의 연속 밑줄을 쓰지 마라 (예: FOO__BAR)

POSIX도 많은 심볼을 예약해요. https://pubs.opengroup.org/onlinepubs/9699919799/functions/V2_chap02.html의 섹션 2.2.2를 참고하세요. Perl도 그와 충돌이 있어요.

Perl은 Perl, perl, PL_로 시작하는 모든 심볼을 자기 용도로 예약해요. 그 규칙을 따르지 않는 매크로를 헤더 파일에 도입할 때마다, 다음과 같이 제한하지 않으면 기존 XS 모듈과의 네임스페이스 충돌 가능성을 만드는 거예요:

#ifdef PERL_CORE
#  define my_symbol
#endif

이 형태가 아닌 헤더 심볼도 많아요. config.h의 거의 모든 것처럼, 의도적이든 아니든 XS 네임스페이스에서 접근 가능하죠.

이런 접두사를 써야 한다는 게 코드 가독성을 떨어뜨리고, 이건 별로 사소하지 않은 이름들에서 실제 문제가 된 적은 없어요. perl이 자기만의 MAX 매크로를 정의한 것 같은 일은 문제가 됐지만, 그건 빨리 발견됐고 #ifdef PERL_CORE 가드가 추가됐어요.

그러니 그런 심볼을 쓰는 데 강제된 규칙은 없어요. 다만 그 문제를 인지하고 있기만 하면 돼요.

좋은 심볼 이름 고르기

이상적으로는 심볼 이름이 의도된 목적을 정확하고 명확하게 묘사해야 해요. 하지만 그와, 지나치게 길어서 입력·읽기 어려운 이름을 만드는 것 사이에 긴장이 있어요. 은유(시적인 이름)가 도움이 될 수도 있지만, 문화적으로 특수한 경향이 있고 영어가 모국어가 아닌 사람이나 다른 문화 배경의 사람에겐 통하지 않을 수 있어요. 게다가 시를 쓰는 재능은 프로그래머에게 드물어 보여요.

어떤 심볼 이름은 목적을 반영하지 않지만, 오랜 관례로 인해 쓰기 좋아요. 이런 것들은 흔히 수학에서 시작됐어요. ij는 첨자로, n은 개수로 자주 쓰이죠. 1950년대 이래 컴퓨터 프로그램은 i 등을 루프 변수로 사용해 왔어요.

우리의 조언은 목적을 합리적으로 묘사하는 이름을 고르고, 선언에 더 정확한 주석을 다는 거예요.

오해의 소지가 있거나 모호한 이름은 확실히 피해야 해요. last_foo는 마지막 foo나 이전 foo 둘 다를 뜻할 수 있어서, 독자나 몇 달 다른 일을 하다 돌아온 작성자조차 헷갈릴 수 있어요. 때로 프로그래머는 특정 사고 흐름을 염두에 두다 보니 모호성이 있다는 걸 떠올리지 못해요.

av_lensv_len 같은 다른 -len 구성의 의미와 일치하지 않아서, 아마 여전히 많은 off-by-1 버그가 있을 거예요. 그 진짜 의미를 전달하기 위해 어색한(그리고 논란이 있는) 동의어(av_top_index)가 대신 만들어지기도 했어요. 결국 누군가 대부분의 사람들이 -len이 뜻한다고 생각하는 것을 나타내는 새 이름을 만들자는 더 나은 아이디어를 냈죠. 그래서 av_count가 탄생했어요. 우리는 그게 훨씬 일찍 생각났더라면 좋겠어요.

더 안전한 매크로 작성하기

Perl 코어는 호출자로부터 내부 세부 사항을 숨기는 용도로 매크로를 광범위하게 사용해요. 예를 들어 대부분의 코드 줄은 자신이 스레드 vs 비스레드 perl에서 실행되는지 알 필요가 없어요. 그 세부 사항은 자동으로 대부분 숨겨지거든요.

매크로 대신 인라인 함수를 쓰는 게 더 나을 때가 많아요. 인라인 함수는 호출자와의 이름 충돌에 면역이고, 부작용이 있는 표현식의 인자로 호출될 때 문제를 증폭시키지 않아요. 한때는 인라인 함수에 대한 컴파일러 지원이 매우 제한적이라 매크로를 택하는 경우도 있었어요. 일부 컴파일러는 컴파일에서 만난 처음 두어 개만 실제로 인라인했죠. 하지만 그 시절은 오래 지났고, 현대 컴파일러는 인라인 함수를 완전히 지원해요.

그럼에도 함수로는 안 되고 매크로가 필요한 상황이 있어요. 한 예가 매개변수가 여러 타입 중 하나일 수 있을 때예요. 함수는 단일 명시적 매개변수 타입으로 선언돼야 하므로, 매크로가 요구될 수 있어요.

혹은 관련 코드가 너무 사소해서 함수라면 불필요한 과잉일 때도 있어요. 매크로가 어떤 상수값에 기억하기 쉬운 이름을 만들어 줄 때 같은 경우죠.

사소하지 않은 매크로를 쓰기로 했다면, 피할 수 있는 함정이 몇 가지 있다는 걸 알아 두세요. 매크로는 호출된 소스의 각 위치의 어휘적 컨텍스트 안에서 확장된다는 점을 기억하세요. 매크로에 foo라는 토큰이 있고 소스에도 우연히 foo가 있다면, 매크로의 foo 의미가 호출자의 것이 돼요. 때로는 그것이 정확히 원하는 동작일 수도 있지만, 나중에 헷갈리기 쉽다는 점을 알아 두세요. 이는 사실상 매크로를 호출하는 어떤 코드에도 foo를 예약어로 만들며, 이 사실은 보통 문서화되지도 고려되지도 않아요. foo를 매개변수로 전달하는 편이 더 안전해요. 그러면 foo가 호출자에게 자유롭게 남고 매크로 인터페이스가 명시적으로 지정되니까요.

더 나쁜 것은 두 foo 사이의 등가성이 우연일 때예요. 매크로가 변수를 선언한다고 해 보죠:

int foo

호출자가 foo라는 문자열을 어떤 식으로든 정의하지 않는 한 잘 동작해요. 그리고 foo가 쓰이는 경우가 등장할 때까지 수년이 걸릴 수도 있어요. 예를 들어 미래의 호출자가 이렇게 할 수 있어요:

#define foo  bar

그러면 매크로 안의 foo 선언이 갑자기

int bar

이 돼요. 이는 의도와 완전히 다른 일이 벌어질 수 있다는 뜻이에요. 디버깅하기 어려워요. 매크로와 호출이 같은 파일에 없는 수도 있어서, 알아내려면 꽤 파고들고 이를 갈아야 해요.

따라서 매크로가 변수를 사용한다면, 그 이름은 지금이든 영원히든 어떤 호출자와도 충돌할 가능성이 매우 낮아야 해요. 지금 perl 소스에서 쓰이는 한 가지 방법은 매크로 자신의 이름을 매크로 안 각 변수 이름의 일부로 포함하는 거예요. 매크로 이름이 SvPV라고 하면:

int foo_svpv_ = 0;

이건 평범한 foo보다 읽기 어렵지만, 호출자가 순진하게 foo_svpv_를 쓰는 일은 거의 확실히 없을 거예요. (소문자로 만드는 게 이게 변수라는 걸 더 분명하게 하지만, 글자 대소문자만 다른 두 요소는 없을 거라고 가정해요.) 끝의 밑줄은 규약상 비공개 변수 이름을 뜻하므로 충돌 가능성을 더욱 낮춰요. (사용 가능한 이름의 제약은 "합법적인 심볼 이름 고르기"를 참고하세요.)

이런 종류의 이름 충돌은 매크로의 형식 매개변수에서는 일어나지 않으므로, 복잡한 이름이 필요 없어요. 하지만 매개변수가 표현식이거나 Perl 매직이 붙어 있으면 함정이 있어요. 함수를 호출할 때 C는 매개변수를 한 번 평가하고 결과를 함수에 전달해요. 하지만 매크로를 호출할 때 매개변수는 C 전처리기가 매크로 안의 각 인스턴스에 있는 그대로 복사해요. 이는 부작용이 있는 매개변수를 평가할 때 함수와 매크로의 결과가 다르다는 의미예요. 특히 매개변수에 오버로드 매직이 있을 때, 이를테면 평가할 때마다 파일에서 다음 줄을 읽는 tied 변수일 때 위험해요. 호출당 여러 줄을 읽는 건 아마 호출자가 의도한 게 아니에요. 매크로가 잠재적으로 오버로드 가능한 매개변수를 두 번 이상 참조한다면, 먼저 복사본을 만든 다음 나머지 동안 그 복사본을 써야 해요. perl 코어에는 이를 위반하는 매크로가 있지만, 보통 인라인 함수로 바꾸는 식으로 점진적으로 변환되고 있어요.

위에서 "먼저 복사본을 만들어라"고 했어요. 매크로에서 이는 말처럼 쉬운 일이 아니에요. 매크로 보통 표현식이고, 표현식에는 선언이 허용되지 않기 때문이에요. 하지만 perlapi의 STMT_START에 설명된 STMT_START .. STMT_END 구성은 반환값이 필요 없을 때 대부분의 컨텍스트에서 선언을 허용해요. 반환값이 필요하다면, 포인터를 구성에 넘겨 그 결과를 거기에 저장하도록 인터페이스를 만들 수 있어요. (또는 GCC brace groups를 쓸 수 있어요. 하지만 이는 C의 비표준 확장이 없는 플랫폼에서 실행될 수 있다면 폴백이 필요해요. 그리고 그 폴백은 또 다른 코드 경로가 되어 brace group과 동기화가 어긋날 수 있으므로, 이렇게 하는 건 권장하지 않아요.) 다른 방법이 없는 상황에서 Perl은 PL_SvPL_na를 일부 흔한 경우에 쓸 수 있게 제공해요 (약간의 성능 손실이 따르지만). 하지만 이것들을 쓰는 여러 매크로가 얽힌 호출 체인은 서로의 사용을 무효화시킬 수 있음을 주의하세요. 이것들은 디버깅하기 매우 어려웠어요.

이런 함정들이 작동하는 구체적인 예는 https://perlmonks.org/?node_id=11144355를 참고하세요.

이식성 문제 (Portability problems)

다음은 Perl 그 자체에만 국한되지 않는 컴파일 및/또는 실행 실패의 흔한 원인들이에요. C FAQ는 잠자기 전 읽기 좋은 책이에요. 여러분의 변경 사항을 가능한 한 많은 C 컴파일러와 플랫폼으로 테스트해 주세요. 어차피 우리도 할 테지만요. 그리고 공개적인 망신을 스스로 피하는 건 좋은 일이에요.

perlport도 주의 깊게 공부하세요. 운영체제, 파일시스템, 문자셋 등에 대한 나쁜 가정을 피하려면요.

운영체제가 특정 컴파일러를 의미한다고 가정하지 마세요.

  • 포인터를 정수로 또는 정수를 포인터로 캐스팅하기
void castaway(U8* p)
{
  IV i = p;

또는

void castaway(U8* p)
{
  IV i = (IV)p;

둘 다 나쁘고, 깨졌고, 이식 불가능해요. 제대로 하는 PTR2IV() 매크로를 사용하세요. (마찬가지로 PTR2UV(), PTR2NV(), INT2PTR(), NUM2PTR()도 있어요.)

  • 함수 포인터와 데이터 포인터 사이를 캐스팅하기

엄밀히 말하면 함수 포인터와 데이터 포인터 사이의 캐스팅은 이식 불가능하고 정의되지 않았지만, 실제로는 동작하는 것 같아요. 하지만 FPTR2DPTR()와 DPTR2FPTR() 매크로를 사용해야 해요. 때로 union을 가지고 장난칠 수도 있어요.

  • sizeof(int) == sizeof(long)라고 가정하기

long이 64비트인 플랫폼이 있고 int가 64비트인 플랫폼이 있어요. 충격을 드리자면 short가 64비트인 플랫폼도 있답니다. 이는 모두 C 표준에 따른 합법적인 것이에요. (다시 말해 long long은 64비트를 지정하는 이식 가능한 방법이 아니고, long longlong보다 넓다는 보장조차 없어요.)

대신 IV, UV, IVSIZE, I32SIZE 등의 정의를 사용하세요. I32 같은 것을 피하세요. I32정확히 32비트라는 보장이 없고(적어도 32비트예요), intlong이라는 보장도 없기 때문이에요. 64비트 변수가 명시적으로 필요하면 I64U64를 사용하세요.

  • 어떤 타입의 데이터든 어떤 타입의 포인터든 역참조할 수 있다고 가정하기
char *p = ...;
long pony = *(long *)p;    /* BAD */

여러 플랫폼은 아주 타당하게도, p가 정확히 정렬되어 있지 않다면 pony 대신 core dump를 줄 거예요.

  • Lvalue 캐스팅
(int)*p = ...;    /* BAD */

이식 불가능해요. lvalue를 올바른 타입으로 만들거나, 임시 변수를 쓰거나, union을 쓴 지저분한 트릭을 쓰세요.

  • struct에 대해 무엇이든 가정하기 (특히 시스템 헤더에서 오는 것처럼 우리가 통제할 수 없는 것들)

    • struct에 특정 필드가 존재한다는 것
    • 여러분이 아는 필드 외에 다른 필드가 없다는 것
    • 필드가 특정 signedness, sizeof, 타입이라는 것
    • 필드가 특정 순서로 있다는 것 — C는 struct 정의에 지정된 순서를 보장하지만, 플랫폼마다 정의가 다를 수 있어요
    • sizeof(struct)나 정렬이 어디서든 같다는 것 — 필드를 정렬하기 위해 필드 사이에 패딩 바이트가 있을 수 있고, 그 바이트는 무엇이든 될 수 있어요
    • struct는 필드가 요구하는 최대 정렬로 정렬돼야 한다는 것 — 네이티브 타입의 경우 보통 sizeof(the_field)와 같아요
  • 문자셋이 ASCII 계열이라고 가정하기

Perl은 EBCDIC 플랫폼에서도 컴파일되고 실행될 수 있어요. perlebcdic 참고. 대부분 투명하지만, 문자셋이 다르므로 문자를 참조할 때 숫자(10진, 8진, 16진) 상수를 쓰면 안 돼요. 'A'는 안전하게 말할 수 있지만 0x41은 안 돼요. '\n'은 안전하지만 \012는 안 돼요. 하지만 utf8.h에 정의된 매크로를 사용하면 임의 코드 포인트를 이식 가능하게 지정할 수 있어요. LATIN1_TO_NATIVE(0xDF)는 실행 플랫폼이 무엇이든 LATIN SMALL LETTER SHARP S를 뜻하는 코드 포인트가 돼요 (ASCII 플랫폼에서는 추가 코드 없이 컴파일되므로 성능 손실이 전혀 없어요). LATIN1_TO_NATIVE의 허용 입력은 0x00부터 0xFF까지예요. 입력이 그 범위 안이라고 보장할 수 없다면 UNICODE_TO_NATIVE를 대신 사용하세요. NATIVE_TO_LATIN1NATIVE_TO_UNICODE는 반대 방향으로 변환해요.

C에 니모닉 이름이 없는 문자의 문자열 표현이 필요하다면, regen/unicode_constants.pl의 목록에 추가하고, Perl이 현재 플랫폼을 기준으로 #define을 만들어 주게 하세요.

handy.his*FOO*to*FOO* 매크로는 네이티브 코드 포인트와 문자열에서 제대로 동작함을 참고하세요.

또한 ASCII에서의 'A'-'Z' 범위는 26개의 대문자 알파벳의 깨지지 않는 연속이에요. EBCDIC에서는 그렇지 않아요. 'a'부터 'z'도 마찬가지예요. 하지만 '0'-'9'는 두 시스템 모두에서 깨지지 않는 범위예요. 다른 범위에 대해서는 아무것도 가정하지 마세요. (정규식 패턴과 transliteration에서 범위의 특별 처리가 위 범위들이 모두 깨지지 않는다고 Perl 코드에 보이게 만든다는 점을 참고하세요.)

기존 코드의 많은 주석은 EBCDIC의 가능성을 무시해서, 코드가 동작하더라도 틀릴 수 있어요. 이는 실제로 기존 코드를 바꾸지 않고 EBCDIC을 처리할 수 있게 투명하게 삽입한 것의 성공에 대한 경의예요.

UTF-8과 UTF-EBCDIC은 유니코드 코드 포인트를 바이트 시퀀스로 나타내는 데 쓰이는 두 가지 다른 인코딩이에요. utf8.hutfebcdic.h의 같은 이름(다른 정의)을 가진 매크로가, 호출 코드가 그런 인코딩이 하나만 있다고 생각하게 만드는 데 쓰여요. 이것은 거의 항상 utf8이라 불리지만, EBCDIC 버전도 뜻해요. 다시 말하지만 코드 속 주석은 코드 자체가 옳아도 틀리기 쉬워요. 예를 들어 UTF-8 invariant characters의 개념은 ASCII와 EBCDIC에서 달라요. ASCII 플랫폼에서는 상위 비트가 설정되지 않은 문자(즉 순수 ASCII 0-127인 것)만 invariant이고, 코드의 문서와 주석은 그걸 가정해서 종종 hibit 같은 것을 언급해요. EBCDIC 머신에서는 상황이 다르고 그리 단순하지 않지만, 코드 자체가 NATIVE_IS_INVARIANT() 매크로를 적절히 사용한다면 주석이 틀려도 동작해요.

perlhack의 "TESTING"에서 언급했듯이, 테스트 스크립트를 작성할 때 t/charset_tools.pl 파일에는 ASCII와 EBCDIC 플랫폼 모두에서 유효한 테스트를 쓰는 데 도움이 되는 유용한 함수들이 있어요. 하지만 때로 테스트가 함수를 못 쓸 때도 있고, 플랫폼에 따라 다른 테스트 버전을 갖는 건 불편해요. Perl이 현재 인식하는 4개 문자셋(3개의 EBCDIC 코드 페이지 + ISO 8859-1(ASCII/Latin1)) 모두에서 같은 20개의 코드 포인트가 있어요. 이런 테스트에 그들을 쓸 수 있는데, Perl이 또 다른 문자셋에서 쓰이게 되어 테스트를 깨뜨릴 작은 가능성은 있어요. 이 코드 포인트들 중 하나를 제외하고 모두 C0 제어 문자예요. 같은 것 중 가장 중요한 제어 문자는 \0, \r, \N{VT} (또한 \cK, \x0B, \N{U+0B}, \013으로도 지정 가능)이에요. 유일한 비제어 문자는 U+00B6 PILCROW SIGN이에요. 같은 제어 문자는 포함된 문자열의 UTF8 여부와 무관하게 4개 문자셋 모두에서 같은 비트 패턴을 가져요. U+B6의 비트 패턴은 비UTF8 문자열에선 4개 모두 같지만, 포함 문자열이 UTF-8로 인코딩되면 각각 달라요. 4개 문자셋 모두에서 어떤 종류의 동일성을 갖는 다른 코드 포인트는 0xDC와 0xFC 쌍뿐이에요. 함께 이들은 대소문자 LATIN LETTER U WITH DIAERESIS를 나타내는데, 어느 쪽이 대문자인지는 뒤집힐 수 있어요: Latin1에서 0xDC가 대문자이고 0xFC가 소문자인 반면, EBCDIC에서는 0xFC가 대문자이고 0xDC가 소문자예요. 이 사실은 4개 문자셋 모두에서 같은 대소문자 무시 테스트를 쓸 때 활용될 수 있어요.

  • 문자셋이 그냥 ASCII라고 가정하기

ASCII는 7비트 인코딩이지만 바이트는 8비트예요. 추가 128개의 문자는 로케일에 따라 다른 의미를 가져요. 로케일이 없으면 이 추가 문자들은 일반적으로 배정되지 않은 것으로 여겨졌고, 이는 몇 가지 문제를 일으켰어요. 5.12부터 이 문자가 Latin-1(ISO-8859-1)로 간주될 수 있도록 바뀌고 있어요.

  • #define과 #ifdef를 섞기
#define BURGLE(x) ... \
#ifdef BURGLE_OLD_STYLE        /* BAD */
... do it the old way ... \
#else
... do it the new way ... \
#endif

cpp 지시문을 이식 가능하게 "쌓을" 수 없어요. 예를 들어 위에서는 각 #ifdef 분기마다 별도의 BURGLE() #define이 두 개 필요해요.

  • #endif나 #else 뒤에 비주석 내용 추가하기
#ifdef SNOSH
...
#else !SNOSH    /* BAD */
...
#endif SNOSH    /* BAD */

#end와 #else 뒤에는 이식 가능하게 비주석 내용을 둘 수 없어요. 무슨 일인지 문서화하고 싶다면(특히 분기가 길다면 좋은 생각이에요), (C) 주석을 쓰세요:

#ifdef SNOSH
...
#else /* !SNOSH */
...
#endif /* SNOSH */

gcc 옵션 -Wendif-labels는 (Perl 5.9.4부터 기본으로) 나쁜 변형에 대해 경고해요.

  • enum 목록의 마지막 요소 뒤에 쉼표 두기
enum color {
  CERULEAN,
  CHARTREUSE,
  CINNABAR,     /* BAD */
};

이식 불가능해요. 마지막 쉼표를 빼세요.

또한 enum이 int로 암묵적으로 모프될 수 있는지는 컴파일러에 따라 다르므로, (int)가 필요할 수 있음을 참고하세요.

  • signed char 포인터를 unsigned char 포인터와 섞기
int foo(char *s) { ... }
...
unsigned char *t = ...; /* Or U8* t = ... */
foo(t);   /* BAD */

이건 합법적인 관행이지만 확실히 의심스럽고, 적어도 한 플랫폼에서는 치명적이에요: 예를 들어 VMS cc는 이를 치명적 오류로 간주해요. 사람들이 자주 이 실수를 하는 한 가지 이유는 "벗은 char"와 따라서 "벗은 char 포인터"의 역참조가 정의되지 않은 signedness를 갖기 때문이에요: 결과가 signed인지 unsigned인지는 컴파일러와 컴파일러 플래그, 기본 플랫폼에 달려 있어요. 바로 이 이유 때문에 'char'를 배열 인덱스로 쓰는 것도 나빠요.

  • 문자열 상수와 그 인자를 문자열 상수의 부분 문자열로 갖는 매크로
#define FOO(n) printf("number = %d\n", n)    /* BAD */
FOO(10);

ANSI 이전 의미론에서는 이게 다음과 동등했어요:

printf("10umber = %d\10");

아마 기대한 게 아닐 거예요. 불행히도 적어도 하나의 꽤 흔하고 현대적인 C 컴파일러가 여기서 "진짜 하위 호환성"을 하며, AIX에서는 컴파일러의 나머지 부분이 아주 행복하게 C89임에도 여전히 그런 일이 일어나요.

  • 기본이 아닌 C 타입에 printf 포맷 쓰기
IV i = ...;
printf("i = %d\n", i);    /* BAD */

이건 어떤 플랫폼에서 우연히(IV가 int일 때) 동작할 수 있지만, 일반적으로는 안 돼요. IV는 더 큰 것일 수 있어요. 더 구체적인 타입(config.h에서 Perl의 설정 단계가 정의한)의 상황은 더 나쁘죠:

Uid_t who = ...;
printf("who = %d\n", who);    /* BAD */

여기서 문제는 Uid_t가 int 폭이 아닐 뿐 아니라 unsigned일 수도 있다는 거예요. 그 경우 큰 uid가 음수 값으로 출력될 거예요.

printf()의 제한된 지능 때문에 간단한 해결책은 없지만, 많은 타입에 대해 올바른 포맷이 'f'나 '_f' 접미사로 제공돼요:

IVdf /* IV in decimal */
UVxf /* UV in hexadecimal */
U32of /* A U32 in octal */

printf("i = %"IVdf"\n", i); /* The IVdf is a string constant. */

Uid_t_f /* Uid_t in decimal */

printf("who = %"Uid_t_f"\n", who);

아니면 "충분히 넓은" 타입으로 캐스팅할 수도 있어요:

printf("i = %"IVdf"\n", (IV)something_very_small_and_signed);

이들을 출력하는 방법은 perlguts의 "Formatted Printing of Size_t and SSize_t"를 참고하세요.

또한 %p 포맷은 정말 void 포인터를 필요로 함을 기억하세요:

U8* p = ...;
printf("p = %p\n", (void*)p);

gcc 옵션 -Wformat이 그런 문제를 스캔해요.

  • 맹목적으로 va_list 넘기기

모든 플랫폼이 va_list를 추가 varargs(stdarg) 함수에 넘기는 것을 지원하지는 않아요. 올바른 방법은 NEED_VA_COPY가 정의되어 있다면 Perl_va_copy()를 사용해 va_list를 복사하는 거예요.

  • gcc 문장 표현식 사용하기
val = ({...;...;...});    /* BAD */

좋은 확장이긴 하지만 이식 불가능해요. 역사적으로 Perl은 가능하면 매크로에서 약간의 추가 속도를 얻기 위해(본질적으로 특이한 형태의 인라이닝) 이것을 사용했어요. 하지만 이제 C99 static inline 함수를 지원(또는 에뮬레이션)하므로 그것을 대신 사용하세요. 함수를 PERL_STATIC_INLINE으로 선언하면 필요할 때 에뮬레이션으로 투명하게 폴백해요.

  • 매크로에서 여러 문장을 묶기

STMT_STARTSTMT_END 매크로를 사용하세요.

STMT_START {
   ...
} STMT_END

하지만 이것들로 미묘한(제대로 하면 피할 수 있는) 버그가 생길 수 있어요. 사용의 모범 사례는 perlapi의 STMT_START를 참고하세요.

  • 기능(feature)을 테스트해야 할 때 운영체제나 버전을 테스트하기
#ifdef __FOONIX__    /* BAD */
foo = quux();
#endif

quux()가 "Foonix" 운영체제에만 있고, "Foonix"의 과거·현재·미래 모든 버전에서 항상 이용 가능하며 제대로 동작한다는 것을 100% 확신하지 않는 한, 위는 매우 틀렸어요. 다음이 더 정확해요 (아직 완벽하진 않지만, 아래는 컴파일 타임 검사이기 때문이에요):

#ifdef HAS_QUUX
foo = quux();
#endif

HAS_QUUX는 필요한 곳에서 어떻게 정의될까요? Foonix가 Configure 스크립트를 실행할 만큼 Unix에 가깝고, Configure가 quux()를 감지·테스트하도록 교육받았다면 HAS_QUUX는 올바르게 정의될 거예요. 다른 플랫폼에서는 해당 설정 단계가 같은 일을 하길 바래요.

급할 때, Configure가 교육받기를 기다릴 수 없거나 quux()가 어디에 있을지 좋은 직감이 있다면, 임시로 다음을 시도할 수 있어요:

#if (defined(__FOONIX__)|defined(__BARNIX__))
# define HAS_QUUX
#endif

...

#ifdef HAS_QUUX
foo = quux();
#endif

하지만 어느 경우든 기능과 운영체제를 분리하려고 노력하세요.

다양한 운영체제·컴파일러 등에 대한 미리 정의된 매크로에 관한 좋은 자료는 https://sourceforge.net/p/predef/wiki/Home/이에요.

  • C 라이브러리 함수의 Perl 래퍼 반환 값이 가리키는 정적 메모리 내용이 변하지 않는다고 가정하는 것. 많은 C 라이브러리 함수가 정적 저장소를 가리키는 포인터를 반환하는데, 그 저장소는 같은 또는 관련 함수에 대한 후속 호출로 덮어쓸 수 있어요. 그 저장소를 공유하는 함수 중 하나가 호출되기 전에 그 반환값을 처리한다면 괜찮지만, 임베디드 perl이나 스레드 사용 시에는 처리할 기회를 얻기 전에 그런 함수가 호출될 수 있어요.

perlclib의 "Dealing with embedded perls and threads"에는 문제가 되는 함수 목록과 대처 방법에 대한 좋은 조언이 있어요.

문제가 있는 시스템 인터페이스 (Problematic System Interfaces)

다양한 C 라이브러리 함수의 사용에는 보안 관련을 포함해 많은 문제가 있어요. 상세히 다루는 perlclib을 읽어 보세요.

Perl 문자열은 C 문자열과 같지 않음을 기억하세요: Perl 문자열은 NUL 문자를 포함할 수 있지만, C 문자열은 첫 NUL로 종료돼요. 그래서 Perl API 함수가 문자열을 다룰 때 일반적으로 첫 바이트를 가리키는 포인터와 길이(또는 마지막 바이트 바로 다음을 가리키는 포인터)를 받는 거예요.

이것이 많은 C 라이브러리 문자열 처리 함수를 쓰지 말아야 하는 이유예요. 그들은 Perl 문자열의 완전한 일반성을 다루지 못해요. 테스트 케이스에 임베디드 NUL이 없어서 테스트는 통과하지만, 실제 세계에서는 실패하는 경우가 결국 나타날 수 있어요. 여기의 교훈은 테스트에 NUL을 포함하라는 거예요. 실제 세계의 대부분의 경우 NUL을 만나기 드물어서, 어느 날 NUL이 나타나기 전까지는 코드가 동작하는 것처럼 보일 거예요.

예를 들어 볼게요. 수십 년 동안 perl 코어에서 strchr("list", c)를 써서 문자 c"list"에 주어진 문자들 중 하나인지 확인하는 게 흔한 패러다임이었어요. cNUL이 아닌 한 동작해요. 하지만 cNUL이면 strchr"list"의 종료 NUL을 가리키는 포인터를 반환해요. 호출자가 그 끝 포인터를 읽기 시작점으로 쓰면 세그폴트나 보안 문제를 일으킬 가능성이 높아요.

이것과 많은 유사한 문제의 해결책은 memfoo C 라이브러리 함수를 대신 사용하는 거예요. 이 경우 memchrc"list"에 있는지 확인하는 데 쓸 수 있고 cNUL이어도 동작해요. 이 함수들은 문자열 길이를 주는 추가 매개변수가 필요해요. 리터럴 문자열 매개변수의 경우 perl이 길이를 계산해 주는 매크로를 정의했어요. perlapi의 "String Handling"을 참고하세요.

디버깅 (DEBUGGING)

대부분의 코드는 처음 실행됐을 때 의도대로 동작하지 않아서 디버그되고 수정돼야 해요. 더 무서운 것은 동작하는 것처럼 보이지만 해커가 악의적인 행동을 하는 데 쓸 수 있는 숨은 결함이 있는 코드예요. 두 유형을 모두 감지하는 방법이 있어요.

Perl 찔러보기 (Poking at Perl)

첫 번째 유형부터 시작할게요. 예상대로 가는지 확인하는 많은 내장 검사를 활성화하는 특별한 디버깅 버전의 Perl을 컴파일할 수 있어요. 하나가 실패하면 인터프리터가 무엇이 잘못됐는지 메시지와 함께 갑자기 멈춰요. 많은 사람들이 코드 개발 중에 항상 이 옵션을 사용해요. 또한 하나 이상의 특정 실행 영역의 추적을 활성화하고, 디버거를 훨씬 더 편리하게 사용할 수 있게 해줘요.

이런 Perl을 만들려면:

./Configure -d -DDEBUGGING
make

-DDEBUGGING은 C 컴파일러의 -g 플래그를 켜서 실행 중인 프로그램을 단계적으로 밟고 현재 어떤 C 함수에 있는지 볼 수 있게 해주는 디버깅 정보를 생성하게 해요 (디버깅 정보가 없으면 함수의 수치 주소만 보일 수 있는데 그리 도움이 안 돼요). 자세한 내용은 "소스 레벨 디버거 사용하기"를 참고하세요.

-DDEBUGGING은 또한 각 컴파일된 파일에 DEBUGGING 심볼이 #define되게 해요. 이는 Perl의 모든 내부 디버깅 코드를 활성화하고, 아주 많은 것의 추적 능력을 포함해요. perlrun의 -Dletters가 모두 나열해요. 그리고 그것들에 대해 아는 가장 좋은 방법은 가지고 놀아 보는 거예요. 무슨 일이 일어나는지 전반적으로 보기에 가장 유용한 옵션은 아마:

l  Context (loop) stack processing
s  Stack snapshots (with v, displays all stacks)
t  Trace execution
o  Method and overloading resolution
c  String/numeric conversions

예를 들어

$ perl -Dst -e '$x + 1'
....
(-e:1)	gvsv(main::x)
    =>  UNDEF
(-e:1)	const(IV(1))
    =>  UNDEF  IV(1)
(-e:1)	add
    =>  NV(1)

모든 옵션은 "-Dletters" in perlrun에 나열돼 있어요. -DU는 여러분의 (임시) 사용을 위해 예약되어 있음을 참고하세요. 그 스위치만으로 제어되는 자신만의 디버깅 문장을 추가할 수 있어요.

디버깅 코드의 일부 기능은 XS 모듈을 사용해 비디버깅 perl로도 달성할 수 있어요:

-Dr => use re 'debug'
-Dx => use O 'Debug'

더 자세한 내용은 perlguts의 "Tracing the interpreter execution"에 있어요.

숨은 결함 잡기 (Catching hidden flaws)

코드의 숨은 결함을 찾는 다양한 도구가 있어요. 하지만 이들은 보통 용납할 수 없는 속도 저하를 일으켜서, 해커가 피해를 입힌 후일 수도 있는 문제가 발견되기 전까지는 쓰이지 않는 경향이 있어요. 자세한 내용은 "메모리 디버거"를 참고하세요.

하지만 perl에는 초기화되지 않은 값 사용이라는 중요한 결함의 많은 사례를 잡아내고, 그 도구들보다 훨씬 적게(약 5% 정도) 느려지게 하는 컴파일 옵션이 있어요. 활성화하려면 Configure 호출에 이 옵션을 추가하세요:

./Configure ... -Accflags=-DPERL_POISON
make

이것은 perl이 생성하거나 해제된 어떤 메모리든 (포인터로서는, 그리고 부동소수점으로서도) 불법이어야 하고, 정수로서도 충분히 놀랍길 바라는 특정 비트 패턴으로 설정하게 해요. 그래서 생각 없이 데이터를 사용하려는 어떤 코드든 일찍 깨지게 만들죠.

특정 메모리 할당에 대해 항상 이렇게 하고 싶을 수도 있어요. Zero()와 비슷한 인자를 가진 다음 매크로가 이걸 해냅니다:

PoisonWith(dst, n, t, b)    scribble memory with byte b
PoisonNew(dst, n, t)        equal to PoisonWith(dst, n, t, 0xAB)
PoisonFree(dst, n, t)       equal to PoisonWith(dst, n, t, 0xEF)
Poison(dst, n, t)           equal to PoisonFree(dst, n, t)

소스 레벨 디버거 사용하기 (Using a source-level debugger)

-D의 디버깅 출력으로도 부족하다면, 소스 레벨 디버거로 뛰어들 시간이에요. core dump의 스택 트레이스를 보거나(버그 리포트에 매우 유용), perl 실행을 단계적으로 밟으며 core dump 전에 무엇이 잘못됐는지 알아내기 위해서죠.

  • 여기서는 gdb를 예로 쓸게요. 원리는 어떤 디버거(많은 벤더는 디버거를 dbx라고 부르죠)에도 적용되지만, 사용 중인 디버거의 매뉴얼을 확인하세요.

디버거를 켜려면:

gdb ./perl

core dump가 있다면:

gdb ./perl core

디버거가 소스 코드를 읽을 수 있도록 Perl 소스 트리에서 하는 게 좋아요. 저작권 메시지 뒤에 프롬프트가 보일 거예요:

(gdb)

help는 문서로 안내하지만, 가장 유용한 명령은:

  • run [args] — 주어진 인자로 프로그램을 실행.
  • break function_name / break source.c:xxx — 명명된 함수나 명명된 소스 파일의 주어진 줄에 도달하면 실행을 멈추라고 디버거에 지시. (perlguts의 "Internal Functions" 참고!)
  • step — 프로그램을 한 줄씩 단계 진행.
  • next — 함수로 들어가지 않고 프로그램을 한 줄씩 단계 진행.
  • continue — 다음 브레이크포인트까지 실행.
  • finish — 현재 함수의 끝까지 실행하고 다시 정지.
  • 'enter' — Enter만 눌러도 가장 최근의 작업을 다시 함. 수 마일의 소스 코드를 단계 진행할 때 축복이에요.
  • ptype — 주어진 인자의 C 정의를 출력.
(gdb) ptype PL_op
type = struct op {
    OP *op_next;
    OP *op_sibparent;
    OP *(*op_ppaddr)(void);
    PADOFFSET op_targ;
    unsigned int op_type : 9;
    unsigned int op_opt : 1;
    unsigned int op_slabbed : 1;
    unsigned int op_savefree : 1;
    unsigned int op_static : 1;
    unsigned int op_folded : 1;
    unsigned int op_spare : 2;
    U8 op_flags;
    U8 op_private;
} *
  • print — 주어진 C 코드를 실행하고 결과를 출력. 경고: Perl은 매크로를 많이 사용하고 gdb는 매크로를 반드시 지원하지는 않아요 (나중의 "gdb 매크로 지원" 참고). 스스로 치환하거나 소스 코드 파일에 cpp를 호출해야 해요 ("The .i Targets" 참고). 예를 들어 다음은 안 되고:
print SvPV_nolen(sv)

이렇게 해야 해요:

print Perl_sv_2pv_nolen(sv)

cpp -dM perl.c | sort라고 말해 만들 수 있는 "매크로 사전"을 두는 게 도움이 될 수 있어요. 그런데도 cpp는 그 매크로를 재귀적으로 적용해 주지는 않아요.

gdb 매크로 지원 (gdb macro support)

최근 버전의 gdb는 매크로 지원이 꽤 좋지만, 사용하려면 디버깅 정보에 매크로 정의가 포함되도록 perl을 컴파일해야 해요. gcc 3.1을 사용한다면 -Doptimize=-g3로 구성하는 걸 뜻해요. 다른 컴파일러는 다른 스위치를 쓸 수 있어요 (매크로 디버깅을 지원한다면).

Perl 데이터 구조 덤프하기 (Dumping Perl Data Structures)

이 매크로 지옥을 우회하는 한 가지 방법은 dump.c의 덤프 함수를 사용하는 거예요. 이들은 내부 Devel::Peek처럼 약간 동작하지만, Perl에서 접근할 수 없는 OP와 그 밖의 구조도 다뤄요. 예를 들어 볼게요. 이전에 쓴 $x = $y + $z를 쓰되 약간의 컨텍스트를 주겠어요: $y = "6XXXX"; $z = 2.3;. 멈추고 찔러보기 좋은 곳은 어디일까요?

+ 연산자를 구현하는 이전에 살펴본 함수 pp_add는 어때요:

(gdb) break Perl_pp_add
Breakpoint 1 at 0x46249f: file pp_hot.c, line 309.

pp_add가 아닌 Perl_pp_add를 쓰는 걸 보세요 — perlguts의 "Internal Functions" 참고. 브레이크포인트를 설치했으니 프로그램을 실행할 수 있어요:

(gdb) run -e '$y = "6XXXX"; $z = 2.3; $x = $y + $z'

gdb가 관련 소스 파일과 라이브러리를 읽어 들이며 많은 잡동사니가 지나가고, 그러면:

Breakpoint 1, Perl_pp_add () at pp_hot.c:309
1396    dSP; dATARGET; bool useleft; SV *svl, *svr;
(gdb) step
311	   dPOPTOPnnrl_ul;
(gdb)

이 코드 조각을 전에 봤죠. dPOPTOPnnrl_ul이 두 개의 NVleftright에 배치하도록 마련한다고 했어요 — 약간 확장해 보죠:

#define dPOPTOPnnrl_ul  NV right = POPn; \
                        SV *leftsv = TOPs; \
                        NV left = USE_LEFT(leftsv) ? SvNV(leftsv) : 0.0

POPn은 스택 위에서 SV를 가져와 (SvNOK가 설정되어 있으면 직접, 아니면 sv_2nv 함수를 호출해) 그 NV를 얻어요. TOPs는 스택 위에서 다음 SV를 가져오지만 제거하지는 않아요 — 네, POPnTOPs를 쓰죠. 그 다음 SvNV를 사용해 전과 같은 방식으로 leftsv에서 NV를 얻어요 — 네, POPnSvNV를 써요.

$y에 대한 NV가 없으므로 sv_2nv를 사용해 변환해야 해요. 다시 한 단계 밟으면 거기 도달해요:

(gdb) step
Perl_sv_2nv (sv=0xa0675d0) at sv.c:1669
1669        if (!sv)
(gdb)

이제 Perl_sv_dump를 사용해 SV를 조사할 수 있어요:

(gdb) print Perl_sv_dump(sv)
SV = PV(0xa057cc0) at 0xa0675d0
REFCNT = 1
FLAGS = (POK,pPOK)
PV = 0xa06a510 "6XXXX"\0
CUR = 5
LEN = 6
$1 = void

여기서 6을 얻을 거란 걸 아니까 서브루틴을 마치자:

(gdb) finish
Run till exit from #0  Perl_sv_2nv (sv=0xa0675d0) at sv.c:1671
0x462669 in Perl_pp_add () at pp_hot.c:311
311	   dPOPTOPnnrl_ul;

이 op도 덤프할 수 있어요: 현재 op는 항상 PL_op에 저장되고, Perl_op_dump로 덤프할 수 있어요. 이것은 CPAN 모듈 B::Debug와 비슷한 출력을 줄 거예요.

(gdb) print Perl_op_dump(PL_op)
{
13  TYPE = add  ===> 14
    TARG = 1
    FLAGS = (SCALAR,KIDS)
    {
        TYPE = null  ===> (12)
          (was rv2sv)
        FLAGS = (SCALAR,KIDS)
        {
11          TYPE = gvsv  ===> 12
            FLAGS = (SCALAR)
            GV = main::b
        }
    }

gdb로 프로그램의 특정 부분 보기 (Using gdb to look at specific parts of a program)

위 예에서는 Perl_pp_add를 찾아야 한다는 걸 알았지만, 도처에 그것에 대한 여러 호출이 있거나 찾고 있는 op가 무엇인지 몰랐다면 어떨까요?

한 가지 방법은 찾고 있는 것 근처 어딘가에 희귀한 호출을 주입하는 거예요. 예를 들어 메서드 앞에 study를 추가할 수 있어요:

study;

그리고 gdb에서:

(gdb) break Perl_pp_study

그다음 찾는 것에 닿을 때까지 단계 진행해요. 특정 반복에서만 멈추고 싶을 때 루프에서 잘 동작해요:

for my $i (1..100) {
    study if $i == 50;
}

gdb로 파서/렉서가 하는 일 보기 (Using gdb to look at what the parser/lexer are doing)

코드를 파싱/렉싱할 때 perl이 무엇을 하는지 보고 싶다면 BEGIN {}을 쓸 수 있어요:

print "Before\n";
BEGIN { study; }
print "After\n";

그리고 gdb에서:

(gdb) break Perl_pp_study

if 블록 안 등에서 파서/렉서가 하는 일을 보려면 좀 더 교묘해야 해요:

if ($x && $y && do { BEGIN { study } 1 } && $z) { ... }

소스 코드 정적 분석 (SOURCE CODE STATIC ANALYSIS)

C 소스 코드를 동적이 아닌 정적으로, 즉 코드를 실행하지 않고 분석하는 다양한 도구가 있어요. C 코드를 파싱하고 결과 그래프를 살펴보는 것만으로 리소스 누수, 정의되지 않은 동작, 타입 불일치, 이식성 문제, 불법 메모리 접근을 유발하는 코드 경로, 그 밖의 유사한 문제를 감지할 수 있어요. 실행과 데이터 흐름에 대해 무엇을 알려주는지 말이죠. 사실 이것이 바로 C 컴파일러가 의심스러운 코드에 대해 경고를 주는 방법이에요.

lint

오래된 좋은 C 코드 품질 검사기인 lint은 여러 플랫폼에서 사용 가능하지만, 벤더마다 구현이 여러 개라서 플래그가 플랫폼마다 동일하지 않다는 점을 알아 두세요.

Makefile에 lint 타겟이 있지만, 플래그를 조정해야 할 수도 있어요 (위 참고).

Coverity

Coverity (https://www.coverity.com/)는 lint와 비슷한 제품이고, 자사 제품의 시험대로 주기적으로 여러 오픈소스 프로젝트를 검사하며, 오픈소스 개발자에게 결함 데이터베이스 계정을 나눠줘요.

perl5 프로젝트를 위한 Coverity 설정이 있어요: https://scan.coverity.com/projects/perl5

HP-UX cadvise (Code Advisor)

HP에는 HP-UX용 C/C++ 정적 분석기 제품인 Code Advisor가 있어요. (URL이 몹시 길고 몹시 불안정해 보여서 여기 링크는 주지 않아요. 좋아하는 검색 엔진으로 찾아보세요.) Configure ... -Dcc=./cadvise_cccadvise_cc 레시피를 쓰는 것(cadvise "User Guide" 참고)과 +wall을 쓰는 것을 권장해요.

cpd (cut-and-paste detector)

cpd 도구는 복사-붙여넣기 코딩을 감지해요. 복사-붙여넣기된 코드의 한 인스턴스가 바뀌면 다른 곳들도 아마 바뀌어야 할 거예요. 따라서 그런 코드는 서브루틴이나 매크로로 바꿔야 할 거예요.

cpd (https://docs.pmd-code.org/latest/pmd_userdocs_cpd.html)는 pmd 프로젝트 (https://pmd.github.io/)의 일부예요. pmd는 원래 Java 코드의 정적 분석을 위해 작성됐지만, 나중에 그 cpd 부분이 C와 C++도 파싱하도록 확장됐어요.

SourceForge 사이트에서 pmd-bin-X.Y.zip ()를 다운로드하고, 그 안의 pmd-X.Y.jar를 추출한 뒤, 소스 코드에 대해 이렇게 실행하세요:

java -cp pmd-X.Y.jar net.sourceforge.pmd.cpd.CPD \
 --minimum-tokens 100 --files /some/where/src --language c > cpd.txt

메모리 한계에 부딪힐 수 있는데, 그 경우 -Xmx 옵션을 쓰세요:

java -Xmx512M ...

gcc 경고 (gcc warnings)

gcc 경고의 비일관성과 커버리지 문제에 대해(예: -Wall이 "모든 경고"를 뜻하지 않는다거나, -Wall이 다루지 않는 흔한 이식성 문제, -ansi-pedantic이 둘 다 제대로 정의되지 않은 경고의 모음 등) 많은 것을 쓸 수 있지만, gcc는 여전히 우리 코딩을 깨끗하게 유지하는 데 유용한 도구예요.

-Wall은 기본적으로 켜져 있어요.

-pedantic이 항상 켜져 있으면 좋겠지만, 불행히도 모든 플랫폼에서 안전하진 않아요 — 예를 들어 시스템 헤더와의 치명적 충돌(Solaris가 대표적)이 있죠. Configure -Dgccansipedantic을 사용하면 cflags 프론트엔드가 안전하다고 알려진 플랫폼에서 -pedantic을 선택해요.

다음 추가 플래그가 더해져요:

  • -Wendif-labels
  • -Wextra
  • -Wc++-compat
  • -Wwrite-strings
  • -Werror=pointer-arith
  • -Werror=vla

다음 플래그는 있으면 좋겠지만, 먼저 그들만의 안정을 필요로 해요:

  • -Wshadow
  • -Wstrict-prototypes

-Wtraditional은 gcc가 한 스위치 아래 많은 경고를 묶는 성가신 경향의 또 다른 예예요 (많이 불평할 테니 실제로 배포하기는 불가능할 거예요). 하지만 마크로 인자를 포함하는 매크로 안의 문자열 상수에 대한 경고처럼, 자체로 쓰면 유익한 일부 경고를 포함해요. 이는 ANSI 이전과 ANSI에서 다르게 동작했고 일부 C 컴파일러는 여전히 전환 중이며, AIX가 그 예예요.

다른 C 컴파일러의 경고 (Warnings of other C compilers)

다른 C 컴파일러(gcc 말고 다른 C 컴파일러도 있어요)는 종종 "엄격한 ANSI" 또는 "약간의 이식성 확장이 있는 엄격한 ANSI" 모드가 켜져 있어요. 예를 들어 Sun Workshop은 -Xa 모드가(암묵적으로지만) 켜져 있고, DEC(요즘은 HP...)는 -std1 모드가 켜져 있어요.

메모리 디버거 (MEMORY DEBUGGERS)

NOTE 1: Purify, valgrind, Third Degree 같은 옛 메모리 디버거 아래에서 실행하면 실행이 크게 느려져요: 초가 분이 되고 분이 시간이 되죠. 예를 들어 Perl 5.8.1 기준으로 ext/Encode/t/Unicode.t 테스트는 Purify, Third Degree, valgrind 아래에서 완료하는 데 엄청 오래 걸려요. valgrind 아래에서는 빠른 컴퓨터에서도 6시간 이상 걸려요. 그 테스트는 메모리 디버거에 꽤 불친절한 일을 하고 있는 게 분명해요. 기다리기 싫다면 perl 프로세스를 그냥 죽여도 돼요. 대략 valgrind는 실행을 10배, AddressSanitizer는 2배 느리게 해요.

(또한 "숨은 결함 잡기" 참고.)

NOTE 2: 메모리 누수 오탐지 수를 최소화하려면 (자세한 내용은 "PERL_DESTRUCT_LEVEL" 참고) 환경 변수 PERL_DESTRUCT_LEVEL을 2로 설정해야 해요. 예를 들어 이렇게:

env PERL_DESTRUCT_LEVEL=2 valgrind ./perl -Ilib ...

NOTE 3: eval이나 require 안에 컴파일 타임 오류가 있으면 알려진 메모리 누수가 있어요. 콜 스택에 S_doeval이 보이는 게 이런 것들의 좋은 신호예요. 불행히도 이 누수 수정은 사소하지 않지만 결국 고쳐야 해요.

NOTE 4: Perl이 Configure 옵션 -Accflags=-DDL_UNLOAD_ALL_AT_EXIT로 빌드되지 않으면 DynaLoader는 스스로 완전히 정리하지 않아요.

valgrind

valgrind 도구는 메모리 누수와 불법 힙 메모리 접근을 모두 찾는 데 쓸 수 있어요. 3.3.0 버전 기준으로 Valgrind는 x86, x86-64, PowerPC의 Linux와 x86·x86-64의 Darwin(OS X)만 지원해요. 특별한 "test.valgrind" 타겟으로 valgrind 아래에서 테스트를 실행할 수 있어요. 발견된 오류와 메모리 누수는 testfile.valgrind라는 파일에 기록되고, 기본적으로 출력은 인라인으로 표시돼요.

사용 예:

make test.valgrind

valgrind는 상당한 오버헤드를 추가하므로 테스트가 훨씬 오래 걸려요. valgrind 테스트는 병렬 실행을 지원해 이에 대처할 수 있어요:

TEST_JOBS=9 make test.valgrind

위 두 호출은 도달 가능한 메모리와 누수 검사가 기본으로 켜져 있어 매우 장황해요. 순수 오류만 보려면:

VG_OPTS='-q --leak-check=no --show-reachable=no' TEST_JOBS=9 \
    make test.valgrind

valgrind는 또한 cachegrind 도구도 제공해요. perl에 대해 이렇게 호출하죠:

VG_OPTS=--tool=cachegrind make test.valgrind

시스템 라이브러리(특히 glibc)도 오류를 유발하므로, valgrind는 억제 파일로 그런 오류를 억제하게 해줘요. valgrind에 딸린 기본 억제 파일이 이미 많이 잡아요. 일부 추가 억제는 t/perl.supp에 정의돼 있어요.

valgrind를 얻고 자세한 정보를 보려면 https://valgrind.org/를 참고하세요.

AddressSanitizer

AddressSanitizer("ASan")은 컴파일러 계측 모듈과 런타임 malloc 라이브러리로 구성돼요. ASan은 다양한 아키텍처, 운영체제, 컴파일러에서 사용 가능해요 (아래 프로젝트 링크 참고). use after free와 버퍼 오버플로 조건 같은 안전하지 않은 메모리 사용을 검사하고, 디버깅 perl이나 최적화 perl을 쉽게 컴파일할 만큼 빠르며, 대부분의 플랫폼에서 최신 버전의 ASan이 기본으로 메모리 누수를 검사해요. 그 외에는(예: x86_64 OS X) ASAN_OPTIONS=detect_leaks=1로 이 기능을 활성화할 수 있어요.

AddressSanitizer로 perl을 빌드하려면 Configure 호출이 이렇게 보여야 해요:

sh Configure -des -Dcc=clang \
 -Accflags="-fsanitize=address -fsanitize-ignorelist=$PWD/asan_ignore" \
 -Aldflags="-fsanitize=address" -Alddlflags="-shared -fsanitize=address"

각 인자의 의미:

  • -Dcc=clang — clang 실행 파일이 PATH에 없다면 전체 경로로 바꿔야 해요.
  • -Accflags="-fsanitize=address -fsanitize-ignorelist=$PWD/asan_ignore" — AddressSanitizer로 perl과 확장 소스를 컴파일. AddressSanitizer는 asan_ignore 파일에 나열된 함수를 무시해요 (이 파일은 각 함수가 나열된 짧은 이유를 담아야 해요).
  • -Aldflags="-fsanitize=address" — perl 실행 파일을 AddressSanitizer로 링크.
  • -Alddlflags="-shared -fsanitize=address" — 동적 확장을 AddressSanitizer로 링크. -shared를 수동으로 지정해야 해요. -Alddlflags=-shared를 쓰면 Configure가 (보통 -shared를 포함하는) lddlflags의 기본값 설정을 막기 때문이에요 (적어도 Linux에서는).

또한 https://github.com/google/sanitizers/wiki/AddressSanitizer를 참고하세요.

Dr Memory

Dr. Memory는 valgrind와 비슷한 도구로 Windows와 Linux에서 사용할 수 있어요.

valgrind의 memcheck와 같은 힙 검사를 지원해요. 다른 도구도 포함돼 있어요.

https://drmemory.org/ 참고.

프로파일링 (PROFILING)

플랫폼에 따라 Perl을 프로파일링하는 여러 방법이 있어요.

실행 파일 프로파일링의 두 가지 흔한 기법이 있어요: *통계적 시간 샘플링(statistical time-sampling)*과 기본 블록 카운팅(basic-block counting).

첫 번째 방법은 CPU 프로그램 카운터를 주기적으로 샘플링해요. 프로그램 카운터는 함수에 대해 생성된 코드와 연관 지을 수 있으므로, 프로그램이 어떤 함수에서 시간을 보내는지에 대한 통계적 관점을 얻을 수 있어요. 주의점은 매우 작은/빠른 함수는 프로파일에 나타날 확률이 낮고, 프로그램을 주기적으로 중단하는 것(보통 밀리초 단위로 꽤 자주)이 결과를 왜곡할 수 있는 추가 오버헤드를 부과한다는 거예요. 첫 번째 문제는 코드를 더 오래 실행하는 것으로 완화될 수 있고(일반적으로 프로파일링에서 좋은 생각이에요), 두 번째 문제는 보통 프로파일링 도구 자체가 방지해요.

두 번째 방법은 생성된 코드를 *기본 블록(basic blocks)*으로 나눠요. 기본 블록은 시작에서만 들어가고 끝에서만 나가는 코드 섹션이에요. 예를 들어 조건부 점프는 기본 블록을 시작해요. 기본 블록 프로파일링은 보통 enter basic block #nnnn 기록 코드를 생성된 코드에 추가함으로써 코드를 *계측(instrumenting)*하며 동작해요. 코드 실행 중에 기본 블록 카운터가 그에 맞게 갱신돼요. 주의점은 추가된 추가 코드가 결과를 왜곡할 수 있다는 거예요: 다시 말하지만 프로파일링 도구는 보통 자기 자신의 효과를 결과에서 제거하려 해요.

Gprof 프로파일링 (Gprof Profiling)

gprof는 많은 Unix 플랫폼에서 사용 가능한 프로파일링 도구로, 통계적 시간 샘플링을 사용해요. gcc로 -pg 플래그를 써서 perl의 프로파일 버전을 빌드할 수 있어요. config.sh를 편집하거나 Configure를 다시 실행하세요. 프로파일 버전의 Perl을 실행하면 실행 중 수집된 프로파일링 데이터를 담은 gmon.out이라는 출력 파일이 생성돼요.

빠른 힌트:

$ sh Configure -des -Dusedevel -Accflags='-pg' \
    -Aldflags='-pg' -Alddlflags='-pg -shared' \
    && make perl
$ ./perl ... # creates gmon.out in current directory
$ gprof ./perl > out
$ less out

(RT #118199이 해결될 때까지 <-Alddlflags> 줄에 -shared를 추가해야 할 거예요.)

그러면 gprof 도구가 수집된 데이터를 다양한 방식으로 표시할 수 있어요. 보통 gprof는 다음 옵션을 이해해요:

  • -a — 프로파일에서 정적으로 정의된 함수를 제외.
  • -b — 프로파일에서 장황한 설명을 제외.
  • -e routine — 주어진 루틴과 그 하위 항목을 프로파일에서 제외.
  • -f routine — 주어진 루틴과 그 하위 항목만 프로파일에 표시.
  • -sgmon.sum라는 요약 파일을 생성. 이후의 gprof 실행에 주어 여러 실행에 걸쳐 데이터를 누적할 수 있어요.
  • -z — 사용량이 0인 루틴을 표시.

사용 가능한 명령과 출력 형식의 더 자세한 설명은 여러분 로컬의 gprof 문서를 참고하세요.

GCC gcov 프로파일링 (GCC gcov Profiling)

기본 블록 프로파일링은 gcc 3.0 이상에서 공식적으로 사용 가능해요. gcc로 -fprofile-arcs -ftest-coverage 플래그를 써서 perl의 프로파일 버전을 빌드할 수 있어요. config.sh를 편집하거나 Configure를 다시 실행하세요.

빠른 힌트:

$ sh Configure -des -Dusedevel -Doptimize='-g' \
    -Accflags='-fprofile-arcs -ftest-coverage' \
    -Aldflags='-fprofile-arcs -ftest-coverage' \
    -Alddlflags='-fprofile-arcs -ftest-coverage -shared' \
    && make perl
$ rm -f regexec.c.gcov regexec.gcda
$ ./perl ...
$ gcov regexec.c
$ less regexec.c.gcov

(RT #118199이 해결될 때까지 <-Alddlflags> 줄에 -shared를 추가해야 할 거예요.)

프로파일 버전의 Perl을 실행하면 프로파일 출력이 생성돼요. 각 소스 파일마다 함께 쓰는 .gcda 파일이 생성돼요.

결과를 표시하려면 gcov 유틸리티를 사용해요 (gcc 3.0 이상이 설치되어 있다면 설치되어 있을 거예요). gcov는 소스 코드 파일에 대해 이렇게 실행돼요:

gcov sv.c

이러면 sv.c.gcov가 생성돼요. .gcov 파일은 "#" 마커로 표시된 실행의 상대 빈도가 주석으로 달린 소스 코드를 담아요. 모든 프로파일된 오브젝트 파일에 대해 .gcov 파일을 생성하려면 이렇게 실행할 수 있어요:

for file in `find . -name \*.gcno`
do sh -c "cd `dirname $file` && gcov `basename $file .gcno`"
done

gcov의 유용한 옵션으로는 기본 블록, 분기, 함수 호출 커버리지를 요약하는 -b와 상대 빈도 대신 실제 카운트를 쓰는 -c가 있어요. gcov와 gcc 기본 블록 프로파일링 사용에 대한 자세한 내용은 최신 GNU CC 매뉴얼을 참고하세요. gcc 4.8 기준으로 https://gcc.gnu.org/onlinedocs/gcc/Gcov-Intro.html#Gcov-Intro에 있어요.

callgrind 프로파일링 (callgrind profiling)

callgrind는 소스 코드 프로파일링을 위한 valgrind 도구예요. kcachegrind(Qt 기반 UI)와 함께 쓰면 코드가 시간을 어디서 보내는지 개괄적으로 볼 수 있고, 호출자, 호출 트리 등을 검사하는 능력도 있어요. 이점 중 하나는 디버깅 심볼로 컴파일되지 않은 perl과 XS 모듈에도 쓸 수 있다는 거예요.

perl이 디버깅 심볼(-g)로 컴파일된다면, 주석이 달린 소스를 보고 클릭하며 돌아다닐 수 있어요. Devel::NYTProf의 HTML 출력처럼요.

기본 사용:

valgrind --tool=callgrind ./perl ...

기본적으로 callgrind.out.PID에 출력을 쓰지만 --callgrind-out-file=...로 바꿀 수 있어요.

데이터를 보려면:

kcachegrind callgrind.out.PID

터미널에서 데이터를 보는 걸 선호한다면 callgrind_annotate를 쓸 수 있어요. 기본 형태:

callgrind_annotate callgrind.out.PID | less

몇 가지 유용한 옵션:

  • --threshold — 관심 있는 (기본 정렬 이벤트의) 카운트 백분율. 기본은 99%이고, 100%는 빠진 것처럼 보이는 것들을 보여줄 수 있어요.
  • --auto — 이벤트 카운트 임계값을 달성하는 데 기여한 함수를 담은 모든 소스 파일을 주석 처리.

profiler 프로파일링 (Cygwin)

Cygwin은 gprof 프로파일링과 gcov 커버리지 테스트를 허용하지만, 메인 실행 파일만 프로파일해요.

profiler 도구를 사용해 샘플 기반 프로파일링을 수행할 수 있어요. 디버깅 심볼 외에 실행 파일을 특별히 준비할 필요가 없어요.

이것은 gprof로 처리할 수 있는 샘플링 데이터를 생성해요.

Cygwin 웹 사이트에 제한적인 문서가 있어요.

Visual Studio 프로파일링 (Visual Studio Profiling)

명령줄에서 perl을 빌드하더라도 MSVC로 perl을 빌드했다면 Visual Studio 프로파일러로 perl을 프로파일할 수 있어요. CFG=DebugCFG=DebugSymbols로 perl을 빌드해야 해요.

Visual Studio 프로파일러는 샘플링 프로파일러예요.

시작하려면 Visual Studio 문서를 참고하세요.

기타 트릭 (MISCELLANEOUS TRICKS)

PERL_DESTRUCT_LEVEL

예를 들어 valgrind로 직접 테스트를 수동 실행하고 싶다면, 기본적으로 perl은 할당한 모든 메모리(전역 메모리 아레나 같은)를 명시적으로 정리하지 않고 대신 전체 프로그램의 exit()에 그런 할당을 "맡긴다"는 점을 참고하세요. 이를 "객체의 전역 소멸"이라고도 해요.

perl에게 완전한 정리를 하라고 말하는 방법이 있어요: 환경 변수 PERL_DESTRUCT_LEVEL을 0이 아닌 값으로 설정하세요. t/TEST 래퍼는 이를 2로 설정하는데, "전역 누수"를 보고 싶지 않다면 여러분도 그렇게 해야 해요. 예를 들어 valgrind 아래에서 실행하려면:

env PERL_DESTRUCT_LEVEL=2 valgrind ./perl -Ilib t/foo/bar.t

(참고: mod_perl Apache 모듈은 이 환경 변수를 자체 목적으로 사용하고 그 의미를 확장해요. 자세한 내용은 mod_perl 문서를 참고하세요. 또한 생성된 스레드는 이 변수를 1로 설정하는 것과 동등한 일을 해요.)

실행 끝에 N scalars leaked 메시지를 얻으면 -DDEBUG_LEAKING_SCALARS (Configure -Accflags=-DDEBUG_LEAKING_SCALARS)로 다시 컴파일할 수 있어요. 이러면 누수된 모든 SV의 주소와 각 SV가 원래 어디서 할당됐는지에 대한 세부 사항이 덤프돼요. 이 정보는 Devel::Peek도 표시해요. 각 SV에 기록되는 추가 세부 사항이 메모리 사용량을 늘리므로 프로덕션 환경에서는 쓰면 안 됨을 참고하세요. 또한 new_SV()를 매크로에서 실제 함수로 바꾸므로, 좋아하는 디버거로 그 성가신 SV들이 어디서 할당됐는지 발견할 수 있어요.

런타임에 메모리를 누수하고 있는 것 같은데, valgrind도 -DDEBUG_LEAKING_SCALARS도 아무것도 찾지 못한다면, 아마 아직 도달 가능하고 인터프리터 소멸 중에 제대로 정리될 SV들을 누수하고 있는 거예요. 그런 경우 -Dm 스위치가 누수 원인을 가리켜 줄 수 있어요. 실행 파일이 -DDEBUG_LEAKING_SCALARS로 빌드됐다면 -Dm은 메모리 할당 외에 SV 할당도 출력해요. 각 SV 할당은 distinct 일련번호를 가지며, 그 일련번호는 SV의 생성과 소멸 시에 기록돼요. 그래서 누수 코드를 루프에서 실행한다면 각 사이클 사이에 생성되었지만 소멸되지 않은 SV를 찾아야 해요. 그런 SV가 발견되면 new_SV() 안에 조건부 브레이크포인트를 설정하고 PL_sv_serial이 누수 SV의 일련번호와 같을 때만 멈추게 해요. 그러면 누수 SV가 할당되는 바로 그 상태에서 인터프리터를 잡게 되고, 이는 많은 경우 누수 원인을 찾기에 충분해요.

-Dm은 출력에 PerlIO 레이어를 사용하므로, 그 자체로 꽤 많은 SV를 할당하는데, 재귀를 피하기 위해 숨겨져 있어요. -DPERL_MEM_LOG가 제공하는 SV 로깅을 대신 사용하면 PerlIO 레이어를 우회할 수 있어요.

누수 SV 발견: sv_mark_arenas()와 sv_sweep_arenas()

이 함수들은 DEBUGGING 빌드에만 존재해요. 첫 번째는 SV 아레나에서 찾을 수 있는 모든 살아있는 SV를 SVf_BREAK 플래그로 표시해요. 두 번째는 플래그가 설정되지 않은 그런 SV를 나열하고, 나머지의 플래그를 재설정해요. 이들은 코드의 두 지점 사이에서 생성되었지만 해제되지 않는 SV를 식별하기 위한 것이에요. 코드의 관련 지점에서 그것들에 대한 호출을 임시로 추가하거나, 디버거에서 직접 호출함으로써 사용할 수 있어요.

예를 들어 다음 코드가 누수한다고 발견됐다고 해 보죠:

while (1) { eval '\(1..3)' }

스레드 perl의 gdb 세션은 이렇게 보일 수 있어요:

$ gdb ./perl
(gdb) break Perl_pp_entereval
(gdb) run -e'while (1) { eval q{\(1..3)} }'
...
Breakpoint 1, Perl_pp_entereval ....
(gdb) call Perl_sv_mark_arenas(my_perl)
(gdb) continue
...
Breakpoint 1, Perl_pp_entereval ....`
(gdb) call Perl_sv_sweep_arenas(my_perl)
Unmarked SV: 0xaf23a8: AV()
Unmarked SV: 0xaf2408: IV(1)
Unmarked SV: 0xaf2468: IV(2)
Unmarked SV: 0xaf24c8: IV(3)
Unmarked SV: 0xace6c8: PV("AV()"\0)
Unmarked SV: 0xace848: PV("IV(1)"\0)
(gdb)

여기서 첫 pp_entereval() 호출 시작에 모든 기존 SV가 표시돼요. 그런 다음 두 번째 호출 시작에 그 이후 생성됐지만 아직 해제되지 않은 모든 SV를 나열해요. 배열과 그 세 요소가 아마 해제되지 않을 것이라는 게 빠르게 분명해지죠. 상수 폴딩 중 버그의 결과일 수 있어요. 마지막 두 SV는 디버깅 출력 중 생성된 임시일 뿐이므로 무시할 수 있어요.

이 트릭은 SVf_BREAK 플래그가 그 외에 쓰이지 않는다는 사실에 의존해요. 이 플래그는 보통 전역 소멸 중에만 쓰이지만, 가끔 리스트 할당 양쪽의 공통 요소를 찾는 mark and sweep 연산에도 쓰여요. 플래그의 존재는 코어의 특정 동작(문자열 SV를 복사할지 COW할지 선택 같은)을 바꿀 수도 있어요. 그래서 켜면 코드 동작을 약간 바꿀 수 있어요.

PERL_MEM_LOG

-DPERL_MEM_LOG (-Accflags=-DPERL_MEM_LOG)로 컴파일하면 메모리와 SV 할당 모두 로깅 함수를 거쳐, 브레이크포인트 설정에 편리해요.

-DPERL_MEM_LOG_NOIMPL (-Accflags=-DPERL_MEM_LOG_NOIMPL)도 함께 컴파일되지 않는 한, 로깅 함수는 $ENV{PERL_MEM_LOG}을 읽어 이벤트를 로깅할지, 로깅한다면 어떻게 할지 결정해요:

$ENV{PERL_MEM_LOG} =~ /m/           Log all memory ops
$ENV{PERL_MEM_LOG} =~ /s/           Log all SV ops
$ENV{PERL_MEM_LOG} =~ /c/           Additionally log C backtrace for
                                    new_SV events
$ENV{PERL_MEM_LOG} =~ /t/           include timestamp in Log
$ENV{PERL_MEM_LOG} =~ /^(\d+)/      write to FD given (default is 2)

메모리 로깅은 -Dm과 다소 비슷하지만 -DDEBUGGING과 독립적이고 더 높은 레벨이에요. Newx(), Renew(), Safefree()의 모든 사용이 호출자의 소스 파일과 줄 번호(그리고 C 컴파일러가 지원한다면 C 함수 이름)와 함께 로깅돼요. 대조적으로 -Dmmalloc() 지점에 직접 있어요. SV 로깅도 비슷해요.

로깅이 PerlIO를 사용하지 않으므로 모든 SV 할당이 로깅되고, 로깅을 활성화해도 추가 SV 할당이 도입되지 않아요. -DDEBUG_LEAKING_SCALARS로 컴파일하면 각 SV 할당의 일련번호도 로깅돼요.

c 옵션은 Perl_c_backtrace 기능을 사용하므로, 접근하기 위해 추가로 Configure -Dusecbacktrace 컴파일 플래그가 필요해요.

gdb 위의 DDD (DDD over gdb)

gdb 위의 DDD 프론트엔드로 perl을 디버깅하는 분들은 다음이 유용할 거예요.

데이터 변환 단축키 메뉴를 확장할 수 있어요. 예를 들어 입력 없이 클릭 한 번으로 SV의 IV 값을 표시할 수 있죠. 그렇게 하려면 ~/.ddd/init 파일을 편집하고 다음 뒤에:

! Display shortcuts.
Ddd*gdbDisplayShortcuts: \
/t ()   // Convert to Bin\n\
/d ()   // Convert to Dec\n\
/x ()   // Convert to Hex\n\
/o ()   // Convert to Oct(\n\

다음 두 줄을 추가하세요:

((XPV*) (())->sv_any )->xpv_pv  // 2pvx\n\
((XPVIV*) (())->sv_any )->xiv_iv // 2ivx

이제 ivx와 pvx 조회를 할 수 있고, 거기에 sv_peek "변환"을 꽂을 수도 있어요:

Perl_sv_peek(my_perl, (SV*)()) // sv_peek

(my_perl은 스레드 빌드를 위한 거예요.) 마지막 줄을 제외한 모든 줄은 \n\로 끝나야 함을 기억하세요.

또는 init 파일을 인터랙티브하게 편집하세요: 3번째 마우스 버튼 -> New Display -> Edit Menu

참고: gdb 섹션에서 최대 20개의 변환 단축키를 정의할 수 있어요.

C 백트레이스 (C backtrace)

일부 플랫폼에서 Perl은 C 레벨 백트레이스 검색을 지원해요 (gdb 같은 기호 디버거가 하는 것과 유사해요).

백트레이스는 C 호출 프레임의 스택 트레이스를 반환하며, 심볼 이름(함수 이름), 객체 이름(예: "perl"), 가능하면 소스 코드 위치(file:line)를 담아요.

지원 플랫폼은 Linux와 OS X예요 (일부 *BSD는 적어도 부분적으로 동작할 수 있지만 아직 테스트되진 않았어요).

이 기능은 여러 스레드로 테스트되지 않았지만, 백트레이스를 하는 그 스레드의 백트레이스만 보여줄 거예요.

이 기능은 Configure -Dusecbacktrace로 활성화해야 해요.

-Dusecbacktrace는 또한 컴파일/링크할 때 디버그 정보 유지(흔히 -g)를 활성화해요. 많은 컴파일러/링커가 최적화와 디버그 정보 유지 둘 다를 지원해요. 디버그 정보는 심볼 이름과 소스 위치에 필요해요.

정적 함수는 백트레이스에 보이지 않을 수 있어요.

소스 코드 위치는, 사용 가능하더라도, 컴파일러가 예를 들어 코드를 인라인했다면 종종 누락되거나 오해의 소지가 있을 수 있어요. 최적화자는 소스 코드와 오브젝트 코드를 매칭하는 것을 꽤 어렵게 만들 수 있어요.

Linux

BFD (-lbfd) 라이브러리가 설치되어 있어야 합니다. 그렇지 않으면 perl이 링크에 실패해요. BFD는 보통 GNU binutils의 일부로 배포돼요.

요약: Configure ... -Dusecbacktrace 그리고 -lbfd가 필요해요.

OS X

소스 코드 위치는 Developer Tools가 설치된 경우에 지원돼요. (BFD는 필요 없어요.)

요약: Configure ... -Dusecbacktrace 그리고 Developer Tools 설치가 좋아요.

선택적으로, 이 기능을 시험해 보기 위해, 경고나 croak(die) 메시지가 방출되기 직전에 백트레이스 자동 덤프를 활성화할 수 있어요. Configure에 -Accflags=-DUSE_C_BACKTRACE_ON_ERROR를 추가하세요.

위의 추가 기능이 활성화되지 않는 한, Perl/XS 레벨을 제외하고 백트레이스 기능에 대해 아무것도 보이지 않아요.

또한, 이 기능을 컴파일하도록 활성화했더라도 런타임에 환경 변수로 활성화해야 해요: PERL_C_BACKTRACE_ON_ERROR=10. 원하는 프레임 수를 알려주는 0보다 큰 정수여야 해요.

Perl 레벨에서(예를 들어 XS 확장을 사용해) 백트레이스를 검색하는 건 기대만큼 흥미롭지 않을 거예요: 보통 runops, entersub 정도만 보이고 그 외는 별로 없어요. 이 API는 Perl 레벨 실행이 아니라 Perl 구현 내부에서 호출되도록 의도됐어요.

백트레이스의 C API는 다음과 같아요:

  • get_c_backtrace
  • free_c_backtrace
  • get_c_backtrace_dump
  • dump_c_backtrace

Poison

디버거에서 메모리 영역이 신비롭게 0xABABABAB나 0xEFEFEFEF로 가득 찬 것을 보게 된다면 Poison() 매크로의 효과를 보는 것일 수 있어요. perlclib 참고.

읽기 전용 optree (Read-only optrees)

ithreads 아래에서 optree는 읽기 전용이에요. 이를 강제해 버그가 있는 코드의 쓰기 접근을 검사하려면 -Accflags=-DPERL_DEBUG_READONLY_OPS로 컴파일해 mmap을 통해 op 메모리를 할당하고, 서브루틴에 붙일 때 읽기 전용으로 설정하는 코드를 활성화하세요. op에 대한 어떤 쓰기 접근도 SIGBUS와 abort를 일으켜요.

이 코드는 개발 전용으로 의도됐고, 모든 Unix 변종에 이식 가능하지 않을 수 있어요. 또한 80% 해법이에요. 모든 op를 읽기 전용으로 만들 수는 없거든요. 특히 BEGIN 블록에 속한 op 슬랩에는 적용되지 않아요.

하지만 80% 해법으로도 여전히 효과적이에요. 과거에 버그를 잡아냈으니까요.

bool이 bool이 아닌 때 (When is a bool not a bool?)

C99 이전 컴파일러에는 표준 bool 타입이 반드시 있지는 않았고, 그래서 몇 가지 우회책이 만들어졌어요. TRUEFALSE 매크로는 여전히 truefalse의 대안으로 사용 가능해요. 그리고 모든 상황에서 참/거짓 값으로 올바르게 캐스팅하기 위해 cBOOL 매크로가 만들어졌지만, 더 이상 필요하지 않아야 해요. (bool) expr을 쓰는 게 이제 항상 동작해야 해요.

TRUE, FALSE, cBOOL 중 어느 것도 제거할 계획은 없어요.

안전하지 않은 잘림 찾기 (Finding unsafe truncations)

다음과 같이 Configure를 실행하고 싶을 수 있어요:

-Accflags='-Wconversion -Wno-sign-conversion -Wno-shorten-64-to-32'

또는 여러분 컴파일러의 등가물. 나타나는 안전하지 않은 잘림을 더 쉽게 발견할 수 있게요.

The .i Targets

foo.c 파일의 매크로를 확장할 수 있어요:

make foo.i

cpp를 사용해 매크로를 확장할 거예요. 결과에 겁먹지 마세요.

저자 (AUTHOR)

이 문서는 원래 Nathan Torkington이 작성했고, perl5-porters 메일링 리스트가 관리해요.