Slide 1

Slide 1 text

Go言語低レイヤー入門 Hello world が 画面に表示されるまで @DQNEO (ドキュネオ) Go Conference Tokyo 2021 2021-04-24

Slide 2

Slide 2 text

自己紹介 ● @DQNEO (ドキュネオ) ● メルカリUSAを開発しています ● Goコンパイラ babygo の作者 ○ https://github.com/DQNEO/babygo ● 公式Goコンパイラ、Go言語仕様書 コントリビュート歴有り

Slide 3

Slide 3 text

目次 ● 最小のプログラムはどう動いてるのか ● Go言語とruntime ● Go言語とシステムコール ● Hello world のシステムコールを見てみよう ○ straceを使う ○ gdbを使う ○ コードを読む ● Goアセンブリの読み方 ● [ライブコーディング] Hello world を低レイヤー版で書き直す

Slide 4

Slide 4 text

今日の話の前提 ● CPU: x86-64 ● OS: Linux (Docker可) ● go 1.15で動作確認済み

Slide 5

Slide 5 text

package main import ( "fmt" ) func main() { fmt.Print("Hello world\n") } Hello world

Slide 6

Slide 6 text

どのようなプロセスを経て 画面に文字が表示されるのでしょうか? 少し想像してみてください (※OS内部や表示装置の挙動は除く) Hello world

Slide 7

Slide 7 text

Goの最小のプログラム package main func main() { } なにもしないプログラム

Slide 8

Slide 8 text

プログラムは どこから始まってどこで終わるのか? package main func main() { }

Slide 9

Slide 9 text

func main() { } プログラム (バイナリ実行ファイル ) mainパッケージ プログラムはどこから始まるのか?

Slide 10

Slide 10 text

func main() { } プログラム (バイナリ実行ファイル ) mainパッケージ プログラムはどこから始まるのか? _rt0_amd64_linux TEXT _rt0_amd64_linux(SB),NOSPLIT,$-8 JMP _rt0_amd64(SB) src/runtime/rt0_linux_amd64.s 開始

Slide 11

Slide 11 text

func main() { } プログラム (バイナリ実行ファイル ) mainパッケージ プログラムはどこから始まるのか? _rt0_amd64_linux 初期化処理 main.main() 開始

Slide 12

Slide 12 text

func main() { } プログラム (バイナリ実行ファイル ) mainパッケージ プログラムはどこで終わるのか? _rt0_amd64_linux ... main.main() ... 「exit 0してね」 終了 開始 TEXT runtime·exit(SB),NOSPLIT,$0-4 MOVL code+0(FP), DI MOVL $SYS_exit_group, AX SYSCALL RET src/runtime/sys_linux_amd64.s

Slide 13

Slide 13 text

Goの最小のプログラム package main func main() { } なにもしないプログラム exit 0 するプログラム

Slide 14

Slide 14 text

func main() { } プログラム (バイナリ実行ファイル ) mainパッケージ プログラムとruntime _rt0_amd64_linux …. main.main() … 「exit 0してね」 runtime

Slide 15

Slide 15 text

func main() { } プログラム (バイナリ実行ファイル ) mainパッケージ プログラムとruntime _rt0_amd64_linux …. main.main() … 「exit 0してね」 runtime runtimeとは、 ビルド時にプログラムに自動で組み込まれるコードのこと

Slide 16

Slide 16 text

package main func main() { } なにもしないプログラム exit 0 するプログラム runtimeの力を借りて exit 0 するプログラム Goの最小のプログラム

Slide 17

Slide 17 text

exit 0って、 終了したら勝手にそうなるのでは?

Slide 18

Slide 18 text

プログラムとOS プログラム (バイナリ実行ファイル ) OS (Kernel) コンピュータで動くコードは主にこの2つ

Slide 19

Slide 19 text

1 + 2 s = “hello” OS (Kernel) 簡単な計算や代入などは、OSの助けなしに行うことができる (CPUが機械語列をそのまま実行する) プログラム (バイナリ実行ファイル ) プログラムとOS

