Skip to content

Instantly share code, notes, and snippets.

@greymd
Last active July 10, 2021 14:04
Show Gist options
  • Save greymd/15cd3719c6351abc94fe068dde14c5d8 to your computer and use it in GitHub Desktop.
Save greymd/15cd3719c6351abc94fe068dde14c5d8 to your computer and use it in GitHub Desktop.
pack/unpack 練習

Perl書くくせにpack/unpackの動作が全然わからなかったため休日に少し練習。 以下、チラシの裏程度のメモ書き。校正しておらず、自分が思いついた思考の順序で一発書きしたもの。 たぶん間違えた内容を含んでます。

pack

packは、引数に配列、あるいは文字列を受け取って、それを規則に従ってバイナリに変換する。

入力: 配列 or 文字列 出力: バイナリ

CとかHとか、色々なテンプレートがある。 http://perldoc.jp/func/pack

10進数の整数が入った配列を入力

例えば、10進数表記で "ABC(改行)"は

$ echo ABC | od -tu1c
0000000    65  66  67  10
           A   B   C  \n
0000004

つまり 65, 66, 67, 10 で表すことができる。 ということで

1文字目を取り出す。

$ perl -e 'print pack("C1",65,66,67,10)'
A
$ perl -e 'print pack("C2",65,66,67,10)'
AB
$ perl -e 'print pack("C3",65,66,67,10)'
ABC
$ perl -e 'print pack("C4",65,66,67,10)'
ABC(改行)

Cというのは unsigned char 値を表す。つまり、1-255の10進数の整数値。 C(数字)で、その数字分のオクテットを読んでくれる。 C4だと、4オクテット分解釈してくれる。 基本的に10進数なので、バイト列に変換しても8オクテットに満たない。なので0埋めする。 たとえば、ABC(改行)を2進数で表すと下記のようになる。

$ perl -e 'print pack("C4",65,66,67,10)' | xxd -b -c1
00000000: 01000001  A
00000001: 01000010  B
00000002: 01000011  C
00000003: 00001010  .

しかし実際に65,66,67,10を2進数で表すと、8ビットにはならない。なので、8ビットになるように0埋めしてくれる。それがCの動作。

$ echo 'obase=2;ibase=10;65;66;67;10' | bc
1000001
1000010
1000011
1010

255以上の数字を与えた時は、2進数にした後に最初の8ビットを採用するらしい。

$ perl -e 'print pack("C*",65666710)' | od -tx1c
0000000    96
         226
0000001

!! 実際のバイト列は?
$ perl -e 'print pack("C*",65666710)' | xxd -b
00000000: 10010110 .

!! 数字を2進数にすると、最初の8バイトだけが残っている。
$ echo 'obase=2;ibase=10;65666710' | bc
11111010011111111010010110

なお、perlは0x<16進数数値>という書き方ができるため、下記のようにできる。

$ perl -e 'print pack("C*",0x41,0x42,0x43,0x0a)' | awk 1
ABC

0xという書き方は、表記は16進数だが、10進数の数値と同じ扱いを受ける。

$ perl -e 'print 0x43' | awk 1
67

そして、数値リテラルではなくても動く。 この時点で混乱してきた。

$ perl -e 'print pack("C4","65","66","67","10")'
ABC(改行)

16進数の数値を表した文字列を入力

packは常にリスト型を受け取ると思っていた。じゃあ入力したい情報が10進数なら良いけど、16進数を数値リテラルでそのまま表しつつ、文字列に変換したい場合どうするのか。 →その場合は文字列として16進数の数値を入力する。例えば

$ echo ABC | od -tx1c
0000000 41 42 43 0a
A B C \n
0000004

なので ABC(改行)は16進数だと"4142430a"という文字列。

$ perl -e 'print pack("H*","4142430a")'
ABC

になる。リストを受け取ったり、文字列を受け取ったり、packはややこしい。。。 ただ、10進数のときと違い、16進数であれば、かならず2文字で1オクテットなので、一つの文字列でも大丈夫なのかもしれない。

もう一つ例を。 "うんこ(改行)"という文字列を16進数で表すと

