Writing a simple init process in OCaml

I found the Building a Tiny Linux post by @blinry this week, and was reminded of the days when I tried to work through the Linux From Scratch exercises and being frustrated about not going very far with them. I decided to reproduce the steps listed out in the blog post, except to try writing the simple init process in OCaml (instead of Rust).

The simple init process in the post just replies with a simple string response, whatever the input. And is pretty simple to write in any language (including OCaml).

open Printf

let rec main_loop () =
  printf "$ " ;
  flush stdout ;
  try
    let _ = read_line () in
    printf "OCaml! I don't know how to do that.\n" ;
    main_loop ()
  with End_of_file -> ()

let () = main_loop ()

But, it needs to be a “statically linked” binary, as in, shouldn’t depend on any system libraries to be able to be run by the kernel as the init program.

At work, I’ve been plagued by some linking related problems recently and I decided to play around with this to learn more…

How to build a static binary with OCaml?

The code, as I said, is pretty simple to write. How does one compile a static binary from this OCaml source?

I run this command to compile the binary using the OCaml native code compiler (as opposed to the bytecode one – ocamlc)

https://ocaml.org/docs/using-the-ocaml-compiler-toolchain

ocamlopt -ccopt -static -o bin/init init.ml

The command essentially passes a -static argument to the C compiler, apart from specifying a location for generating the executable.

> bin/init
$ hi
OCaml! I don't know how to do that.
$ ls
OCaml! I don't know how to do that.
$ exit
OCaml! I don't know how to do that.
$ ^D

The compilation works, and the executable does the expected thing, but I see this warning during the compilation:

/usr/bin/ld: /home/punchagan/.opam/default/lib/ocaml/libasmrun.a(unix.n.o): in function `caml_dlopen':

/home/punchagan/.opam/default/.opam-switch/build/ocaml-base-compiler.5.1.1/runtime/unix.c:282:(.text.caml_dlopen+0x13): warning: Using 'dlopen' in statically linked applications requires at runtime the shared libraries from the glibc version used for linking

What does the warning mean?

It looks like in the final binary generated, the OCaml runtime code brings in a dependency on the dlopen function from glibc, possibly to be able to load other dynamic libraries, if required!

But, this isn’t what we expect from a static binary! We don’t want to depend on any system libraries being available.

> ldd bin/init
        not a dynamic executable

So, even though ldd says it’s not a dynamic executable, it kinda is?!

For instance, let’s change the program to a simplistic dig clone – it DNS looks-up and prints the IP address of any domain name that we enter

ocamlopt -I +unix unix.cmxa -cc musl-gcc -ccopt -static -o bin/init init.ml

The compilation spews out a whole lot of warnings about functions that implicitly depend on some system libraries at runtime:

/usr/bin/ld: /home/punchagan/.opam/semgrep/lib/ocaml/libunixnat.a(getgr.n.o): in function `caml_unix_getgrgid':
/home/punchagan/.opam/semgrep/.opam-switch/build/ocaml-base-compiler.5.2.0/otherlibs/unix/getgr.c:64:(.text.caml_unix_getgrgid+0x22): warning: Using 'getgrgid' in statically linked applications requires at runtime the shared libraries from the glibc version used for linking
/usr/bin/ld: /home/punchagan/.opam/semgrep/lib/ocaml/libunixnat.a(getgr.n.o): in function `caml_unix_getgrnam':
/home/punchagan/.opam/semgrep/.opam-switch/build/ocaml-base-compiler.5.2.0/otherlibs/unix/getgr.c:49:(.text.caml_unix_getgrnam+0x28): warning: Using 'getgrnam' in statically linked applications requires at runtime the shared libraries from the glibc version used for linking
/usr/bin/ld: /home/punchagan/.opam/semgrep/lib/ocaml/libunixnat.a(initgroups.n.o): in function `caml_unix_initgroups':
/home/punchagan/.opam/semgrep/.opam-switch/build/ocaml-base-compiler.5.2.0/otherlibs/unix/initgroups.c:35:(.text.caml_unix_initgroups+0x23): warning: Using 'initgroups' in statically linked applications requires at runtime the shared libraries from the glibc version used for linking
/usr/bin/ld: /home/punchagan/.opam/semgrep/lib/ocaml/libunixnat.a(getaddrinfo.n.o): in function `caml_unix_getaddrinfo':
/home/punchagan/.opam/semgrep/.opam-switch/build/ocaml-base-compiler.5.2.0/otherlibs/unix/getaddrinfo.c:113:(.text.caml_unix_getaddrinfo+0x1e9): warning: Using 'getaddrinfo' in statically linked applications requires at runtime the shared libraries from the glibc version used for linking

<... truncated ...>

When we run the compiled binary on the system we compiled it on, it works perfectly fine!

But, if we try to run it without minimal linux kernel, for instance, which doesn’t have any of the shared libraries, it would crash majestically!

SCREENSHOT!

How to really build a static binary in OCaml?

The common advice here see is to use Alpine since it uses musl library, which doesn’t have the limitation of glibc! https://www.reddit.com/r/AlpineLinux/comments/mfjj5s/explanation_alpine_linux_is_built_around_musl/

We start an Alpine container with OCaml and stuff already setup on it.

docker run --volume .:/home/opam/app -it ocaml/opam:alpine-3.20-ocaml-5.2 bash

And in the Alpine bash we just run the same compilation commands.

cd ~/app/
ocamlopt -I +unix unix.cmxa -cc musl-gcc -ccopt -static -o bin/init init.ml

And copy the binary down using docker cp

> docker cp 504e98c2d5c5:/home/opam/app/bin/init bin/
Successfully copied 2.7MB to /home/punchagan/code/tiny-linux/linux/initramfs/bin/

If you don’t want to use Alpine, there’s also a way to compile OCaml using musl on your OS.

opam switch create my-static-switch 5.2.1+options ocaml-option-musl

While writing this blog post, I stumbled on this excellent blog post from the folks at OCamlPro that goes more into detail about static linking!

Feel free to send me an email with your comments.

Want a weekly digest of these blog posts?