Featured image of post Cの基本的な事

Cの基本的な事

目次

背景

  • ハードウェアに近い所のプログラミングでもCは必須
  • しかし、大昔にやったきりでほぼ覚えていない
  • そこで、改めて基本的なところからCの再学習をしたのでメモ

基本

Cの流れ

C では、プログラムは 1回で全部完成するのではなく、大きく、次の流れで作られる。

  1. .c(と.h)コードを用意
  2. 前処理
  3. コンパイル
  4. アセンブル
  5. リンク
  6. 実行

コード

.h = インターフェイス

宣言を書く場所

例:

1
int add(int a, int b);

上記のように、

  • 関数名
  • 引数の型
  • 戻り値の型

を他のファイルに知らせ。

.c = 実装

実装を書く場所

例:

1
2
3
int add(int a, int b) {
    return a + b;
}

上記のように、実際の処理の中身を書く。


なぜ .h.c を分けるのか

小さいプログラムなら、1ファイルに全部書いても動く。 でも複数ファイルになると、ある .c から別の .c の関数を使いたくなる。

たとえば

  • main.c
  • add.c
  • sub.c

があるとき、main.cadd()sub() の存在を知らないと呼べない。

そのために .h を使って、複数の .c ファイルで共有する宣言をまとめるということ。

つまり .h は、複数ファイルのための共通の説明書ということ。

前処理

前処理とは

前処理は、C のソースコードをコンパイルする前に加工する処理。

  • .c をそれぞれ .i にする
1
gcc -E main.c -o main.i

#include の意味

1
#include "calc.h"

は、

その場所に 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 にする
1
gcc -S main.c -o main.s

このとき作られる main.s は、まだ人間が読めるテキスト形式のアセンブリ。

1
2
movl    $0, %eax
ret

みたいな低水準の命令になる。

つまりコンパイルは、C言語 → アセンブリとなる。

アセンブル

アセンブルは、アセンブリコードを 機械語を含むオブジェクトファイルに変換する処理。

  • .s をそれぞれ .o にする
1
as main.s -o main.o

この main.o はバイナリ形式なので、普通にテキストエディタで読めるものではない。

つまりアセンブルは、アセンブリ → オブジェクトファイルという事。

リンク

オブジェクトファイルから、実行ファイルを作る。

  • .o 同士をつないで実行ファイルにする
1
2
3
main.o + add.o + sub.o + libc など
  ↓ リンク
app

疑問

コンパイルとリンクの違い

たとえば

  • main.c
  • add.c
  • sub.c

があるとき、

1
2
3
gcc -c main.c   # main.o
gcc -c add.c    # add.o
gcc -c sub.c    # sub.o

のように、**各 .c は別々にコンパイル(とアセンブル)**される。

そのあとで

1
gcc main.o add.o sub.o -o app

として、最後に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.cadd() を呼んでいても、コンパイル時には

1
int add(int a, int b);

が分かっていればよいということ。

でも最終的にリンクするときに add() の本体がないと、

  • undefined reference

のようなエラーになる。

つまり、

コンパイラは「呼び方」を確認し、リンカは「本体の所在」を確認する

実装ファイルが自分のヘッダを include する理由

たとえば

1
2
3
4
5
6
// sub.c
#include "calc.h"

int sub(int a, int b) {
    return a - b;
}

のように、sub.c でもヘッダを include することがある。

これは sub.csub() を使うためではなく、

ヘッダに書いた宣言と、自分の実装が一致しているか確認するため

つまり、

  • main.c が include する → 使うため
  • sub.c が include する → 宣言と実装の答え合わせのため

include guard とは何か

ヘッダの先頭によくある

1
2
3
4
5
6
#ifndef ADD_H
#define ADD_H

...

#endif

これは include guard

これは、

同じヘッダが複数回読み込まれても、1回だけ有効にする

ための仕組み。

これは文法上の絶対ルールではなく、昔からの定番の書き方。

Tool

Driver

Driver は、preprocessor / compiler / assembler / linker などを適切な順番で呼び出すツール。

代表例は gccg++clang

たとえば、

