Perl pack/unpack 튜토리얼

Perl pack/unpack 튜토리얼 (perlpacktut)

packunpack을 처음부터 차근차근 배우는 튜토리얼이에요. 이 두 함수는 직관적이지 않아서 Perl에서 가장 오해받고 간과되기 쉬운 함수인데, 이 문서가 그 신비를 풀어드릴게요.

출처: Perl 공식 문서 - perlpacktut

본문

이름 (NAME)

perlpacktut - packunpack 튜토리얼

설명 (DESCRIPTION)

packunpack은 사용자가 정의한 템플릿에 따라 데이터를 변환하는 두 함수예요. Perl이 값을 안전하게 보관하는 방식과, Perl 프로그램이 처하는 환경에서 요구될 수 있는 잘 정의된 표현 사이를 오가도록 도와주죠. 아쉽게도 이 둘은 Perl이 제공하는 함수 중 가장 오해받고 가장 자주 간과되는 함수이기도 해요. 이 튜토리얼이 그 미스터리를 풀어드릴게요.

기본 원리 (The Basic Principle)

대부분의 프로그래밍 언어는 변수가 저장되는 메모리를 숨기지 않아요. 예를 들어 C에서는 어떤 변수의 주소를 얻을 수 있고, sizeof 연산자를 쓰면 그 변수에 몇 바이트가 할당됐는지 알 수 있어요. 주소와 크기를 이용해 저장 공간을 마음껏 접근할 수 있죠.

Perl에서는 메모리에 임의로 접근할 수 없지만, packunpack이 제공하는 구조적·표현적 변환이 훌륭한 대안이 돼요. pack 함수는 주어진 명세, 즉 "템플릿" 인자에 따라 값들을 바이트 시퀀스로 변환해요. unpack은 그 반대 과정으로, 바이트 문자열의 내용에서 값들을 뽑아내죠. (다만 전부 다 깔끔하게 언팩할 수 있는 건 아니라는 점은 주의하세요. 경험 많은 여행자라면 확인할 수 있듯 아주 흔한 일이에요.)

왜 이진 표현으로 값을 담은 메모리 덩어리가 필요할까 하고 물으실 수 있어요. 한 가지 좋은 이유는 파일·장치·네트워크 연결에 접근하는 입출력인데, 이때 이진 표현이 강제되거나 처리에 이점을 주거든요. 또 다른 이유는 Perl 함수로는 제공되지 않는 시스템 콜에 데이터를 넘길 때예요. syscall은 C 프로그램에서 쓰는 방식 그대로 저장된 매개변수를 요구하거든요. 심지어 텍스트 처리(다음 절에서 보여드릴게요)도 이 두 함수를 적절히 쓰면 단순해질 수 있어요.

(un)pack이 어떻게 동작하는지 보기 위해, 변환이 저속인 간단한 템플릿 코드부터 시작할게요. 바이트 시퀀스의 내용과 16진수 문자열 사이의 변환이에요. unpack을 써볼게요. 이건 프로그램이 실패 직전에 뱉는 크래시 메시지나 hex dump를 떠올리게 할 수도 있겠네요. $mem 변수가 뜻을 전혀 가정하지 않고 검사하고 싶은 바이트 시퀀스를 담고 있다고 하면, 이렇게 쓸 수 있어요:

my( $hex ) = unpack( 'H*', $mem );
print "$hex\n";

그러면 다음과 같은 화면을 보게 될 거예요. 각 16진수 한 쌍이 한 바이트에 해당하죠:

41204d414e204120504c414e20412043414e414c2050414e414d41

이 메모리 덩어리 안에는 뭐가 들어 있었을까요? 숫자? 문자? 아니면 둘의 혼합? ASCII(또는 비슷한 인코딩)를 쓰는 컴퓨터라고 가정해볼게요. 0x40~0x5A 범위의 16진수 값은 대문자를, 0x20은 공백을 나타내요. 그래서 아마 텍스트 조각일 거예요. 어떤 사람은 타블로이드처럼 읽겠지만, 어떤 사람은 ASCII 표를 들춰보고 초등학교 1학년 기분을 되살려야 하겠죠. 어느 쪽으로 읽을지는 그다지 신경 쓰지 않고, unpack이 템플릿 코드 H로 바이트 시퀀스의 내용을 흔한 16진수 표기로 변환한다는 점을 기억하세요. "시퀀스의"라는 건 양에 대한 상당히 막연한 표현이라, H는 반복 횟수(repeat count)가 뒤에 오지 않으면 단지 16진수 한 자리만 변환하도록 정의되어 있어요. 반복 횟수로 *를 쓰면 남은 전부를 쓰라는 뜻이에요.

역연산 - 16진수 문자열에서 바이트 내용을 pack하는 것 - 도 똑같이 쉽게 쓸 수 있어요. 예를 들면:

my $s = pack( 'H2' x 10, 30..39 );
print "$s\n";

pack에 10개의 두 자리 16진수 문자열 리스트를 넘기므로, pack 템플릿에는 10개의 pack 코드가 들어가야 해요. ASCII 문자 인코딩을 쓰는 컴퓨터에서 실행하면 0123456789를 출력할 거예요.

텍스트 pack 하기 (Packing Text)

다음과 같은 데이터 파일을 읽어야 한다고 가정해볼게요:

Date      |Description                | Income|Expenditure
01/24/2001 Zed's Camel Emporium                    1147.99
01/28/2001 Flea spray                                24.99
01/29/2001 Camel rides to tourists      235.00

어떻게 처리할까요? 먼저 split을 떠올릴 수 있어요. 그런데 split은 빈 필드를 합쳐버리므로, 레코드가 수입인지 지출인지를 결코 알 수 없어요. 이런. 음, 언제나 substr을 쓸 수 있겠죠:

while (<>) {
    my $date   = substr($_,  0, 11);
    my $desc   = substr($_, 12, 27);
    my $income = substr($_, 40,  7);
    my $expend = substr($_, 52,  7);
    ...
}

딱히 재미있는 방법은 아니죠? 사실 보기보다 더 나빠요. 눈 밝은 분은 알아채셨겠지만, 첫 필드는 10자 너비여야 하는데 그 오류가 다른 숫자들까지 전파돼버렸고, 그걸 손으로 세어야 했거든요. 그래서 오류가 나기 쉽고 몹시 비우호적이에요.

아니면 정규표현식을 쓸 수도 있어요:

while (<>) {
    my($date, $desc, $income, $expend) =
        m|(\d\d/\d\d/\d{4}) (.{27}) (.{7})(.*)|;
    ...
}

으. 조금 나아지긴 했지만... 그걸 유지보수하고 싶으신가요?

야, Perl이 이런 걸 쉽게 만들어주는 거 아니었나요? 네, 맞아요. 올바른 도구를 쓰면요. packunpack은 위 같은 고정 폭 데이터를 다룰 때 도움을 주도록 설계되었어요. unpack을 쓰는 해법을 살펴볼게요:

while (<>) {
    my($date, $desc, $income, $expend) = unpack("A10xA27xA7A*", $_);
    ...
}

조금 나아 보이지만, 그 이상한 템플릿을 뜯어봐야겠죠. 그게 어디서 나온 걸까요?

좋아요, 데이터를 다시 한번 살펴볼게요. 사실 헤더와 손쉬운 위치 표시용 눈금자를 포함해볼게요.

             1         2         3         4         5
    1234567890123456789012345678901234567890123456789012345678
    Date      |Description                | Income|Expenditure
    01/28/2001 Flea spray                                24.99
    01/29/2001 Camel rides to tourists      235.00

여기서 날짜 열이 1열부터 10열까지 뻗어 있음을 알 수 있어요. 열 개 문자 너비죠. "문자"를 뜻하는 pack어(팩줄임말)는 A고, 열 개면 A10이에요. 그래서 날짜만 추출하고 싶다면 이렇게 말할 수 있어요:

