Skip to content

Latest commit

 

History

History
560 lines (412 loc) · 20.8 KB

c_study_day3.markdown

File metadata and controls

560 lines (412 loc) · 20.8 KB

WebエンジニアのためのC言語入門ハンズオン #3

今日習得すること

  • C言語の規格
  • コンパイル
  • ビルド
  • autotools
  • unittest(おまけ)

1. C言語の規格

Wikipedia等で調査した所、以下の4つの規格がC言語に存在しています。
(K&Rは規格という概念で良いのか分かりませんが・・・)

  1. K&R
    所謂、カーニハン&リッチーの「プログラミング言語C」に書かれている規格
  2. C89/90
    ANSIによって、最初に定められた標準規格。
  3. C99
    C89/90に一部改定を行った規格
  4. C11
    最新規格だが、gccclangは一部のみ対応

一般プログラマが規格を意識すべきか?

最初のうちは、そんなに意識しなくて良い!と思います。
そんなことより、Segmentation Faultが何故起こるのかをきちんと理解した方がいいです。

規格が鍵になってくるのは、ソフトウェアの配布を考えた時だと思います。
例えば、以下のような書き方はC89ではエラーになります。

for(int i=0; i < 10; i++){
  printf("%d¥n", i);
}

for文の中で、intの変数を宣言出来ないのです。
しかし、初学者がここまで意識すべきか?と言われると疑問です。

ソフトウェアをコンスタントに作るようになってから、おいおい理解していけば良いと思います。

規格を指定したコンパイル

-std=c89等のコンパイルオプションで、規格を指定したコンパイルが行えます。
配布を考える時は、古い規格でもコンパイル出来るように、オプションを指定したコンパイルをやってみましょう。

2. コンパイル

コンパイルとは・・・

プログラミング言語で書かれたコンピュータプログラム(ソースコード)を解析し、コンピュータが直接実行可能な形式のプログラム(オブジェクトコード)に変換すること。
IT用語辞典 e-Words - コンパイルとは

演習1 単数ファイルのコンパイル

繰返しになる人も多いと思いますが、復習を兼ねてとりあえずソースコードを書いてみましょう。

ファイル名comp01.c

#include <stdio.h>

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

int main (){
    printf("answer is %d\n", add(4, 5));
}

コンパイルコマンド

gcc -o comp01 comp01.c

作成されたcom01コマンドを実行してみましょう。

演習1のgccコマンドが行っていること

コンパイルという一言に対して、以下のような一連のプログラムが動作しています。

  1. プリプロセッサ
    字句解析、マクロの展開、定数の数値への置換え等を行う。
  2. パーサー
    構文解析、意味解析
  3. コードジェネレータ
    オブジェクトコードを生成する => ほとんど機械語の状態です。しかし、実行可能ではありません。
  4. アセンブラ
    オブジェクトコード内のアセンブラ言語(MOV, ADD, DIV等)を機械語に変換します。
  5. オプティマイザ
    ソースコード内の、冗長な表現などを自動で最適なコードに変換してくれるものです。 C言語黎明期には、多数のC言語コンパイラが存在し、そのほとんどが有料でした。 金額の高さは、安定性とオプティマイザの性能にあったそうです。 今ではgccやclangを使うのが当たり前なので、隔世の感があります。
  6. リンカ
    リンカは、オブジェクトファイル内の動的リンク、静的リンクを結合して、1ファイル内で完結する実行可能ファイルを作成します。 なんじゃらほい?という感じですが、全然難しくない概念です。後で、順を追って解説します。

演習1のgccコマンドは、上記の一連のコマンドが裏で実行されています。

gcc -o comp01 comp01.c

余談

アセンブラを扱っていると、ニーモニックという単語が良く出てきます。

アセンブラでは、機械語だと人間に覚えられないから、機械語命令を簡単なアルファベットに置換えています。
置換えた命令(MOV, ADDとか)をニーモニックといいます。

日本においてニーモニックと言うと、アセンブラ言語のことを直接指すことが多いようですが、
海外では、それ以外にも人間が覚えにくい事柄を、適切な英単語で置き換えることをニーモニック(発音ではニモニック)と呼ぶようです。

英和辞書では、下記のような意味です。

記憶を助ける,記憶術の.
menemonic - Weblio

演習2 複数ファイルのコンパイル(静的リンク)

複数ファイルをコンパイルするのは、ある程度の規模のC言語プロジェクトであれば当然です。
しかし、どのようにgccコマンドを実行すれば良いのでしょうか?