1
gcc main.c -o app

を実行すると、gcc は内部で必要なツールを順番に呼び出して、最終的に実行ファイルを作る。

つまり gcc / clang は、日常的には compiler と呼ばれることが多いが、厳密には compiler driver としても働く。

Driver は、以下を行う。

  • 入力ファイルの種類を判定する
  • preprocessor / compiler / assembler / linker を呼び出す
  • ライブラリやヘッダの探索パスを設定する
  • linker に必要な startup code や libc などを渡す

つまり、Driverはビルド全体を制御する司令塔。

Preprocessor

Preprocessor は、Cのソースコードに対して前処理を行うツール。

主に以下を処理する。

  • #include
  • #define
  • #ifdef
  • #ifndef
  • #if
  • コメント除去

たとえば、

1
2
3
4
5
6
7
#include <stdio.h>

#define MESSAGE "hello"

int main() {
    printf(MESSAGE);
}

のようなコードでは、#include によってヘッダファイルの内容が展開され、#define によってマクロが置き換えられる。

preprocess の結果だけを見たい場合は、

1
gcc -E main.c -o main.i

とする。

1
2
3
.c
  ↓ preprocessor
.i

.i は前処理済みの C ソースファイル。

1
2
3
.c
  ↓ preprocessor
.i

ソースコード → 前処理済みソースコード

を担当する。

Compiler

Compiler は、高水準言語のソースコードを、より低水準な表現へ変換するツール。

Cの場合、狭い意味での compiler は、前処理済みソースコードをアセンブリコードに変換する。

1
2
3
.i
  ↓ compiler
.s

たとえば、

1
gcc -S main.c -o main.s

とすると、C のソースコードからアセンブリファイル main.s が作られる。

.s はアセンブリファイルで、人間が読めるテキスト形式の低水準コード。

Compiler は、主に以下を行う。

  • 構文解析
  • 型チェック
  • 最適化
  • アセンブリコード生成

つまり Compiler は、狭い意味では、前処理済みソースコード → アセンブリを担当する。

ただし日常的には、gcc / clang のような driver も含めて compiler と呼ぶことが多い。

Assembler

Assembler は、アセンブリファイルをオブジェクトファイルに変換するツール。

代表例はasコマンド。

1
2
3
.s / .S
  ↓ assembler
.o

たとえば、

1
as main.s -o main.o

とすると、アセンブリファイル main.s からオブジェクトファイル main.o が作られる。

.o は object file で、機械語を含む中間生成物。

ただし .o はまだ単体では実行できないことが多い。
外部関数やライブラリへの参照が未解決のまま残っていることがあるため。

Assembler は、アセンブリ → オブジェクトファイルを担当する。

Linker

Linker は、複数のオブジェクトファイルやライブラリを結合して、実行ファイルや共有ライブラリを作るツール。

代表例は ldコマンド。

1
2
3
.o + .o + .a/.so
  ↓ linker
実行ファイル / .so

たとえば、

1
gcc main.o foo.o -o app

とすると、main.ofoo.o がリンクされて、実行ファイル app が作られる。

Linker は、主に以下を行う。

  • 複数の .o を結合する
  • 未解決のシンボルを解決する
  • ライブラリを結びつける
  • 実行ファイルのメモリ配置を決める
  • 必要な startup code を含める

たとえば printf を使っている場合、コンパイル時点では printf の実体はまだ自分の .o には入っていない。
linker が libc などを探して、printf の参照を解決する。

Linker は、オブジェクトファイル + ライブラリ → 実行ファイル / 共有ライブラリを担当する。

全体の流れ

全体の流れはこう。

1
2
3
4
5
6
7
8
9
.c 
  ↓ preprocessor
.i 
  ↓ compiler
.s
  ↓ assembler
.o
  ↓ linker
実行ファイル / .so

より具体的には、C の場合はこう。

1
2
3
4
5
6
7
8
9
main.c
  ↓ preprocessor
main.i
  ↓ compiler
main.s
  ↓ assembler
main.o
  ↓ linker
app

役割の違い

それぞれの担当は以下。

