本記事はNetBSD Advent Calendar 2025 11日目の記事として書かれた。
前置き
去年、趣味で svc-hook というバイナリ書き換えを利用した高速な(ptraceの1000倍以上)ARM64向けシステムコールフックを開発した。当初はLinuxのみのサポートであったが、他のUnix系OSにも対応したら面白いだろうということで、FreeBSDとNetBSDに対応した。今回はNetBSDに限って、どのような対応が必要だったのかを解説する。svc-hook公開時のブログ記事では、初期の設計について解説している。
なお、svc-hookはACM/IFIP Middleware 2025の論文として採択された。
ARM64 Linuxの場合
NetBSDに入る前に、ARM64 LinuxにおけるSystem Call ABIとそれを踏まえたsvc-hookの作りを説明する。
Linuxでは、x8レジスタにシステムコール番号を、x0からx7に引数を入れた上で svc 命令を実行することでシステムコールが発行される。x86のsyscall/sysenterと異なり、 svc命令自身も16-bitの即値 #immを取るが、Linuxではこの即値は必ず0が与えられており、使われていない。
この「x8にシステムコール番号を入れる」という規約はFreeBSDでも同様である。
svc-hookでは、svc を b命令に置き換えてトランポリンを経由したあとにユーザが設定したhook functionを呼び出すようにしている。このとき、x8へのアクセスは発生しないので、hook functionの中からx8を読み出すことで容易にシステムコール番号を知ることができる。
ARM64 NetBSDの場合
一方、NetBSDでは事情が異なる。NetBSDでは、Linux/FreeBSDでは使用されていない svc 命令の即値 #imm にシステムコール番号を設定して呼び出す。通常、EL0からシステムコールが発行されると、EL1/EL2のexception vectorに飛んだときに ESR_ELx.ISSに即値の値が入るので、x8に入っている場合とあまり変わらない手順でシステムコール番号を知ることができる。
一方、svc-hookのようにユーザ空間に飛ばす場合は事情が異なる。b命令で svcを即値もろとも書き換えるので、そのままでは即値の情報が失われてしまう。そこで、
- 即値は命令をデコードすれば簡単にわかる
- svc-hookでは各
svcごとにトランポリンを持っている
という二つの特徴から、トランポリン作成時に各 svc の即値をトランポリンに埋め込んであげることでhook functionからシステムコール番号がわかるようにした。
また、Linux/FreeBSDではsvc-hookから svc命令を実行する場合には、ただ一つの svc #0を用意してそこにジャンプするだけでよかったが、NetBSDの場合は即値は0~216 - 1 の値を取りうる。このため、svc-hookでは、初期化時に svc #n; ret; (合計8bytes) のシステムコールテーブルを用意して対応している。対応する即値を持ったシステムコールを発行したい場合には、nr_syscall * 8 のオフセットに関数呼び出しをするだけでよい。
雑多に思うこと
そもそもなぜNetBSDはLinux/FreeBSDと異なるシステムコール番号の渡し方を採用したのか、というのは気になるところである。おそらくARM社がARM64のソフトウェア対応を始めたときには真っ先にLinux (Android)などのモバイル環境をターゲットにしたはずであり、であれば先行しているLinuxなどに挙動を合わせることも可能であったはずである。しかし、そうはなっていない。私はArmv8よりも前のARMに詳しくないので想像だが、かつての組み込みではレジスタ数が少なく、可能な限りシステムコール発行時に使用するレジスタ数を減らすというのがセオリーだった名残ではないかと推測している。この辺り、NetBSDにポートされた2014年当時を知る方がいらっしゃったら教えていただきたい。
終わりに
この記事ではsvc-hookのNetBSD固有の対応が必要だった部分について解説した。「ARM64でのシステムコール番号の渡し方」という非常にニッチな話題ながら、BSD系ですら違いがあるのは非常に面白く、年末の酒の肴にピッタリの小ネタであるといえる。

