これはNetBSD Advent Calendar 2015の13日めの記事です。
ここ数ヶ月(足掛け1年以上にもなります)、NetBSD/arm環境でGo言語を使えるようにしようと悪戦苦闘してます。 未だにちゃんと動いているとは言い難くまだまだ問題山積みなのですが、一旦整理したいと思っていました。そこで最も苦しんだところについて思い出しつつ、確認もしながら記してみます。
これまでの概要は今年の6月のOSC北海道2015で話したり(発表資料)、7月のNetBSD BoFでも話したりしてますがその後10月になって大きく進展してそれなりに動作するまでになりました(2015/10月NBUGでの発表資料)。
簡単にまとめると、
という感じです。
これも先の資料に書いてありますので簡単に。
これが今年の6月頃の状況でした。
NetBSD/arm 6.xで動くようになったので、できたバイナリ(goコマンド本体や、goで作ったプログラムの実行バイナリ)をNetBSD/arm 7.0(当時はまだBETA)で動かしてみました。 先に書いたように、NetBSD 7.0からはEABIがデフォルトになりましたが、従来の形式(ここではOABIとします)で作ることもできます。また、実際に正式なNetBSD配布セットでもOABI形式バイナリも提供されています(※注2)。
NetBSD 7.0にはこれまでの旧バージョンで作ったバイナリを動かすためのCOMPAT_60オプションがあり、また、COMPAT_NETBSD32オプションを使ってEABI環境でもOABIバイナリを動かすことができるのですが、NetBSD 7.0ではEABI環境でもOABI環境でもどちらでもGo言語関連は動いてくれませんでした。
どうやら問題はEABI/OABIの違いによるものでは無さそうです。そこで以後はまずNetBSD 6.xと同じバイナリ形式であるOABI環境を使ってNetBSD/arm 7.0上で動かない原因を探ることにしました。 なお、デバッグ環境として利用したのは主にRaspberry Pi(初代, ARMv6のシングルコア)です。
さて、「動かない、動きが変だ」だけでは何もわかりません。まずは何が問題かを把握しなければなりません。今回の現象は次の様になります。
まずgo_bootstrapでOSが無応答になる現象を調べます。コマンドやCntl-C、新規のネットワークからのsshセッションは無応答でしたので、システムコンソールでカーネルデバッガに入ってみます。Raspberry Pi(以降RPI)ではフレームバッファコンソールとUSBキーボードが使え、さらにPCと同じくCntl-Alt-Escでカーネルデバッガに制御を渡すことができるのですが、RPIのUSBドライバかキーボードドライバか、はたまたRPIのフレームバッファ画面ドライバかが不安定で、カーネルデバッガ自体が頻繁にpanicを起こしてしまいました。これはこれで問題だとは思うのですが今回の件とは無関係に見えたのでひとまず置いておき、さっささとシリアルコンソールに切り替えてそこで作業します。
RPIでシリアルコンソールにするにはbootパーティションにあるcmdline.txtに何も書かずにしておけば良いのでそうします(フレームバッファコンソールを使う場合にはconsole=fbと記入します)。なお、この指定はNetBSD kernelの起動時にコマンドライン引数を追加するためのものです。config.txtに書いても無意味です(config.txtはRPIのファームウェア側のブートプログラムに対するオプションの設定です。両者はまったく異なります)。
シリアルコンソールからカーネルデバッガに制御を移すには Break信号を送れば良いようです。ここではcu(1)を使っていたので、~#で良いようです。早速go14のbuildを実行して応答が無くなったらデバッガに渡し、バックトレースを見てみます。
db> bt 0x9b321bcc: netbsd:plcomintr+0xc 0x9b321be8: netbsd:pic_dispatch+0x28 0x9b321c5c: netbsd:pic_do_pending_ints+0x384 0x9b321cc0: netbsd:irq_entry+0x64 0x9b321cf8: netbsd:vmcmd_readvn+0xc8 0x9b321d90: netbsd:execve_runproc+0x1ac 0x9b321f00: netbsd:execve1+0x44 0x9b321f14: netbsd:sys_execve+0x20 0x9b321f80: netbsd:syscall+0x8c 0x9b321fac: netbsd:swi_handler+0x9c
よくわかりませんが、どうやらexecveの先で止まっているようです。カーネルデバッガのpsコマンドで現在のプロセスを見てみます。
db> ps PID LID S CPU FLAGS STRUCT LWP * NAME WAIT 19327> 1 7 0 0 9b33d500 bash 4413 1 3 0 80 9ba78060 bash wait 2985 1 3 0 1000080 9b33dce0 sh wait 4340 1 3 0 1000080 9b33d7a0 make wait 2918 1 3 0 1000080 9b33da40 sh wait
現在のプロセス名は、bashとなっています。go_bootstrapではありません。また、バックトレースの結果から、どうやらgo_bootstrap自体の実行前に止まっているように見えます。つまり、バイナリのロード中あるいはプロセスの初期化中で無限ループしているようです。この処理はkernel内で行われている、つまりシステムコール呼び出し中の処理であるため、ユーザプロセスからの制御は受け付けられません(コンテキストスイッチが呼ばれる場合等、処理によっては受け付けられる場合もありえますが、どうもそういう箇所ではないようです)。
うーん、kernelのローダ内となるとちょっと辛いところです。ローダの処理内容を理解していませんし、これまでその辺りのソースを追いかけたこともありません。カーネルデバッガだけの調査もなかなか辛いところです。一旦状況確認だけに留めておきます。
次にhelloバイナリのAbort trapを調べてみます。まずAbortと言われているので何らかの理由によりSIGABRTシグナルを受け付けたのだと考えられます。とりあえずgdbでコマンドを実行してみます。
gdb ./hello (gdb) r Starting program: /home/oshima/hello During startup program terminated with signal SIGABRT, Aborted. (gdb)
gdbでプログラム実行時にSIGNALを受けると、普通はシグナルハンドラで止まってくれるのですがどうも様子が違います。 SIGABRTでスタートアップが終ったと出ます。なんだがよくわかりません。そこでブレークポイントを設定してみます。goではlibcもcrt0.oも使っておらず、そのためエントリポイントはmainでもcrt0のstartでもありません。最初のエントリーポイントはNetBSD/arm用の場合_rt0_arm_netbsdで、ユーザプログラムの実行開始関数はmain.mainとなります。
(gdb) b _rt0_arm_netbsd Breakpoint 1 at 0x46de0: file /usr/pkgsrc/lang/go14/work/go/src/runtime/rt0_netbsd_arm.s, line 11. (gdb) b eain.main Breakpoint 2 at 0x10cc0: file /home/oshima/hello.go, line 7. (gdb) r Starting program: /home/oshima/hello During startup program terminated with signal SIGABRT, Aborted. (gdb)
ブレークポイントに引っかからず同じように終ってしまいました。main.mainならともかく、バイナリの本当の最初の入り口であるrt0_arm_netbsdにさえも実行されていないということでしょうか。
よくわからないので今度はktraceを使ってみます。
$ ktrace ./hello [1] Abort trap ./hello $ kdump 3644 1 ktrace EMUL "netbsd" 3644 1 ktrace CALL execve(0x7fffde83,0x7fffcda8,0x7fffcdb0) 3644 1 ktrace NAMI "./hello" $
通常であればプロセス名ktraceの後に対象のプログラム名で実行されるシステムコールが出力されるはずですがそれがなく本当に実行ファイルのバイナリが一切実行されずにプロセスが終っているようです。
最初のgo_bootstrapもバイナリのロード部分のようだったので、 これはfork(2)後のexecve(2)で問題があるのでは、execve(2)を行う親プロセスに対してktraceすれば何かわかるんじゃないかと思いつきました。この場合の親は誰かというと、shellです。そこでshellを観察してみました。なお、子プロセスまでトレース対象とするためにktraceのコマンドラインオプション-iを追加しています。
$ ktrace -i sh -c ./hello [1] Abort trap ./hello $ kdump (略) 16527 1 sh RET __sigprocmask14 0 16527 1 sh CALL __vfork14 4699 1 sh EMUL "netbsd" 4699 1 sh RET fork 0 4699 1 sh CALL execve(0x431f4,0x43204,0x4320c) 4699 1 sh NAMI "./hello" 16527 1 sh RET __vfork14 4699/0x125b 16527 1 sh CALL getpgrp 16527 1 sh RET getpgrp 16527/0x408f 16527 1 sh CALL __wait450(0xffffffff,0x7fffc9e4,0,0) 16527 1 sh RET __wait450 4699/0x125b 16527 1 sh CALL write(2,0x40324050,0x10) 16527 1 sh GIO fd 2 wrote 16 bytes "[1] Abort trap" 16527 1 sh RET write 16/0x10 16527 1 sh CALL write(2,0x40324050,0x15) 16527 1 sh GIO fd 2 wrote 21 bytes " ./hello" 16527 1 sh RET write 21/0x15 16527 1 sh CALL write(2,0x40324050,1) 16527 1 sh GIO fd 2 wrote 1 bytes "\n" 16527 1 sh RET write 1 16527 1 sh CALL exit(0x86)
親プロセス(この場合16527)から__vfork14直後でプロセス(4699)が作成されており、その先で./helloというプログラムをexecve(2)していることがわかります。しかしすぐに終ってしまったようで、親プロセスがAbort trapメッセージを出力しています。
あんまり変わりありません。これはもう、kernel側のexecve(2)システムコールを調べるしかなさそうです。
helloバイナリがいきなり終っているのは、その実行形式ファイルを読み込みプログラムとして実行を行う execve(2)システムコールが何らかの要因で終っているためだろうということがわかりました。 では一体なぜいきなり終わってるかを知りたいところです。そのためにはNetBSDのカーネルソースを見なければなりません。 ユーザ空間で動作するプログラムだったはずですが、とうとうカーネル空間側を調べるハメになってしまいました。システムハングアップの時点で予想はしていましたが。
今回動かしているのはNetBSD 7.0です。ですので(currentではなく)NetBSD 7.0のソースを読みます(実際には手元でビルドしていたカーネルである当初7.0_BETAのソース、次第にRC1からRC3と調査中にも変わっていき最後はリリース版になりまました)。
execve(2)の先でSIGABRTあるいはABRTになるっぽいような場所を探したところ、sys/kern/kern_exec.cのexecve_runproc() という関数の最後の方にありました。他の箇所は無さそうだし関数のの名前からしてもどうやらここで間違いなさそうです。
static int execve_runproc(struct lwp *l, struct execve_data * restrict data, bool no_local_exec_lock, bool is_spawn) { (途中略) exec_abort: (途中略) /* Acquire the sched-state mutex (exit1() will release it). */ if (!is_spawn) { mutex_enter(p->p_lock); exit1(l, W_EXITCODE(error, SIGABRT)); } return error; }
ここは既にエラー処理になっているので、このexec_abortに飛ばされる条件を見てみると何箇所かあることが分かりました。 どれが実際に発生してる問題なのかわからなかったので、箇所がわかるようにdebug用printfを入れて確認してみました。なお、本来であれば-g付きでソースコードデバッグ可能なカーネルを作ってデバッグ実行というのが良かったのですが、深く考えずにprintfを突っ込んでいました。
ここから先はいろいろとデバッグ文入れつつ、カーネルデバッガでもブレークポイントで止めたりしつつ、とにかくどこでエラーになっているかを調べたところ、少なくともhelloバイナリを動かした際には src/sys/uvm/uvm_map.cの1782行めからあるuvm_map_findspace() がNULLリターン(mapされてるはずのページエントリが探しても見つからない?)になってたようです。これが何を意味するのかこの時はさっぱりわかりませんでした。
おかしいのはどうやらuvm_mapあたりということがわかりましたが、この部分はarm固有ではなく全てのNetBSDで共通の部分です(たぶん)。他のOSでは全然問題なく動いています。なぜこれがNetBSD/arm の、7.0だけ問題があるのでしょうか。そもそもNetBSD/armでも普通のバイナリは問題なく動作しています。これもしかしたらRPI(や、RPI2)固有の問題だったりするのではないでしょうか。
ということで、他の環境で試してみたくなりました。また、RPIだけでデバッグというのもなかなか辛い部分があります(リブートで固まられたり、カーネル入れ替えが頻繁になると、正直面倒くさくなってくる)。さらに行き詰まったのでちょっと気分を変えたいな、と思っていたところ、x86/amd64上のQemuでINTEGRATOR_CPが動くという話を思い出しました。
早速このWikiの手順に従って環境を作りテストしてみました。ただし使用するNetBSD/evbarmは7.0のOABI用のevbarm-arm, カーネルもOABI用のINTEGRATOR_CPを使います。
するとどうでしょう。なんと、NetBSD/arm 6.x上のGo 1.4で作ったhelloバイナリがちゃんと正常に動くではありませんか! さらにpkgsrc/lang/go14を、NetBSD/arm 6.xと同じ修正を加えるとちゃんとgo_bootstapが動いてビルドが正常に終了するではありませんか! これは一体、どういうことなのでしょう???
NetBSD/arm 7.0でも動く環境があることが分かった以上、何らかの構成/設定/環境に違いがあるはずです。カーネル以外は同じバイナリですし、同じバイナリが動かなければならないはずなのですから。
とりあえずいろいろ考えて、思いつくものを片っ端から試していきます。
その結果、一つの大きな違いがありました。RPIのカーネルコンフィグには以下の行があり、INTEGRATOR_CPには無かったのです。
makeoptions CPUFLAGS="-march=armv6z -mtune=arm1176jzf-s -mfpu=vfp"
これはカーネルをコンパイルするときのコンパイルオプションです。-march=armv6zや-mfpu=vfpを使用すると、これらを実装されアーキテクチャのCPUでしか使えない専用の命令が使われるようになります。 INTEGRATOR_CP自体は定義からすると同じarmv6のCPUのようですが、カーネルコンフィグにもmk.XXXにもstd.XXXにもありません。つまりarm一般の命令を使って作られることになります。そこでこのオプションを外してRPIのカーネルを作ってみます。一部のドライバやオプションがコンパイルできなくなったりしましたが、今必要なのはとにかくカーネルと、helloバイナリが実行できるくらいのカーネルの機能だけです。
その結果どうなったかというと、なんとRPIでもNetBSD/arm 7.0上でhelloバイナリが動くようになりました。犯人(?)は-march=armv6zのようです。
ということは、これは次の可能性も出てきました。
そんなこと考えたく無いのですが、わりと最近(?)、昨年末にgccのコンパイラバグ、それもgcc4.8になってからの最適化バグで苦労させられていたため疑ってしまいました。そこでコンパイラを古くしようとしたり、古いコンパイラで同じオプションを指定したり、ここも試行錯誤で思い当たることを手当たり次第確認していきます。
さて、結論ですがコンパイラのバグではありませんでした。
このmarch=armv6z指定の影響がどこにあるか調べていくのと同時に、過去のカーネルの変更内容も一緒に調べていました。これはcurrentのソースを日付別にcheeckoutし、どこまで問題なく動くのか、どこから問題が発生するようになったのか。 これを二分木探索していくのです。そしたら、例えば2014年の初頭(1月)のcurrentカーネルではこのオプションが付いていてもちゃんと動くのです。しかし、2014年の6月のカーネルでは動かないのです。というとことは、これらの間の変更で、このオプションの関係する内容が怪しい、ということになります。
二分木探索を続けていき、とうとうその部分がわかりました。それは以下の変更です。
これはPAGE_SIZEが2^13=8192になることを意味します。armv6以降は、カーネルのデフォルトPAGE_SIZEがそれまでの4Kから8Kに変更されていたのです。実際に起動したRPIのカーネルでgetconfやsysctl hw.pagesizeを参照すると、8192になっているとことが確認できます。一方でINTERGRATOR_CPは確かに4096でした。
rpiarm$ getconf PAGE_SIZE 8192 rpiarm$ sysctl hw.pagesize 8192
qemu-igcp$ getconf PAGE_SIZE 4096 qemu-igcp$ sysctl hw.pagesize 4096
念のために、RPIの標準カーネルのコンフィグオプションに options PAGE_SIZE=4096を付けて作成したカーネルで動かしてみると、なんとちゃんとgoの実行バイナリも、goで作ったhelloバイナリも動くではありませんか!
PAGE_SIZEが4096の場合は動いて、8192の場合は動かない。同一のバイナリでこんなことが起こるのはなぜでしょう。実は当初からGoが作るELFバイナリのヘッダ内容を疑っており、その内容を検証しなければ、と思っていました。ですが私はELF形式についての知識なんてまったく持ち合わせていません。ですので、どこから手を付ければよいか、さっぱりわからなかったのです。
今回PAGE_SIZEの4Kから8Kへ変更という具体的な内容があったので、それを念頭にGoの作ったバイナリのヘッダ情報を眺めて、また、NetBSD標準のバイナリである/sbin/initと比べてみました。/sbin/initはダイナミックリンクバイナリ、goの作るhelloバイナリはスタティックリンクバイナリと大きく違っていますが、そこはそれ、まずは見てみるです。
ヘッダ情報の参照にはreadelfコマンドを使います。helloバイナリは以下のようになっていました。ちょっとはしょって、ここでは関係ある部分(と、後でわかった)だけを示します。
$ readelf -e hello (略) Program Headers: Type Offset VirtAddr PhysAddr FileSiz MemSiz Flg Align PHDR 0x000034 0x00010034 0x00010034 0x000a0 0x000a0 R 0x1000 NOTE 0x000be8 0x00010be8 0x00010be8 0x00018 0x00018 R 0x4 LOAD 0x000000 0x00010000 0x00010000 0x98fcc 0x98fcc R E 0x1000 LOAD 0x099000 0x000a9000 0x000a9000 0x74aa4 0x74aa4 R 0x1000 LOAD 0x10e000 0x0011e000 0x0011e000 0x04960 0x14090 RW 0x1000
一方で/sbin/initは
$ readelf -e /sbin/init (略) Program Headers: Type Offset VirtAddr PhysAddr FileSiz MemSiz Flg Align PHDR 0x000034 0x00010034 0x00010034 0x000c0 0x000c0 R E 0x4 INTERP 0x0000f4 0x000100f4 0x000100f4 0x00013 0x00013 R 0x1 [Requesting program interpreter: /libexec/ld.elf_so] LOAD 0x000000 0x00010000 0x00010000 0x04440 0x04440 R E 0x10000 LOAD 0x004440 0x00024440 0x00024440 0x0026c 0x007d8 RW 0x10000 DYNAMIC 0x004458 0x00024458 0x00024458 0x000e8 0x000e8 RW 0x4 NOTE 0x000108 0x00010108 0x00010108 0x00044 0x00044 R 0x4
もちろん全然異なるプログラムなので比較は難しいのですが、注目すべきは、TypeがLOADの行のAlignです。 最初同じだと思っていましたが、良く見ると一桁違います。goで作ったhelloバイナリは0x1000、つまり4096。一方標準コンパイラ/リンカ出作られた/sbin/initは0x10000で、これは64Kです。これはあまりにも違いがあります。この値は何かと調べてみると、include/elf.hに定義されているElfPhdrのp_alignメンバの値で、どうやらプログラムオブジェクトをロードする際の配置を決める値になるようです。
NetBSDの標準開発環境(gccとGNUツールチェイン)により作成された実行バイナリ、例えば/bin/shや/sbin/init等のバイナリとhelloバイナリとの比較から、Elfヘッダのp_alignの値が大きく違うことがわかりました。goのリンカでこの値を決めているのは以下の部分です。
case Hlinux:/* arm elf */ case Hfreebsd: case Hnetbsd: debug['d'] = 0;// with dynamic linking elfinit(); HEADR = ELFRESERVE; if(INITTEXT == -1) INITTEXT = 0x10000 + HEADR; if(INITDAT == -1) INITDAT = 0; if(INITRND == -1) INITRND = 4096; break;
見ればわかるように、Linux/FreeBSD/NetBSDではすべてINITRNDに4096を指定しています。はて、この4096とは何でしょうか。NetBSD 7.0以降はarmv6以降のPAGE_SIZEが4Kから8Kに変わって、でもPAGE_SIZEが4Kのままの環境では動いて、8Kの環境では動かない、という事実と無関係とは思えません。ということで、とりあえず以下の用にNetBSD/arm標準バイナリと同じ、64Kを指定するように変更してGo1.4のビルドをしてみます。
--- src.orig/cmd/5l/obj.c 2015-09-23 13:37:36.000000000 +0900 +++ src/cmd/5l/obj.c 2015-11-19 07:27:45.000000000 +0900 @@ -82,7 +82,6 @@ break; case Hlinux: /* arm elf */ case Hfreebsd: - case Hnetbsd: debug['d'] = 0; // with dynamic linking elfinit(); HEADR = ELFRESERVE; @@ -93,6 +92,17 @@ if(INITRND == -1) INITRND = 4096; break; + case Hnetbsd: + debug['d'] = 0; // with dynamic linking + elfinit(); + HEADR = ELFRESERVE; + if(INITTEXT == -1) + INITTEXT = 0x10000 + HEADR; + if(INITDAT == -1) + INITDAT = 0; + if(INITRND == -1) + INITRND = 0x10000; + break; case Hnacl: elfinit(); HEADR = 0x10000;
ビンゴ! ちゃんとGo1.4のビルドは止まらず、go_bootstrapはgoのランタイムライブラリを次々とビルドしていきます。そして正常にビルドが終わり、できたGo環境を使ってhello.goがコンパイルでき、その結果できた実行バイナリもちゃんと動くではありませんか!
直接的な原因はここにあったのです。
他の環境を一切考慮しないのであれば,上記のINITRNDの箇所を4096から例えば8192にするだけで動作するようになります。わずか1行のみ。
if(INITRND == -1) - INITRND = 4096; + INITRND = 8192; break;
p_alignがプログラムバイナリをロードする場所の配置にそのまま使われるとすると、ページサイズがこれよりも大きい場合、あるアドレスを違うページと思ってマップしようとしたら既にマップされたページになってしまってもおかしくありません。あるいは要求するアドレスをページサイズで正規化すると同じアドレスになってしまうのかもしれません。 ただカーネル内のプログラムローダに私はほんとうに詳しくないし、その理屈まで調べてきれていません(というか、それよりも他に先に進みたい部分が多すぎる)。
少なくともGoの内蔵linkerではこの値がソースコード上決め打ちになっていて、その値がNetBSDの8K Pageのカーネルには小さすぎる、ということが直接の原因なのでしょう。 この値を今の私の手元では上記のようにNetBSDの標準バイナリと同じく64Kに設定していますが、これが正しいのかどうかはよくわかりません。もしかしたら仮想空間を無駄に使うことになるかもしれません。が、少なくとも大きくしたことで動くようになったのは事実です。
気づいてから数ヶ月を要してようやくNetBSD/evbarm(RPI) 7.0でgo(1.4)のセルフビルドやgo buildでのコンパイル、また、goで作成したバイナリが動作するようになりましたが、まだ問題が残っていましたので以下のような変更を行っています。また、EABI環境への対応もオリジナルでは不十分だったため修正を行ってEABI環境で動作するようにしました(その一方でOABI環境では動作しなくなります)。
これらの変更差分は以下のgistに置いてあります。時々内容を変える可能性があるのでちゃんとレポジトリにいれようと思ってますがまだ不慣れでできてません。
まだまだ検証が足らず、特にハードコーディングだったページサイズの動的取得部分については本家Go言語の処理とロジックそのものを変更してしまうものです。これがいいのかわるいのか、現時点では判断できてません。
なんとかgo1.4, go1.5がNetBSD/earm環境で動き出したのですが、まだたくさん問題があることがわかっています。
他にもきっとまだたくさんあるでしょう。
これまでの試行錯誤をつらつらと書いていたら非常に長くなってしまいました。実際には本当にもう、試行錯誤の連続で、もっともっとたくさんのことを試した気がします。それでもパッチにすれば実は数行、本質的には定数1個のみ。そんなこともあるんだなぁーという思いと、なんだったんだこれはという思いが入り混じってます。
ただ、当初は「これは手に負えん、どこから手を付ければいいか見当もつかない、無理だ」と思っていたものでも時間をかければそれなりに進むんだなぁ、というのが体感できたのは大きな成果であったと思います。
まだ調査中のものもたくさんありますが、go側の問題とか変更とかは上流(golangの開発コミュニティ)に報告しようと準備中です。 (もうちょっと時間下さい週末趣味プログラマーにはいろいろ厳しい&日本語でさえちゃんと説明難しい…)
- 注1: ただしこの変更は不十分で、他にも変更しなければならない箇所があることが後に判明しています。
- 注2: オフィシャルサイトのバイナリは以下のように分かれています。
この他にも使用するarmの命令を世代別に変えた物やビッグエンディアン用のバイナリもあります。なお、今回の対応はリトルエンディアンのみとなります。
- NetBSD/evbarm EABI用バイナリ: evbarm-earm
- NetBSD/evbarm OABI用(旧形式)バイナリ: evbarm-arm
- 注3: Go言語のビルドは最初にネイティブ環境のCコンパイラ等を使って独自のCコンパイラ/Goコンパイラ単体/アセンブラ/リンカ等のビルドに必要なツールを作り、次にそれらを使って最小限のGo言語処理系が作られます。これがgo_bootstrapです。これは初めて実行されるGo言語で書かれたプログラムでもあります。
- 注4: 以下のhello worldプログラム
package main import "fmt" func main() { fmt.Println("Hello, Go world!") }