APIとかABIとかシステムコールとか

はじめに

本記事はLinux環境における次のようなことをざっくり理解するための記事です。

この手の情報はググればwikipediaやらにいろいろ情報が載ってるんですが、初心者が理解するには細かいことまで書かれすぎていて、かつ、それぞれの関係がわかりにくいです。なので、用語を逐一解説するのではなく、ありがちな質問のQAという形をとりました。人によって用語の意味の揺らぎがあったりするんですが、私の解釈ということで。あからさまに間違っていたら指摘していただけると嬉しいです。

これを書こうと思ったきっかけは、以前こんなtweetを見かけたことです。それから「そういえば最近使われる言語はコードやデータがどういうバイナリに落ちるかが見えないものが多いので、この手のことはあんまり知られてないなかなー、知ってると楽しいんだけどなー」と思ったことです。

APIって何?

APIは、ソースコードレベルで関数(OOPLのメソッドも含む)やデータ(OOPLのクラスも含む)の仕様を規定したものです。それらの関数やデータがどのようなバイナリデータとして表現されるかは気にしません。

ここ最近ではプログラミング言語のAPIだけではなく、webサービスにリクエストを出すときにどういうURLにどういうリクエストを出すとどういうレスポンスが得られるかを規定したWeb APIのほうがなじみがあるかもしれません。

ソースをもとにAPIを説明します。以下のようなCソースを書くとします。

int plus(int x, int y) {
  return x + y;
}

int main(void) {
  int a = 1;
  int b = 2;
  ..
  plus(a, b):
  ..
}

このときplus()のAPIは「第一引数にint型の値、第二引数にint型の値を渡すと、それを足し合わせたint型の値を返す」です。繰り返しになりますが、CPUのアーキテクチャやCコンパイラの実装に依存するエンディアンやintのバイト数などは気にしないです。

ABIって何?

ABIはバイナリレベルで「関数(OOPLのメソッドも含む)やデータ(OOPLのクラスも含む)の仕様を規定したものです。エンディアンとかデータのサイズも気にします。もっというと前節に記載したソースでいうとplus()呼び出し時にアセンブリ言語レベルで各引数をどういうアドレスに配置してからどういう命令を呼び出すかという呼び出し規約も気にします。

呼び出し規約については(とくにx86_64アーキテクチャの)アセンブリ言語を若干でも知っている人は以下の情報を見るとなんとなくわかるかと思います。

ABIはたとえば次のようなときに姿を現します

  • バイナリ形式ライブラリの関数を別のアプリないしライブラリから呼び出すとき
  • プログラムから使うプロトコルのバイトオーダーが指定されているとき

「APIを叩く」ってどういう意味?

「APIに従って関数を呼ぶ」ことを意味するスラングです。あまり深く考えなくてもいいです。

POSIX APIとシステムコール呼び出しの違い

POSIX APIはPOSIXと呼ばれる規格において定義されている、主にUNIX系のOS間で移植性を高めるために作られたC言語関数のAPIセットです。linuxはPOSIXに準拠していませんが、POSIXに定義されているAPIはおおよそ備えています。Ubuntuにおいてはmanpages-posixをインストールしてman 3 readというコマンドを実行するとPOSIX APIとしてのread()の仕様が読めます。

システムコールはプログラムからハードウェアを操作したいなどの要求をカーネルに依頼する方法です。こちらはAPI(たとえばman 2 readとするとread()システムコールの定義を調べられる)と共にABIも決まっています。

通常はレジスタないしメモリ上の所定の位置に引数の値を書き込んでから特殊なアセンブリ命令を呼び出す、という方法で呼びます1。

POSIX APIのうちのカーネルを呼び出す必要がある関数についてはABIに基づいてシステムコールを呼び出す必要がありますが、関数名とシステムコールの名前が一対一対応している必要はありません。たとえばglibcにおいてはfork()関数を呼び出すと内部的にはclone()システムコールを呼び出します。POSIX APIとしてfork()の仕様に書いてあることが実現できたら、内部的にどのシステムコールを使うかという実装詳細はなんでもいいのです。

システムコールはC言語からしか直接呼び出せないの?

「直接」の定義にもよりますが、実は私の意見は「アセンブリ言語以外のC言語を含むあらゆる言語は直接システムコールを呼び出せない」です。

C言語においてもシステムコールを呼び出すときは内部的にはアセンブリ言語を使っています。例えばglibcにおいてはプログラマがglibcのread()を呼び出すと、この関数の中ではインラインアセンブラという機能を使ってアセンブリ言語によってシステムコールを呼び出しています。あるいはアセンブリ言語で書かれたシステムコールを呼び出す関数を含んだライブラリを呼び出すという手もあります。こういう方法を「直接」呼び出していると思うかどうかは人によるでしょうが、上述の通りわたしはそう思いません。

C言語以外の「直接システムコールを呼び出せる」と言われている言語においても最終的には上記と似たような方法を使っています。

あるプログラミング言語から他のプログラミング言語の関数はどうやって呼び出すの?

呼び出し元の言語と呼び出し先の言語の間を繋ぐなんらかの仕組みを使います。たとえばCとpythonをつなぐ方法は次の通り。

なんで低レイヤプログラミングだとCがよく使われるの?

ここは人によって様々でしょうが私が思う理由は次の通り。

  • プログラマがメモリをほぼ剥き出しの形で扱える(裏を返せば「扱わなくてはいけない」)2
  • 異なるコンパイラ、異なるバージョン間でのABI互換性が高い
  • 言語仕様が小さいため、処理系が比較的作りやすく、かつ、メモリ消費量も小さい
  • 低レベルのAPI規格はPOSIX APIのようにC言語用のものが多い
  • ある言語から別言語を呼び出すためにCを中継することが多い(前節を参照)
  • 十分に枯れており、周辺ツールがたくさんある

おわりに

気が向けば次のようなことを扱った続編を書くかもしれません。

  • 互換性とは何か
  • ライブラリのバージョニング
  • ELF symbol versioning

  1. アセンブリ言語レベルの命令を使わなくても所定のメモリに書き込めばシステムコールを呼び出せる、というような実装も考えられますが、それは置いときます。

  2. このことよりC言語は「高級アセンブリ言語」と呼ばれることがある