Slide 20

Slide 20 text

1 + 2 s = “hello” OS (Kernel) OSの助けなしでは、画面表示/ファイル操作/ネットワーク通信などが できない プログラムを正常終了 (exit 0)することもできない プログラムとOS プログラム (バイナリ実行ファイル )

Slide 21

Slide 21 text

exit 0 の仕組み OS (Kernel) runtime 「exit 0 してね」 func main() { } 「やります」 プログラム (バイナリ実行ファイル ) システムコール プログラムの最後で OS に exit 0を依頼している (さもないとクラッシュ)

Slide 22

Slide 22 text

package main func main() { } なにもしないプログラム exit 0 するプログラム runtimeの力を借りて exit 0 する OSに exit 0 を依頼するプログラム Goの最小のプログラム

Slide 23

Slide 23 text

低レイヤは実感しづらい OS (Kernel) runtime 「0でexitしてね」 package main func main() { } 「やります」 プログラム (バイナリ実行ファイル ) このへん

Slide 24

Slide 24 text

そうだ自分でやってみよう OS (Kernel) runtime 「やります」 プログラム (バイナリ実行ファイル ) package main 「0でexitしてね」 ココ

Slide 25

Slide 25 text

自力で exit(0) する -- a.s -- TEXT main·main(SB),0,$0 MOVQ $231, AX MOVQ $0, DI SYSCALL -- main.go -- package main func main() https://play.golang.org/p/vnILQ4zvyFg runtimeの力を借りずに OS にexit 0を依頼するプログラム

Slide 26

Slide 26 text

-- a.s -- TEXT main·main(SB),0,$0 MOVQ $231, AX MOVQ $0, DI SYSCALL -- main.go -- package main func main() Kernel に 231番目のカーネル命令 (exit_group)を 引数0で 実行をお願いする(syscall) 自力で exit(0) する func main() { os.Exit(0) } ≒ main関数 のボディ

Slide 27

Slide 27 text

-- a.s -- TEXT main·main(SB),0,$0 MOVQ $231, AX MOVQ $1, DI SYSCALL -- main.go -- package main func main() 引数を1にすると、1でexitする ちょっと変えてみる func main() { os.Exit(1) } ≒

Slide 28

Slide 28 text

青文字の箇所は今は気にしないでください ちなみに -- a.s -- TEXT main·main(SB),0,$0 MOVQ $231, AX MOVQ $0, DI SYSCALL -- main.go -- package main func main()

Slide 29

Slide 29 text

現状確認 プログラム OS (Kernel) runtime func main() { } 「やります」 「0でexitしてね」 システムコール

Slide 30

Slide 30 text

システムコールでできること ● プログラム終了 ● ファイル操作 ● ネットワーク通信 ● 動的メモリ確保 (make, append, map set) ● etc

Slide 31

Slide 31 text

package main import ( "fmt" ) func main() { fmt.Print("Hello world\n") } Hello world

Slide 32

Slide 32 text

Hello world プログラム OS (Kernel) runtime func main() { } 「やります」 fmt.Print(“H..”) システムコール? システムコールで実現されてそう...? これを確かめたい

Slide 33

Slide 33 text

システムコールを確認する方法 ● straceコマンド ● gdb ● ソース読む ● etc

Slide 34

Slide 34 text

straceコマンド ● systemcall を trace する ● プログラムが呼び出すシステムコールを実行時に表示 ● SREのひとがよく使う ○ 参考 http://blog.livedoor.jp/sonots/archives/18193659.html

Slide 35

Slide 35 text

strace使い方 $ strace ./hello > /dev/null

Slide 36

Slide 36 text

straceのデモ

Slide 37

Slide 37 text

straceの出力

Slide 38

Slide 38 text

writeシステムコールで画面表示 プログラム OS (Kernel) func main() { } 処理 fmt.Print(“H..”) write(1, “H… ) hello world ターミナル

