brand new note

ジャズ屋が技術の話をするところ

コンパイルをもう一度理解する

ちゃんとわからないといけなくなったので調べ直した。

Inside Linux Software オープンソースソフトウェアのからくりとしくみ

コンパイルを含め、開発を覚えようと思ったらこの本が詳しい。

Inside Linux Softwareを読んだ - brand new note

過去にエントリも書いたけど、これもかなり前のレビューなので参考になるかはちょっとわからない。途中までしか感想を書けていない。

記事の前半部は試行錯誤になってるので伝えたいことの本意ではない。読む意味があるかどうかは人それぞれだと思う。この記事の後半から、この本を使ってコンパイルの仕組みを簡単にまとめている。

目次

まず簡単に

コンパイルって何? |【エン転職】

コンパイルとは、プログラミング言語で書かれた文字列(ソースコード)を、コンピュータ上で実行可能な形式(オブジェクトコード)に変換することです。 コンパイルを行うソフトウェアをコンパイラといい、変換されるプログラミング言語コンパイラ型言語と呼びます。

「変換されるプログラミング言語コンパイラ型言語と呼びます。 」これは論理的には合ってるけど、じゃあ言語の数だけコンパイラがあるんだなと納得するとそれは間違い。コンパイラを必要としないインタプリタ言語もあるよという話になるんだけど、用語説明としてはここでは省かれるべきなのでそれは書かれていないらしい。

f:id:frazz:20210513130008p:plain

世のあらゆる技術者は当たり前のようにコンパイルのことやコンパイラのことについて言及してる。これらを全て読んだら本質的なコンパイルコンパイラの意味を文脈から理解することはできるけど、関連する1000も10000もある文章を全て読み漁るのはフルスタックエンジニアになることと同義だ。そんな時間はかけられない。

コンパイルの流れをざっくり掴んでいく - Qiita

WebAssemblyは言語ではない。でも今揚げ足を取っても仕方がない。自分も嘘は書く。善意なのも痛いほどわかる。寄り道してごめん。

コンパイルの流れ

f:id:frazz:20210513132700j:plain

本に戻ってこれを理解していく。

ソースファイルとヘッダファイル

raw_tcp_socket/raw_tcp_socket.c at master · rbaron/raw_tcp_socket · GitHub

とりあえず昔読もうとして挫折したソースコードを引っ張ってきた。内容はともかくとしてこの.cで書かれたもの(raw_tcp_socket.c)をソースファイルといい、内部の以下のような記述を「ヘッダファイル(.hで書かれたファイル)をインクルードする」という。ヘッダファイルが何なのかについてはコンパイルの流れとは別の話なので最後に書く。ソースファイルは紹介するものの言語がCだから今回.cなだけであって、他の言語でもいい。

#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h> //memset()
#include <unistd.h> //sleep()

f:id:frazz:20210513134120p:plain

ライブラリはどんなアプリケーションを開発するときにも使われるとても普遍的な概念であるから、機能のまとまりというふうに覚えておけばいいと思う。なんでまとまりなのかと言われればまとめておいた方が便利だからである。ライブラリは自身の環境にあらかじめインストールされていれば画像の部分にわざわざ同梱する必要はない。とりあえず開発環境は各々用意するものであって、コンパイラコンパイラ型言語を扱うのであれば、そのなかの道具の一つとして使うべきであることを改めて確認した。

なんでわざわざこんなことを書いているのかというと、理解のプロセスを経るのが下手くそすぎて4年前から脳機能が停止しているからである。自分は優秀だとでも思っていたのだろうか。

コンパイルとビルドの違い - brand new note

ビルドに関してはもう少し実際のアプリケーションの構成を目で見ないとイメージしにくいかもしれません。少し前まで、他人のgithubとかを見て「なんでひとつのアプリケーションは複数のファイルで構成されているんだろう、readmeとかよくあるし、どうやって連動して動作しているんだろう」と思っていました。自分はそこまで書けないので。

今見ると目も当てられない。うーんまあでも今もあんまり変わってないのか。

プリプロセス

絵の解読に戻る。

プリプロセス→(狭義の)コンパイル→最適化→アセンブル→リンク

この流れがgcc(GNU Compiler Collection)内部で起きている広義のコンパイル処理である。

プリプロセスは直訳すると事前処理となるけれど、文字通りソースファイルはコンパイラに直接突っ込んでもいけないことがここでわかると思う。正しくコンパイラに入力するためには"プリプロセス済みソースファイル"を生成する必要がある。

この過程でプリプロセッサはこんなことをしている。

  • コメントの除去
  • マクロの展開
  • #includeで指定されたファイルの取り込み
  • プリプロセッサに対する条件指定の処理

