boot OS (runtime)

Table of Contents

go asm

解説&翻訳 - A Quick Guide to Go's Assembler

Goアセンブリの書き方

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

が生成されます。

ここが「プロセステーブル誕生地点」。


この順で追うと美しい:

  1. schedinit
  2. newproc
  3. mstart
  4. schedule
  5. 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.

gomacro

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

etc

README

ネズミはなぜ回し車で走るのか なんて本を読んだ。

野生のネズミも回し車で走るって研究成果がオランダで発表されたって事を 以前に記した。んで、その続きだ。アルゼンチンだかで確認した所、そうでは なかったらしい。世界中で検証して欲しいぞ。

飼われているネズミは暇だから走る? それともダイエットが目的? ストレス発散? 色々な仮説を建てて、それを実証するために、研究者は たゆまぬ努力をしている。論文書いてナンボだからね。

一番走る種で、一晩に43Kmも走るそうだ。毎晩フルマラソンですぜ。 頭が下りますよ。

野ネズミは生きる為に走る。捕食者から逃れる為、自分の食料を調達する為にね。 これは馬も一緒、ってか、捕食者から逃れるのが主目的。ドックレースは、日本 では行なわれていないけど、外国では盛んだそうだ。ドックレースの時に、 ぬいぐるみのウサギを先行させるそうな。それは、犬が食料のウサギを追いかける 狼からの本能の応用だそうだ。

競馬の馬は、ゴールに到達した時に、それを認識しているかって言う重大な問題 があるそうな。騎手によって意見が分かれるそうで、武豊は、わかっています派。 岡部幸雄騎手は、認識していない派とか。武さん曰く、ゴールあたりで、ひとしきり 歓声が大きくなる、騎手の姿勢や挙動が変化するのを敏感に察知してるだろうってね。 捕食者に追われた場合、そこにはゴールなんて無い訳だから、本当の所はどうなんで しょうね。

著者は動物心理学を研究されてるそうだけど、次から次へと難問が出てきて、楽しい だろうね。こういう人生を走り続けたいものだ。


This year's Index

Home