my($date) = unpack("A10", $_);

좋아요, 다음은 뭘까요? 날짜와 설명 사이에 빈 열이 있는데, 그걸 건너뛰고 싶어요. x 템플릿은 "앞으로 건너뛰기"를 뜻하므로, 그게 하나 필요해요. 다음으로 또 다른 문자 배치가 있어요. 12열부터 38열까지요. 27개 더니까 A27이에요. (울타리 기둥 오류(fencepost error)를 조심하세요. 12와 38 사이에는 26이 아니라 27개 문자가 있어요. 세어보세요!)

이제 한 문자를 더 건너뛰고 다음 7개 문자를 집어올게요:

my($date,$description,$income) = unpack("A10xA27xA7", $_);

이제 영리한 부분이 나와요. 우리 장부의 수입만 있고 지출이 없는 줄은 46열에서 끝날지도 몰라요. 그래서 unpack 패턴에 또 다른 12자를 반드시 찾아내라고 말하고 싶지 않아요. 그냥 "남은 게 있으면 가져가라"고 말할게요. 정규표현식에서 짐작하시겠지만, 그게 *의 뜻이에요: "남은 걸 전부 써라".

  • 다만 경고할게요. 정규표현식과 달리, unpack 템플릿이 들어온 데이터와 맞지 않으면 Perl이 비명을 지르며 죽어요(die).

그래서 전부 합치면:

my ($date, $description, $income, $expend) =
    unpack("A10xA27xA7xA*", $_);

이제 데이터가 파싱됐어요. 이제 수입과 지출을 합산해서, 같은 형식으로 장부 끝에 한 줄을 더 추가해 얼마를 벌었고 얼마를 썼는지 표시하고 싶다고 생각해볼게요:

while (<>) {
    my ($date, $desc, $income, $expend) =
        unpack("A10xA27xA7xA*", $_);
    $tot_income += $income;
    $tot_expend += $expend;
}

$tot_income = sprintf("%.2f", $tot_income); # Get them into
$tot_expend = sprintf("%.2f", $tot_expend); # "financial" format

$date = POSIX::strftime("%m/%d/%Y", localtime);

# OK, let's go:

print pack("A10xA27xA7xA*", $date, "Totals",
    $tot_income, $tot_expend);

어, 으음. 제대로 안 됐네요. 무슨 일이 있었는지 볼게요:

01/24/2001 Zed's Camel Emporium                     1147.99
01/28/2001 Flea spray                                 24.99
01/29/2001 Camel rides to tourists     1235.00
03/23/2001Totals                     1235.001172.98

좋아요, 시작은 됐는데 공백은 어디로 갔을까요? x를 넣지 않았나요? 앞으로 건너뛰어야 하지 않나요? "pack" in perlfunc이 뭐라고 하는지 볼게요:

x   A null byte.

우웩. 당연하죠. "null byte"(문자 0)와 "space"(문자 32)는 큰 차이가 있어요. Perl이 날짜와 설명 사이에 뭔가를 넣긴 했는데, 안타깝게도 그걸 볼 수가 없네요!

우리가 실제로 해야 할 일은 필드의 폭을 늘리는 거예요. A 형식은 존재하지 않는 문자를 공백으로 채우므로, 그 추가 공백을 이용해 필드를 정렬할 수 있어요. 이렇게요:

print pack("A11 A28 A8 A*", $date, "Totals",
    $tot_income, $tot_expend);

(템플릿에 공백을 넣어 더 읽기 쉽게 할 수 있지만, 그게 출력의 공백으로 이어지진 않는다는 점을 기억하세요.) 이번엔 이렇게 나왔어요:

01/24/2001 Zed's Camel Emporium                     1147.99
01/28/2001 Flea spray                                 24.99
01/29/2001 Camel rides to tourists     1235.00
03/23/2001 Totals                      1235.00 1172.98

조금 낫지만, 아직 마지막 열을 더 오른쪽으로 옮겨야 해요. 고치기 쉬운 방법이 있어요. 안타깝게도 pack으로는 필드를 오른쪽 정렬할 수 없지만, sprintf로는 할 수 있어요:

$tot_income = sprintf("%.2f", $tot_income);
$tot_expend = sprintf("%12.2f", $tot_expend);
$date = POSIX::strftime("%m/%d/%Y", localtime);
print pack("A11 A28 A8 A*", $date, "Totals",
    $tot_income, $tot_expend);

이번엔 올바른 답을 얻었어요:

01/28/2001 Flea spray                                 24.99
01/29/2001 Camel rides to tourists     1235.00
03/23/2001 Totals                      1235.00      1172.98

이게 바로 고정 폭 데이터를 소비하고 생산하는 방법이에요. 지금까지 본 packunpack을 정리해볼게요:

  • 여러 조각의 데이터에서 고정 폭 버전 하나로 가려면 pack을, 고정 폭 문자열을 여러 조각의 데이터로 바꾸려면 unpack을 쓰세요.

  • pack 형식 A는 "어떤 문자든"을 뜻해요. pack하다가 채울 것이 떨어지면 pack이 나머지를 공백으로 채워요.

  • xunpack할 때 "한 바이트 건너뛰기"를 뜻하고, pack할 때는 "null byte 넣기"를 뜻해요. 평범한 텍스트를 다룰 때 의도한 것과는 아마 다를 거예요.

  • 형식 뒤에 숫자를 붙여 그 형식이 영향을 줄 문자 수를 지정할 수 있어요. A12는 "12개 문자 가져가기", x6은 "6바이트 건너뛰기"(문자 0을 6번)를 뜻해요.

  • 숫자 대신 *를 써서 "남은 전부 소비"를 뜻할 수 있어요.

경고: 여러 조각의 데이터를 pack할 때, *는 단지 "현재 조각의 데이터를 전부 소비"를 뜻해요. 즉

pack("A*A*", $one, $two)

$one 전부를 첫 A*에, $two 전부를 둘째 A*에 넣어요. 이건 일반 원리예요. 각 형식 문자는 pack될 데이터 한 조각에 대응하죠.

숫자 pack 하기 (Packing Numbers)

텍스트 데이터는 이만하면 됐어요. 이제 packunpack이 가장 잘하는 본론으로 들어갈게요. 숫자의 이진 형식 처리죠. 물론 이진 형식이 하나뿐인 건 아니에요. 인생이 그렇게 단순했으면 좋겠지만요, Perl이 그 모든 까다로운 작업을 대신 해줘요.

정수 (Integers)

숫자를 pack·unpack한다는 것은 어떤 특정한 이진 표현으로 (그리고 그로부터) 변환함을 뜻해요. 부동소수점은 잠시 치워두고, 그런 표현의 두드러진 속성은 이래요:

  • 정수를 저장하는 데 쓰는 바이트 수,
  • 내용이 부호 있는 수인지 부호 없는 수인지 해석되는지,
  • 바이트 순서: 첫 바이트가 최하위 바이트인지 최상위 바이트인지 (각각 리틀엔디언 또는 빅엔디언).

예를 들어 20302를 여러분 컴퓨터의 표현으로 부호 있는 16비트 정수로 pack하려면 이렇게 써요:

my $ps = pack( 's', 20302 );

다시 말하지만 결과는 문자열인데, 이번엔 2바이트를 담고 있어요. 이 문자열을 출력하면(보통 권장하지 않아요) 시스템의 바이트 순서에 따라 ON이나 NO가 보일 수 있고, 컴퓨터가 ASCII 문자 인코딩을 안 쓰면 아주 다른 게 보일 수 있어요. $ps를 같은 템플릿으로 unpack하면 원래 정수 값을 돌려받아요:

my( $s ) = unpack( 's', $ps );

