boot OS (runtime)
Table of Contents
go asm
解説&翻訳 - A Quick Guide to Go's Assembler
Q: 話はちょっと逸れますが、*.s なんてのが出てきたので、それを観察してみます。
vm$ wc `ls *.s | grep openbsd`
17 63 454 rt0_openbsd_386.s
11 40 311 rt0_openbsd_amd64.s
11 40 298 rt0_openbsd_arm.s
79 238 2008 rt0_openbsd_arm64.s
36 151 976 rt0_openbsd_mips64.s
16 57 370 rt0_openbsd_ppc64.s
14 55 372 rt0_openbsd_riscv64.s
990 3630 20890 sys_openbsd_386.s
666 2594 15912 sys_openbsd_amd64.s
827 3229 18904 sys_openbsd_arm.s
652 2527 15414 sys_openbsd_arm64.s
388 1460 9018 sys_openbsd_mips64.s
655 2587 15664 sys_openbsd_ppc64.s
742 2823 17205 sys_openbsd_riscv64.s
goもちゃんとOpenBSDのarchにちゃんと追従してますね。で、入口部分を観察。 ソースを眺めるならCISCよりRISCが良いかな。
vm$ diff -u rt0_openbsd_amd64.s rt0_openbsd_riscv64.s --- rt0_openbsd_amd64.s Thu Aug 28 00:49:40 2025 +++ rt0_openbsd_riscv64.s Thu Aug 28 00:49:40 2025 @@ -1,11 +1,14 @@ -// Copyright 2009 The Go Authors. All rights reserved. +// Copyright 2022 The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. #include "textflag.h" -TEXT _rt0_amd64_openbsd(SB),NOSPLIT,$-8 - JMP _rt0_amd64(SB) +TEXT _rt0_riscv64_openbsd(SB),NOSPLIT|NOFRAME,$0 + MOV 0(X2), A0 // argc + ADD $8, X2, A1 // argv + JMP main(SB) -TEXT _rt0_amd64_openbsd_lib(SB),NOSPLIT,$0 - JMP _rt0_amd64_lib(SB) +TEXT main(SB),NOSPLIT|NOFRAME,$0 + MOV $runtime rt0_go(SB), T0 + JALR ZERO, T0
riscv64が入ったのは2022年と、最近の事なんですね。それはさておき、 TEXTのプロローグ部分が何を意味してるのでしょう? SBって何?、その右側に 記述されてるフラグっぽいものの意味を説明してください。またコードを読む 上で、これは知っておいた方が良いってのがあったら提示してください。
A: いいですね……完全に Goアセンブラ読解フェーズ に入りましたね 😄 ここは「Go独自アセンブラ文法」を理解するのが最初の壁です。
あなたが見ているのは:
src/runtime/rt0_openbsd_*.s
つまり:
Goランタイムのエントリポイント(crt0相当)
です。
Goアセンブラでは:
TEXT symbol(SB), flags, $framesize-argsize
という形式になります。
例:
TEXT _rt0_riscv64_openbsd(SB),NOSPLIT|NOFRAME,$0
意味は:
「rt0riscv64openbsd という関数(シンボル)を定義する」
です。
Cで言えば:
void _rt0_riscv64_openbsd(void) {
}
に相当します。
これは Static Base の略です。
Goアセンブラ特有の概念で:
symbol(SB)
は
グローバルシンボル
を意味します。
つまり:
main(SB) runtime.rt0_go(SB)
は
プログラム全体で一意なシンボル
という意味です。
## 他にもある擬似レジスタ
| 記号 | 意味 |
| -- | ------------------ |
| SB | static base(グローバル) |
| FP | frame pointer |
| SP | stack pointer |
| PC | program counter |
これはGoアセンブラ独自表現で、 実CPUレジスタとは少し抽象化されています。
例:
NOSPLIT NOFRAME
## 🔹 NOSPLIT
Goのgoroutineは:
- 小さいスタック
- 足りなくなると grow する
通常関数は:
スタック足りる?チェック
が入ります。
しかし:
NOSPLIT
を付けると:
スタックチェックしない
という意味です。
なぜ?
- rt0はまだruntime初期化前
- スタック拡張不能
だからです。
## 🔹 NOFRAME
通常関数は:
push fp sub sp
のようなフレームを作ります。
NOFRAME
は:
フレームを作らない
超低レベルコード専用です。
$framesize
です。
$0
は:
ローカルスタック領域なし
TEXT _rt0_riscv64_openbsd(SB),NOSPLIT|NOFRAME,$0
MOV 0(X2), A0 // argc
ADD $8, X2, A1 // argv
JMP main(SB)
これは:
- X2 = SP(RISC-V ABI)
- 0(SP) → argc
- SP+8 → argv
つまり:
UNIX ABI から Go ABI へ変換
しているだけです。
TEXT main(SB),NOSPLIT|NOFRAME,$0
MOV $runtime rt0_go(SB), T0
JALR ZERO, T0
ここが本質。
runtime.rt0_go
に飛んでいます。
つまり:
Goランタイムを起動する
amd64では:
JMP _rt0_amd64(SB)
と別ファイルへ飛ばします。
riscv64は直接書いているだけ。
構造は同じです。
Goアセンブラは:
- Plan 9 アセンブラ由来
- AT&TともIntelとも違う
- レジスタ順が独特
例:
MOV src, dst
Intelと同じ順番です(AT&Tとは逆)。
### ① ABIInternal / ABI0
Goは:
- Go専用ABI
- C ABI
を分けています。
runtime/abi 周辺が重要。
### ② asmcgocall
CとGoの境界。
### ③ mstart / schedule
OS的心臓部。
### ④ stack growth
morestack 系関数。
| UNIX | Go |
| ------------ | ---------------- |
| crt0 | rt0* |
| main | runtime.=rt0go= |
| scheduler | runtime.schedule |
| syscall stub | sysopenbsd*.s |
あなたが見ている:
sys_openbsd_*.s
は
libc の syscall stub 相当
です。
- TEXT = 関数定義
- SB = グローバルシンボル基点
- NOSPLIT = スタック拡張禁止
- NOFRAME = フレーム作らない
- $0 = ローカル領域なし
- rt0* = crt0相当
もし読むなら:
runtime/asm_386.s runtime/proc.go runtime/signal_openbsd.go
この3つを行き来すると
Goの“ユーザ空間OS”
が完全に見えます。
ここから先は:
A) stack grow のアセンブラを読む B) syscall stub を読む C) signal trampoline を読む D) Go ABI の詳細に入る
どれ行きますか?
boot OS (runtime)
Q: OS(runtime)がbootする所を捉えてみます。
ob$ readelf -h a.out ELF Header: Magic: 7f 45 4c 46 01 01 01 0c 00 00 00 00 00 00 00 00 Class: ELF32 : Entry point address: 0x80c98f0
だったので、
(gdb) b *0x80c98f0 Breakpoint 1 at 0x80c98f0: file /usr/local/go/src/runtime/rt0_openbsd_386.s, li\ ne 8. (gdb) r Starting program: /tmp/work/a.out Breakpoint 1, _rt0_386_openbsd () at /usr/local/go/src/runtime/rt0_openbsd_386s:8 8 JMP _rt0_386(SB) (gdb) n _rt0_386 () at /usr/local/go/src/runtime/asm_386.s:14 14 TEXT _rt0_386(SB),NOSPLIT,$8
こんな風に開始して、CPUIDがどうだとかcgoを使ってるかのチェックが行なわれ、229 行目あたりから、やっとCALLが出現してきました。
=> CALL runtime·emptyfunc(SB) // fault if stack check is wrong
CALL runtime·check(SB)
// saved argc, argv
MOVL 120(SP), AX
MOVL AX, 0(SP)
MOVL 124(SP), AX
MOVL AX, 4(SP)
CALL runtime·args(SB)
CALL runtime·osinit(SB)
CALL runtime·schedinit(SB)
// create a new goroutine to start program
PUSHL $runtime·mainPC(SB) // entry
CALL runtime·newproc(SB)
POPL AX
// start this M
CALL runtime·mstart(SB)
CALL runtime·abort(SB)
RET
カーネルのbootで言ったら /sbin/init の誕生したあたりでしょうか。
A: はい―― その理解、/かなり本質を突いています。/
あなたが捕まえているこの地点は:
「Goランタイムという“ユーザ空間OS”が起動して、最初のプロセス(goroutine)を生成する瞬間」
です。
構造を、OpenBSDカーネルのbootと対応させて整理してみます。
止めた地点:
_rt0_386_openbsd
→ _rt0_386
→ runtime.args
→ runtime.osinit
→ runtime.schedinit
→ runtime.newproc
→ runtime.mstart
これは UNIX に例えると:
ELF entry ↓ crt0 ↓ libc 初期化 ↓ kernel サブシステム初期化 ↓ init プロセス生成 ↓ スケジューラ開始
に対応します。
あなたの抜粋を見ていきます。
## runtime.emptyfunc
CALL runtime·emptyfunc(SB)
スタックチェック検証。
これは:
MMUや例外機構の sanity check
に近いものです。
## runtime.check
CPU機能確認(CPUID等)。
これは:
CPU feature probe (SSE / AVX / atomic)
OpenBSDカーネルの:
cpu_attach() identifycpu()
相当。
## runtime.args
MOVL 120(SP), AX CALL runtime·args
argc/argv を Go 管理領域へコピー。
これは:
proc0 の u-area 初期化
に近い。
## runtime.osinit
ここが面白い。
OpenBSD版では:
- CPU数取得
- page size取得
- signal handler準備
つまり:
OSとの接続部分初期化
カーネル的に言うと:
init386() pmap_bootstrap()
的ポジション。
## runtime.schedinit
ここが本丸。
- allp 配列作成
- G0生成
- M0生成
- run queue 初期化
つまり:
スケジューラ初期化
OpenBSDで言えば:
scheduler init proc0 初期化
に近い。
この部分:
PUSHL $runtime·mainPC CALL runtime·newproc
これは:
「main goroutine」を生成している
のです。
UNIX対応で言うと:
fork() → execve("/sbin/init")
に非常に近い。
ただし重要な違い:
| UNIX | Go |
| --------------- | ----------------- |
| init は PID 1 | main は goroutine |
| kernelは常に特権 | runtimeはユーザ空間 |
| schedulerはハード依存 | Go schedulerは言語実装 |
なので:
「/sbin/init が誕生した瞬間」という理解はかなり良いですが より正確には「PID 1 を fork した瞬間」
に近いです。
CALL runtime·mstart
これが:
スケジューラループ突入
です。
mstart → mstart1 → schedule()
あなたが以前ブレークした:
runtime.schedule
にここから到達します。
OpenBSD:
locore.S
→ main()
→ scheduler_start()
→ fork1(init)
Go:
rt0_*.s
→ runtime.rt0_go
→ schedinit()
→ newproc(main)
→ mstart()
構造がほぼ同じなのが分かりますね。
あなたが前に言っていた:
runtimeは専用OSですね
これは半分正解で、半分違う。
正確には:
Go runtime = 「libc + pthread + scheduler + signal handler + GC + TLS + small kernel」
です。
libcより遥かに大きい。 でもカーネルよりは小さい。
もし boot の核心を捕まえたいなら:
### runtime.schedinit を読む
ここで:
- G0
- M0
- allp
- runq
が生成されます。
ここが「プロセステーブル誕生地点」。
この順で追うと美しい:
- schedinit
- newproc
- mstart
- schedule
- execute
ここで:
- G が実行され
- main.main が呼ばれる
あなたが次に疑問に思うであろう点:
- main goroutine はどこで main.main を呼ぶ?
- g0 は何をしている?
- mstart の無限ループはどこ?
- preemption はいつ始まる?
全部 schedinit 以降にあります。
「/sbin/init の誕生あたりでしょうか?」
かなり良い視点です。
より正確に言うと:
「カーネルが最初のユーザプロセスを fork した瞬間」
に相当します。
もし次へ行くなら:
A) schedinitの中身をgdbで追う B) newprocでGがどう作られるか見る C) g0 と main goroutine の関係を可視化する D) main.main に制御が渡る瞬間を捕まえる
どこを捕まえますか?
go on FreeBSD
この所、FreeBSDとは無沙汰だったので、1月ぶりぐらいに火を入れた。 オイラーの現在の感心中心である、go を入れてみた。マシンはi386のVMWare 仮想環境ね。
何も考えずに、pkg install go したら、go124-1.24.13 が入った。 Release History によると、ちと古すぎるきらいが有るな。2、8月にバージョンが 微妙に上がる様だから、暫らくはこのまま使うか。まだ、go126 はpkgとして 供給されてはいないしね。
試運転としてLubuntu側で利用してる go/src/work を持ってきた。そして それをMakeしたよ。Makefileはこんなのだ。
sakae@fb:~/go/src/work $ cat Makefile
BINARY_NAME=a.out
ob: main.go
go build -o ${BINARY_NAME} -gcflags=all="-N -l" main.go
run: ob
./${BINARY_NAME}
rel: main.go
go build -o REL -ldflags="-s -w" -trimpath main.go
clean:
rm -f ${BINARY_NAME} REL main
go clean -cache
rm -f TAGS
rm -rf std ext
cleanでTAGSやらキャッシュをクリアする様にしてる。
で、あろう事か新らし目のgoをネットからDLして、その環境でコンパイルしたぞ。 work-dir内にリナ用のgo.modファイルが残ってて、それが go 1.25.3 に設定 されてたんだ。GOTOOLCHAIN='auto' で、勝手にバージョンを合わせてくれるんだな。
これが、何処に鎮座してるかと言うと
sakae@fb:~ $ ls go/pkg/mod/golang.org/toolchain\@v0.0.1-go1.25.3.freebsd-386/ CONTRIBUTING.md SECURITY.md go.env LICENSE VERSION lib/ PATENTS bin/ pkg/ README.md codereview.cfg src/
下記は、pkg installした物。
sakae@fb:~ $ ls /usr/local/go124/ CONTRIBUTING.md api/ misc/ LICENSE bin/ pkg/ PATENTS codereview.cfg src/ README.md doc/ test/ SECURITY.md go.env VERSION lib/
微妙に、サイズが異なるので、du -sh で確認してみた。
210M go/pkg/mod/golang.org/toolchain@v0.0.1-go1.25.3.freebsd-386 280M /usr/local/go124
gomacro
ports/langの下に面白いものが有ったので、試してみるかな。
gomacro is an interactive Go interpreter and debugger with REPL, Eval, generics and Lisp-like macros, implemented in pure Go. It offers both an interactive and a scripting mode, and does not require a Go toolchain at runtime.
sakae@lu:tmp$ go install github.com/cosmos72/gomacro@latest go: downloading github.com/cosmos72/gomacro v0.0.0-20251011163654-53dd3d49f67b go: downloading github.com/peterh/liner v1.2.2 go: downloading golang.org/x/tools v0.25.1 go: downloading github.com/mattn/go-runewidth v0.0.15 go: downloading golang.org/x/mod v0.21.0
README
ネズミはなぜ回し車で走るのか なんて本を読んだ。
野生のネズミも回し車で走るって研究成果がオランダで発表されたって事を 以前に記した。んで、その続きだ。アルゼンチンだかで確認した所、そうでは なかったらしい。世界中で検証して欲しいぞ。
飼われているネズミは暇だから走る? それともダイエットが目的? ストレス発散? 色々な仮説を建てて、それを実証するために、研究者は たゆまぬ努力をしている。論文書いてナンボだからね。
一番走る種で、一晩に43Kmも走るそうだ。毎晩フルマラソンですぜ。 頭が下りますよ。
野ネズミは生きる為に走る。捕食者から逃れる為、自分の食料を調達する為にね。 これは馬も一緒、ってか、捕食者から逃れるのが主目的。ドックレースは、日本 では行なわれていないけど、外国では盛んだそうだ。ドックレースの時に、 ぬいぐるみのウサギを先行させるそうな。それは、犬が食料のウサギを追いかける 狼からの本能の応用だそうだ。
競馬の馬は、ゴールに到達した時に、それを認識しているかって言う重大な問題 があるそうな。騎手によって意見が分かれるそうで、武豊は、わかっています派。 岡部幸雄騎手は、認識していない派とか。武さん曰く、ゴールあたりで、ひとしきり 歓声が大きくなる、騎手の姿勢や挙動が変化するのを敏感に察知してるだろうってね。 捕食者に追われた場合、そこにはゴールなんて無い訳だから、本当の所はどうなんで しょうね。
著者は動物心理学を研究されてるそうだけど、次から次へと難問が出てきて、楽しい だろうね。こういう人生を走り続けたいものだ。