Tool入力出力役割
Driver.c, .o など最終成果物全体を制御する
Preprocessor.c.i#include#define を処理する
Compiler.i.sソースコードをアセンブリに変換する
Assembler.s, .S.oアセンブリをオブジェクトファイルに変換する
Linker.o, .a, .so実行ファイル, .soオブジェクトやライブラリを結合する

Toolのまとめ

まとめると、概念的には、次の処理をまとめて行っている。

1
2
3
4
5
6
main.c → main.i → main.s → main.o
foo.c  → foo.i  → foo.s  → foo.o

main.o + foo.o + libraries
  ↓ linker
app

役割は以下:

1
2
3
4
5
Driver       = 全体を制御する
Preprocessor = #include や #define を処理する
Compiler     = ソースコードをアセンブリに変換する
Assembler    = アセンブリを .o に変換する
Linker       = .o やライブラリをまとめて実行ファイルを作る

ファイル

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 をコンパイルした結果 もしくは、sasコマンド に渡してアセンブルすると、 .o になる。

.a

静的ライブラリ 複数の .o をまとめたもの

.so

共有ライブラリ(動的ライブラリ) これも元は .o から作るが、実行時に読み込まれる

a.out

実行ファイルのデフォルト名 gcc main.c のように -o を省略したときにできることがある

疑問

.a.so の違い

どちらも元は .o から作れる。

  • .a = 静的ライブラリ
  • .so = 共有ライブラリ

違いは「元の材料」ではなく、できあがったものの使われ方

  • .a はリンク時に実行ファイルへ取り込まれる
  • .so は実行時に外から読み込まれる

ar rcs の意味

1
ar rcs libmylib.a foo.o bar.o

は、.o をまとめて .a を作るarchiveコマンド。

  • r = 入れる / 置き換える
  • c = 新規作成
  • s = シンボル索引を作る

つまり

静的ライブラリを作って、リンクしやすい形に整える

ということ。

GCCオプション

-E

-E は、前処理だけを行うオプション。

1
gcc -E main.c -o main.i

この場合、main.c に対して #include#define などの前処理だけを行い、前処理済みソース main.i を出力する。

1
2
3
main.c
  ↓ 前処理
main.i

main.i はまだ C のソースコードに近いテキストファイル。


-S

-S は、アセンブリファイルまで生成するオプション。

1
gcc -S main.c -o main.s

この場合、main.c を処理して、アセンブリファイル main.s を出力する。

1
2
3
4
5
main.c
  ↓ 前処理
main.i
  ↓ コンパイル
main.s

main.s は、人間が読めるテキスト形式のアセンブリコード。


-c

-c は、オブジェクトファイルまで生成するオプション。

1
gcc -c main.c -o main.o

この場合、main.c から main.o を作る。
ただし、リンクは行わない

1
2
3
4
5
6
7
main.c
  ↓ 前処理
main.i
  ↓ コンパイル
main.s
  ↓ アセンブル
main.o

複数ファイルの C プログラムでは、まず各 .c.o にして、あとでリンクすることが多い。

1
2
3
gcc -c main.c -o main.o
gcc -c add.c -o add.o
gcc -c sub.c -o sub.o

-o

-o は、出力ファイル名を指定するオプション。

1
gcc main.c -o app

この場合、実行ファイル名は app になる。

-o を省略すると、環境によっては a.out という名前の実行ファイルが作られる。

1
gcc main.c
1
a.out

-I

-I は、ヘッダファイルの探索パスを追加するオプション。

たとえば、ヘッダファイルが include/ にある場合:

1
2
3
4
5
project/
  include/
    calc.h
  src/
    main.c

次のように指定する。

1
gcc -Iinclude -c src/main.c -o main.o

これにより、#include "calc.h" などを書いたときに、include/ の中も探してくれる。

1
#include "calc.h"

つまり、

1
-I = include path を追加する

ということ。


-L(大文字L)

-L は、ライブラリの探索パスを追加するオプション。

たとえば、現在のディレクトリに libmylib.solibmylib.a がある場合:

1
gcc main.o -L. -lmylib -o app

ここで、

1
-L.