이건 모든 숫자 템플릿 코드에 해당돼요. 하지만 기적은 기대하지 마세요. pack된 값이 할당된 바이트 용량을 초과하면 상위 비트가 조용히 버려지고, unpack은 그걸 마법의 모자에서 꺼내줄 수 없어요. 또 s 같은 부호 있는 템플릿 코드로 pack할 때, 초과 값은 부호 비트가 설정되게 만들 수 있고, 이걸 unpack하면 영리하게 음수 값을 돌려줘요.

16비트로는 정수 처리가 빡빡하지만, 부호 있는 32비트와 부호 없는 32비트용 lL이 있어요. 그것도 부족하고 시스템이 64비트 정수를 지원하면 pack 코드 qQ로 그 한계를 무한에 훨씬 가깝게 밀어붙일 수 있어요. 주목할 예외는 "지역 관습" 종류의 부호 있는·부호 없는 정수를 위한 iI예요. 이런 정수는 지역 C 컴파일러가 sizeof(int)에 돌려주는 만큼의 바이트를 차지하지만, 최소한 32비트는 써요.

정수 pack 코드 sSlLqQ 각각은 프로그램을 어디서 실행하든 고정된 바이트 수를 내놓아요. 이건 어떤 응용에는 유용할 수 있지만, Perl과 C 프로그램 사이에 (XS 확장이나 Perl 함수 syscall을 호출할 때 흔히 벌어지는) 데이터 구조를 넘기거나, 이진 파일을 읽고 쓸 때 이식 가능한 방법을 제공하지는 않아요. 이때 필요한 건, 예를 들어 short이나 unsigned long을 코딩할 때 여러분 지역 C 컴파일러가 컴파일하는 것에 의존하는 템플릿 코드예요. 그 코드들과 대응하는 바이트 길이는 아래 표에 있어요. C 표준은 이 데이터 타입들의 상대 크기에 관해 많은 재량을 남겨두므로 실제 값은 달라질 수 있고, 그래서 값이 C와 Perl의 표현식으로 주어져 있어요. (프로그램에서 %Config 값을 쓰고 싶다면 use Config로 임포트해야 해요.)

    signed unsigned  byte length in C   byte length in Perl
      s!     S!      sizeof(short)      $Config{shortsize}
      i!     I!      sizeof(int)        $Config{intsize}
      l!     L!      sizeof(long)       $Config{longsize}
      q!     Q!      sizeof(long long)  $Config{longlongsize}

i!I! 코드는 iI와 다르지 않아요. 완전성을 위해 허용된 것일 뿐이에요.

스택 프레임 unpack 하기 (Unpacking a Stack Frame)

특정 아키텍처에서 온 이진 데이터를 다룰 때, 프로그램은 전혀 다른 시스템에서 실행될 수 있으므로 특정 바이트 순서를 요청하는 게 필요할 수 있어요. 예를 들어 Intel 8086에서 생기는 스택 프레임을 담은 24바이트가 있다고 가정해볼게요:

         +---------+        +----+----+               +---------+
    TOS: |   IP    |  TOS+4:| FL | FH | FLAGS  TOS+14:|   SI    |
         +---------+        +----+----+               +---------+
         |   CS    |        | AL | AH | AX            |   DI    |
         +---------+        +----+----+               +---------+
                            | BL | BH | BX            |   BP    |
                            +----+----+               +---------+
                            | CL | CH | CX            |   DS    |
                            +----+----+               +---------+
                            | DL | DH | DX            |   ES    |
                            +----+----+               +---------+

먼저, 이 유서 깊은 16비트 CPU가 리틀엔디언 순서를 쓰고, 그래서 하위 바이트가 낮은 주소에 저장된다는 점을 주목할게요. 그런 (부호 없는) short를 unpack하려면 코드 v를 써야 해요. 반복 횟수로 12개 short를 전부 unpack해요:

my( $ip, $cs, $flags, $ax, $bx, $cx, $dx, $si, $di, $bp, $ds, $es ) =
  unpack( 'v12', $frame );

대안으로 C를 써서 개별 접근 가능한 바이트 레지스터 FL, FH, AL, AH 등을 unpack할 수도 있어요:

my( $fl, $fh, $al, $ah, $bl, $bh, $cl, $ch, $dl, $dh ) =
  unpack( 'C10', substr( $frame, 4, 10 ) );

한 번에 다 처리할 수 있으면 좋겠죠. short를 unpack하고, 조금 뒤로 가서 2바이트를 unpack하고. Perl은 친절하게도 한 바이트 뒤로 가는 템플릿 코드 X를 제공해요. 이걸 전부 합치면 이렇게 쓸 수 있어요:

my( $ip, $cs,
    $flags,$fl,$fh,
    $ax,$al,$ah, $bx,$bl,$bh, $cx,$cl,$ch, $dx,$dl,$dh,
    $si, $di, $bp, $ds, $es ) =
unpack( 'v2' . ('vXXCC' x 5) . 'v5', $frame );

(템플릿의 어색한 구성은 피할 수 있어요. 계속 읽어보세요!)

우리는 템플릿이 프레임 버퍼의 내용과 맞도록 애를 좀 썼어요. 그렇지 않으면 정의되지 않은 값을 얻거나, unpack이 전부 unpack하지 못할 거예요. pack이 항목이 떨어지면 null 문자열을 공급하는데, pack 코드가 그렇게 하라고 할 때마다 0으로 강제돼요.

네트워크에서 계란 먹는 법 (How to Eat an Egg on a Net)

빅엔디언(높은 바이트가 낮은 주소에)용 pack 코드는 16비트에 n, 32비트 정수에 N이에요. 데이터가 규격을 따르는 아키텍처에서 온다는 걸 알 때 이 코드를 써요. 하지만 놀랍게도, 네트워크를 통해 거의 아는 게 없는 시스템과 이진 데이터를 주고받을 때도 이 pack 코드를 써야 해요. 그 이유는 간단해요. 이 순서가 네트워크 순서(network order)로 채택되었고, 표준을 두려워하는 모든 프로그램이 이 관례를 따라야 하기 때문이에요. (이건 물론 릴리푸트 당파 하나에 대한 엄격한 지지로, 그곳의 정치적 발전에도 영향을 줄 수 있어요.) 그래서 프로토콜이 길이를 먼저 보낸 다음 그만큼의 바이트를 보내라고 기대한다면, 이렇게 쓸 수 있어요:

my $buf = pack( 'N', length( $msg ) ) . $msg;

심지어 이렇게:

my $buf = pack( 'NA*', length( $msg ), $msg );

그리고 $buf를 send 루틴에 넘겨요. 어떤 프로토콜은 개수에 개수 자신의 길이까지 포함하라고 요구해요. 그러면 데이터 길이에 4를 더하면 돼요. (하지만 실제로 코딩하기 전에 "길이와 폭 (Lengths and Widths)"을 꼭 읽어보세요!)

바이트 순서 수정자 (Byte-order modifiers)

이전 절들에서 n, N, v, V로 빅엔디언·리틀엔디언 바이트 순서의 정수를 pack·unpack하는 법을 배웠어요. 좋긴 하지만 여전히 제한적이에요. 모든 종류의 부호 있는 정수와 64비트 정수를 빼먹거든요. 예를 들어 부호 있는 빅엔디언 16비트 정수 시퀀스를 플랫폼 독립적으로 unpack하려면, 이렇게 써야 해요:

my @data = unpack 's*', pack 'S*', unpack 'n*', $buf;

못생겼죠. Perl 5.9.2부터는 특정 바이트 순서에 대한 바람을 표현하는 훨씬 더 나은 방법이 있어요. >< 수정자예요. >는 빅엔디언 수정자, <는 리틀엔디언 수정자예요. 이들을 쓰면 위 코드를 이렇게 다시 쓸 수 있어요:

my @data = unpack 's>*', $buf;

보시다시피 화살표의 "큰 끝"이 s에 닿아요. 그래서 >가 빅엔디언 수정자임을 기억하기 좋죠. <도 분명히 같은 방식으로, "작은 끝"이 코드에 닿아요.

