$30 off During Our Annual Pro Sale. View Details »

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

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

DQNEO

April 24, 2021
Tweet

More Decks by DQNEO

Other Decks in Programming

Transcript

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  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

    View Slide

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

    View Slide

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

    「exit 0してね」
    runtime

    View Slide

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

    「exit 0してね」
    runtime
    runtimeとは、
    ビルド時にプログラムに自動で組み込まれるコードのこと

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  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を依頼するプログラム

    View Slide

  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関数
    のボディ

    View Slide

  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)
    }

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  36. straceのデモ

    View Slide

  37. straceの出力

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  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=, 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

    View Slide

  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のバックトレース

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  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

    View Slide

  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)
    シグネチャ
    ボディ

    View Slide

  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)
    シグネチャ
    ボディ

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  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)

    View Slide

  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)

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  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
    バックトレースを逆順で再現していきます

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  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

    View Slide

  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 に

    View Slide

  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)

    View Slide

  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

    View Slide

  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 (配列もアセンブリ)

    View Slide

  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 (文字をバイナリで)

    View Slide

  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

    View Slide

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

    View Slide

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

    View Slide

  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

    View Slide

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

    View Slide