は、ライブラリを探す場所に現在のディレクトリ . を追加する、という意味。

つまり、

1
-L = library path を追加する

ということ。

注意点として、-L は基本的に リンク時 の探索パスを指定するもの。

実行時に .so を探す場所とは別。

-l(小文字L)

-l は、リンクするライブラリを指定するオプション。

1
gcc main.o -lmylib -o app

この場合、-lmylib は次のようなファイルを探す。

1
2
libmylib.so
libmylib.a

つまり、

1
-lmylib = libmylib.so または libmylib.a を探してリンクする

という意味。

lib の部分と、.so / .a の拡張子は書かない。

たとえば libm.so をリンクしたい場合は、次のように書く。

1
gcc main.o -lm -o app

これは数学ライブラリ libm をリンクする例。

-fPIC

-fPIC は、Position-Independent Code、つまり位置独立コードを生成するオプション。

1
gcc -fPIC -c foo.c -o foo.o

共有ライブラリ .so を作るときによく使う。

PIC は、簡単に言うと、

1
どのメモリアドレスに配置されても動きやすいコード

のこと。

共有ライブラリは実行時にメモリ上のどこへ読み込まれるか分からないため、PIC にしておくのが基本。

PIC とは何か

Position-Independent Code位置独立コードとは、どのメモリアドレスに置かれても動きやすいコード

共有ライブラリ .so を作るときによく使う。

1
2
gcc -fPIC -c foo.c -o foo.o
gcc -shared -o libmylib.so foo.o

PICで大事なこと

PICで大事なのは以下:

  • -fPIC.o の作り方
  • .so はその .o から作る共有ライブラリ

しかも、PIC な .o から .a を作ることもできる。 つまり -fPIC.a を禁止するものではなく、単に中身の .o の性質。

-shared

-shared は、共有ライブラリを作るオプション。

1
gcc -shared -o libmylib.so foo.o

たとえば、共有ライブラリを作る基本形はこう。

1
2
gcc -fPIC -c foo.c -o foo.o
gcc -shared -o libmylib.so foo.o

または、1行で書くこともできる。

1
gcc -fPIC -shared -o libmylib.so foo.c

この結果、共有ライブラリ libmylib.so が作られる。

1
2
3
4
5
foo.c
  ↓ gcc -fPIC -c
foo.o
  ↓ gcc -shared
libmylib.so

GCCオプションのまとめ

オプション役割
-E前処理だけを行う
-Sアセンブリファイル .s まで生成する
-cオブジェクトファイル .o まで生成する。リンクはしない
-o出力ファイル名を指定する
-Iヘッダファイルの探索パスを追加する
-Lライブラリの探索パスを追加する
-lリンクするライブラリを指定する
-fPIC位置独立コードを生成する
-shared共有ライブラリ .so を作る

流れで見ると、こう。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
gcc -E main.c -o main.i
  前処理だけ

gcc -S main.c -o main.s
  アセンブリまで生成

gcc -c main.c -o main.o
  オブジェクトファイルまで生成

gcc main.o -o app
  リンクして実行ファイルを生成

共有ライブラリを作る場合は、こう。

1
2
gcc -fPIC -c foo.c -o foo.o
gcc -shared -o libmylib.so foo.o

フォルダ構成

include / src / third_party

よくあるフォルダ構成は以下:

  • include/ = 公開ヘッダ
  • src/ = 実装(非公開)
  • third_party/ = 外部ライブラリ

SDKのパターン

  • include/ = 利用者に見せる API
  • src/ = SDK 作者の実装ソース
  • 配布物 = include/ + ビルド済みライブラリ
  • 利用者 = ヘッダを include して、そのライブラリにリンク

Makefile / CMake

Makefileの手順

昔の Cプロジェクトのビルド手順:

1
2
3
./configure   # 環境チェック・Makefile 生成
make          # コンパイル
make install  # インストール

Configure

configure は、ビルド前に環境を調べて、Makefile を作るためのスクリプト。

主に以下をする。

  • コンパイラはあるか
  • 必要なライブラリはあるか
  • 必要なヘッダはあるか
  • OSやCPUアーキテクチャは何か
  • インストール先はどこか

