Go言語のsyscall.Sysctlは最後のNULを落とす

カーネルのパラメータを引いたり設定したりする時に便利なのが sysctl コマンドです。

 $ sysctl kern.ostype
kern.ostype: Darwin

このコマンドのシステムコールをGo言語から叩いて、OSの種類を引いてみましょう。

func main() {
    ret, _ := syscall.Sysctl("kern.ostype")
    fmt.Printf("%s\n", ret)
}
Darwin

問題ないですね。 数字を返すものを叩いてみましょう。

 $ sysctl machdep.cpu.feature_bits
machdep.cpu.feature_bits: 9221959987971750911
func main() {
    ret, _ := syscall.Sysctl("machdep.cpu.feature_bits")
    val := *(*uint64)(unsafe.Pointer(&[]byte(ret)[0]))
    fmt.Printf("%d\n", val)
}

出力結果

9221959987971750911

unsafeパッケージは使いたくないだって? アーキテクチャエンディアン固定になっちゃうけどなぁ。

   val := binary.LittleEndian.Uint64([]byte(ret))
9221959987971750911

次は extfeature_bits を見たい?

 $ sysctl machdep.cpu.extfeature_bits
machdep.cpu.extfeature_bits: 1241984796928
func main() {
    ret, _ := syscall.Sysctl("machdep.cpu.extfeature_bits")
    val := binary.LittleEndian.Uint64([]byte(ret))
    fmt.Printf("%d\n", val)
}

実行してみます

panic: runtime error: index out of range

goroutine 1 [running]:
encoding/binary.binary.littleEndian.Uint64(...)
    /usr/local/Cellar/go/1.9.2/libexec/src/encoding/binary/binary.go:76
main.main()
    /private/tmp/main.go:16 +0x1d7
exit status 2

!!!> index out of range <!!!

なにが起きたのか。 syscall.Sysctl の返り値 (string) の長さを見てみると、すぐにわかります。

ret, _ := syscall.Sysctl("machdep.cpu.feature_bits")
fmt.Printf("%d\n", len(ret))
ret, _ = syscall.Sysctl("machdep.cpu.extfeature_bits")
fmt.Printf("%d\n", len(ret))
8
7

え、つまりこれは… https://github.com/golang/go/blob/8776be153540cf450eafd847cf8efde0a01774dc/src/syscall/syscall_bsd.go#L474

   // Throw away terminating NUL.
    if n > 0 && buf[n-1] == '\x00' {
        n--
    }
    return string(buf[0:n]), nil

な、なんということを…

つまりstructでも…?

ret, _ := syscall.Sysctl("kern.boottime")
fmt.Printf("%d\n", len(ret))
15

お、おう… わかった… わかったよ…

  • sysctl.Sysctl は最後のNULを落とす。
  • 文字列を返すような場合はよい挙動だが、数字や構造体の場合は注意が必要。64bit整数を返す場合、その値に依存して8byteだったり7byteだったりする。
  • unsafe.Pointer で数字や構造体にキャストする分には問題ないように思われる (ほんまやろか?)。 + "\x00" してから処理したほうがいいかもしれない。

困っている人もいる () が、指摘されているように sysctl パッケージは変更を受けつけていないので、この挙動が変わることや別の関数が追加されることはなさそう。 そもそも string で返すのなんかおかしくない? []byte で欲しいよね…

それあります! golang.org/x/sys パッケージの unix.SysctlRaw を使いましょう。 https://github.com/golang/sys/blob/53aa286056ef226755cd898109dbcdaba8ac0b81/unix/syscall_bsd.go#L524

func main() {
    ret, _ := unix.SysctlRaw("vm.loadavg")
    fmt.Printf("%d\n", len(ret))
    ret, _ = unix.SysctlRaw("kern.boottime")
    fmt.Printf("%d\n", len(ret))
    ret, _ = unix.SysctlRaw("machdep.cpu.extfeature_bits")
    fmt.Printf("%d\n", len(ret))
}
24
16
8

これで安心して眠れそうですね。

sysctl.Sysctl は最後のNULを落とします。文字列として欲しい時はこれで良いが、バイト列として欲しい時は unix.SysctlRaw を使いましょう。