빅·리틀엔디언 C 구조체를 다뤄야 한다면 이 수정자들을 훨씬 더 유용하게 쓰실 거예요. 자세한 내용은 "C 구조체의 Packing과 Unpacking (Packing and Unpacking C Structures)"을 꼭 읽어보세요.

부동소수점 (Floating point Numbers)

부동소수점을 pack하려면 pack 코드 f, d, F, D 중에서 선택할 수 있어요. fd는 여러분 시스템이 제공하는 단정밀도·배정밀도 표현으로 pack(unpack)해요. 시스템이 지원한다면 D로 (long double) 값을 pack·unpack할 수 있는데, fd보다 더 높은 해상도를 제공할 수 있어요. long double 형식이 여러 가지라는 점을 주의하세요.

F는 Perl이 내부적으로 쓰는 부동소수점 타입인 NV를 pack해요.

실수(reals)를 위한 네트워크 표현 같은 건 없어요. 그래서 컴퓨터 경계를 넘어 실수를 보내고 싶다면, 상대편에 뭐가 있는지 확실히 알지 않는 한 16진수 부동소수점 형식을 써서(십진 변환 손실을 피하며) 텍스트 표현을 고수하는 게 좋아요. 더 모험심 있는 분들을 위해, 이전 절의 바이트 순서 수정자를 부동소수점 코드에도 쓸 수 있어요.

이색 템플릿 (Exotic Templates)

비트 문자열 (Bit Strings)

비트는 메모리 세계의 원자예요. 개별 비트에 대한 접근은 최후의 수단으로, 또는 데이터를 다루는 가장 편리한 방법으로 쓰여야 해요. 비트 문자열 (un)pack은 일련의 01 문자를 담은 문자열과, 각각 8비트 그룹을 담은 바이트 시퀀스 사이를 변환해요. 이건 들리는 것만큼 거의 단순한데, 다만 한 바이트의 내용을 비트 문자열로 쓰는 방법이 두 가지라는 점이 있어요. 주석 달린 바이트를 살펴볼게요:

      7 6 5 4 3 2 1 0
    +-----------------+
    | 1 0 0 0 1 1 0 0 |
    +-----------------+
     MSB           LSB

또 계란 먹기 문제네요. 어떤 이는 비트 문자열로 이건 "10001100", 즉 최상위 비트(Most Significant Bit)로 시작해야 한다고 생각하고, 다른 이는 "00110001"이라고 주장해요. 음, Perl은 편향되지 않아요. 그래서 두 가지 비트 문자열 코드가 있어요:

$byte = pack( 'B8', '10001100' ); # start with MSB
$byte = pack( 'b8', '00110001' ); # start with LSB

비트 필드를 pack·unpack하는 건 불가능해요. 정수 바이트만 가능하죠. pack은 항상 다음 바이트 경계에서 시작하고, 필요하면 0비트를 추가해 8의 배수로 "반올림"해요. (비트 필드를 원한다면 "vec" in perlfunc가 있어요. 아니면 빈 비트 문자열에서 split, substr, 결합으로 문자 문자열 수준에서 비트 필드 처리를 구현할 수도 있어요.)

비트 문자열 unpack을 설명하기 위해, 간단한 상태 레지스터를 분해해볼게요. ("-"는 "예약된" 비트를 뜻해요):

    +-----------------+-----------------+
    | S Z - A - P - C | - - - - O D I T |
    +-----------------+-----------------+
     MSB           LSB MSB           LSB

이 두 바이트를 문자열로 변환하는 건 unpack 템플릿 'b16'으로 할 수 있어요. 비트 문자열에서 개별 비트 값을 얻으려면 개별 문자로 분해하는 "빈" 구분자 패턴으로 split을 써요. "예약된" 위치의 비트 값은 그냥 undef에 할당해요. "이게 어디로 갈지는 상관없어"라는 편리한 표기법이죠.