実際にやってみましょう。

ファイル構成が複雑になってきますので、comp02というディレクトリを作成して下さい。
その中に以下の3つのファイルを作成して下さい。

  • main.c
#include <stdio.h>
#include "common.h"

int main (){
    printf("answer is %d\n", add(4 , 5));
}
  • func.c
int add (int a, int b){
    return (a + b);
}
  • common.h
int add (int a, int b);

演習2のプログラムは、演習1のプログラムとやっていることは同じです。
異なるのは、ファイルを分割し、add関数を別のファイルに切り出しました。

非常に単純な例ですが、世の中のC言語プログラムは、大体同じ構造で作成されています。

ファイルの作成が終わったら、コンパイルを行います。
下記のとおりに、正確に順番にコマンドを実行して下さい。
※コマンドを実行した後は、ls -lコマンドで実行結果で、どんなファイルが作られたかを確認して下さい。

(1) func.cをコンパイル

gcc -c func.c

=> func.oが出来る

(2) main.cをコンパイル

gcc -c main.c

=> main.oが出来る

(3) リンク

gcc -o add main.o func.o

=> addコマンドが出来る

addコマンドを実行すると、演習1と同じ結果になります。

コマンド毎の細かい解説

(1) func.cをコンパイル
gccのオプションに-cを指定しています。 つまり、先ほど解説したコンパイルの流れの中の、コードジェネレータの部分まで実行しています。 作成されたfunc.oはオブジェクトファイル(オブジェクトコードともいう)です。

ちなみに、-cを付けずにgcc -o func func.cとするとエラーになります。
main関数が無いため、実行可能ファイルを作ることが出来ないからです。
=> 是非、試して見てください。

(2) main.cをコンパイル
同様に、main.cをコンパイルしています。
しかし、add関数はmain.cで定義されていないのに、何故コンパイル可能なのでしょうか?

理由は、common.hにプロトタイプ宣言(関数の定義)が書かれているためです。
そのため、リンクする前の段階までなら、gcc -cコマンドでオブジェクトファイルを生成することが可能です。

(3) リンク
最後のコマンドで、二種類のリンクを行っています。

一つ目は、静的リンクです。

common.hに記述された定義でしかなかったadd関数に関数実体をリンクさせています。
main.o内のadd関数と、func.oadd関数の実体がリンクします。

このように、コードとして実際に書かれている関数をリンクすることを静的リンクといいます。
=> 要するに自分が書いたコードです。

二つ目は、動的リンクです。

main.cprintfコマンドを実行しています。これは共有ライブラリの関数を実行しています。
#include < >で指定されたヘッダーファイルに記述されている関数は共有ライブラリの関数です。

これらの関数は、実行ファイル内に含めることが出来ません。
そのため、実行時に共有ライブラリを参照する形で実行します。
このように、関数実体ではなく、共有ライブラリを参照する形式でのリンクを動的リンクと呼びます。

リンクのイメージ図
linkのイメージ

ハンズオン第一回のおさらいになりますが、以下のコマンドを実行して見てください。
動的リンクの共有ライブラリが表示されます。

  • Linux
ldd add 
  • Mac
otool -L add

余談

オープンソースライセンスとして有名なGPLについて、下記のような議論がよく交わされます。

「動的リンク」は、ライセンス違反にあたるのか?否か?

このGPLの議論は、リンクという概念を理解した後じゃないと全く分かりません。
先ほど学んだ内容から考えれば、実行可能ファイル内に含まれず、実行時に共有ライブラリを参照する行為は、ライセンス違反にあたるのか?ということです。

静的リンクが可能な場合は、実行可能ファイル内に他者の著作物を含んでしまっているのでアウトなんだな!
っていう話も、リンクを理解してこそ、腹落ちしてきます。

動的リンクについて、もう少し深く

動的リンクは、コンパイル時にどのようにコンパイラに参照されるのでしょうか?
そこを正確に理解することは、外部のミドルウェアのコンパイル等でも非常に役立ちますので、もう少しだけ深く見てみましょう。

(1) オブジェクトファイル生成時
オブジェクトファイル生成時は、ヘッダーファイル内にプロトタイプ宣言があれば、コンパイル出来ます。

共有ライブラリのヘッダーファイルは、以下の順番で探索されます。(公式ドキュメントより)

  • /usr/local/include
  • libdir/gcc/target/version/include
  • /usr/target/include
  • /usr/include
    GCC - 2.3 Search Path

