1 of 83

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

@DQNEO (ドキュネオ)

Go Conference Tokyo 2021

2021-04-24

2 of 83

自己紹介

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

3 of 83

目次

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

4 of 83

今日の話の前提

  • CPU: x86-64
  • OS: Linux (Docker可)
  • go 1.15で動作確認済み

5 of 83

package main

import (

"fmt"

)

func main() {

fmt.Print("Hello world\n")

}

Hello world

6 of 83

Hello world

どのようなプロセスを経て

画面に文字が表示されるのでしょうか?

少し想像してみてください

�(※OS内部や表示装置の挙動は除く)

7 of 83

package main

func main() {

}

Goの最小のプログラム

なにもしないプログラム

8 of 83

package main

func main() {

}

プログラムは�どこから始まってどこで終わるのか?

9 of 83

プログラムはどこから始まるのか?

func main() { }

プログラム�(バイナリ実行ファイル)

mainパッケージ

10 of 83

プログラムはどこから始まるのか?

func main() { }

プログラム�(バイナリ実行ファイル)

mainパッケージ

_rt0_amd64_linux

TEXT _rt0_amd64_linux(SB),NOSPLIT,$-8

JMP _rt0_amd64(SB)

src/runtime/rt0_linux_amd64.s

é–‹å§‹

11 of 83

プログラムはどこから始まるのか?

func main() { }

プログラム�(バイナリ実行ファイル)

mainパッケージ

_rt0_amd64_linux

初期化処理

main.main()

é–‹å§‹

12 of 83

プログラムはどこで終わるのか?

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 of 83

package main

func main() {

}

Goの最小のプログラム

なにもしないプログラム�exit 0 するプログラム

14 of 83

プログラムとruntime

func main() { }

プログラム�(バイナリ実行ファイル)

mainパッケージ

_rt0_amd64_linux

….

main.main()

…

「exit 0してね」

runtime

15 of 83

プログラムとruntime

func main() { }

プログラム�(バイナリ実行ファイル)

mainパッケージ

_rt0_amd64_linux

….

main.main()

…

「exit 0してね」

runtime

runtimeとは、

ビルド時にプログラムに自動で組み込まれるコードのこと

16 of 83

package main

func main() {

}

Goの最小のプログラム

なにもしないプログラム�exit 0 するプログラム

runtimeの力を借りて exit 0 するプログラム

17 of 83

exit 0って、

終了したら勝手にそうなるのでは?

18 of 83

プログラムとOS

プログラム�(バイナリ実行ファイル)

OS (Kernel)

コンピュータで動くコードは主にこの2つ

19 of 83

プログラムとOS

1 + 2

s = “hello”

OS (Kernel)

簡単な計算や代入などは、OSの助けなしに行うことができる

(CPUが機械語列をそのまま実行する)

プログラム�(バイナリ実行ファイル)

20 of 83

プログラムとOS

1 + 2

s = “hello”

OS (Kernel)

OSの助けなしでは、画面表示/ファイル操作/ネットワーク通信などができない

プログラムを正常終了 (exit 0)することもできない

プログラム�(バイナリ実行ファイル)

21 of 83

exit 0 の仕組み

OS (Kernel)

runtime

「exit 0 してね」

func main() { }

「やります」

プログラム�(バイナリ実行ファイル)

システムコール

プログラムの最後で OS に exit 0を依頼している�(さもないとクラッシュ)

22 of 83

package main

func main() {

}

Goの最小のプログラム

なにもしないプログラム�exit 0 するプログラム

runtimeの力を借りて exit 0 する�OSに exit 0 を依頼するプログラム

23 of 83

低レイヤは実感しづらい

OS (Kernel)

runtime

「0でexitしてね」

package main

func main() { }

「やります」

プログラム�(バイナリ実行ファイル)

このへん

24 of 83

そうだ自分でやってみよう

OS (Kernel)

runtime

「やります」

プログラム�(バイナリ実行ファイル)

package main

「0でexitしてね」

ココ

25 of 83

-- a.s --

TEXT main·main(SB),0,$0

MOVQ $231, AX

MOVQ $0, DI

SYSCALL

-- main.go --

package main

func main()

自力で exit(0) する

runtimeの力を借りずに

OS にexit 0を依頼するプログラム

26 of 83

-- a.s --

TEXT main·main(SB),0,$0

MOVQ $231, AX

MOVQ $0, DI

SYSCALL

-- main.go --

package main

func main()

func main() {

os.Exit(0)

}

自力で exit(0) する

Kernel に�231番目のカーネル命令 (exit_group)を�引数0で�実行をお願いする(syscall)

≒

main関数

のボディ

27 of 83

-- a.s --

TEXT main·main(SB),0,$0

MOVQ $231, AX

MOVQ $1, DI

SYSCALL

-- main.go --

package main

func main()

func main() {

os.Exit(1)

}

ちょっと変えてみる

引数を1にすると、1でexitする

≒

28 of 83

-- a.s --

TEXT main·main(SB),0,$0

MOVQ $231, AX

MOVQ $0, DI

SYSCALL

-- main.go --

package main

func main()

ちなみに

