Linuxプログラミング入門

97年9月17日執筆

from DOS to Linux(2)
プログラムが動くまで(1)


本連載の方針について

前回の「はじめに」ではちょっとグチを書いただけで終わってしまっているの で、本連載の方針について書いておこう。

本連載は「advanced」なところを狙った、「Linuxプログラミング」の入門で ある。「Linuxによるプログラミング入門」ではない。つまり、ある程度のプ ログラミングに関する知識を要求する。と言っても、バリバリとプログラムを 書けるということを要求するわけではなく、「C入門は終わった」という程度 を想定している。だから、Cについての基本的なことはやらないつもりである。

また、狙いは「Linux上でプログラムが作れるようになる」ということにある。 だから、移植とか改造については、ほとんど触れない。しかし、今回の解説は MS-DOSのプログラミングとの対比で行う予定なので、その気で読めば移植のヒ ントにはなると思う。とは言え、ここでは「プログラムを移植」することが目 的ではなく、「プログラマを移植」することが目的である。だから、「DOSで はこうだったが、Linuxはこうである」という話と共に、「こう書くとLinuxら しい」といった話をしようと思う。

言語は前回も言ったように、Cを基本にする。これはCはLinuxの母国語であ るということからである。C++くらい抽象化されてしまうと、タテマエ上はプ ラットホーム依存の部分が隠蔽されてしまうし、そうかと言ってアセンブラと なると、見通しが悪くなるし、Linuxではアセンブラでないと書けないといっ た類のアプリはほとんどないので、アセンブラを使う必要性もない。また、 Tcl/Tkとかperl, bashといったスクリプト言語だと、「プログラミング」といっ た感じではなく、「その場限りのやっつけ仕事」といった感じが強くなり、ど うも「プログラミングを楽しむ」といった感じではなくなってしまう。という わけで、結局Cが一番手頃ということになる。もっともずっと後で出て来るで あろう「便利なライブラリ」達の内容によっては他の言語になることもあるか も知れない。また、gas (アセンブラ)を使うのも、それなりに楽しい話題なの で、案外気が向けばするかも知れない。

最終的には、Linux上でバリバリとプログラムが書けるようになることを目標 としたいので、出来ればXのプログラミングとかもやりたいと思う。Xのプログ ラミングは昔はかなり敷居が高かったのであるが、今は結構便利なライブラリ があるので、かなり敷居は低くなって来たようである。かっこいいGUIのプロ グラムが、あまり手をかけずに作成出来るとしたら、これはなかなか面白いこ とではないか。

Linux上プログラミング環境

コンパイル環境

「MS-DOSとの違い」ということで、まず手始めにコンパイル環境について書い ておこう。

MS-DOS上のcc(MSCとか)の場合、自分で

  1. ccでコンパイルして
  2. linkで結合

といったことを、それぞれ起動してやらなくてはならないことが多い。もちろ んccで全て行うことは可能であるが、MS-DOSはコマンドラインの長さの制限が 厳しいので、あまり使わなかったと思う。

しかし、後でもまた説明するが、Linuxではコマンドラインの長さについては、 事実上制限はない(ないわけではない。後で制限のある例を出す)。だから、コ ンパイルもアセンブル(gccはgasというアセンブラソースを吐く)もリンクも全 てccで行うことが基本である。もちろんそれぞれのステップを個別に行うこと もないわけではないが(例えばccの吐いたアセンブラをsed等で加工するとか)、 それでもgasやcc1やldを直接使うことは少なく、みなccのオプションで行うこ とが多い。