そして、環境に合った Makefile を生成する。

Autoconf

configureも作る、autoconfを使った代表的な流れ:

1
2
3
4
5
6
7
8
9
開発者が書く:
configure.ac
Makefile.am

  ↓ autoconf / automake などを実行

自動生成される:
configure
Makefile.in

.ac(autoconf),.am(automake),.in(input)は、autoconf系のコマンドの名前。

つまり、Makefileを作るconfigureを作るのがAutoconfのconfigure.acということ。

MakefileとCmakeの違い

今までのmain.cadd.csub.ccalc.hを使う場合は以下のようになる。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
app: main.o add.o sub.o
	gcc main.o add.o sub.o -o app

main.o: main.c calc.h
	gcc -c main.c -o main.o

add.o: add.c calc.h
	gcc -c add.c -o add.o

sub.o: sub.c calc.h
	gcc -c sub.c -o sub.o

clean:
	rm -f *.o app

もしくはもっときれいにかける。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
CC = gcc
CFLAGS = -Wall -Wextra

app: main.o add.o sub.o
	$(CC) main.o add.o sub.o -o $@

%.o: %.c calc.h
	$(CC) $(CFLAGS) -c $< -o $@

clean:
	rm -f *.o app

CMakeの場合は以下になる。

1
2
3
4
5
6
7
8
cmake_minimum_required(VERSION 3.16)
project(calc C)

add_executable(app
    main.c
    add.c
    sub.c
)

Cmakeの実行例

基本的に、2ステップでやる。

  1. 1回目(初回のみ): 設定
  2. 2回目(コード変更のたびに): ビルド
1
2
3
4
5
6
7
8
cmake -S . -B build \
	-DCMAKE_POLICY_VERSION_MINIMUM=3.5 \
	-DOpenCV_DIR=/usr/lib/x86_64-linux-gnu/cmake/opencv4 \
	-DCMAKE_C_COMPILER=/usr/bin/gcc \
	-DCMAKE_CXX_COMPILER=/usr/bin/g++ \
	-DCMAKE_EXE_LINKER_FLAGS="-B/usr/bin -L/usr/lib/x86_64-linux-gnu"

cmake --build build -j$(nproc)

-S . がソースディレクトリ、-B build がビルドディレクトリの指定している。

他には、次のようなパターンもよく見る。

1
2
3
mkdir build && cd build
cmake ..
cmake --build . --config Release

CMakePresets.json

  • CMakePresets.jsonは、CMake のプリセット設定 。
  • 正確には、CMakePresets.json に入れる形式の JSON で、ビルド設定を名前付きで保存しておくためのもの

プリセットを使わない場合:

1
2
3
4
5
cmake -S . -B build \
  -DCMAKE_TOOLCHAIN_FILE=... \
  -DCMAKE_SYSTEM_NAME=Android \
  -DCMAKE_ANDROID_ARCH_ABI=arm64-v8a \
  ...

使った場合:

1
2
cmake --preset linux-arm64
cmake --preset android-arm64

ABI

ABIとは

  • ABI は Application Binary Interface の略
  • 日本語だと バイナリ間の取り決め くらいの意味

たとえば、

  • 関数呼び出しのしかた
  • 引数をレジスタに入れるかスタックに積むか
  • 戻り値をどこに置くか
  • 構造体のメモリ配置
  • シンボル名の扱い
  • どの形式の .so / 実行ファイルになるか

みたいなことを決める。

APIとABIの違い

API

ソースコードレベルの約束。

例:

1
int add(int a, int b);

ABI

コンパイル後の機械語レベルの約束。

同じ add(int, int) でも、ABI が違うと

  • 引数の渡し方
  • 関数名の見え方
  • 呼び出し規約

が違って、リンクできなかったり実行時に壊れたりする。

また、アーキによって(例えば、x86_64aarch64 )、バイナリの ABI が違うので注意。

共有ライブラリ

共有ライブラリを使うときの探索パス

共有ライブラリ .so を使う場合、注意することが2つある。

  1. リンク時にライブラリを見つけること
  2. 実行時にライブラリを見つけること

