type t (* void *)

ソフトウエアのこととか

OCaml: hello world!が実行されるまで

前一度調べたので記事をまとめておく。
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_1203caml_ml_outputを呼んで、
そこからwrite ()システムコールが出るはず。 caml_ml_output_bytesbyterun/io.cで定義されるC関数であとはCのせかい。

こんなもので。