C# のHelloworldの基礎を説明できる人、弊社に一発で入社できますよ。ってかんじだ。
— きょん@うさみみモード (@kyon_mm) 2017年5月16日
@func_hs 別に言語はとわないつもりですけど、 C# だとうれしい。
— きょん@うさみみモード (@kyon_mm) 2017年5月16日
前一度調べたので記事をまとめておく。
OCamlプログラムの初期化は複雑だなぁ思った次第です。
コンパイルコマンド
ocamlopt -verbose
で実際に動くコマンドが見えるのでまずコレをチェックしておく。
結構色々なことが分かる
$ ocamlopt -verbose -o caml_hello hello.ml + as -o 'hello.o' '/tmp/camlasmfdcbdf.s' + as -o '/tmp/camlstartupfccd4a.o' '/tmp/camlstartup0a1072.s' + gcc -o 'caml_hello' '-L/home/nomado/.opam/4.04.0/lib/ocaml' '/tmp/camlstartupfccd4a.o' '\ /home/nomado/.opam/4.04.0/lib/ocaml/std_exit.o' 'hello.o' '/home/nomado/.opam/4.04.0/lib/ocam\ l/stdlib.a' '/home/nomado/.opam/4.04.0/lib/ocaml/libasmrun.a' -lm -ldl
コレを見るに、ocamlopt
では.s
ファイルを生成して、as
に投げ、
リンク自体はlinkerではなくgccに投げているということがわかる。
つまり初期化とか諸々はgccでcをコンパイルするのと一緒なわけです。
そのため以下の様なことは容易に想像できる。
プログラム起動 -> アーキ依存の初期化 -> libcの初期化
libcの初期化は__libc_start_main@plt
が呼ばれているので簡単にわかる。
gdb
で_start
とかでブレークポイントを設定すれば良い。
main以降
cの仕組みと一緒なのでmainが存在している。
main.c自体はocamlのbyterun/main.cにあるのでCで読めるよ!
コレ以降は全部Cで実装が存在する(一部除く)。
main |- caml_main |- caml_init_ieee_floats |- caml_init_custom_operations |- caml_ext_table_init |- caml_parse_ocamlrunparam |- caml_read_section_descriptors |- caml_init_gc |- caml_init_stack |- caml_init_backtrace |- caml_interprete |- ... |- caml_start_program (アーキごとにアセンブリ直書きされてる) |- caml_program
caml_program
からはユーザがよく知っている世界に入る。
ここからはまず標準ライブラリを含む各モジュールの初期化を行っていく。
caml_program
ocamlopt
は書く.ml
ファイルをコンパイルする際、caml"module名"__entry
という関数を生成する。
これを順番に呼び出していく。
今回は以下の関数がある。
caml_program |- camlCamlinternalFormatBasics__entry |- camlPervasives__entry |- camlHello__entry |- amlStd_exit__entry
もっとモジュールをリンクしていればこの__entry
関数は増えていく。
__entry
関数は概ね実行する処理が全部はいっている。ちなみにリンクの順番に並んでいるはず。
実行が伴うからリンクの順番に意味があるし下にかいた関数から上の関数は呼べないのや……。。
hello.ml
let id x = x let a = 12 + int_of_string Sys.argv.(1) let () = print_int a
このプログラムで行うことは
- helloのファイル全体を1つのモジュールだと思って、モジュールのフィールドにid, aを登録する
- aの評価を行う
let () = ...
部分の評価を行う
ことです。ocamlではファイルは1つのモジュールで、ファンクタに渡したり出来る。
そのためモジュールを表す構造を作る必要がある(camlhelloとアセンブラには書いてある)。
hello.sの一部のみ。
ちなみに配列をイチイチいじると境界チェックのコードがでるので-unsafe
をつけてコンパイルしている。
この状態で配列の境界アクセス違反を起こすとsegvする。
あとはだいたいアセンブラを読めば分かる。
camlHello__1: // モジュールを表す構造体 .quad camlHello__id_1199 // id関数、アルファ変換されてる .quad 3 // let a = ...の評価結果が入る場所 .text .align 16 .globl camlHello__entry camlHello__entry: .cfi_startproc subq $8, %rsp .cfi_adjust_cfa_offset 8 .L104: movq camlHello__1@GOTPCREL(%rip), %rax movq camlHello@GOTPCREL(%rip), %rbx movq %rax, (%rbx) movq camlSys@GOTPCREL(%rip), %rax movq (%rax), %rax movq 8(%rax), %rdi .loc 1 3 13 movq caml_int_of_string@GOTPCREL(%rip), %rax call caml_c_call@PLT .L101: movq caml_young_ptr@GOTPCREL(%rip), %r11 movq (%r11), %r15 addq $24, %rax movq camlHello@GOTPCREL(%rip), %rbx movq %rax, 8(%rbx) movq 8(%rbx), %rax .file 2 "pervasives.ml" .loc 2 450 39 call camlPervasives__string_of_int_1146@PLT .L102: movq %rax, %rbx movq camlPervasives@GOTPCREL(%rip), %rax movq 184(%rax), %rax .loc 2 450 18 call camlPervasives__output_string_1203@PLT .L103: movq $1, %rax addq $8, %rsp .cfi_adjust_cfa_offset -8 ret
hogehoge@GOTPCREL
はhogehogeのアドレスを表す。
as
がいい感じに変換してくれるはずのものです。
モジュールを表す構造を表すポインタから関数を取得してcallとかやってますね。
あとはCと一緒最終的にはcamlPervasives__output_string_1203
がcaml_ml_output
を呼んで、
そこからwrite ()
システムコールが出るはず。
caml_ml_output_bytes
はbyterun/io.c
で定義されるC関数であとはCのせかい。
こんなもので。