以下では簡単にgccのコマンドラインについて説明しよう。gccはこれ以外にも 豊富なコマンドオプションできめ細かく色々なことが指定出来るが、一応これ くらいを知っておけば普通のプログラムを書くのには不自由しないはずである。

  1. 生成するファイルを決定するもの
    -c
    

    cppとcc1とgasを実行してオブジェクトファイルを生成する。この時、特にファ イル名を指定しなければ、ソースファイル名の.cを.oに変えたものになる。

    -E
    

    cppだけ実行する。この時、特にファイル名を指定しなければ、標準出力に結 果が出力される。

    -S
    

    cppとcc1を実行してgasのソースファイルを生成する。この時、特にファイル 名を指定しなければ、ソースファイル名の.cを.sに変えたものになる。gasの 勉強をするのに有効かも知れない。

    -M
    

    makefileで使うための依存関係の雛型を生成する。この時、特にファイル名を 指定しなければ、標準出力に結果が出力される。どのように使うかは、make depを必要とするプログラムのMakefileあたりを見るとわかるだろう。

    (なし)
    

    cpp, cc1, gas, ldを実行して、実行形式ファイルを生成する。この時、特に ファイル名を指定しなければ、a.outという名前のファイルを生成する。

  2. ファイル名の指定に関するもの
    -o <ファイル名>
    

    出力ファイル名を指定する。

    (なし)
    

    入力ファイル名を指定する。入力ファイル名の拡張子として許されるものは、. o(オブジェクトモジユール) .c(Cのソース) .s(gasのソース) .a(ライブラリ) 等があり、それぞれはそれぞれの意味に応じて処理される。

    -I <ディレクトリ名>
    

    cppがinclude処理する時のincludeファイルのあるディレクトリを指定する

    -L <ディレクトリ名>
    

    ldが結合するライブラリのあるディレクトリを指定する。

    -l<ライブラリ名>
    

    結合するべきライブラリを指定する。実際のライブラリのファイル名は`lib' という文字列が前置されたものとなる。普通にccでリンクまで行う場合は、標 準ライブラリであるlibcは自動的に指定される。

  3. その他
    -O<レベル>
    

    最適化の指定。Oの後に数字を書くことによって、レベルの指定が出来る。一 般にこの数字は大きい程高いレベルということであるが、3くらいが有効な数 値の上限である。もっとも大きだけなら、上限を越えた分だけ無視するので、 「大は小を兼ねる」ということで、6とか9とか指定しても害はないし、将来よ り高度な最適化がされるようになった時に良いかも知れない。

    最適化に関しては、-fによるより細かい指定も可能であるが、ここでは割愛す る。

    -W<レベル>
    

    ワーニングを出すレベルの指定である。いろいろ細かい指定が可能であるが、 一番厳しいのは-Wallであり、これはlintのように厳しいチェックを行う。移 植、特に古くに書かれたプログラムを移植する場合は、-Wは指定しない方が無 意味な警告を見ることが少なくて良いだろう。また、自分で新規にプログラム を書く場合には、-Wallを指定して細かいワーニングにも目を通しておくとデ バッグの助けになるだろう。

    -D<シンボル>
    

    #defineでシンボルを定義したのと同じに働く。

ccの他によく使うコンパイルツールとしてmakeがあるが、御存知のように Linuxでの標準はGnu Makeである。Gnuの例にもれず、かなり独自の拡張がされ ている。しかし、基本的な部分に関してはMS make等と大差ないので、これは 特に気にする必要はないだろう。必要なことがあれば、またあらためて説明し よう。

実行環境

コンパイルした結果を実行する分には特別に説明するべきことはない。ちょっ と特別だと思われるのは、-oで出力ファイル名を指定しなかった場合、生成さ れる実行ファイルはa.outという名前になるということくらいである。だから、

    $ gcc hello.c

としてコンパイルした場合、実行するには、

    $ a.out

として実行する。これを

    $ hello

として実行するためには、既に説明したように、

    $ gcc hello.c -o hello

というようにコンパイルすれば良い。

いずれの場合でも、「ダイナミックリンク」としてリンクされているので、実 行時に標準ライブラリに対応した「ダイナミックリンクライブラリ」を結合し て動く。このダイナミックリンクライブラリの実体は、/lib等にある。ではコ ンパイル時のリンクは何をしているかと言えば、「ダイナミックリンクライブ ラリを呼び出すという処理」をリンクしているのである。この「呼び出す処理」 は処理そのものの実体と比べて小さいため、ダイナミックリンクを使った実行 バイナリは、スタティックリンクを使った実行バイナリと比べて小さくて済む。

歴史的な事情から、Linuxの実行バイナリには、いわゆる「a.out形式」と 「ELF形式」とがある。かつては「a.out形式」しかなかったのであるが、ライ ブラリ開発上の問題や実行効率等の問題から、「ELF形式」に移行した。この ELF形式は、実行バイナリだけの形式ではなく、汎用バイナリ形式で、オブジェ クトモジュールもELF形式で作ることが可能である。これらの形式自体につい ては、特に知る必要はないのではあるが、どうしても知りたいと思う人は、 /usr/incluce/linuxにa.out.hとelf.hというファイルがあるので、それを見る とヘッダの内容くらいはわかるだろう。

通常の設定のカーネルでは、いずれの形式でも実行可能であるが、現在の世の 中の主流はELF形式であるので、最近の環境では特に何も指定しなければELF形 式のバイナリを作るようになっている。どうしてもa.out形式にしたい場合は、 出来ないこともないが、ELF形式とa.out形式が混在しても良いことは何もない ので、絶対にやらないことをお勧めする。基本的にa.out形式はa.out形式のバ イナリしか配布されていないプログラムを仕方なしに動かすためにあるものだ と思っておいた方がいい。昔の本連載では「ELF形式はまだ不安定」というこ とを言っていたのだが、今は「基本はELF形式」になっている。

いずれの形式でも、ダイナミックリンクを使うようにバイナリを作ると、当然 のことながらダイナミックリンクライブラリを必要とする。前の方で説明した ように、ダイナミックリンクを使った方がバイナリが小さいし、その結果プロ グラムの起動も高速になる等のメリットが大きいのであるが、ダイナミックリ ンクライブラリが使えない状態になっている時には動かすことが出来ない。そ のためダイナミックリンクライブラリを操作するようなコマンドはダイナミッ クリンクを使わないようにリンクしておくのが無難である。また、ダイナミッ クリンクを使うと、実行時のメモリ空間の様子の把握が難しくなるので、メモ リ空間の操作が必要なプログラムは、ダイナミックリンクでは使えないことも 少なくない。これらのような事情でダイナミックリンクを使わないためには、 コンパイルオプション(正確にはldに渡るのでリンクオプション)として、

        -static

を使用すると、スタティックリンクでバイナリを生成するようになる。

Linuxの実行される空間は、いわゆる「多重仮想空間」になる。これは、プロ セス毎に完全に独立した空間が割り当てられ、カーネルやダイナミックリンク のための領域は、それぞれの空間に写像されて存在する(実体そのものは別に ある)。このことは、各プロセスはその空間の中では他のプロセスのことを考 慮する必要がないということである。

このことは、スタックやヒープについては、空間を全部使ってしまうか、全体 の仮想空間を全部使ってしまうまでは、いくらでも成長させることが出来ると いうことである。MS系OSの上ではMS LINKでリンクする時には、「実行時のス タックの大きさ」を指定して、あらかじめスタックのための領域を予約してや る必要があったのだが、Linuxではその心配をする必要がないということであ る。だからLinuxのldには実行時のスタック領域の大きさを指定してやるオプ ションは存在しない。また、無限に近い大きさのスタックが使えるので、 MS-DOSでの制約であった「auto変数はやたらに大きく取れない」ということは ないし、関数の再帰呼び出しも、そう邪悪なテクニックではない。

特別な細工をしない限り、プログラムのコード領域は読み出し専用の領域とな る。だから、仮にポインタが異常になったとしても、コード領域が書き替えら れることはない。仮に書き替えようとすれば、保護例外ということになり、当 該プロセスはカーネルによって殺されることになる。このことはマトモなOSを 使う場合は「あたりまえ」のことではあるが、例えば、

    char    *str = "abcdefg";
          ...
    str[2] = 'C';

といったコードは書けないということになる。なぜなら、この例での "abcdefg"という文字列はコード領域に生成されるため、strに入っているポイ ンタはコード領域を指すことになる。そのためstr[2]は書き替えることの出来 ない領域になってしまうのである。このようなプログラムは領域保護機構のな いMS-DOSでは問題なく動いていたし、文字列をコード領域ではなくデータ領域 に作るようなccの場合も問題なく動いていた。しかし、Linuxとgccの組み合わ せでは一般にこのようなことは出来ないので、このようなプログラムをどうし ても動かしたい場合は、

    -fwaritable-strings

といったオプションを指定して、データ領域に文字列を作るようにしてやる必 要がある。


コラム
Gnu Hello

Cで最初に書くプログラムは一般には、Hello Worldと呼ばれる、

#include 
extern  int
main(
    int     argc,
    char    **argv)
{
    printf("Hello World!\n");
}

であろう。この誰でも即興で書ける簡単なプログラムに、何と`Gnu版'という ものが存在しているのである。手元にある最新はVer 1.3となっている。 ChangeLog(更新履歴)を見ると、一番最初は