たとえば、次のように共有ライブラリを作ったとする。

1
2
gcc -fPIC -c foo.c -o foo.o
gcc -shared -o libmylib.so foo.o

このライブラリを main.c から使って実行ファイルを作る場合は、リンク時にライブラリの場所を教える必要がある。

1
gcc main.c -L. -lmylib -o app

ここで、

  • -L. は、ライブラリを探す場所に現在のディレクトリを追加する指定
  • -lmylib は、libmylib.so または libmylib.a を探してリンクする指定

という意味。

ただし、これだけだと実行時に次のようなエラーになることがある。

1
error while loading shared libraries: libmylib.so: cannot open shared object file

これは、リンク時には libmylib.so を見つけられたが、実行時に動的リンカが libmylib.so を見つけられなかった、という意味。

このとき、一時的に共有ライブラリの場所を指定するには LD_LIBRARY_PATH を使う。

1
LD_LIBRARY_PATH=. ./app

または、先に環境変数として設定しておく。

1
2
export LD_LIBRARY_PATH=.
./app

つまり、

  • -Lリンク時 のライブラリ探索パス
  • LD_LIBRARY_PATH実行時 の共有ライブラリ探索パス

という違いがある。

LD_LIBRARY_PATHについて

ただし、LD_LIBRARY_PATHは絶対パスにするのがおすすめ。

xxxをコンパイルした後に、こんな感じにセットする。

1
export LD_LIBRARY_PATH="$(pwd)/xxx/build/bin${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}"

意味は、

  • xxxというアプリをコンパイルして、それを動かす時に、buildされた共有オブジェクトのpathを指定している
  • LD_LIBRARY_PATH がすでに設定されているなら、:$LD_LIBRARY_PATH を付け足す
  • 設定されていないなら、何も付けない
  • :+演算子を使った${変数A:+変数A}というパラメーター展開の構文

ldd で共有ライブラリを確認する

実行ファイルがどの共有ライブラリに依存しているかは、ldd で確認できる。

1
ldd ./app

たとえば、libmylib.so が見つからない場合は、次のように表示されることがある。

1
libmylib.so => not found

この場合は、実行時のライブラリ探索パスに libmylib.so の場所が入っていない。

rpath を使う方法

LD_LIBRARY_PATH は一時的な指定には便利だが、毎回指定するのは面倒。

そこで、実行ファイルの中に共有ライブラリの探索パスを埋め込む方法もある。
これを rpath という。

たとえば、実行ファイルと同じディレクトリにある .so を探させたい場合は、次のようにする。

1
gcc main.c -L. -lmylib -Wl,-rpath,'$ORIGIN' -o app

$ORIGIN は、実行ファイル自身が置かれているディレクトリを表す。

つまりこの指定を入れると、app と同じディレクトリにある libmylib.so を実行時に探せるようになる。

共有ライブラリのまとめ

共有ライブラリ .so では、リンク時と実行時でライブラリ探索の話が分かれる。

  • -L はリンク時にライブラリを探す場所を指定する
  • -lxxxlibxxx.solibxxx.a をリンクする指定
  • LD_LIBRARY_PATH は実行時に .so を探す場所を指定する
  • ldd で実行ファイルが依存している .so を確認できる
  • rpath を使うと、実行ファイル側に .so の探索パスを埋め込める

まとめ

一言でまとめると、

C では、ヘッダに宣言、ソースに実装を書き、各ソースを別々にコンパイルし、最後にリンクでつなぐ。

さらにもう少しかみ砕くと、

  • .h は「こう使ってね」という説明書
  • .c は実際の中身
  • #include は説明書を貼り付ける前処理
  • コンパイルは各ファイルを別々に処理
  • リンクはバラバラの結果を最後につなぐ

最後に超短く言うと:

  • 宣言を共有するために .h
  • 実装を書くために .c
  • コンパイル.o
  • リンクで実行ファイル
  • .a.so はどちらも .o から作れるが、使い方が違う

参考文献

Built with Hugo
テーマ StackJimmy によって設計されています。