($carry, undef, $parity, undef, $auxcarry, undef, $zero, $sign,
 $trace, $interrupt, $direction, $overflow) =
   split( //, unpack( 'b16', $status ) );

마지막 4비트는 어차피 무시할 수 있으므로 unpack 템플릿 'b12'를 써도 됐어요.

Uuencoding

템플릿 알파벳의 또 다른 이색 문자는 u로, "uuencoded 문자열"을 pack해요. ("uu"는 Unix-to-Unix의 줄임말이에요.) 단순 ASCII 데이터 외에는 지원하지 않는 옛날 전송 매체의 한계를 극복하려고 발명된 이 인코딩 기법이 필요할 일은 아마 없을 거예요. 핵심 레시피는 간단해요. 3바이트, 즉 24비트를 가져와요. 그것들을 4개의 6비트 묶음으로 쪼개고, 각각에 공백(0x20)을 더해요. 모든 데이터가 섞일 때까지 반복해요. 4바이트 그룹을 60자를 넘지 않는 줄로 접고, 앞에 원래 바이트 수(0x20만큼 증가)를, 끝에 "\n"을 넣어요. - pack 셰프가 메뉴에서 pack 코드 u를 고르면 즉석에서(a la minute) 준비해줄 거예요:

my $uubuf = pack( 'u', $bindat );

u 다음의 반복 횟수는 uuencoded 한 줄에 넣을 바이트 수를 설정해요. 기본 최댓값은 45지만, 3의 (작은) 정수 배수로 설정할 수 있어요. unpack은 반복 횟수를 그냥 무시해요.

합 구하기 (Doing Sums)

더 이상한 템플릿 코드는 %<숫자>예요. 첫째, 다른 템플릿 코드의 접두사로 쓰이기 때문이고, 둘째 pack에는 전혀 쓸 수 없기 때문이며, 셋째 unpack에서도 앞에 오는 템플릿 코드가 정의한 데이터를 돌려주지 않기 때문이에요. 대신 데이터 값에서 합을 계산해서 숫자 비트의 정수를 줘요. 숫자 unpack 코드에서는 큰 위업이 없어요:

my $buf = pack( 'iii', 100, 20, 3 );
print unpack( '%32i3', $buf ), "\n";  # prints 123

문자열 값에서 %는 바이트 값의 합을 돌려주므로, substrord로 합 루프를 돌리는 수고를 덜어줘요:

print unpack( '%32A*', "\x01\x10" ), "\n";  # prints 17

% 코드가 "체크섬"을 돌려준다고 문서화되어 있지만, 그런 값에 신뢰를 두지 마세요! 적은 수의 바이트에 적용해도 눈에 띄는 해밍 거리(Hamming distance)를 보장하지 않아요.

bB와 함께 쓰면 %는 그냥 비트를 더해요. 설정된 비트를 효율적으로 세는 데 유용하게 쓸 수 있어요:

my $bitcount = unpack( '%32b*', $mask );

짝수 패리티 비트는 이렇게 결정할 수 있어요:

my $evenparity = unpack( '%1b*', $mask );

유니코드 (Unicode)

유니코드는 세계 대부분 언어의 대부분 문자를 표현할 수 있는 문자셋으로, 백만 개가 넘는 서로 다른 문자를 위한 공간을 제공해요. Unicode 3.1은 94,140자를 규정해요. 기본 라틴 문자는 0~127에 배정되고, 여러 유럽 언어에서 쓰는 문자를 담은 Latin-1 보충 범위는 255까지예요. 라틴 확장을 좀 더 지나면 로마자가 아닌 알파벳을 쓰는 언어의 문자셋이 나오는데, 통화 기호, Zapf Dingbats, 점자 같은 다양한 기호셋이 섞여 있어요. (몇 개를 보려면 https://www.unicode.org/를 방문해보세요. 제 개인적인 취향은 텔루구어와 칸나다어예요.)

유니코드 문자셋은 문자를 정수와 연결해요. 이 숫자들을 같은 수의 바이트로 인코딩하면 라틴 알파벳 텍스트를 저장하는 요구량이 두 배 이상이 돼요. UTF-8 인코딩은 (서양 관점에서) 가장 흔한 문자를 한 바이트에 저장하고, 드문 문자는 세 바이트 이상으로 인코딩해 이를 피해요.

Perl은 대부분의 유니코드 문자열에 내부적으로 UTF-8을 사용해요.

이게 pack과 무슨 관련이 있을까요? 음, 유니코드 문자열(내부적으로 UTF-8로 인코딩된)을 만들고 싶다면 템플릿 코드 U로 할 수 있어요. 예를 들어 유로 통화 기호(코드 번호 0x20AC)를 만들어볼게요:

$UTF8{Euro} = pack( 'U', 0x20AC );
# Equivalent to: $UTF8{Euro} = "\x{20ac}";

$UTF8{Euro}를 검사해보면 3바이트 "\xe2\x82\xac"를 담고 있어요. 하지만 문자는 1개뿐이고, 번호는 0x20AC예요. 왕복은 unpack으로 완성할 수 있어요:

$Unicode{Euro} = unpack( 'U', $UTF8{Euro} );

U 템플릿 코드로 unpack하는 것은 UTF-8 인코딩된 바이트 문자열에서도 동작해요.

보통 UTF-8 문자열을 pack·unpack하고 싶을 거예요:

# pack and unpack the Hebrew alphabet
my $alefbet = pack( 'U*', 0x05d0..0x05ea );
my @hebrew = unpack( 'U*', $utf );

참고하세요. 일반적인 경우에는 UTF-8 인코딩된 바이트 문자열을 Perl 유니코드 문자열로 디코딩하려면 Encode::decode('UTF-8', $utf)를, Perl 유니코드 문자열을 UTF-8 바이트로 인코딩하려면 Encode::encode('UTF-8', $str)를 쓰는 게 더 나아요. 이 함수들은 유효하지 않은 바이트 시퀀스를 처리하는 수단을 제공하고 대체로 더 친근한 인터페이스를 가져요.

또 다른 이식 가능한 이진 인코딩 (Another Portable Binary Encoding)

pack 코드 w는 단순한 정수를 훨씬 넘어서는 이식 가능한 이진 데이터 인코딩 방식을 지원하기 위해 추가되었어요. (자세한 내용은 Scarab 프로젝트의 https://github.com/mworks-project/mw_scarab/blob/master/Scarab-0.1.00d19/doc/binary-serialization.txt에 있어요.) BER(Binary Encoded Representation) 압축 부호 없는 정수는 밑 128 자릿수를 최상위 자릿수부터, 가능한 한 적은 자릿수로 저장해요. 마지막 바이트를 제외하고 각 바이트에 8번째 비트(하이 비트)가 설정돼요. BER 인코딩에는 크기 제한이 없지만, Perl이 극단까지 가진 않아요.

my $berbuf = pack( 'w*', 1, 128, 128+1, 128*128+127 );

올바른 위치에 공백을 넣은 $berbuf의 hex dump는 01 8100 8101 81807F를 보여줘요. 마지막 바이트는 항상 128보다 작으므로 unpack은 어디서 멈춰야 할지 알아요.

템플릿 그룹화 (Template Grouping)

Perl 5.8 이전에는 템플릿 반복을 템플릿 문자열의 x-곱셈으로 만들어야 했어요. 이제는 pack 코드 ()를 반복 횟수와 함께 쓸 수 있는 더 나은 방법이 있어요. 스택 프레임 예제의 unpack 템플릿은 이렇게 간단히 쓸 수 있어요:

unpack( 'v2 (vXXCC)5 v5', $frame )

이 기능을 좀 더 탐구해볼게요. 다음의 동치부터 시작할게요:

join( '', map( substr( $_, 0, 1 ), @str ) )

이건 각 문자열의 첫 문자로 구성된 문자열을 돌려줘요. pack을 쓰면 이렇게 쓸 수 있어요:

pack( '(A)'.@str, @str )

또는 반복 횟수 *가 "필요한 만큼 반복"을 뜻하므로, 그냥:

pack( '(A)*', @str )

(A* 템플릿은 $str[0]만 전체 길이로 pack했을 거라는 점을 기억하세요.)

@dates 배열에 (일, 월, 년) 트리플로 저장된 날짜들을 바이트, 바이트, short 정수 시퀀스로 pack하려면 이렇게 쓸 수 있어요:

$pd = pack( '(CCS)*', map( @$_, @dates ) );

(짝수 길이) 문자열에서 문자 쌍을 교환하려면 여러 기법을 쓸 수 있어요. 먼저 xX로 앞뒤로 건너뛰는 방법을 써볼게요:

$s = pack( '(A)*', unpack( '(xAXXAx)*', $s ) );

@를 써서 오프셋으로 점프할 수도 있는데, 0은 마지막 (를 만났던 위치예요:

$s = pack( '(A)*', unpack( '(@1A @0A @2)*', $s ) );

마지막으로 빅엔디언 short를 unpack해 바이트 순서를 뒤집어 pack하는 완전히 다른 접근도 있어요:

$s = pack( '(v)*', unpack( '(n)*', $s );

길이와 폭 (Lengths and Widths)

문자열 길이 (String Lengths)

이전 절에서 이진 메시지 길이를 실제 메시지 앞에 붙여 만든 네트워크 메시지를 봤어요. 길이를 pack한 다음 그만큼의 데이터 바이트를 붙이는 건 자주 쓰는 레시피라는 걸 알게 될 거예요. 데이터에 null 바이트가 포함될 수 있으면 null 바이트를 붙이는 방식이 안 통하거든요. 두 기법이 모두 쓰인 예가 있어요. source와 destination 주소의 null 종료 문자열 두 개 뒤에, Short Message(휴대폰으로)를 길이 바이트 다음에 보내는 경우예요:

my $msg = pack( 'Z*Z*CA*', $src, $dst, length( $sm ), $sm );

이 메시지를 unpack하는 건 같은 템플릿으로 할 수 있어요:

( $src, $dst, $len, $sm ) = unpack( 'Z*Z*CA*', $msg );

숨어 있는 미묘한 함정이 있어요. Short Message(변수 $sm) 뒤에 필드를 하나 더 추가하는 건 pack할 때는 괜찮지만, 이걸 순진하게 unpack할 수는 없어요:

# pack a message
my $msg = pack( 'Z*Z*CA*C', $src, $dst, length( $sm ), $sm, $prio );

# unpack fails - $prio remains undefined!
( $src, $dst, $len, $sm, $prio ) = unpack( 'Z*Z*CA*C', $msg );

pack 코드 A*가 남은 모든 바이트를 삼켜버려서 $prio가 정의되지 않은 채 남아요! 실망이 사기를 꺾기 전에, Perl이 이 트릭도 할 비장의 카드를 소매에서 조금만 더 꺼내 들고 있다는 걸 말해줄게요. 이걸 보세요:

# pack a message: ASCIIZ, ASCIIZ, length/string, byte
my $msg = pack( 'Z* Z* C/A* C', $src, $dst, $sm, $prio );

# unpack
( $src, $dst, $sm, $prio ) = unpack( 'Z* Z* C/A* C', $msg );

두 pack 코드를 슬래시(/)로 결합하면 인자 목록의 단일 값과 연결돼요. pack에서 인자의 길이를 취해 첫 번째 코드대로 pack하고, 인자 자체는 슬래시 다음의 템플릿 코드로 변환한 뒤 추가해요. 이렇게 하면 length 호출을 넣는 수고가 줄지만, 진짜 이득은 unpack에서 얻어요. 길이 바이트의 값이 버퍼에서 가져올 문자열의 끝을 표시하죠. 이 조합은 두 번째 pack 코드가 a*, A*, Z*가 아닐 때는 말이 안 되므로, Perl이 막아요.

/ 앞에 오는 pack 코드는 숫자를 나타내기에 적합한 어떤 것이든 돼요. 모든 숫자 이진 pack 코드, 심지어 A4Z* 같은 텍스트 코드도요:

# pack/unpack a string preceded by its length in ASCII
my $buf = pack( 'A4/A*', "Humpty-Dumpty" );
# unpack $buf: '13  Humpty-Dumpty'
my $txt = unpack( 'A4/A*', $buf );

/는 Perl 5.6 이전에는 구현되지 않았어요. 그래서 코드가 옛 Perl에서 동작해야 한다면 unpack( 'Z* Z* C')로 길이를 얻은 다음, 그걸로 새 unpack 문자열을 만들어야 해요. 예를 들어:

# pack a message: ASCIIZ, ASCIIZ, length, string, byte
# (5.005 compatible)
my $msg = pack( 'Z* Z* C A* C', $src, $dst, length $sm, $sm, $prio );

# unpack
( undef, undef, $len) = unpack( 'Z* Z* C', $msg );
($src, $dst, $sm, $prio) = unpack ( "Z* Z* x A$len C", $msg );

하지만 그 두 번째 unpack은 앞서나가고 있어요. 템플릿에 단순한 리터럴 문자열을 쓰지 않죠. 그래서 아마 소개를 해야겠어요...

동적 템플릿 (Dynamic Templates)

지금까지 리터럴을 템플릿으로 쓰는 걸 봤어요. pack 항목 리스트의 길이가 고정이 아니라면, 템플릿을 구성하는 표현식이 필요해요 (어떤 이유로 ()*를 쓸 수 없을 때). 예가 있어요. C 프로그램이 편리하게 파싱할 수 있는 방식으로 명명된 문자열 값을 저장하려면, 이름과 null 종료 ASCII 문자열의 시퀀스를 만들되 이름과 값 사이에 =를 넣고, 끝에 추가 구분 null 바이트를 붙여요. 이렇게요:

my $env = pack( '(A*A*Z*)' . keys( %Env ) . 'C',
                map( { ( $_, '=', $Env{$_} ) } keys( %Env ) ), 0 );

이 바이트 공장의 톱니바퀴를 하나씩 살펴볼게요. $env 버퍼에 채워 넣을 항목을 만드는 map 호출이 있어요. 각 키($_)에 = 구분자와 해시 엔트리 값을 더하죠. 각 트리플은 키 수에 따라 반복되는 템플릿 코드 시퀀스 A*A*Z*로 pack돼요. (네, 그게 스칼라 컨텍스트에서 keys 함수가 돌려주는 값이에요.) 마지막 null 바이트를 얻으려면 pack 리스트 끝에 0을 추가해 C로 pack해요. (세심한 독자는 0을 생략할 수도 있었다는 걸 알아챘을 거예요.)

역연산을 위해, unpack이 그것을 분해하도록 놓기 전에 버퍼의 항목 수를 결정해야 해요:

my $n = $env =~ tr/\0// - 1;
my %env = map( split( /=/, $_ ), unpack( "(Z*)$n", $env ) );

tr이 null 바이트를 세요. unpack 호출은 각각이 map 블록에서 분해되는 이름-값 쌍 리스트를 돌려줘요.

반복 횟수 세기 (Counting Repetitions)

데이터 항목(또는 항목 리스트) 끝에 센티널을 저장하는 대신, 데이터 앞에 개수를 붙일 수 있어요. 다시 해시의 키와 값을 pack하는데, 각각 앞에 부호 없는 short 길이 개수를 붙이고, 맨 앞에는 쌍의 수를 저장해요:

my $env = pack( 'S(S/A* S/A*)*', scalar keys( %Env ), %Env );

이렇게 하면 반복 횟수를 / 코드로 unpack할 수 있으므로 역연산이 단순해져요:

my %env = unpack( 'S/(S/A* S/A*)', $env );

pack()-그룹의 반복 횟수를 결정할 수 없으므로, 이건 packunpack에 같은 템플릿을 쓰지 못하는 드문 경우 중 하나라는 점을 기억하세요.

Intel HEX

Intel HEX는 이진 데이터를 표현하는 파일 형식으로, 주로 다양한 칩을 프로그래밍하기 위해 텍스트 파일로 나타내요. (자세한 설명은 https://en.wikipedia.org/wiki/.hex, Motorola S-record 형식은 https://en.wikipedia.org/wiki/SREC_(file_format)를 보세요. S-record는 같은 기법으로 풀 수 있어요.) 각 줄은 콜론(':')으로 시작하고, 16진수 문자 시퀀스가 뒤따라 바이트 수 n(8비트), 주소(16비트, 빅엔디언), 레코드 타입(8비트), n 데이터 바이트, 그리고 앞선 바이트들의 2의 보수 합의 최하위 바이트로 계산된 체크섬(8비트)을 지정해요. 예: :0300300002337A1E.

이런 줄을 처리하는 첫 단계는 체크섬을 확인하면서 네 필드를 얻기 위해 16진수 데이터를 이진으로 변환하는 거예요. 놀랄 것 없이, 전부 이진으로 변환하는 간단한 pack 호출부터 시작할게요:

my $binrec = pack( 'H*', substr( $hexrec, 1 ) );

결과 바이트 시퀀스는 체크섬을 확인하기에 가장 편리해요. 이 문자열 바이트의 ord 값을 더하는 for 루프로 프로그램을 느리게 만들지 마세요. unpack 코드 %가 모든 바이트의 8비트 합을 계산하는 데 쓰이는 것인데, 그 값은 반드시 0이어야 해요:

die unless unpack( "%8C*", $binrec ) == 0;

마지막으로 네 필드를 얻을게요. 이쯤 되면 처음 세 필드는 문제없겠지만, 첫 필드의 데이터 바이트 수를 데이터 필드의 길이로 어떻게 쓸까요? 여기서 xX 코드가 구원자예요. 문자열 안을 앞뒤로 뛰어다니며 unpack을 허용하거든요.

my( $addr, $type, $data ) = unpack( "x n C X4 C x3 /a", $bin );

코드 x는 바이트 하나를 건너뛰어요. 아직 개수는 필요 없으니까요. 코드 n이 16비트 빅엔디언 정수 주소를 처리하고, C가 레코드 타입을 unpack해요. 데이터가 시작되는 오프셋 4에 있을 때 개수가 필요해요. X4가 0 오프셋의 바이트인 원점으로 되돌려요. 이제 개수를 집어올리고 오프셋 4로 앞으로 날아가면, 정확한 수의 데이터 바이트를 추출할 충분한 장비를 갖추게 돼요. 뒤에 남는 체크섬 바이트는 그대로 두죠.

C 구조체의 Packing과 Unpacking (Packing and Unpacking C Structures)

이전 절들에서 숫자와 문자 문자열을 pack하는 법을 봤어요. 몇 가지 걸림돌만 없다면, C 구조체는 다른 걸 담고 있지 않으므로 이미 다 알게 됐다는 짧은 말로 이 절을 마칠 수 있었을 거예요. 미안하지만, 아닙니다. 계속 읽어주세요.

많은 C 구조체를 다뤄야 하고 모든 템플릿 문자열을 수동으로 해킹하고 싶지 않다면, CPAN 모듈 Convert::Binary::C를 살펴볼 가치가 있어요. C 소스를 직접 파싱할 수 있을 뿐 아니라, 이 절에서 더 설명할 모든 이색적인 것들에 대한 내장 지원도 있어요.

정렬 함정 (The Alignment Pit)

속도와 메모리 요구사항 사이의 고려에서 저울은 더 빠른 실행 쪽으로 기울었어요. 이게 C 컴파일러가 구조체에 메모리를 할당하는 방식에 영향을 줬어요. 16비트나 32비트 피연산자가 메모리의 위치들 사이에서, 또는 CPU 레지스터로/로부터 짝수·4의 배수·8의 배수 주소에 정렬되면 더 빨리 이동할 수 있는 아키텍처에서, C 컴파일러는 구조체에 여분의 바이트를 채워 넣어 이 속도 이점을 주거든요. C 해안선을 건너지 않으면 이게 고통을 주진 않을 거예요. (다만 대형 데이터 구조를 설계할 때나, 코드를 아키텍처들 사이에서 이식 가능하게 만들고 싶을 때는 신경 써야 해요. 그걸 원하시죠, 그렇죠?)

이게 pack·unpack에 어떻게 영향을 주는지 보려면, 두 C 구조체를 비교해볼게요:

typedef struct {
  char     c1;
  short    s;
  char     c2;
  long     l;
} gappy_t;

typedef struct {
  long     l;
  short    s;
  char     c1;
  char     c2;
} dense_t;

보통 C 컴파일러는 gappy_t 변수에 12바이트를 할당하지만, dense_t에는 8바이트만 필요해요. 더 조사해보면, 여분의 4바이트가 어디 숨었는지 보여주는 메모리 맵을 그릴 수 있어요:

    0           +4          +8          +12
    +--+--+--+--+--+--+--+--+--+--+--+--+
    |c1|xx|  s  |c2|xx|xx|xx|     l     |    xx = fill byte
    +--+--+--+--+--+--+--+--+--+--+--+--+
    gappy_t

    0           +4          +8
    +--+--+--+--+--+--+--+--+
    |     l     |  s  |c1|c2|
    +--+--+--+--+--+--+--+--+
    dense_t

그리고 그게 첫 번째 특이점이 왜 터지는지예요. pack·unpack 템플릿은 여분의 채움 바이트를 얻기 위해 x 코드로 채워야 해요.

"왜 Perl이 그 틈을 보상하지 못하지?"라는 자연스러운 질문에는 답이 필요해요. 좋은 이유 하나는 C 컴파일러가 (비-ANSI) 확장을 제공해 구조체 정렬 방식을 개별 구조체 필드 수준까지 화려하게 제어할 수 있게 하기 때문이에요. 그리고 그것만으로 부족하다면, 채움 바이트의 양을 다음 항목의 정렬만으로는 유도할 수 없는 악랄한 것인 union이 있어요.

좋아요, 이를 악물고 해보죠. 항목 리스트에서 대응 항목을 취하지 않는 템플릿 코드 x를 삽입해 정렬을 맞추는 방법 하나가 있어요:

my $gappy = pack( 'cxs cxxx l!', $c1, $s, $c2, $l );

l 뒤의 !를 주목하세요. C 컴파일러가 컴파일하는 대로 long 정수를 pack하고 싶다는 걸 확실히 하려는 거예요. 그리고 지금도, 컴파일러가 위처럼 정렬하는 플랫폼에서만 동작할 거예요. 그리고 어딘가 그렇게 하지 않는 플랫폼을 가진 사람이 있어요. [아마 Cray일 거예요. 거기서는 shortintlong도 모두 8바이트거든요. :-)]

긴 구조체에서 바이트를 세고 정렬을 지켜보는 건 지루하기 짝이 없어요. 간단한 프로그램으로 템플릿을 만들 방법이 없을까요? 트릭을 해내는 C 프로그램이 있어요:

#include <stdio.h>
#include <stddef.h>

typedef struct {
  char     fc1;
  short    fs;
  char     fc2;
  long     fl;
} gappy_t;

#define Pt(struct,field,tchar) \
  printf( "@%d%s ", offsetof(struct,field), # tchar );

int main() {
  Pt( gappy_t, fc1, c  );
  Pt( gappy_t, fs,  s! );
  Pt( gappy_t, fc2, c  );
  Pt( gappy_t, fl,  l! );
  printf( "\n" );
}

출력 줄은 pack·unpack 호출의 템플릿으로 쓸 수 있어요:

my $gappy = pack( '@0c @2s! @4c @8l!', $c1, $s, $c2, $l );

야, 또 하나의 템플릿 코드네요. 충분히 많지 않은 것처럼요. 하지만 @는 pack 버퍼의 시작부터 다음 항목까지의 오프셋을 지정하게 해서 하루를 구해줘요. 이건 <stddef.h>에 정의된 offsetof 매크로가 struct 타입과 필드 이름 중 하나("member-designator"라고 C 표준어로 부르는)를 받았을 때 돌려주는 값이에요.

오프셋을 쓰거나 x를 더해 틈을 메우는 것 모두 만족스럽지 않아요. (구조체가 바뀌면 어떻게 될지 상상해보세요.) 우리가 정말 필요한 건 "N의 다음 배수까지 필요한 만큼의 바이트를 건너뛰어라"라고 말하는 방법이에요. 유창한 템플릿에서는 x!N이라고 말하는데, N은 적절한 값으로 치환돼요. 다음 버전의 구조체 패킹이에요:

my $gappy = pack( 'c x!2 s c x!4 l!', $c1, $s, $c2, $l );

확실히 낫지만, 여전히 모든 정수가 얼마나 긴지 알아야 하고, 이식성은 멀었어요. 예를 들어 2 대신 "short가 아무리 길든"이라고 말하고 싶어요. 이건 적절한 pack 코드를 대괄호로 감싸서 할 수 있어요: [s]. 그래서 여기 우리가 할 수 있는 최선이 있어요:

my $gappy = pack( 'c x![s] s c x![l!] l!', $c1, $s, $c2, $l );

엔디언 처리하기 (Dealing with Endian-ness)

이제 다른 바이트 순서를 가진 머신을 위한 데이터를 pack하고 싶다고 상상해볼게요. 먼저 대상 머신의 데이터 타입이 실제로 얼마나 큰지 알아내야 해요. long이 32비트, short가 16비트라고 가정해볼게요. 그러면 템플릿을 이렇게 다시 쓸 수 있어요:

my $gappy = pack( 'c x![s] s c x![l] l', $c1, $s, $c2, $l );

대상 머신이 리틀엔디언이라면 이렇게 쓸 수 있어요:

my $gappy = pack( 'c x![s] s< c x![l] l<', $c1, $s, $c2, $l );

이건 short와 long 멤버를 리틀엔디언으로 강제하고, 구조체 멤버가 많지 않다면 문제없어요. 하지만 그룹에 바이트 순서 수정자를 써서 다음처럼 쓸 수도 있어요:

my $gappy = pack( '( c x![s] s c x![l] l )<', $c1, $s, $c2, $l );

이전처럼 짧지는 않지만, 전체 그룹에 대해 리틀엔디언 순서를 의도한다는 게 더 명확해져요. 개별 템플릿 코드만이 아니라요. 더 읽기 쉽고 유지보수하기도 쉬워요.

정렬, 2탄 (Alignment, Take 2)

정렬 함정이 아직 끝나지 않아서 걱정이에요. 구조체 배열을 pack할 때 히드라가 또 하나의 못생긴 머리를 들어올려요:

typedef struct {
  short    count;
  char     glyph;
} cell_t;

typedef cell_t buffer_t[BUFLEN];

함정이 어디 있을까요? 패딩은 첫 필드 count 앞에도, 이 필드와 다음 필드 glyph 사이에도 필요 없어요. 그럼 이렇게 단순히 pack하면 안 되나요?

# something goes wrong here:
pack( 's!a' x @buffer,
      map{ ( $_->{count}, $_->{glyph} ) } @buffer );

이건 3*@buffer 바이트를 pack하지만, buffer_t의 크기가 BUFLEN의 네 배라는 게 밝혀져요! 이 이야기의 교훈은, 구조체나 배열의 요구 정렬이 더 높은 수준으로 전파되어 각 구성 요소의 에도 패딩을 고려해야 한다는 거예요. 그래서 올바른 템플릿은:

pack( 's!ax' x @buffer,
      map{ ( $_->{count}, $_->{glyph} ) } @buffer );

정렬, 3탄 (Alignment, Take 3)

그리고 위의 전부를 고려하더라도, ANSI는 여전히 이게 크기가 달라지게 허용해요:

typedef struct {
  char     foo[2];
} foo_t;

구조체의 정렬 제약이 그 요소들 중 어느 것보다 클 수 있어요. [그리고 이게 흔한 것에 영향을 주지 않는다고 생각한다면, 다음에 보이는 휴대폰을 분해해보세요. 많은 휴대폰이 ARM 코어를 쓰는데, ARM 구조체 규칙은 sizeof (foo_t) == 4로 만들어요.]

포인터 사용법 (Pointers for How to Use Them)

이 절의 제목은 C 구조체를 pack할 때 조만간 마주칠 두 번째 문제를 가리켜요. 호출하려는 함수가, 말하자면, void * 값을 기대한다면, Perl 변수에 대한 참조를 단순히 가져올 수 없어요. (그 값이 분명 메모리 주소이긴 하지만, 변수 내용이 저장된 주소는 아니에요.)

템플릿 코드 P는 "고정 길이 문자열에 대한 포인터"를 pack하겠다고 약속해요. 이게 우리가 원하는 거 아닌가요? 시도해볼게요:

# allocate some storage and pack a pointer to it
my $memory = "\x00" x $size;
my $memptr = pack( 'P', $memory );

잠깐, pack은 그냥 바이트 시퀀스를 돌려주지 않나요? 결국 숫자에 불과한 포인터를 기대하는 C 코드에 이 바이트 문자열을 어떻게 넘길까요? 답은 간단해요. pack이 돌려준 바이트에서 숫자 주소를 얻어야 해요.

my $ptr = unpack( 'L!', $memptr );

분명히 이건 포인터를 unsigned long으로 그리고 그 반대로 타입캐스트하는 게 가능하다고 가정해요. 자주 동작하지만 보편 법칙으로 받아들여선 안 돼요. - 이제 이 포인터가 생겼으니 다음 질문은: 어떻게 유용하게 쓰지? 포인터가 기대되는 C 함수 호출이 필요해요. read(2) 시스템 콜이 떠오르네요:

ssize_t read(int fd, void *buf, size_t count);

perlfunc에서 syscall 사용법을 설명하는 걸 읽고 나면, 파일을 표준 출력으로 복사하는 이 Perl 함수를 쓸 수 있어요:

require 'syscall.ph'; # run h2ph to generate this file
sub cat($){
    my $path = shift();
    my $size = -s $path;
    my $memory = "\x00" x $size;  # allocate some memory
    my $ptr = unpack( 'L', pack( 'P', $memory ) );
    open( F, $path ) || die( "$path: cannot open ($!)\n" );
    my $fd = fileno(F);
    my $res = syscall( &SYS_read, fileno(F), $ptr, $size );
    print $memory;
    close( F );
}

이건 단순함의 표본도, 이식성의 모범도 아니지만 요점을 보여줘요. 우리는 배후에 몰래 들어가 Perl의 평소 잘 보호되는 메모리에 접근할 수 있어요! (중요한 참고: Perl의 syscall은 이런 우회 방식으로 포인터를 만들 것을 요구하지 않아요. 문자열 변수를 넘기기만 하면 Perl이 주소를 전달해요.)

Punpack은 어떻게 동작할까요? 버퍼 속 어떤 포인터가 unpack되려고 한다고 상상해볼게요. null 포인터가 아니라면(그건 영리하게 undef 값을 생산할 거예요) 시작 주소가 있어요. 그런데 그다음은? Perl은 이 "고정 길이 문자열"이 얼마나 긴지 알 방법이 없어요. 그래서 실제 크기를 P 뒤의 명시적 길이로 지정하는 건 여러분 몫이에요.

my $mem = "abcdefghijklmn";
print unpack( 'P5', pack( 'P', $mem ) ); # prints "abcde"

결과적으로 packP 뒤의 숫자나 *를 무시해요.

이제 P가 동작하는 걸 봤으니 p도 시험해볼게요. 포인터를 pack하기 위한 두 번째 템플릿 코드가 왜 필요한 걸까요? 답은 간단한 사실 뒤에 있어요. punpack하면 버퍼에서 가져온 주소에서 시작하는 null 종료 문자열을 약속하고, 이는 돌려받을 데이터 항목의 길이를 함의해요:

my $buf = pack( 'p', "abc\x00efhijklmn" );
print unpack( 'p', $buf );    # prints "abc"

이건 혼란스러울 수 있어요. 길이가 문자열의 길이로 함의되기 때문에, pack 코드 p 뒤의 숫자는 P 뒤처럼 길이가 아니라 반복 횟수예요.

Pppack(..., $x)를 써서 $x가 실제로 저장된 주소를 얻는 것은 신중하게 다뤄야 해요. Perl의 내부 기계는 변수와 그 주소 사이의 관계를 자기만의 사적인 일로 여기고, 우리가 복사본을 얻었다는 걸 그다지 신경 쓰지 않아요. 그러므로:

  • 그 주소의 메모리 사용을 끝내기 전에 범위를 벗어나는 변수(그리고 그 메모리를 해제하는)의 주소를 얻으려 pPpack을 쓰지 마세요.

  • 변수 값을 바꾸는 Perl 연산에 아주 주의하세요. 예를 들어 변수에 뭔가를 추가하면 저장 공간이 재할당될 수 있고, 무인 지대(no-man's land)를 가리키는 포인터가 남을 수 있어요.

  • 변수가 정수나 double로 저장되어 있을 때 Perl 변수의 주소를 얻을 수 있다고 생각하지 마세요! pack('P', $x)$x .= ''라고 쓴 것처럼 변수의 내부 표현을 문자열로 강제해요.

하지만 문자열 리터럴을 P- 또는 p-pack하는 건 안전해요. Perl이 그냥 익명 변수를 할당하기 때문이에요.

Pack 레시피 (Pack Recipes)

packunpack을 위한 (어쩌면) 유용한 즉석 레시피 모음이에요:

# Convert IP address for socket functions
pack( "C4", split /\./, "123.4.5.6" );

# Count the bits in a chunk of memory (e.g. a select vector)
unpack( '%32b*', $mask );

# Determine the endianness of your system
$is_little_endian = unpack( 'c', pack( 's', 1 ) );
$is_big_endian = unpack( 'xc', pack( 's', 1 ) );

# Determine the number of bits in a native integer
$bits = unpack( '%32I!', ~0 );

# Prepare argument for the nanosleep system call
my $timespec = pack( 'L!L!', $secs, $nanosecs );

간단한 메모리 덤프를 위해 몇 바이트를 똑같은 수의 16진수 쌍으로 unpack하고, map을 써서 전통적인 간격을 처리해요. 줄마다 16바이트죠:

my $i;
print map( ++$i % 16 ? "$_ " : "$_\n",
           unpack( 'H2' x length( $mem ), $mem ) ),
      length( $mem ) % 16 ? "\n" : '';

재미 섹션 (Funnies Section)

# Pulling digits out of nowhere...
print unpack( 'C', pack( 'x' ) ),
      unpack( '%B*', pack( 'A' ) ),
      unpack( 'H', pack( 'A' ) ),
      unpack( 'A', unpack( 'C', pack( 'A' ) ) ), "\n";

# One for the road ;-)
my $advice = pack( 'all u can in a van' );

저자 (Authors)

Simon Cozens and Wolfgang Laun.

Perldoc Browser는 Dan Book(DBOOK)이 유지보수해요. 사이트 자체, 검색, 문서 렌더링 관련 문제는 GitHub issue tracker로 연락하세요.

Perl 문서는 Perl 5 Porters가 Perl 개발 과정에서 유지보수해요. 문서 내용이나 형식 관련 문제는 Perl issue tracker, 메일링 리스트, 또는 IRC로 연락하세요.