glibc に感謝せよ

この記事はいくつかの言語のランタイムで glibc が利用されていることを確認し、glibc に感謝するものです。

Python3

Python3 の処理系は CPython。

# python3 -VV
Python 3.6.8 (default, Nov 21 2019, 19:31:34)
[GCC 8.3.1 20190507 (Red Hat 8.3.1-4)]

これはインタプリタ型。このインタプリタの実装に glibc が利用されていることを確認する。

python3 にリンクされているライブラリを確認すると、libc.so.6 が存在する。

# ldd $(which python3)
       linux-vdso.so.1 (0x00007ffe3c9aa000)
       libpython3.6m.so.1.0 => /lib64/libpython3.6m.so.1.0 (0x00007fa4cd52e000)
       libpthread.so.0 => /lib64/libpthread.so.0 (0x00007fa4cd30e000)
       libdl.so.2 => /lib64/libdl.so.2 (0x00007fa4cd10a000)
       libutil.so.1 => /lib64/libutil.so.1 (0x00007fa4ccf06000)
       libm.so.6 => /lib64/libm.so.6 (0x00007fa4ccb84000)
       libc.so.6 => /lib64/libc.so.6 (0x00007fa4cc7c2000)
       /lib64/ld-linux-x86-64.so.2 (0x00007fa4cdc8a000)

以下のような HTTP アクセスするコードを用意し、

#!/usr/bin/env python3
import urllib.request

resp = urllib.request.urlopen("http://example.com")
print(resp.getcode())

bpftrace で glibc の関数が実行されていることを確認する。

terminal A]# python3 main.py
200

terminal B]# bpftrace -e 'uprobe:/lib64/libc.so.6:connect /comm == "python3" / { printf("%s called %s\n", comm, probe); } '
Attaching 1 probe...
python3 called uprobe:/lib64/libc.so.6:connect
python3 called uprobe:/lib64/libc.so.6:connect
python3 called uprobe:/lib64/libc.so.6:connect
python3 called uprobe:/lib64/libc.so.6:connect

ということで、Python3 は glibc の関数を利用している(こともある)。

Java

Java の処理系は OpneJDK 1.8。

# java -version
openjdk version "1.8.0_242"
OpenJDK Runtime Environment (build 1.8.0_242-b08)
OpenJDK 64-Bit Server VM (build 25.242-b08, mixed mode)

これはソースコードを Java 仮想マシンの機械語にコンパイルしておき、実行時にインタプリタ型のように機械語に変換する(ここでは、JIT は考えない)。このインタプリタの実装に glibc が利用されていることを確認する。

Java にリンクされているライブラリを確認すると、libc.so.6 が存在する。

# ldd $(which java)
       linux-vdso.so.1 (0x00007ffc351f1000)
       libpthread.so.0 => /lib64/libpthread.so.0 (0x00007f6653fbb000)
       libz.so.1 => /lib64/libz.so.1 (0x00007f6653da4000)
       libjli.so => not found
       libdl.so.2 => /lib64/libdl.so.2 (0x00007f6653ba0000)
       libc.so.6 => /lib64/libc.so.6 (0x00007f66537de000)
       /lib64/ld-linux-x86-64.so.2 (0x00007f66543dd000)

以下のような HTTP アクセスするコードを用意し、

import java.net.HttpURLConnection;
import java.net.URL;

public class Main {
        public static void main(String[] args) throws Exception {
                URL url = new URL("http://example.com");
                HttpURLConnection urlConn = (HttpURLConnection) url.openConnection();
                urlConn.setRequestMethod("GET");
                urlConn.connect();
                System.out.println(urlConn.getResponseCode());
        }
}

bpftrace で glibc の関数が実行されていることを確認する。

terminal A]# javac Main.java
terminal A]# java Main
200

terminal B]# bpftrace -e 'uprobe:/lib64/libc.so.6:connect /comm == "java" / { printf("%s called %s\n", comm, p
robe); } '
Attaching 1 probe...
java called uprobe:/lib64/libc.so.6:connect
java called uprobe:/lib64/libc.so.6:connect
java called uprobe:/lib64/libc.so.6:connect
java called uprobe:/lib64/libc.so.6:connect
java called uprobe:/lib64/libc.so.6:connect
java called uprobe:/lib64/libc.so.6:connect
^C

ということで、java は glibc の関数を利用している(こともある)。

Go

たしか Go はシングルバイナリ、かつ、glibc などに依存しない(マルチプラットフォーム対応) と聞いたことがある。確認してみよう。

Go の処理系が複数あるのかは知らないが、goenv でインストールしたやつ。バージョンは 1.14。

# go version
go version go1.14.3 linux/amd64

これはコンパイル型言語なので、Go 自体にリンクされているライブラリは確認しない。

以下のような HTTP アクセスするコードを用意し、

package main

import (
        "fmt"
        "net/http"
)

func main() {
        resp, _ := http.Get("http://example.com")
        defer resp.Body.Close()

        fmt.Println(resp.StatusCode)
}

ビルドすると、動的リンク(dynamically linked)されたバイナリが生成された。アレ?

# go build main.go
# file main
main: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, not stripped

しかも glibc が依存しているぞ。

# ldd main
        linux-vdso.so.1 (0x00007ffef6165000)
        libpthread.so.0 => /lib64/libpthread.so.0 (0x00007f613055f000)
        libc.so.6 => /lib64/libc.so.6 (0x00007f613019d000)
        /lib64/ld-linux-x86-64.so.2 (0x00007f613077f000)

ググると、 net パッケージを使っているとデフォルトで動的リンクになるらしい。

okzk.hatenablog.com

ということで CGO_ENABLED=0(CGO は C ライブラリを Go から呼び出す仕組みらしい) を無効にしてもういちどコンパイルすると、静的リンクされたバイナリが生成された。

# CGO_ENABLED=0 go build main.go
# file main
main: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, not stripped
# ldd main
        動的実行ファイルではありません

bpftrace で glibc の関数をトレースしても、表示されない。

terminal A]# ./main
200

terminal B]# bpftrace -e 'uprobe:/lib64/libc.so.6:connect  { printf("%s called %s\n", comm, probe);}'
Attaching 1 probe...
^C

念のため nm コマンドでシンボルを表示しても、glibc の connect がリンクされてなさそうな事がわかる。

# nm  /lib64/libc.so.6  | grep "connect$"
00000000000fcec0 t __GI___connect
00000000000fcec0 W __connect
00000000000fcec0 t __libc_connect
00000000000fcec0 W connect

# nm main | grep "connect$"
0000000000545900 T net.(*netFD).connect
0000000000618190 T net/http.(*socksDialer).connect
00000000004996f0 T syscall.connect

ということでオプション設定すればバイナリは glibc に依存してなさそう(すごい)。

まあでも go コンパイラは glibc に依存してますから。

# ldd /root/.goenv/versions/1.14.3/bin/go
        linux-vdso.so.1 (0x00007fffb2bd6000)
        libpthread.so.0 => /lib64/libpthread.so.0 (0x00007f9a7ddd0000)
        libc.so.6 => /lib64/libc.so.6 (0x00007f9a7da0e000)
        /lib64/ld-linux-x86-64.so.2 (0x00007f9a7dff0000)

まとめ

我々は glibc という巨人の肩の上に立っていたのだ。glibc さんありがとう。