Sat Apr  1 00:27:19 1978  Brian Kernighan  (bwk at research)

    * Initial version.

ということでカーニハンが作ったことになっており、その後

Thu Nov 24 00:00:01 1983  Richard M. Stallman  (rms at prep)

    * Begin GNU project; add copyleft.

ということで、ストールマンによってcopyleftにされたことがわかる。

これだけ見ると、単なる高級なお遊びのようにも見えるが、「それだけ」では ないのである。

アーカイブを展開すると、随分とたくさん(21個)のファイルが出来る。そこに はGnuソフトには当然ついているCOPYING等の「お約束」のファイルもあるし、 いくつかのソースファイルもあれば、TeXInfoによるドキュメントもある。さ らにはconfigureによる自動設定まである。だから、「たかがHello World」と 言えど、かなりしっかりと揃っている。

まぁここまで見ても、やはり「高級なお遊び」的ではあるのだが、中身をよく 読んでみよう。これは最も基本的なGnuソフトウェアのセットなのである。さ らに詳しくソースを読むと、getoptというコマンドライン解析手続きのサンプ ルであった。また同時に、Gnuスタイルのアーカカイブの作り方や、コーディ ングスタイルのサンプルとなっている。

よく考えてみると、`Hello World'は、「Cのプログラムの基本的なものはこ うだよ」といったことを示すサンプルという意味がある。そういったことを考 えると、このGnu Helloは「Gnuソフトウェアを作る時の基本はこうだよ」とい うことを示すサンプルなのかも知れない。実際にそのような作りになっている。 たかがHello Worldであるが、なかなかあなどりがたい。


もどる