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

Go言語低レイヤー入門 Hello world が 画面に表示されるまで / Introduc...

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. 自己紹介 • @DQNEO (ドキュネオ) • メルカリUSAを開発しています • Goコンパイラ babygo の作者

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

    のシステムコールを見てみよう ◦ straceを使う ◦ gdbを使う ◦ コードを読む • Goアセンブリの読み方 • [ライブコーディング] Hello world を低レイヤー版で書き直す
  3. 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
  4. func main() { } プログラム (バイナリ実行ファイル ) mainパッケージ プログラムとruntime _rt0_amd64_linux

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

    runtimeの力を借りて exit 0 するプログラム Goの最小のプログラム
  6. 1 + 2 s = “hello” OS (Kernel) OSの助けなしでは、画面表示/ファイル操作/ネットワーク通信などが できない

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

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

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

    } 「やります」 プログラム (バイナリ実行ファイル ) このへん
  10. 自力で 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を依頼するプログラム
  11. -- 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関数 のボディ
  12. -- 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) } ≒
  13. Hello world プログラム OS (Kernel) runtime func main() { }

    「やります」 fmt.Print(“H..”) システムコール? システムコールで実現されてそう...? これを確かめたい
  14. 例: write • $ man 2 write • ググる: “Linux

    manual write” システムコールの使い方を調べる
  15. 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
  16. 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
  17. #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のバックトレース
  18. 最低レイヤーは 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
  19. 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) シグネチャ ボディ
  20. 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) シグネチャ ボディ
  21. Goアセンブリの読み方 MOVQ a1+8(FP), DI CPU命令 引数1 引数2 , 引数には、 •

    メモリアドレス • レジスタの名前(後述) • 数値リテラル などを指定できる
  22. Goアセンブリの読み方 MOVQ a1+8(FP), DI MOV : Move (コピー命令) Q :

    Quad word (4word = 4*16bit = 64bit) 「引数1から引数2に 64bit分コピーしてね」 意味的には MOVQ というより COPY64
  23. 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)
  24. 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)
  25. 余談:レジスタの名前がわかりにくい • RAX, RDI, RSI, RDX,...... • 完全に歴史的事情 • 別に

    R0, R1, R2, R3 という名前でもよかった ◦ そういうCPUもある
  26. package main import ( "fmt" ) func main() { fmt.Print("Hello

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

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

    []byte("Hello world\n")) } syscall.Write
  30. 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
  31. 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 に
  32. -- 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)
  33. -- 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
  34. -- 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 (配列もアセンブリ)
  35. -- 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 (文字をバイナリで)
  36. 自力で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
  37. 参考 • 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