目次
背景
- ハードウェアに近い所のプログラミングでもCは必須
- しかし、大昔にやったきりでほぼ覚えていない
- そこで、改めて基本的なところからCの再学習をしたのでメモ
基本
Cの流れ
C では、プログラムは 1回で全部完成するのではなく、大きく、次の流れで作られる。
- .c(と.h)コードを用意
- 前処理
- コンパイル
- アセンブル
- リンク
- 実行
コード
.h = インターフェイス
宣言を書く場所
例:
| |
上記のように、
- 関数名
- 引数の型
- 戻り値の型
を他のファイルに知らせ。
.c = 実装
実装を書く場所。
例:
| |
上記のように、実際の処理の中身を書く。
なぜ .h と .c を分けるのか
小さいプログラムなら、1ファイルに全部書いても動く。
でも複数ファイルになると、ある .c から別の .c の関数を使いたくなる。
たとえば
main.cadd.csub.c
があるとき、main.c は add() や sub() の存在を知らないと呼べない。
そのために .h を使って、複数の .c ファイルで共有する宣言をまとめるということ。
つまり .h は、複数ファイルのための共通の説明書ということ。
前処理
前処理とは
前処理は、C のソースコードをコンパイルする前に加工する処理。
- 各
.cをそれぞれ.iにする
| |
#include の意味
| |
は、
その場所に calc.h の中身を貼り付ける
と思えばOK。
なので main.c に #include "calc.h" があると、コンパイラは main.c だけでなく、そこに貼られた calc.h の内容も見る。
< > と " "
#include <stdio.h>標準ライブラリなど、システム側のヘッダを探す#include "calc.h"自分のプロジェクト内のヘッダを探す
#include は前処理
#include はコンパイルそのものではなく、前処理。
前処理の例
#include#define#ifndef
コンパイル
コンパイルは、Cのソースコードを アセンブリコードに変換する処理。
- 各
.i / .cをそれぞれ.sにする
| |
このとき作られる main.s は、まだ人間が読めるテキスト形式のアセンブリ。
| |
みたいな低水準の命令になる。
つまりコンパイルは、C言語 → アセンブリとなる。
アセンブル
アセンブルは、アセンブリコードを 機械語を含むオブジェクトファイルに変換する処理。
- 各
.sをそれぞれ.oにする
| |
この main.o はバイナリ形式なので、普通にテキストエディタで読めるものではない。
つまりアセンブルは、アセンブリ → オブジェクトファイルという事。
リンク
オブジェクトファイルから、実行ファイルを作る。
.o同士をつないで実行ファイルにする
| |
疑問
コンパイルとリンクの違い
たとえば
main.cadd.csub.c
があるとき、
| |
のように、**各 .c は別々にコンパイル(とアセンブル)**される。
そのあとで
| |
として、最後に1つの実行ファイルへまとめる。
つまり、
.c同士が自動で1つのソースになるわけではない- まず別々に
.oを作る - 最後にリンクして1つにする
なぜ main.c のコンパイル時に add.c の中身を見ないのか
gcc -c main.c の入力は基本的に main.c と、その中で #include されたものだけ。
だからコンパイラは、main.c をコンパイルするときに add.c を勝手には見ない。
その代わり、add() の宣言が .h にあれば、
addという関数がある- 引数は
int, int - 戻り値は
int
と分かるので、main.c はコンパイルできる。
コンパイル時に実体はなくてもいいのか
YES、普通の関数ならコンパイル時には宣言があれば進められる。
つまり
- コンパイル時: 宣言が必要
- リンク時: 実装が必要
たとえば main.c が add() を呼んでいても、コンパイル時には
| |
が分かっていればよいということ。
でも最終的にリンクするときに add() の本体がないと、
undefined reference
のようなエラーになる。
つまり、
コンパイラは「呼び方」を確認し、リンカは「本体の所在」を確認する
実装ファイルが自分のヘッダを include する理由
たとえば
| |
のように、sub.c でもヘッダを include することがある。
これは sub.c が sub() を使うためではなく、
ヘッダに書いた宣言と、自分の実装が一致しているか確認するため
つまり、
main.cが include する → 使うためsub.cが include する → 宣言と実装の答え合わせのため
include guard とは何か
ヘッダの先頭によくある
| |
これは include guard 。
これは、
同じヘッダが複数回読み込まれても、1回だけ有効にする
ための仕組み。
これは文法上の絶対ルールではなく、昔からの定番の書き方。
Tool
Driver
Driver は、preprocessor / compiler / assembler / linker などを適切な順番で呼び出すツール。
代表例は gcc や g++ や clang。
たとえば、
| |
を実行すると、gcc は内部で必要なツールを順番に呼び出して、最終的に実行ファイルを作る。
つまり gcc / clang は、日常的には compiler と呼ばれることが多いが、厳密には compiler driver としても働く。
Driver は、以下を行う。
- 入力ファイルの種類を判定する
- preprocessor / compiler / assembler / linker を呼び出す
- ライブラリやヘッダの探索パスを設定する
- linker に必要な startup code や libc などを渡す
つまり、Driverはビルド全体を制御する司令塔。
Preprocessor
Preprocessor は、Cのソースコードに対して前処理を行うツール。
主に以下を処理する。
#include#define#ifdef#ifndef#if- コメント除去
たとえば、
| |
のようなコードでは、#include によってヘッダファイルの内容が展開され、#define によってマクロが置き換えられる。
preprocess の結果だけを見たい場合は、
| |
とする。
| |
.i は前処理済みの C ソースファイル。
| |
ソースコード → 前処理済みソースコード
を担当する。
Compiler
Compiler は、高水準言語のソースコードを、より低水準な表現へ変換するツール。
Cの場合、狭い意味での compiler は、前処理済みソースコードをアセンブリコードに変換する。
| |
たとえば、
| |
とすると、C のソースコードからアセンブリファイル main.s が作られる。
.s はアセンブリファイルで、人間が読めるテキスト形式の低水準コード。
Compiler は、主に以下を行う。
- 構文解析
- 型チェック
- 最適化
- アセンブリコード生成
つまり Compiler は、狭い意味では、前処理済みソースコード → アセンブリを担当する。
ただし日常的には、gcc / clang のような driver も含めて compiler と呼ぶことが多い。
Assembler
Assembler は、アセンブリファイルをオブジェクトファイルに変換するツール。
代表例はasコマンド。
| |
たとえば、
| |
とすると、アセンブリファイル main.s からオブジェクトファイル main.o が作られる。
.o は object file で、機械語を含む中間生成物。
ただし .o はまだ単体では実行できないことが多い。
外部関数やライブラリへの参照が未解決のまま残っていることがあるため。
Assembler は、アセンブリ → オブジェクトファイルを担当する。
Linker
Linker は、複数のオブジェクトファイルやライブラリを結合して、実行ファイルや共有ライブラリを作るツール。
代表例は ldコマンド。
| |
たとえば、
| |
とすると、main.o と foo.o がリンクされて、実行ファイル app が作られる。
Linker は、主に以下を行う。
- 複数の
.oを結合する - 未解決のシンボルを解決する
- ライブラリを結びつける
- 実行ファイルのメモリ配置を決める
- 必要な startup code を含める
たとえば printf を使っている場合、コンパイル時点では printf の実体はまだ自分の .o には入っていない。
linker が libc などを探して、printf の参照を解決する。
Linker は、オブジェクトファイル + ライブラリ → 実行ファイル / 共有ライブラリを担当する。
全体の流れ
全体の流れはこう。
| |
より具体的には、C の場合はこう。
| |
役割の違い
それぞれの担当は以下。
| Tool | 入力 | 出力 | 役割 |
|---|---|---|---|
| Driver | .c, .o など | 最終成果物 | 全体を制御する |
| Preprocessor | .c | .i | #include や #define を処理する |
| Compiler | .i | .s | ソースコードをアセンブリに変換する |
| Assembler | .s, .S | .o | アセンブリをオブジェクトファイルに変換する |
| Linker | .o, .a, .so | 実行ファイル, .so | オブジェクトやライブラリを結合する |
Toolのまとめ
まとめると、概念的には、次の処理をまとめて行っている。
| |
役割は以下:
| |
ファイル
h, c, i, s, .o, .a, .so, a.out
.h
C のヘッダファイル。
.c
C ソースファイル。
.i
.i は前処理済みの C ソースファイル。
.s
アセンブリファイル。
.c をコンパイルして得られる。
.S
プリプロセッサ付きアセンブリファイル。
#include や #ifdef などを使える。
前処理されたあと as に渡されて .o になる。
.o
オブジェクトファイル
各 .c をコンパイルした結果
もしくは、sをasコマンド に渡してアセンブルすると、 .o になる。
.a
静的ライブラリ
複数の .o をまとめたもの
.so
共有ライブラリ(動的ライブラリ)
これも元は .o から作るが、実行時に読み込まれる
a.out
実行ファイルのデフォルト名
gcc main.c のように -o を省略したときにできることがある
疑問
.a と .so の違い
どちらも元は .o から作れる。
.a= 静的ライブラリ.so= 共有ライブラリ
違いは「元の材料」ではなく、できあがったものの使われ方。
.aはリンク時に実行ファイルへ取り込まれる.soは実行時に外から読み込まれる
ar rcs の意味
| |
は、.o をまとめて .a を作るarchiveコマンド。
r= 入れる / 置き換えるc= 新規作成s= シンボル索引を作る
つまり
静的ライブラリを作って、リンクしやすい形に整える
ということ。
GCCオプション
-E
-E は、前処理だけを行うオプション。
| |
この場合、main.c に対して #include や #define などの前処理だけを行い、前処理済みソース main.i を出力する。
| |
main.i はまだ C のソースコードに近いテキストファイル。
-S
-S は、アセンブリファイルまで生成するオプション。
| |
この場合、main.c を処理して、アセンブリファイル main.s を出力する。
| |
main.s は、人間が読めるテキスト形式のアセンブリコード。
-c
-c は、オブジェクトファイルまで生成するオプション。
| |
この場合、main.c から main.o を作る。
ただし、リンクは行わない。
| |
複数ファイルの C プログラムでは、まず各 .c を .o にして、あとでリンクすることが多い。
| |
-o
-o は、出力ファイル名を指定するオプション。
| |
この場合、実行ファイル名は app になる。
-o を省略すると、環境によっては a.out という名前の実行ファイルが作られる。
| |
| |
-I
-I は、ヘッダファイルの探索パスを追加するオプション。
たとえば、ヘッダファイルが include/ にある場合:
| |
次のように指定する。
| |
これにより、#include "calc.h" などを書いたときに、include/ の中も探してくれる。
| |
つまり、
| |
ということ。
-L(大文字L)
-L は、ライブラリの探索パスを追加するオプション。
たとえば、現在のディレクトリに libmylib.so や libmylib.a がある場合:
| |
ここで、
| |
は、ライブラリを探す場所に現在のディレクトリ . を追加する、という意味。
つまり、
| |
ということ。
注意点として、-L は基本的に リンク時 の探索パスを指定するもの。
実行時に .so を探す場所とは別。
-l(小文字L)
-l は、リンクするライブラリを指定するオプション。
| |
この場合、-lmylib は次のようなファイルを探す。
| |
つまり、
| |
という意味。
lib の部分と、.so / .a の拡張子は書かない。
たとえば libm.so をリンクしたい場合は、次のように書く。
| |
これは数学ライブラリ libm をリンクする例。
-fPIC
-fPIC は、Position-Independent Code、つまり位置独立コードを生成するオプション。
| |
共有ライブラリ .so を作るときによく使う。
PIC は、簡単に言うと、
| |
のこと。
共有ライブラリは実行時にメモリ上のどこへ読み込まれるか分からないため、PIC にしておくのが基本。
PIC とは何か
Position-Independent Code位置独立コードとは、どのメモリアドレスに置かれても動きやすいコード。
共有ライブラリ .so を作るときによく使う。
| |
PICで大事なこと
PICで大事なのは以下:
-fPICは.oの作り方.soはその.oから作る共有ライブラリ
しかも、PIC な .o から .a を作ることもできる。
つまり -fPIC は .a を禁止するものではなく、単に中身の .o の性質。
-shared
-shared は、共有ライブラリを作るオプション。
| |
たとえば、共有ライブラリを作る基本形はこう。
| |
または、1行で書くこともできる。
| |
この結果、共有ライブラリ libmylib.so が作られる。
| |
GCCオプションのまとめ
| オプション | 役割 |
|---|---|
-E | 前処理だけを行う |
-S | アセンブリファイル .s まで生成する |
-c | オブジェクトファイル .o まで生成する。リンクはしない |
-o | 出力ファイル名を指定する |
-I | ヘッダファイルの探索パスを追加する |
-L | ライブラリの探索パスを追加する |
-l | リンクするライブラリを指定する |
-fPIC | 位置独立コードを生成する |
-shared | 共有ライブラリ .so を作る |
流れで見ると、こう。
| |
共有ライブラリを作る場合は、こう。
| |
フォルダ構成
include / src / third_party
よくあるフォルダ構成は以下:
include/= 公開ヘッダsrc/= 実装(非公開)third_party/= 外部ライブラリ
SDKのパターン
include/= 利用者に見せる APIsrc/= SDK 作者の実装ソース- 配布物 = include/ + ビルド済みライブラリ
- 利用者 = ヘッダを include して、そのライブラリにリンク
Makefile / CMake
Makefileの手順
昔の Cプロジェクトのビルド手順:
| |
Configure
configure は、ビルド前に環境を調べて、Makefile を作るためのスクリプト。
主に以下をする。
- コンパイラはあるか
- 必要なライブラリはあるか
- 必要なヘッダはあるか
- OSやCPUアーキテクチャは何か
- インストール先はどこか
そして、環境に合った Makefile を生成する。
Autoconf
configureも作る、autoconfを使った代表的な流れ:
| |
.ac(autoconf),.am(automake),.in(input)は、autoconf系のコマンドの名前。
つまり、Makefileを作るconfigureを作るのがAutoconfのconfigure.acということ。
MakefileとCmakeの違い
今までのmain.c、add.c、sub.c、calc.hを使う場合は以下のようになる。
| |
もしくはもっときれいにかける。
| |
CMakeの場合は以下になる。
| |
Cmakeの実行例
基本的に、2ステップでやる。
- 1回目(初回のみ): 設定
- 2回目(コード変更のたびに): ビルド
| |
-S . がソースディレクトリ、-B build がビルドディレクトリの指定している。
他には、次のようなパターンもよく見る。
| |
CMakePresets.json
CMakePresets.jsonは、CMake のプリセット設定 。- 正確には、CMakePresets.json に入れる形式の JSON で、ビルド設定を名前付きで保存しておくためのもの
プリセットを使わない場合:
| |
使った場合:
| |
ABI
ABIとは
- ABI は Application Binary Interface の略
- 日本語だと バイナリ間の取り決め くらいの意味
たとえば、
- 関数呼び出しのしかた
- 引数をレジスタに入れるかスタックに積むか
- 戻り値をどこに置くか
- 構造体のメモリ配置
- シンボル名の扱い
- どの形式の
.so/ 実行ファイルになるか
みたいなことを決める。
APIとABIの違い
API
ソースコードレベルの約束。
例:
| |
ABI
コンパイル後の機械語レベルの約束。
同じ add(int, int) でも、ABI が違うと
- 引数の渡し方
- 関数名の見え方
- 呼び出し規約
が違って、リンクできなかったり実行時に壊れたりする。
また、アーキによって(例えば、x86_64 と aarch64 )、バイナリの ABI が違うので注意。
共有ライブラリ
共有ライブラリを使うときの探索パス
共有ライブラリ .so を使う場合、注意することが2つある。
- リンク時にライブラリを見つけること
- 実行時にライブラリを見つけること
たとえば、次のように共有ライブラリを作ったとする。
| |
このライブラリを main.c から使って実行ファイルを作る場合は、リンク時にライブラリの場所を教える必要がある。
| |
ここで、
-L.は、ライブラリを探す場所に現在のディレクトリを追加する指定-lmylibは、libmylib.soまたはlibmylib.aを探してリンクする指定
という意味。
ただし、これだけだと実行時に次のようなエラーになることがある。
| |
これは、リンク時には libmylib.so を見つけられたが、実行時に動的リンカが libmylib.so を見つけられなかった、という意味。
このとき、一時的に共有ライブラリの場所を指定するには LD_LIBRARY_PATH を使う。
| |
または、先に環境変数として設定しておく。
| |
つまり、
-Lは リンク時 のライブラリ探索パスLD_LIBRARY_PATHは 実行時 の共有ライブラリ探索パス
という違いがある。
LD_LIBRARY_PATHについて
ただし、LD_LIBRARY_PATHは絶対パスにするのがおすすめ。
xxxをコンパイルした後に、こんな感じにセットする。
| |
意味は、
- xxxというアプリをコンパイルして、それを動かす時に、buildされた共有オブジェクトのpathを指定している
LD_LIBRARY_PATHがすでに設定されているなら、:$LD_LIBRARY_PATHを付け足す- 設定されていないなら、何も付けない
:+演算子を使った${変数A:+変数A}というパラメーター展開の構文
ldd で共有ライブラリを確認する
実行ファイルがどの共有ライブラリに依存しているかは、ldd で確認できる。
| |
たとえば、libmylib.so が見つからない場合は、次のように表示されることがある。
| |
この場合は、実行時のライブラリ探索パスに libmylib.so の場所が入っていない。
rpath を使う方法
LD_LIBRARY_PATH は一時的な指定には便利だが、毎回指定するのは面倒。
そこで、実行ファイルの中に共有ライブラリの探索パスを埋め込む方法もある。
これを rpath という。
たとえば、実行ファイルと同じディレクトリにある .so を探させたい場合は、次のようにする。
| |
$ORIGIN は、実行ファイル自身が置かれているディレクトリを表す。
つまりこの指定を入れると、app と同じディレクトリにある libmylib.so を実行時に探せるようになる。
共有ライブラリのまとめ
共有ライブラリ .so では、リンク時と実行時でライブラリ探索の話が分かれる。
-Lはリンク時にライブラリを探す場所を指定する-lxxxはlibxxx.soやlibxxx.aをリンクする指定LD_LIBRARY_PATHは実行時に.soを探す場所を指定するlddで実行ファイルが依存している.soを確認できるrpathを使うと、実行ファイル側に.soの探索パスを埋め込める
まとめ
一言でまとめると、
C では、ヘッダに宣言、ソースに実装を書き、各ソースを別々にコンパイルし、最後にリンクでつなぐ。
さらにもう少しかみ砕くと、
.hは「こう使ってね」という説明書.cは実際の中身#includeは説明書を貼り付ける前処理- コンパイルは各ファイルを別々に処理
- リンクはバラバラの結果を最後につなぐ
最後に超短く言うと:
- 宣言を共有するために
.h - 実装を書くために
.c - コンパイルで
.o - リンクで実行ファイル
.aと.soはどちらも.oから作れるが、使い方が違う
