Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Go言語低レイヤー入門 Hello world が 画面に表示されるまで / Introduction to low level programming in Go

7b606c5039f083d13e2d2320ce6ddcfa?s=47 DQNEO
April 24, 2021

Go言語低レイヤー入門 Hello world が 画面に表示されるまで / Introduction to low level programming in Go

Go Conference Tokyo 2021 で発表した資料です。
動画はこちら (ライブコーディングあり) https://youtu.be/uqjujzH-XLE?t=11414

7b606c5039f083d13e2d2320ce6ddcfa?s=128

DQNEO

April 24, 2021
Tweet

Transcript

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

    2021 2021-04-24
  2. 自己紹介 • @DQNEO (ドキュネオ) • メルカリUSAを開発しています • Goコンパイラ babygo の作者

    ◦ https://github.com/DQNEO/babygo • 公式Goコンパイラ、Go言語仕様書 コントリビュート歴有り
  3. 目次 • 最小のプログラムはどう動いてるのか • Go言語とruntime • Go言語とシステムコール • Hello world

    のシステムコールを見てみよう ◦ straceを使う ◦ gdbを使う ◦ コードを読む • Goアセンブリの読み方 • [ライブコーディング] Hello world を低レイヤー版で書き直す
  4. 今日の話の前提 • CPU: x86-64 • OS: Linux (Docker可) • go

    1.15で動作確認済み
  5. package main import ( "fmt" ) func main() { fmt.Print("Hello

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

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

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

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

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

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

    初期化処理 main.main() 開始
  12. 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
  13. Goの最小のプログラム package main func main() { } なにもしないプログラム exit 0

    するプログラム
  14. func main() { } プログラム (バイナリ実行ファイル ) mainパッケージ プログラムとruntime _rt0_amd64_linux

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

    …. main.main() … 「exit 0してね」 runtime runtimeとは、 ビルド時にプログラムに自動で組み込まれるコードのこと
  16. package main func main() { } なにもしないプログラム exit 0 するプログラム

    runtimeの力を借りて exit 0 するプログラム Goの最小のプログラム
  17. exit 0って、 終了したら勝手にそうなるのでは?

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

  19. 1 + 2 s = “hello” OS (Kernel) 簡単な計算や代入などは、OSの助けなしに行うことができる (CPUが機械語列をそのまま実行する)

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

    プログラムを正常終了 (exit 0)することもできない プログラムとOS プログラム (バイナリ実行ファイル )
  21. exit 0 の仕組み OS (Kernel) runtime 「exit 0 してね」 func

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

    runtimeの力を借りて exit 0 する OSに exit 0 を依頼するプログラム Goの最小のプログラム
  23. 低レイヤは実感しづらい OS (Kernel) runtime 「0でexitしてね」 package main func main() {

    } 「やります」 プログラム (バイナリ実行ファイル ) このへん
  24. そうだ自分でやってみよう OS (Kernel) runtime 「やります」 プログラム (バイナリ実行ファイル ) package main

    「0でexitしてね」 ココ
  25. 自力で 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を依頼するプログラム
  26. -- 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関数 のボディ
  27. -- 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) } ≒
  28. 青文字の箇所は今は気にしないでください ちなみに -- a.s -- TEXT main·main(SB),0,$0 MOVQ $231, AX

    MOVQ $0, DI SYSCALL -- main.go -- package main func main()
  29. 現状確認 プログラム OS (Kernel) runtime func main() { } 「やります」

    「0でexitしてね」 システムコール
  30. システムコールでできること • プログラム終了 • ファイル操作 • ネットワーク通信 • 動的メモリ確保 (make,

    append, map set) • etc
  31. package main import ( "fmt" ) func main() { fmt.Print("Hello

    world\n") } Hello world
  32. Hello world プログラム OS (Kernel) runtime func main() { }

    「やります」 fmt.Print(“H..”) システムコール? システムコールで実現されてそう...? これを確かめたい
  33. システムコールを確認する方法 • straceコマンド • gdb • ソース読む • etc

  34. straceコマンド • systemcall を trace する • プログラムが呼び出すシステムコールを実行時に表示 • SREのひとがよく使う

    ◦ 参考 http://blog.livedoor.jp/sonots/archives/18193659.html
  35. strace使い方 $ strace ./hello > /dev/null

  36. straceのデモ

  37. straceの出力

  38. writeシステムコールで画面表示 プログラム OS (Kernel) func main() { } 処理 fmt.Print(“H..”)

    write(1, “H… ) hello world ターミナル
  39. straceの出力 (抜粋) write(1, "Hello world\n", 12) = 12

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

    戻り値
  41. 例: write • $ man 2 write • ググる: “Linux

    manual write” システムコールの使い方を調べる
  42. 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
  43. straceの出力 (抜粋) write(1, "Hello world\n", 12) システムコール 標準出力 12バイト バッファ

    (バイト列の先頭アドレス )
  44. gdb • バイナリ実行ファイルをデバグできる • ブレークポイントを貼って実行を中断 ◦ システムコールで止めることも可能 • 中断した箇所からステップ実行 •

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

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

  47. 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=<optimized out>, err=...) at /usr/lib/go-1.15/src/syscall/zsyscall_linux_amd64.go:914 #2 0x000000000048e877 in syscall.Write (fd=<optimized out>, p=..., n=<optimized out>, err=...) at /usr/lib/go-1.15/src/syscall/syscall_unix.go:212 #3 internal/poll.(*FD).Write.func1 (~r0=<optimized out>, ~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=<optimized out>, ~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=<optimized out>, ~r2=...) at /usr/lib/go-1.15/src/internal/poll/fd_unix.go:267 #6 0x000000000048ecf7 in os.(*File).write (f=0xc0000ba008, b=..., n=<optimized out>, err=...) at /usr/lib/go-1.15/src/os/file_posix.go:48 #7 os.(*File).Write (f=0xc0000ba008, b=..., n=<optimized out>, err=...) at /usr/lib/go-1.15/src/os/file.go:173 #8 0x0000000000492c6b in fmt.Fprint (w=..., a=..., n=<optimized out>, err=...) at /usr/lib/go-1.15/src/fmt/print.go:233 #9 0x00000000004990f5 in fmt.Print (a=..., n=<optimized out>, err=...) at /usr/lib/go-1.15/src/fmt/print.go:242 #10 main.main () at /mnt/main.go:6
  48. #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のバックトレース
  49. ソースコードを fmt.Print()からたどってみよう

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

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

  52. 最低レイヤーは 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
  53. 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) シグネチャ ボディ
  54. 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) シグネチャ ボディ
  55. Goアセンブリの読み方 MOVQ a1+8(FP), DI

  56. Goアセンブリの読み方 MOVQ a1+8(FP), DI CPU命令 引数1 引数2 , 引数には、 •

    メモリアドレス • レジスタの名前(後述) • 数値リテラル などを指定できる
  57. レジスタって? CPU内部の記憶装置 超小容量・超高速 Intel 8086 CPU

  58. Goアセンブリの読み方 MOVQ a1+8(FP), DI MOV : Move (コピー命令) Q :

    Quad word (4word = 4*16bit = 64bit) 「引数1から引数2に 64bit分コピーしてね」 意味的には MOVQ というより COPY64
  59. Goアセンブリの読み方 MOVQ a1+8(FP), DI メモ メモリアドレス (関数ローカルのとある地点 + 8byte後ろ)

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

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

    してね
  62. 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)
  63. 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)
  64. やってること • システムコール番号をRAXレジスタにセットし • 1個目の引数をRDIレジスタにセットし • 2個目の引数をRSIレジスタにセットし • 3個目の引数をRDXレジスタにセットし •

    SYSCALL命令を実行
  65. System V Application Binary Interface 手順はカーネルの呼び出し規約で決まっている

  66. 余談:レジスタの名前がわかりにくい • RAX, RDI, RSI, RDX,...... • 完全に歴史的事情 • 別に

    R0, R1, R2, R3 という名前でもよかった ◦ そういうCPUもある
  67. ライブコーディング 自力でシステムコールを叩いて hello worldを出力しよう

  68. package main import ( "fmt" ) func main() { fmt.Print("Hello

    world\n") } ふつうのHello world https://play.golang.org/p/sa0vP_nSS2V
  69. #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 バックトレースを逆順で再現していきます
  70. package main import ( "fmt" "os" ) func main() {

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

    world\n")) } os.File.Write
  72. package main import ( "syscall" ) func main() { syscall.Write(1,

    []byte("Hello world\n")) } syscall.Write
  73. 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
  74. 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 に
  75. -- 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)
  76. -- 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
  77. -- 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 (配列もアセンブリ)
  78. -- 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 (文字をバイナリで)
  79. 自力で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
  80. まとめ 低レイヤはたのしい

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

  82. 参考 • 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
  83. ご清聴 ありがとうございました