Slide 39

Slide 39 text

straceの出力 (抜粋) write(1, "Hello world\n", 12) = 12

Slide 40

Slide 40 text

straceの出力 (抜粋) write(1, "Hello world\n", 12) = 12 システムコールの種類 引数 戻り値

Slide 41

Slide 41 text

例: write ● $ man 2 write ● ググる: “Linux manual write” システムコールの使い方を調べる

Slide 42

Slide 42 text

ssize_t write(int fd, const void *buf, size_t count); https://man7.org/linux/man-pages/man2/write.2.html ファイルディスクリプタ バッファ (バイト列の先頭アドレス) バイト数 戻り値 実際に書き込んだバイト数 (失敗時は -1) write(2) — Linux manual page

Slide 43

Slide 43 text

straceの出力 (抜粋) write(1, "Hello world\n", 12) システムコール 標準出力 12バイト バッファ (バイト列の先頭アドレス )

Slide 44

Slide 44 text

gdb ● バイナリ実行ファイルをデバグできる ● ブレークポイントを貼って実行を中断 ○ システムコールで止めることも可能 ● 中断した箇所からステップ実行 ● etc

Slide 45

Slide 45 text

gdb でデバグ実行してみよう

Slide 46

Slide 46 text

デモ (Hello World をgdbでデバグ)

Slide 47

Slide 47 text

writeのバックトレース #0 syscall.Syscall () at /usr/lib/go-1.15/src/syscall/asm_linux_amd64.s:24 #1 0x000000000048ccba in syscall.write (fd=1, p=..., n=, err=...) at /usr/lib/go-1.15/src/syscall/zsyscall_linux_amd64.go:914 #2 0x000000000048e877 in syscall.Write (fd=, p=..., n=, err=...) at /usr/lib/go-1.15/src/syscall/syscall_unix.go:212 #3 internal/poll.(*FD).Write.func1 (~r0=, ~r1=...) at /usr/lib/go-1.15/src/internal/poll/fd_unix.go:267 #4 0x000000000048e7a7 in internal/poll.ignoringEINTR (fn={void (int *, error *)} 0xc0000a0da0, ~r1=, ~r2=...) at /usr/lib/go-1.15/src/internal/poll/fd_unix.go:567 #5 0x000000000048e49c in internal/poll.(*FD).Write (fd=0xc0000bc060, p=..., ~r1=, ~r2=...) at /usr/lib/go-1.15/src/internal/poll/fd_unix.go:267 #6 0x000000000048ecf7 in os.(*File).write (f=0xc0000ba008, b=..., n=, err=...) at /usr/lib/go-1.15/src/os/file_posix.go:48 #7 os.(*File).Write (f=0xc0000ba008, b=..., n=, err=...) at /usr/lib/go-1.15/src/os/file.go:173 #8 0x0000000000492c6b in fmt.Fprint (w=..., a=..., n=, err=...) at /usr/lib/go-1.15/src/fmt/print.go:233 #9 0x00000000004990f5 in fmt.Print (a=..., n=, err=...) at /usr/lib/go-1.15/src/fmt/print.go:242 #10 main.main () at /mnt/main.go:6

Slide 48

Slide 48 text

#0 syscall.Syscall () at /usr/lib/go-1.15/src/syscall/asm_linux_amd64.s:24 #1 syscall.write () at /usr/lib/go-1.15/src/syscall/zsyscall_linux_amd64.go:914 #2 syscall.Write () at /usr/lib/go-1.15/src/syscall/syscall_unix.go:212 #3 internal/poll.(*FD).Write.func1 at /usr/lib/go-1.15/src/internal/poll/fd_unix.go:267 #4 internal/poll.ignoringEINTR () at /usr/lib/go-1.15/src/internal/poll/fd_unix.go:567 #5 internal/poll.(*FD).Write () at /usr/lib/go-1.15/src/internal/poll/fd_unix.go:267 #6 os.(*File).write () at /usr/lib/go-1.15/src/os/file_posix.go:48 #7 os.(*File).Write () at /usr/lib/go-1.15/src/os/file.go:173 #8 fmt.Fprint () at /usr/lib/go-1.15/src/fmt/print.go:233 #9 fmt.Print () at /usr/lib/go-1.15/src/fmt/print.go:242 #10 main.main () at /mnt/main.go:6 見やすくするとこんな感じ writeのバックトレース