これはCの勉強も絡むけど、#ifとか#ifdefとかいうのはプリプロセッサが処理する条件コンパイルというやつで、広義のコンパイルの流れからすると比較的序盤に処理されることがわかる。詳細についてはここでは触れない。それと、#includeはCコンパイラが処理できるんだったら何をincludeしてきてもいいらしい。へえ。

(狭義の)コンパイル

プリプロセス済みのCソースコードコンパイラによってコンパイルされる。コンパイルをした後、出力先にはCPUがいるので、そのCPUに向けたアセンブリ言語のプログラムをコンパイラは吐き出す。最適化に関しては、明示的に指定すればやってくれる。これはアセンブリ言語の内容を操作して、よりプログラムを軽量あるいは高速にする作業である。

ここで生成される、アセンブリ言語が保存されたファイルには.sという拡張子がつく。

アセンブル

コンパイラが吐き出したアセンブリ言語のプログラムは、アセンブラによってアセンブルされる。つまりここで機械語のプログラムに変換される。.sだったファイルは機械語のコードのファイルとなり.oに変わる。これはオブジェクトのoで、オブジェクトファイルとも呼ばれる。

リンク

ソースコードから機械語のオブジェクトファイルを生成したが、まだこのままでは実行できない。ライブラリを使用する場合にはライブラリとの紐付けをしなければならない。また、大きなソースコードを一度にコンパイルするケースもあり、どこでミスが起きて失敗するのかソースコードから見分けるのが大変だという場合には、分割コンパイルという手法も存在しており、分割した場合には最後に結合をしなければならない。このような作業のことをファイルのリンクと呼ぶ。リンクを行うツールをリンカと呼ぶ。ldコマンドはその一例である。

ld - コマンド (プログラム) の説明 - Linux コマンド集 一覧表

リンカによってライブラリや分割ファイルのリンクが完了すると、晴れて実行可能形式のファイルが完成する。

ライブラリ

冒頭とここで出てきたライブラリについては、静的ライブラリと共有ライブラリがあり、それぞれ.a、.soという拡張子がつく。何が違うかというと読み込まれるタイミングが違う。静的ライブラリはリンク時にリンクされる。共有ライブラリはその後で、実行時に実行ファイルから読み込まれる。

歴史的に言うと先にできたのは静的ライブラリだけど、デメリットとして同じ機能を何度も繰り返してリンク時に読み出す形態は、完成する実行可能形式のファイルサイズをどんどん膨張させてしまうため、後から共有ライブラリという概念が生まれた。

共有ライブラリは静的ライブラリと違い、オブジェクトファイルではなく元から実行可能形式である。そのためメモリ上に展開することができる。実行可能形式のプログラムが実行されるとき、そのタイミングでメモリ上にすでに展開されている共有ライブラリを参照しに行くことで、静的ライブラリより無駄なくプログラムを動作させられる。静的ライブラリ使用時と比べると、アップデートがあった場合にも共有ライブラリと実行ファイルは独立してアップデートできるため、修正の手間も減る。

ライブラリとは何なのか? - Qiita

ここに詳しく書いてあるけれど、ライブラリは「これはライブラリです」と言い張ればどんなに小さなものでもライブラリである。生成する時に拡張子や静的/動的の指定、サーチパスの指定などをして適切な場所に置きつつ、メインとなる実行ファイルにそれを使用する旨が書かれていれば良い。

ヘッダファイル

Cの場合ヘッダファイルの中身は以下。関数の定義は含まないのがポイント。

  • マクロ定義
  • 関数のプロトタイプ宣言
  • 大域変数の宣言
#DEFINE NULL_VALUE "NULL"
#DEFINE PROGRAM_NAME "SAMPLE"

int get_value();
double calc(int n);

char *env_val;

値は適当だけど例えるとこんなのらしい。マクロは指示した文字列を別の文字列に置き換える仕組み。値の定義を1箇所に集中できるから後からコードを管理しやすい。

事前にこんな関数をmain()内で使いますよということも言っておかなければならない。これがプロトタイプ宣言。関数名、関数が受け取る値、関数が返す値、値の型を言っておく。書いとかないとコンパイラが親切心から型が間違ってるよっていうエラーを返してくれなくなる。

変数はわざわざ説明しないけど全ての関数から参照、変更できる変数を大域変数という。グローバル変数とも。後で「どこでこの値コレになったんだっけ」ってなるからあんまり使わない方がいい。

おわり

座学にならないようにしようと思ったけど座学になっちゃった。

過去の自分はクソ真面目だったので、「こういう方がいいじゃん」という思想の混じった"合理性のある知識”が一切体に入ってこなかった。コードなんてものは別世界の天才が書いているのだと思ってた。今まとめると納得できるけど、相変わらず基礎だ。