特定のライブラリーを使って、何かをコンパイルする場合はdevelパッケージが必要です。
へッダーファイルは、develパッケージにのみついてきます。

ちなみに上記以外の場所にヘッダーファイルがある場合は、下記のオプションでヘッダーファイルの探索pathを追加出来ます。

-I/usr/hogehoge/

(2) 動的リンク時 動的リンクを行う際は、ヘッダーファイルがあっても意味がありません。
共有ライブラリの本体が必要です。

Linuxにおいて、共有ライブラリーのロードは/lib/ld.soが行います。
ldconfigというコマンドで、共有ライブラリーのロードpathを表示出来ます。

Macにおいては、残念ながら分かりませんでした・・・。

動的リンク時にライブラリーが見つからない等のエラーがでた場合は、下記のオプションで共有ライブラリの探索pathを追加できます。

-L/usr/hogehoge/fugafuga

おまけ

ミドルウェアをソースインストールする時に、上手くいかなくてStackoverflowを検索すると
解答の中に-I/fugafugaを指定しろ!とかLD_LIBRARY_PATHに追加しろ!とか書いてあります。

経験された方も多いと思います。
私も、意味も分からず色んなコマンドオプションをつけたり、環境変数を設定してみたりしました。
その当時は、何をやっているのか意味不明でしたが、分かってしまえば簡単です。

ようするに、共有ライブラリーのリンク問題を扱っていたというわけです。

3. ビルド

ビルドとは?

まさに、先ほどやった演習2がビルドです。
つまり、コンパイルしてリンクして、実行可能ファイルを作成する一連の流れがビルドです。

演習1は、あまりにも単純な構成なのでコンパイル≒ビルドになっていたというわけです。

このビルドですが、結構面倒くさいのです。
演習2でさえ、3つのコマンドを打つ必要があります。しかも順番もあります。

C言語では、面倒くさいビルド作業を効率化するために、makeを使います。

とりあえずやってみましょう!

演習3 make

演習2で作ったディレクトリでMakefileを作りましょう。
=> 文字通り、Makefileという名前のファイルを作ります。

Makefile

all: clean func.o main.o
    gcc -o add func.o main.o

func.o:
    gcc -c func.c

main.o:
    gcc -c main.c

clean:
    rm -f *.o add

Makefileのインデントはタブです。

出来上がったら、makeコマンドを叩いて下さい。
addという実行可能ファイルが生成されればOKです。

main.cの足し算の数字を変更して、makeコマンドを再度実行して、ビルドし直されていることを確認して下さい。

解説

makeの良い所は、非常に単純ということです。
makeというコマンドを実行すると、Makefileの先頭のコマンドが実行されます。
コマンドはネストさせることが出来ます。
この例では、allを実行すると、clean, func.o, main.oの順番にコマンドを実行します。

それぞれのコマンドの中身は、それぞれのコマンドの項に書いてある通りです。

コマンドを指定して実行することも出来ます。make cleanと打てばcleanコマンドを実行できます。

とても単純ですが、プロジェクトが大きくなるとMakefileも結構複雑になります。
makeのお陰でビルドは大分楽になるのですが、それでもまだまだ面倒臭いんです。

4. autotools

makefileさえも自動で生成してしまおう!
業界で標準的なmakefileにしちゃおう!
必要なライブラリーのチェックも自動でしちゃおう!

そんな期待に応えるのがautotoolsです。

憧れの./configure, make, make installです。

autotoolsに関しては、とにかくやってみるのが一番です。
本ハンズオンでも、とりあえず写経形式で、autotoolsの関連ファイルを作って行きます。

事前準備

autoconf, automakeをインストールします。

brewをお持ちの方。

brew install autoconf
brew install automake

brewをお持ちで無いmacの方

Installing Autoconf, Automake & Libtool on Mac OSX

yumの方

yum install autoconf
yum install automake

※sudoか、rootユーザで

apt-getの方

apt-get install autoconf
apt-get install automake

※sudoか、rootユーザで

演習4 autotoolsを使ってみる

comp04ディレクトリを作成して下さい。 演習2,3で使ったmain.c, func.c, common.hをコピーして下さい。

スタート時点では、下記の構成です。

comp04
├── common.h
├── func.c
└── main.c

Makefile.am作る

bin_PROGRAMS=add
add_SOURCES=func.c main.c

configure.ac作る

autoscanコマンドで、雛形が出来ます。

autoscan
mv configure.scan configure.ac