青文字の箇所は今は気にしないでください

29 of 83

現状確認

プログラム

OS (Kernel)

runtime

func main() {

}

「やります」

「0でexitしてね」

システムコール

30 of 83

システムコールでできること

  • プログラム終了
  • ファイル操作
  • ネットワーク通信
  • 動的メモリ確保 (make, append, map set)
  • etc

31 of 83

package main

import (

"fmt"

)

func main() {

fmt.Print("Hello world\n")

}

Hello world

32 of 83

Hello world

プログラム

OS (Kernel)

runtime

func main() {

}

「やります」

fmt.Print(“H..”)

システムコール?

システムコールで実現されてそう...?

これを確かめたい

33 of 83

システムコールを確認する方法

  • straceコマンド
  • gdb
  • ソース読む
  • etc

34 of 83

straceコマンド

  • systemcall ã‚’ trace する
  • プログラムが呼び出すシステムコールを実行時に表示
  • SREのひとがよく使う

35 of 83

strace使い方

$ strace ./hello > /dev/null

36 of 83

straceのデモ

37 of 83

straceの出力

38 of 83

writeシステムコールで画面表示

プログラム

OS (Kernel)

func main() {

}

処理

fmt.Print(“H..”)

write(1, “H… )

hello world

ターミナル

39 of 83

straceの出力 (抜粋)

write(1, "Hello world\n", 12) = 12

40 of 83

straceの出力 (抜粋)

write(1, "Hello world\n", 12) = 12

システムコールの種類

引数

戻り値

41 of 83

例: write

  • $ man 2 write
  • ã‚°ã‚°ã‚‹: “Linux manual write”

システムコールの使い方を調べる

42 of 83

ssize_t write(int fd, const void *buf, size_t count);

ファイルディスクリプタ

バッファ

(バイト列の先頭アドレス)

バイト数

戻り値

実際に書き込んだバイト数

(失敗時は -1)

write(2) — Linux manual page

43 of 83

straceの出力 (抜粋)

write(1, "Hello world\n", 12)

システムコール

標準出力

12バイト

バッファ

(バイト列の先頭アドレス)

44 of 83

gdb

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

45 of 83

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

46 of 83

デモ

(Hello World をgdbでデバグ)

47 of 83

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 of 83

writeのバックトレース

#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

見やすくするとこんな感じ

49 of 83

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

50 of 83

Golandの場合

OS, Archを Linux / amd64 に設定しておく

51 of 83

デモ

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

52 of 83

最低レイヤーは 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 of 83

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 of 83

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 of 83

Goアセンブリの読み方

MOVQ a1+8(FP), DI

56 of 83

Goアセンブリの読み方

MOVQ a1+8(FP), DI

CPU命令

引数1

引数2

,

引数には、

  • メモリアドレス
  • レジスタの名前(後述)
  • 数値リテラル

などを指定できる

57 of 83

レジスタって?

CPU内部の記憶装置

超小容量・超高速

Intel 8086 CPU

58 of 83

Goアセンブリの読み方

MOVQ a1+8(FP), DI

MOV : Move (コピー命令)

Q : Quad word (4word = 4*16bit = 64bit)

「引数1から引数2に 64bit分コピーしてね」

意味的には MOVQ というより COPY64

59 of 83

Goアセンブリの読み方

MOVQ a1+8(FP), DI

メモ

メモリアドレス

(関数ローカルのとある地点 + 8byte後ろ)

60 of 83

Goアセンブリの読み方

MOVQ a1+8(FP), DI

レジスタの名前

(本当の名前は RDI)

61 of 83

Goアセンブリの読み方

MOVQ a1+8(FP), DI

  • メモリのとある場所にあるデータを
  • RDIレジスタに
  • 64bit分コピー してね

62 of 83

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 of 83

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 of 83

やってること

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

65 of 83

System V Application Binary Interface

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

66 of 83

余談:レジスタの名前がわかりにくい

  • RAX, RDI, RSI, RDX,......
  • 完全に歴史的事情
  • 別に R0, R1, R2, R3 という名前でもよかった
    • そういうCPUもある

67 of 83

ライブコーディング

自力でシステムコールを叩いて

hello worldを出力しよう

68 of 83

package main

import (

"fmt"

)

func main() {

fmt.Print("Hello world\n")

}

ふつうのHello world

69 of 83

バックトレースを逆順で再現していきます

#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 of 83

package main

import (

"fmt"

"os"

)

func main() {

fmt.Fprint(os.Stdout, "Hello world\n")

}

fmt.Fprint

71 of 83

package main

import (

"os"

)

func main() {

os.Stdout.Write([]byte("Hello world\n"))

}

os.File.Write

72 of 83

package main

import (

"syscall"

)

func main() {

syscall.Write(1, []byte("Hello world\n"))

}

syscall.Write

73 of 83

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 of 83

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 of 83

-- 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 of 83

-- 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 of 83

-- 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 of 83

-- 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 of 83

自力で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

80 of 83

まとめ

低レイヤはたのしい

81 of 83

今日の内容を再現できる環境

82 of 83

参考

83 of 83

ご清聴

ありがとうございました