Slide 49

Slide 49 text

ソースコードを fmt.Print()からたどってみよう

Slide 50

Slide 50 text

Golandの場合 OS, Archを Linux / amd64 に設定しておく

Slide 51

Slide 51 text

デモ (Golandでコードリーディング)

Slide 52

Slide 52 text

最低レイヤーは syscall.Syscall関数 TEXT ·Syscall(SB),NOSPLIT,$0-56 CALL runtime·entersyscall(SB) MOVQ a1+8(FP) , DI MOVQ a2+16(FP) , SI MOVQ a3+24(FP) , DX MOVQ trap+0(FP), AX // syscall entry SYSCALL ... func Syscall(trap, a1, a2, a3 uintptr) (r1, r2 uintptr, err Errno) シグネチャ ボディ src/syscall/asm_linux_amd64.s src/syscall/syscall_unix.go

Slide 53

Slide 53 text

syscall.Syscall関数を読む TEXT ·Syscall(SB),NOSPLIT,$0-56 CALL runtime·entersyscall(SB) MOVQ a1+8(FP) , DI MOVQ a2+16(FP) , SI MOVQ a3+24(FP) , DX MOVQ trap+0(FP), AX SYSCALL ... func Syscall(trap, a1, a2, a3 uintptr) (r1, r2 uintptr, err Errno) シグネチャ ボディ

Slide 54

Slide 54 text

syscall.Syscall関数を読む TEXT ·Syscall(SB),NOSPLIT,$0-56 CALL runtime·entersyscall(SB) MOVQ a1+8(FP) , DI MOVQ a2+16(FP) , SI MOVQ a3+24(FP) , DX MOVQ trap+0(FP), AX SYSCALL ... func Syscall(trap, a1, a2, a3 uintptr) (r1, r2 uintptr, err Errno) シグネチャ ボディ

Slide 55

Slide 55 text

Goアセンブリの読み方 MOVQ a1+8(FP), DI

Slide 56

Slide 56 text

Goアセンブリの読み方 MOVQ a1+8(FP), DI CPU命令 引数1 引数2 , 引数には、 ● メモリアドレス ● レジスタの名前(後述) ● 数値リテラル などを指定できる

Slide 57

Slide 57 text

レジスタって? CPU内部の記憶装置 超小容量・超高速 Intel 8086 CPU

Slide 58

Slide 58 text

Goアセンブリの読み方 MOVQ a1+8(FP), DI MOV : Move (コピー命令) Q : Quad word (4word = 4*16bit = 64bit) 「引数1から引数2に 64bit分コピーしてね」 意味的には MOVQ というより COPY64

Slide 59

Slide 59 text

Goアセンブリの読み方 MOVQ a1+8(FP), DI メモ メモリアドレス (関数ローカルのとある地点 + 8byte後ろ)

Slide 60

Slide 60 text

Goアセンブリの読み方 MOVQ a1+8(FP), DI レジスタの名前 (本当の名前は RDI)

Slide 61

Slide 61 text

Goアセンブリの読み方 MOVQ a1+8(FP), DI ● メモリのとある場所にあるデータを ● RDIレジスタに ● 64bit分コピー してね

Slide 62

Slide 62 text

syscall.Syscall関数の読み方 シグネチャ ボディ TEXT ·Syscall(SB),NOSPLIT,$0-56 CALL runtime·entersyscall(SB) MOVQ a1+8(FP) , DI MOVQ a2+16(FP) , SI MOVQ a3+24(FP) , DX MOVQ trap+0(FP), AX SYSCALL ... func Syscall(trap, a1, a2, a3 uintptr) (r1, r2 uintptr, err Errno)

