ちりもつもればミルキーウェイ

好奇心に可処分時間が奪われる

手でパースして gob バイナリの仕様理解を深める回

なにこれ

gob バイナリの詳細仕様に言及してる記事はあんまりない, かつ公式ドキュメントにも網羅的な言及はあまりない

gob は一応ドキュメントが https://pkg.go.dev/encoding/gob#hdr-Encoding_Details あたりにあるけどわりとボトムアップな説明が多く全体像の理解に手間取った(個人の感想), そのため理解をふかめるにあたって簡単なメッセージのバイナリを目でパースしてみたりいろいろやってた

そんなこんなでバイナリをにらめて眼力パースしたメモが手元にあり, 毎度いちいち探し回るのが面倒なため記事にしておく!

自分向けメモなので仕様についてはあまり言及しない. 過去仕様を調べてるメモもあるのでそっちと合わせてみるといい感じかも

convto.hatenablog.com

それではいってみよー

準備

以下みたいな go コードがあったとする

package main

import (
    "bytes"
    "encoding/gob"
    "fmt"

    "github.com/convto/bit"
)

func main() {
    type item struct {
        Name  string
        Price int
    }
    banana := item{Name: "banana", Price: 100}
    apple := item{Name: "apple", Price: 120}

    var buf bytes.Buffer
    enc := gob.NewEncoder(&buf)
    enc.Encode(banana)
    enc.Encode(apple)

    b := buf.Bytes()
    fmt.Println(bit.Dump(b))
}

ここで github.com/convto/bit は稚作のライブラリで, バイナリを 0 or 1 にして出力できて io stream もくえるくんです
詳細気になる方は解説したことあるのでそっちをみてね

speakerdeck.com

さて, さっきのコードを実行すると以下みたいな出力が得られる

00000000: 00100100 01111111 00000011 00000001 00000001 00000100  $.....
00000006: 01101001 01110100 01100101 01101101 00000001 11111111  item..
0000000c: 10000000 00000000 00000001 00000010 00000001 00000100  ......
00000012: 01001110 01100001 01101101 01100101 00000001 00001100  Name..
00000018: 00000000 00000001 00000101 01010000 01110010 01101001  ...Pri
0000001e: 01100011 01100101 00000001 00000100 00000000 00000000  ce....
00000024: 00000000 00001110 11111111 10000000 00000001 00000110  ......
0000002a: 01100010 01100001 01101110 01100001 01101110 01100001  banana
00000030: 00000001 11111111 11001000 00000000 00001101 11111111  ......
00000036: 10000000 00000001 00000101 01100001 01110000 01110000  ...app
0000003c: 01101100 01100101 00000001 11111111 11110000 00000000  le....

これで準備完了

型情報を目パース

gob は先頭に byte length, その後にその長さのメッセージ, その次にまた byte length ... という形式でメッセージが続きます

また, type と value でメッセージが分かれていて同一 stream で初めて登場するメッセージはそれより前に type 情報が流されます

このメッセージでは, 1バイト目を読むと 00100100 なのでそこから 36 bytes が型情報っぽいですね. まずは先頭から型をみてみましょう!

以下目パース結果です

00000000: 00100100 01111111 00000011 00000001 00000001 00000100  $.....
00000006: 01101001 01110100 01100101 01101101 00000001 11111111  item..
0000000c: 10000000 00000000 00000001 00000010 00000001 00000100  ......
00000012: 01001110 01100001 01101101 01100101 00000001 00001100  Name..
00000018: 00000000 00000001 00000101 01010000 01110010 01101001  ...Pri
0000001e: 01100011 01100101 00000001 00000100 00000000 00000000  ce....
00000024: 00000000

この部分が

00100100 len = 36
    01111111 id=64
    00000011 wireType = structType
        00000001 struct type field 0 (common type で決め打ち)
            00000001 commontype field 0 (name: string で決め打ち)
                00000100 len = 4
                01101001 01110100 01100101 01101101 val = item
            00000001 commontype field 1 (id: int で決め打ち)
                11111111 len = 1 (varint みたいな仕様で読むサイズを明示している。8byteまで)
                10000000 id = 64
            00000000 common type 終端
        00000001 struct type field 1 ([]fieldType で決め打ち)
            00000010 num = 2
            00000001 fieldType[0] field 0 (name: string で決め打ち)
                00000100 len = 4
                01001110 01100001 01101101 01100101 name: Name
            00000001 fieldType[0] field 1 (id: int できめうち)
                00001100 id = 6
            00000000 fieldType[0] 終端
            00000001 fieldType[1] field 0 (name: string で決め打ち)
                00000101 len = 5
                01010000 01110010 01101001 01100011 01100101 name: Price
            00000001 fieldType[1] field 1 (id: int で決め打ち)
                00000100 id = 4
            00000000 fieldType[1] 終端
    00000000 structType 終端
00000000 wireType 終端

こう

自分用メモなので細かい仕様には言及しないけど, ちゃんと構造を完全に表現するための情報を持っている

value を目パース

00000024:          00001110 11111111 10000000 00000001 00000110   .....
0000002a: 01100010 01100001 01101110 01100001 01101110 01100001  banana
00000030: 00000001 11111111 11001000 00000000 00001101 11111111  ......
00000036: 10000000 00000001 00000101 01100001 01110000 01110000  ...app
0000003c: 01101100 01100101 00000001 11111111 11110000 00000000  le....

これが

00001110 len = 14
    11111111 len = 1 (varint みたいな仕様で読むサイズを明示している。8byteまで)
    10000000 id = 64
    00000001 item(id = 64 type) field 0
        00000110 len = 6
        01100010 01100001 01101110 01100001 01101110 01100001 val = banana
    00000001 item(id = 64 type) field 1
        11111111 len = 1 (varint みたいな仕様で読むサイズを明示している。8byteまで)
        11001000 val = 100
00000000 終端

00001101 len = 14
    11111111 len = 1 (varint みたいな仕様で読むサイズを明示している。8byteまで)
    10000000 id = 64
    00000001 item(id = 64 type) field 0
        00000101 len = 5
        01100001 01110000 01110000 01101100 01100101 val = apple
    00000001 item(id = 64 type) field 1
        11111111 len = 1 (varint みたいな仕様で読むサイズを明示している。8byteまで)
        11110000 val = 120
00000000 終端

こう

まとめ

gob の実際のバイナリについて, 簡単なメッセージのものを解釈してみた

gob 好きな人, あるいは大いなる事情によって gob を他の言語で喰いたい人の参考になれば嬉しいっす(見ればわかるけど型の取り扱いとかをうまいこと帳尻つければ他の言語でも全然食えます. 当たり前だけどね)