musl libcの挙動を追いかけて

自作OSのMethylenixの上でbusyboxのinitを動かそうとしていた時に遭遇した話です。

busyboxはユーザーランドのアプリケーションのため、hlt命令は実行できないはずなのに、busyboxがhltを実行しようとしてOSにトラップされる、という状態でした。

最初は命令がうまくメモリに展開されていないのかと思いましたが、objdumpでダンプしても該当アドレスにhlt命令が置かれているため、やはり意図した実行のようです。

busyboxのコードではなく、musl libcの方で何かしていると思ったため、hltでgrepしたところ、crash時にhltを実行していることが分かりました。
libcの中でexitを呼ぶ余力も無い際には、不正な命令を発行してOSにトラップしてもらうようです。ちなみにAArch64の同じファイルにはa_crash関数は定義されておらず、nullptr書き込みでクラッシュする汎用的な方法が取られています。なぜx86はhltなのか、CISCだから1バイト命令にしたかったのか、でもなんでhltなんだろうか、ud2やint3じゃ駄目だったんだろうか、など色々思うところはありますが、とりあえずmusl libcでcrashしていることがわかりました。

musl libcにひたすらputsを入れてデバッグしてましたが、それよりもQEMUで動かしているのでgdbが使用できることを思い出して、該当命令のアドレスにbreakpointをおいてみます。

#0  a_crash () at ./arch/x86_64/atomic_arch.h:108
#1  enframe (g=0x4e0090, idx=0, n=2028, ctr=1) at src/malloc/mallocng/meta.h:205
#2  alloc_group (sc=19, req=<optimized out>) at src/malloc/mallocng/malloc.c:290
#3  alloc_slot (sc=sc@entry=19, req=<optimized out>) at src/malloc/mallocng/malloc.c:314
#4  0x000000000040d8a3 in alloc_group (sc=15, req=<optimized out>) at src/malloc/mallocng/malloc.c:282
#5  alloc_slot (sc=sc@entry=15, req=<optimized out>) at src/malloc/mallocng/malloc.c:314
#6  0x000000000040d8a3 in alloc_group (sc=2, req=48) at src/malloc/mallocng/malloc.c:282
#7  alloc_slot (sc=sc@entry=2, req=req@entry=40) at src/malloc/mallocng/malloc.c:314
#8  0x000000000041a81e in __libc_malloc_impl (n=40) at src/malloc/mallocng/malloc.c:407

なるほど、mallocの中でenframe関数を呼んで落ちているようです。関数の実装を見るとmmapでもらってきたメモリの中でメモリブロックを作ろうとしている際に、メモリが非ゼロの際にクラッシュするようです。

最初はmusl libcのロジックが自作OS上で狂っているのかと思いましたが、Linux上でも自作OS上でも関数の通る道は同じで、最初の実行時に自作OS上だけクラッシュしています。

enframe関数以前でp[-3]に書き込んでいるコードは見渡す限りいません。とすると、このメモリ領域の初期値がゼロであることを期待しており、musl libcではmmap(2) syscallの返したメモリをゼロクリアしているところは見かけません。

mmap(2)のmanを読むと以下のような記述があります。

 MAP_ANONYMOUS
マッピングはどのファイルとも関連付けされない。 マッピングの内容は 0 で初期化される。

返却されるメモリアドレスはOSによってゼロクリアされることが規定されており、musl libcはこの規定に依存していたわけですね。OSでゼロクリアしたところ、クラッシュせずに処理が続行するようになりました。

めでたしめでたし