Slide 63

Slide 63 text

syscall.Syscall関数の読み方 シグネチャ ボディ TEXT ·Syscall(SB),NOSPLIT,$0-56 CALL runtime·entersyscall(SB) MOVQ trap+0(FP), AX MOVQ a1+8(FP) , DI MOVQ a2+16(FP) , SI MOVQ a3+24(FP) , DX SYSCALL ... func Syscall(trap, a1, a2, a3 uintptr) (r1, r2 uintptr, err Errno)

Slide 64

Slide 64 text

やってること ● システムコール番号をRAXレジスタにセットし ● 1個目の引数をRDIレジスタにセットし ● 2個目の引数をRSIレジスタにセットし ● 3個目の引数をRDXレジスタにセットし ● SYSCALL命令を実行

Slide 65

Slide 65 text

System V Application Binary Interface 手順はカーネルの呼び出し規約で決まっている

Slide 66

Slide 66 text

余談:レジスタの名前がわかりにくい ● RAX, RDI, RSI, RDX,...... ● 完全に歴史的事情 ● 別に R0, R1, R2, R3 という名前でもよかった ○ そういうCPUもある

Slide 67

Slide 67 text

ライブコーディング 自力でシステムコールを叩いて hello worldを出力しよう

Slide 68

Slide 68 text

package main import ( "fmt" ) func main() { fmt.Print("Hello world\n") } ふつうのHello world https://play.golang.org/p/sa0vP_nSS2V

Slide 69

Slide 69 text

#0 syscall.Syscall () at /usr/lib/go-1.15/src/syscall/asm_linux_amd64.s:24 #1 syscall.write () at /usr/lib/go-1.15/src/syscall/zsyscall_linux_amd64.go:914 #2 syscall.Write () at /usr/lib/go-1.15/src/syscall/syscall_unix.go:212 #3 internal/poll.(*FD).Write.func1 at /usr/lib/go-1.15/src/internal/poll/fd_unix.go:267 #4 internal/poll.ignoringEINTR () at /usr/lib/go-1.15/src/internal/poll/fd_unix.go:567 #5 internal/poll.(*FD).Write () at /usr/lib/go-1.15/src/internal/poll/fd_unix.go:267 #6 os.(*File).write () at /usr/lib/go-1.15/src/os/file_posix.go:48 #7 os.(*File).Write () at /usr/lib/go-1.15/src/os/file.go:173 #8 fmt.Fprint () at /usr/lib/go-1.15/src/fmt/print.go:233 #9 fmt.Print () at /usr/lib/go-1.15/src/fmt/print.go:242 #10 main.main () at /mnt/main.go:6 バックトレースを逆順で再現していきます

Slide 70

Slide 70 text

package main import ( "fmt" "os" ) func main() { fmt.Fprint(os.Stdout, "Hello world\n") } fmt.Fprint

Slide 71

Slide 71 text

package main import ( "os" ) func main() { os.Stdout.Write([]byte("Hello world\n")) } os.File.Write

Slide 72

Slide 72 text

package main import ( "syscall" ) func main() { syscall.Write(1, []byte("Hello world\n")) } syscall.Write

Slide 73

Slide 73 text

package main import ( "syscall" "unsafe" ) func main() { var buf = []byte("Hello world\n") syscall.Syscall(1, 1, uintptr(unsafe.Pointer(&buf[0])), 12) } syscall.Syscall

Slide 74

Slide 74 text

package main import ( "syscall" "unsafe" ) func main() { var bytes = [6]byte{'H','e','l','l','o','\n'} syscall.Syscall(1, 1, uintptr(unsafe.Pointer(&bytes[0])), 6) } slice を array に

Slide 75

Slide 75 text