configure.acの中身
せっかく雛形作りましたが、最小構成にするため、ほとんど削除します

AC_INIT([ADD PROGRAM], [1.0])
AM_INIT_AUTOMAKE
AC_PROG_CC
AC_CONFIG_FILES([Makefile])
AC_OUTPUT

configureを作成する

aclocal
autoconf

=> aclocalコマンドで、aclocal.m4というファイルが出来ます。autoconfに必要なコマンド類です。

Makefile.inを作成する

automake -a -c --foreign

-a,-cautotoolsが必要とするコマンド群をコピーして配置してくれます。
install-sh, missing, depcomp, compileとかがそれにあたります。

--foreignは、GNUプロジェクトのしきたりに従う下記のファイル群を用意しないことを意味します。

  • NEWS
  • README
  • AUTHORS
  • ChangeLog
    => 今回はシンプルなautotoolsを目指すのでこれらのファイルは無視します。

憧れをやってみる。

./configure
make
make install

/usr/local/binにインストールされるので、後で削除しておきましょう・・・

autoconfの雑な解説

作る必要があるファイル

  • Makefile.am
    => コマンド名、対象ファイルを記述する
  • configure.ac
    => configureコマンドでチェックする中身を記述する => ヘッダーファイルや、ライブラリーのチェックとかも、ここでかけます。
AC_CHECK_HEADERS([stdlib.h string.h sys/param.h unistd.h])

AC_CHECK_LIB(readline, rl_digit_argument, [], [
        echo "Error! readline-devel is required." 
        exit -1
        ])

それ以外のファイルは、autotoolsのコマンドで自動生成します。

ls -lしてみましょう。憧れの代償として、いかに多くのファイルが必要とされるのかが分かりますw
最初は3ファイルでしたね・・・。

ちなみにですが、普通はC言語のプロジェクトではプロジェクトルートにソースコードを配置してないです。
srcというディレクトリを作って、その中に配置することが多いです。

5. unittest

autotoolsまでやれば、通常のC言語プロジェクトに対応できる下地が出来たことになります。
しかし、2015年ですから、ユニットテストについても、基本を抑えておきましょう。

恐れることはありません。今日すでに学んだ内容で充分理解できます。
つまり、リンクです。

演習5 とてもシンプルなユニットテスト

Google TestというC言語のユニットテストツールがGoogleから提供されています。
Travis CIでの実装例もネット上に転がっていますので、初心者にやさしいテスト・ツールです。

演習2のファイル群をもう一度用意して下さい。

comp05
├── common.h
├── func.c
└── main.c

Google Testをチェックアウトして、テスト用ライブラリーをビルドしておきます。
comp05ディレクトリ内で実行して下さい。

svn co http://googletest.googlecode.com/svn/trunk gtest
mkdir build
cd build
cmake ..
make

comp05ディレクトリ直下に、test.cppというファイルを作ってください。
中身は、以下のとおりです。

#include <gtest/gtest.h>
extern "C"
{
#include "common.h"
}

TEST(SetConstructTest, ConstructFromArray)
{
    ASSERT_EQ(12, add(4, 8));
    ASSERT_EQ(9, add(4, 5));
}


int main(int argc, char **argv)
{
    ::testing::InitGoogleTest(&argc, argv);
    return RUN_ALL_TESTS();
}

Test用の実行ファイルを作成する。

gcc -c func.c
clang++ -std=c++11 -g -Wall -Wextra -c test.cpp -I./gtest/include
clang++ -std=c++11 -g -Wall -Wextra -o test func.o test.o -lgtest -L./gtest/build

Test実行

./test

解説

Test自体はc++で書きますが、内容はユニットテストなので、なんとなく分かると思います。

func.cadd関数をテストしています。
テスト用の実行ファイルを作成するために、下記のことをやっています。

  1. func.cのオブジェクトファイルを作成
    => add関数の実体はfunc.oです。
  2. test.cppのオブジェクトファイルを作成
    => add関数のテストを行いますが、定義さえ分かればコンパイルは出来ますよね!なので、先頭でヘッダーファイルをincludeしてます。
  3. func.otest.oをリンクして、実行可能ファイルを作ります。
    => 演習2と基本は一緒です。

つまり、テストの実行可能ファイルは、テスト内容のオブジェクトファイルと、テスト対象の関数のオブジェクトファイルを静的リンクしたものです。

なお、それぞれのコマンドでは-I, -LでそれぞれGoogle Testのヘッダーファイルと、共有ライブラリのPATHを指定してます。