KDOC 179: 『ハロー“Hello, World” OSと標準ライブラリのシゴトとしくみ』
この文書のステータス
- 作成
- 2024-06-10 貴島
- レビュー
- 2024-06-12 貴島
概要
ハロー“Hello, World” OSと標準ライブラリのシゴトとしくみは、おなじみの“Hello, World”を表示するまでの流れを詳しく解説する本。
メモ
- writeとwrite() の違い。writeはシステムコール、OSカーネルのことを指している。それに対してwrite()はPOSIXで定義されているwriteシステムコールを呼び出すためのC言語用API、つまりアプリーケーション用のAPIのことを指している(p143)
- main関数ではいったい何をしているのか。なぜexit()を呼び出すとプログラムは終了するのだろうか。main()から戻った先には何があるのか、を調べる(p159)
- スタートアップは標準Cライブラリの役割であり、GNU/Linuxディストリビューションの場合はglibcによって提供される(p170)
- 標準Cライブラリでもっとも重要なのはシステムコール・ラッパーである(p170)
- Linuxカーネルが提供するのはシステムコールのABIであり、APIを提供するのはglibcの役割。POSIXの
exit()
と_exit()
を実現するための機能としてLinuxにはexit_group
やexit
システムコールがあり、glibcはexit_group
システムコールを呼び出すことで、POSIXの_exit()
を提供している。_exit()
は、システムコール・ラッパーとしてユーザに提供されるAPIである、という(p183) - UNIXライクなシステムでは、新しいプロセスは
fork()
によって生成され、exec()
系の関数により新たなプログラムとして書き換わることで実行されるのが基本形、だという(p191) execve
システムコールが発行されたときカーネルがやること(p191)- 実行ファイルを読み込み、仮想メモリ上にマッピングする
- argc/argv[]、BSSの初期化、環境変数の引き渡しなどをやる
- 実行ファイル上のエントリ・ポイントから実行を開始する
- sysにあるのがカーネルのソースコード、libはユーザに提供されるライブラリのソースコード、だという(p213)
- もともとBSDはAT&TのUNIXカーネルに対して追加の各種ツール類やカーネルへのパッチを配布していたものが、AT&T依存部を追加実装で置き換えることで独自発展してきた。このためBSDはOSを構成する一通りのものがすべて提供されている。いっぽうでLinuxカーネルとglibcは歴史的に異なるものなので、別々に配布されている(p213)
- Newlibは組み込みシステムをターゲットとした標準Cライブラリで、現在RedHatによって開発されている、という(p219)
- gccの実行ログを見ると、「ccl」「as」「collect2」というコマンドが順に発行されているのがわかる。gccというコマンドは実は厳密な意味でのコンパイラではなく、指定されたファイルの種別に応じてこれらのコマンドを適切に組み合わせて適切な引数で呼び出してくれるドライバであるといえる、という(p231)
- ccl: 狭義のコンパイラ
- as: アセンブラ
- collect2: リンカ
- OSとは何か、は人によって曖昧である。万人にしっくりくる定義は、「自身がメインテーマとしている分野に対して、その下層にあってその先は知らなくていいとしている部分」のことをOSと呼ぶ、ということ(p233)
- UNIXではパイプというプロセス間通信機能によって、あるプロセスの出力を別のプロセスの入力に与えるようなことが簡単にできる。ひとつのプログラムに様々な機能をもたせるのではなく、入出力はテキストという標準的なフォーマットを基本として、複数のアプリケーションをパイプで接続して順に処理する、という思想になっている。この特徴はほかの汎用OSには必ずしも当てはまらない(p240)
- 技術的な意味で、「GNU/Linux」と呼ぶべきだという。GNU/LinuxディストリビューションはGNUアプリケーション群に強く依存していて、それをLinuxとだけ呼ぶことに違和感があるという。GCCによるプログラミング環境やbashによるシェル環境のことをLinux環境と呼ぶのは誤解の原因になる(p245)
- 「LinuxはUNIX互換」といわれるが、POSIXのAPIはglibcによって提供され、ユーザインターフェースはbashなどのGNUアプリケーションによって提供される、という。LinuxはUNIX互換のシステムを作り上げるためのベースとしてUNIXに備わっている機能を提供するカーネル、くらいが適当か(p246)
readelf -a /lib/x86_64-linux-gnu/libc.so.6 | head
ELF Header: Magic: 7f 45 4c 46 02 01 01 03 00 00 00 00 00 00 00 00 Class: ELF64 Data: 2's complement, little endian Version: 1 (current) OS/ABI: UNIX - GNU ABI Version: 0 Type: DYN (Shared object file) Machine: Advanced Micro Devices X86-64 Version: 0x1
readelf -a /lib/x86_64-linux-gnu/libdialog.a | head
File: /lib/x86_64-linux-gnu/libdialog.a(trace.o) ELF Header: Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00 Class: ELF64 Data: 2's complement, little endian Version: 1 (current) OS/ABI: UNIX - System V ABI Version: 0 Type: REL (Relocatable file)
- ふだん意識せずに利用しているヘッダファイルやライブラリだが、自然にできることはなく必ず開発した人がいる。ファイルのライセンス表記を見ることで提供元を考えてみると歴史や経緯がわかったりすることは多い。そうした技術とは一見関係なさそうな知識は、解析の際に意外に役に立ったりするものである。そしてGNU/Linuxのシステムがどのように構築されているかを知るための良い手がかりになる、という(p263)
- なんとなくバイナリファイルを眺めるだけでも、32ビット・アーキテクチャであったり、リトルエンディアンであることがなんとなく推測できる。はじめから読めないものとして諦めず、とりあえず見てみて、肌で感じてみるということが重要だという(p265)
- ELFフォーマットに限らず、多くのフォーマットは解析用のヘッダファイルが
/usr/include
にインストールされている場合が多い、という(p277) - readelfの解析結果だけでは、情報がどのように格納されているかを実感しにくい。バイナリダンプだけをいきなり見ても、なかなか解析できるものではない。ここに構造体の定義を含め、3つを照らし合わせて見てみることで、実際にさまざまな情報が格納されていることを実感しながら理解できるだろう(p280)
- セクションとセグメント2つの管理単位がある理由。セクションはリンカのためにあり、セグメントはローダのためにある(p290)
- 動的リンクと共有リンク(p293)
- 「動的リンク」は、実行時にライブラリをリンクするという意味。ハードディスクの容量節約に貢献する。動的リンクだといって動作に共有ライブラリが用いられているとは限らない。単に実行時に動的にライブラリをリンクしているだけで、メモリ上では別々の資源となって動作していることもありうる
- 「共有ライブラリ」は、仮想メモリ機構を使ってライブラリをメモリ上で共有すること。仮想メモリで動作していることが前提の、汎用OS向けの機能。メモリ使用量の節約に貢献する。共有ライブラリは実装上、動的リンクを必要とする場合が一般的である
- 共有ライブラリの位置独立コード。共有ライブラリはどのアドレスにロードされていても動作する必要がある。他の共有ライブラリと衝突しないようなアドレスに自動的にロードされるから。実体はひとつだが、プロセスによってマッピングされるアドレスが異なる。ライブラリ中の関数呼び出し、変数では、絶対アドレスで呼び出し先を指定できない。この解決方法として、関数呼び出しをする際には呼び出し先の関数のポインタを別のところに保持しておき、ポインタ経由で関数呼び出しをするようにする。ポインタはデータ領域に置かれるため、プロセス間で共有はされずに実行コードはそのまま共有できる。このような関数へのポインタの配列領域をGOT(Global Offset Table)とよぶまた、GOTを参照して関数呼び出しを行うような処理の集まりをPLTとよぶ。また、このようなコードをPIC(Position Independent Code)とよぶ(p297)
- 実行ファイルには機器語コードや文字列データ、デバッグ情報やさまざまなコメントなどが格納されている。ヘッダもさまざまなものを持っていて、技術的知見の宝庫になっている、という。機械語コードの解析の際には、実行ファイルに含まれた情報が欠かせず、objdumpによる逆アセンブル結果とreadelfによる解析結果を並べてみることも少なくない(p300)
- 同じアーキテクチャ向けの機械語コードで、同じx86アーキテクチャ上の環境なのに、エミュレーション無しでは動作しないのはなぜか。それはシステムコールのABIの違いがあるから(p325)
- FreeBSDはシステムコールの引数はスタック経由で渡し、エラーはフラグレジスタ上のフラグで返す。Linuxはシステムコールの引数はレジスタ経由で渡し、エラーは負の値の戻り値として返す(p339)
- 世の中にはLinuxやx86アーキテクチャ以外にも、さまざまなプラットフォームやアーキテクチャがある。そしてシステムコールの仕様もPOSIXがすべてではない。そのような目線で見ないと、理解できないことも多い。俯瞰して見られるような視点は大切である、という。何かについて調べるときにはその対象としているものだけを見るのではなく、同じような別の実装を見て比較してみるようにするといい、という(p376)
- アセンブラの解読を避けて図などで無理に理解しようとするよりも、アセンブラを見てみることで素直に理解できることがある(p389)
- 共有ライブラリは静的にローディングされる実行コードと衝突しないように、仮想メモリ機構により異なるアドレスにマッピングされる。このため「アドレスが全然違う値になっている」という現象が見えたら、それは共有ライブラリ上にあると考えるべきである、という(p392)
- vDSO(仮想共有動的オブジェクト)は、
gettimeofday()
のような情報取得サービスの負荷を下げるために利用される。例えばカーネル側が特定の領域に定期的に時刻を書き込み、アプリケーション側からはそれを見るだけにすれば、システムコール例外(割込)を発行せずにサービスを提供できる。vDSOから低レイヤーを幅広く知ることの意義がわかる。モジュール単位での最適化が行われるのは当然であるが、より重要なこととして、モジュール間をまたいでの最適化をいかに行うかということが先にある。そのためには、インターフェースさえ知っていればあとはお互いのことは知らなくてもよい、というわけにはいかない(p428) - 高度で効果的な最適化を行うためには、ひとつの分野に閉じているだけでは不十分である。複数の分野を知り、全体を俯瞰しての設計が必要になる。目安として、自分が専門としている層の、ひとつ上とひとつ下の層を知るように常に意識するとよい、という(p429)
- 下の層に降りていくことに敷居を感じるとき有効なのは、とにかく手を動かして実物を見てみることだという。さらにもうひとつ重要なのはそれを開発しているプロジェクトやコミュニティについて知ること。ソースコードの先に、それを作っている「人」が見えるようになると、より興味を持って読むことができるようになる(p429)
関連
- KDOC 129: 『ポインタ理解のためのアセンブリ入門』。アセンブラがよく出てくるつながり
- Executable and Linkable Format - Wikipedia。ELFフォーマットの概要
- 第7章 オブジェクトファイル形式 (リンカーとライブラリ)。ELFフォーマットの例