-- a.s -- TEXT main·main(SB),0,$0 MOVQ $231, AX MOVQ $0, DI SYSCALL -- main.go -- package main import ( // "syscall" // "unsafe" ) func main() //{ // var bytes = [6]byte{'H','e','l','l','o','\n'} // syscall.Syscall(1, 1, uintptr(unsafe.Pointer(&bytes[0])), 6) //} アセンブリで書きたい (exit 0)

Slide 76

Slide 76 text

-- a.s -- TEXT main·main(SB),0,$0 MOVQ $1, AX // write MOVQ $1, DI // stdout LEAQ main·bytes(SB), SI // address MOVQ $6, DX SYSCALL MOVQ $231, AX MOVQ $0, DI SYSCALL -- main.go -- package main var bytes = [6]byte{'H','e','l','l','o','\n'} func main() アセンブリで Hello

Slide 77

Slide 77 text

-- a.s -- #define READONLY 8 DATA array+0(SB)/1, $'H' DATA array+1(SB)/1, $'e' DATA array+2(SB)/1, $'l' DATA array+3(SB)/1, $'l' DATA array+4(SB)/1, $'o' DATA array+5(SB)/1, $'\n' GLOBL array(SB),READONLY, $6 TEXT main·main(SB),0,$0 MOVQ $1, AX // write MOVQ $1, DI // stdout LEAQ array+0(SB), SI // address MOVQ $6, DX // len SYSCALL MOVQ $231, AX MOVQ $0, DI SYSCALL -- main.go -- package main func main() アセンブリで Hello (配列もアセンブリ)

Slide 78

Slide 78 text

-- a.s -- #define READONLY 8 DATA array+0(SB)/1, $0x48 DATA array+1(SB)/1, $0x65 DATA array+2(SB)/1, $0x6c DATA array+3(SB)/1, $0x6c DATA array+4(SB)/1, $0x6f DATA array+5(SB)/1, $0x0a GLOBL array(SB),READONLY, $6 TEXT main·main(SB),0,$0 MOVQ $1, AX // write MOVQ $1, DI // stdout LEAQ array+0(SB), SI // address MOVQ $6, DX // len SYSCALL MOVQ $231, AX MOVQ $0, DI SYSCALL -- main.go -- package main func main() アセンブリで Hello (文字をバイナリで)

Slide 79

Slide 79 text

自力でHello world -- a.s -- #define READONLY 8 DATA array+0(SB)/1, $0x48 DATA array+1(SB)/1, $0x65 DATA array+2(SB)/1, $0x6c DATA array+3(SB)/1, $0x6c DATA array+4(SB)/1, $0x6f DATA array+5(SB)/1, $0x0a GLOBL array(SB),READONLY, $6 TEXT main·main(SB),0,$0 MOVQ $1, AX // write MOVQ $1, DI // stdout LEAQ array+0(SB), SI // address MOVQ $6, DX // SYSCALL MOVQ $231, AX MOVQ $0, DI SYSCALL -- main.go -- package main func main() 自力でバイト列をメモリに並べ 自力で syscall write 自力で exit 0 https://play.golang.org/p/KuyLu8JJhI0

Slide 80

Slide 80 text

まとめ 低レイヤはたのしい

Slide 81

Slide 81 text

今日の内容を再現できる環境 https://github.com/DQNEO/go-samples/tree/master/gocon2021

Slide 82

Slide 82 text

参考 ● Linux Manual - write(2) ○ https://man7.org/linux/man-pages/man2/write.2.html ● GDB Manual: catch syscall ○ https://sourceware.org/gdb/onlinedocs/gdb/Set-Catchpoints.html ● System V ABI AMD64 ○ https://github.com/hjl-tools/x86-psABI/wiki/x86-64-psABI-1.0.pdf ● A Quick Guide to Go's Assembler ○ https://golang.org/doc/asm ● Plan9 Assembly Manual ○ https://9p.io/sys/doc/asm.html

Slide 83

Slide 83 text

ご清聴 ありがとうございました