$ echo うんこ | od -tx1 -An | tr -dc 'a-z0-9' | awk 1
e38186e38293e381930a

なので、こうなる。

$ perl -e 'print pack ("H*","e38186e38293e381930a")'
うんこ

UTF-8で「う」は3オクテット必要。 Hを使う場合、H1で4ビット(16進数の文字列1文字分)。 つまりH2で1オクテットになる。 よってH6で3オクテットになり、最初の一文字が表示できる。

$ perl -e 'print pack ("H6","e38186e38293e381930a")' | awk 1
う

同様のことを10進数でやろうとすると

$ perl -e 'print pack("C3",227,129,134,227,130,147,227,129,147,10)' | awk 1
う

2進数を表した文字列を入力

2進数で「abc」はこうなる。

$ echo abc | xxd -b -c 1 | awk '{print $2}' | tr -dc '01' | awk 1
01100001011000100110001100001010

なので、こうできる。

$ perl -e 'print pack("B*","01100001011000100110001100001010")'
abc

2指数の文字列を数値のリテラルにしたら、想定した動作はしない。

$ perl -e 'print pack("B*",01100001011000100110001100001010)'
Integer overflow in octal number at -e line 1.

B(数字)で表し、B1で1ビット。つまり、B8で1オクテットとなる。 下記は、一文字だけ取り出す例。

$ perl -e 'print pack("B8","01100001011000100110001100001010")' | awk 1
a

文字列を入力して文字列として表示

aは素直に文字列らしい。 a1で1オクテット。

$ perl -e 'print pack("a1","abc")' | awk 1
a

色々混在した文字列をpackで解釈

Aは2進数で01000001

$ echo -n A | xxd -b -c 1
00000000: 01000001  A

Bは10進数で66

$ echo -n B | od -tu1c
0000000    66
           B
0000001

Cは16進数で43

$ echo -n C | od -tx1c
0000000    43
           C
0000001

\n(改行)は10進数で10ということで。

"01000001",66,"43",10という配列をぶっこむ。

$ perl -e 'print pack ("B8C1H2C1","01000001",66,"43",10)'
ABC(改行)

違う種類の変換が入る時は、配列は別要素にする必要がある。

unpack

packとは逆の動作をする。 バイナリ → 文字列 or 配列

文字列を2進数の文字列に。

$ perl -e 'print unpack("B*","ABC")' | awk 1
010000010100001001000011

一文字だけ取り出す。

$ perl -e 'print unpack("H2","ABC")' | awk 1
41

1文字目は2進数、2文字目は16進数。3文字目はそのまま。

$ perl -e 'print unpack("H2B8A1","ABC")' | awk 1
4101000010C

unpackは文字列だけでなく、配列として結果を返す。 配列の1要素になる単位は、Cだと1オクテットごとになる。

$ perl -e 'print join ".", unpack("C*","ABC")' | awk 1
65.66.67

ほかのフォーマットだと、ならない。

$ perl -e 'print join ".", unpack("H*","ABC")' | awk 1
414243
$ perl -e 'print join ".", unpack("B*","ABC")' | awk 1
010000010100001001000011

unpackには区切りの単位のようなものがあるらしい。 命令に*を使わず、2オクテットごとに命令を分ける。

$ perl -e 'print join ".", unpack("H2H2H2","ABC")' | awk 1
41.42.43

B8B2B8とやると、Bだけ2ビットだけ取り出される。つまりB1回に付き1オクテット読む。B8B2とやったら、1オクテット目の8ビットと2オクテット目の2ビットを取り出すらしい。

$ perl -e 'print join ".", unpack("B8B2B8","ABC")' | awk 1
01000001.01.01000011

括弧を使うと区切りの単位を指定できるらしい。

$ perl -e 'print join ".", unpack("(H2)*","ABC")' | awk 1
41.42.43
$ perl -e 'print join ".", unpack("(H2)2","ABC")' | awk 1
41.42

1-32の数字をサブネットマスクに変換する。

$ echo 14 | perl -nle 'print join(".", unpack("C4",pack("B*", "1"x$_."0"x32)))'
255.252.0.0
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment