C言語のプログラムはたとえばfoo.c
といった名前のファイルとして作成す
る。gcc
(GNU C compiler) でコンパイルして
foo
という名前の
実行形式ファイル(ユーザプログラム)を得るためには、シェルのコマンドライ
ンで:
gcc foo.c -o fooと指示すればよい。得られたユーザプログラムを「
-a 300
」という
引数で実行するには
./foo -a 300のように指示すればよい。最適化をコンパイラに指示するには:
gcc -O2 foo.c -o fooのようにする。(
-O2
の意味は、info や man などで調べること)
実際にはgcc
は、Cコンパイラ(プリプロセッサ付)、 アセンブラ、
リンカを用いて、Cプログラムのファイル→[Cコンパイラ]→ アセン
ブリ言語プログラムのファイル→[アセンブラ]→オブジェクトファイル→[リン
カ]→実行形式ファイルのようにして、ユーザプログラムを作成している。以下
では、この各段階を順にみていく。
C言語のプログラムfoo.c
を(プリプロセッサに通した後)コンパイルして
foo.s
というファイル名のアセンブリ言語プログラムに翻訳するには:
gcc -O2 foo.c -Sとする。出力される
foo.s
はテキストファイルであるので:
less foo.sなどとすればアセンブリ言語のプログラムを読むことができる。
アセンブリ言語は、ユーザプログラムの実行する計算機システムのプロセッサの
命令セットを基にしてに定められているため、少なくともプロセッサの種類ごと
に異なる(機種依存である)。本演習・実験で用いる計算機システムでは、Intel
386 プロセッサ(と浮動小数点コプロセッサ)の命令セットに基づくものになって
いる。
実際のプロセッサは何世代も後継の Intel Core2 Duoで、Core2に
基づくものになっているが、命令セットは同じと考えてもよい。
また,命令セットが同じでもプロセッサの世代が異なれば,性能の引き出し方も
異なるので,Core2 向けに最適化するとよい。
(計算機室のLinux端末のgcc にもCore2 向けに最適化するためのオプション
がある。-mtune=core2
がないかinfo gcc 等で調べてみる)
また、浮動小数点演算は後継で追加されている SSE3 等
を用いたほうがよいかもしれない.
(計算機室のLinux端末のgcc にもそのためのオプションがある。)
gcc foo.c -Sとすればコンパイラ最適化しない場合のアセンブリ言語プログラム foo.s が 得られるが, アセンブリ言語プログラムの出力ファイル名を指定し、かつ、最適化を指示する 場合は:
gcc -O2 foo.c -S -o foo-O2.sとすれば
foo-O2.s
というファイル名のアセンブリ言語の最適化されたプロ
グラムに翻訳できる。ここで、foo.s
と foo-O2.s
は内容的には同じ計算を行うアセンブリ言語のプログラムであるが、
通常、foo-O2.s
のほうが高速実行可能なプログラムになっている。
比較してみて、どのように最適化しているかを考えること。
最適化の程度が小さければ:
diff -u foo.s foo-O2.sとして比較できるかもしれないが、 実際にはプログラムは大きく異なるはずである。
C言語でプログラムを書くことのほうが多いといえるが、場合によってはアセン
ブリ言語でプログラムを書かなくてはならないこともある。その場合は、エディ
タなどでfoo.s
などのプログラムを作成することになる。アセンブリ言語
のプログラムは、テキスト表記の機械語命令を書くtextセクション、初期値を持
つデータを書くdataセクション、初期値を持たないデータ領域確保するbssセク
ションなどから構成される。(bssなどの名前はシステム依存であり、計算機室の
gccでは少し異なる名前を用いているかもしれない)
アセンブリ言語プログラムのファイルから、アセンブラによりオブジェクトファ
イルfoo.o
を得るには:
gcc foo.s -cとする。出力ファイル名を指定したければ、これまで同様「
-o 出力ファイ
ル名
」を指定すればよい。この段階では、テキスト形式からバイナリ形式へと
翻訳される。特にアセンブリ言語プログラムのtextセクションは、実際にプロセッ
サが解釈実行できるバイナリ形式の機械語プログラムに翻訳される。なお、
foo.s
の代わりに
foo.c
を指定すれば、コンパイルとアセンブルが行われ
て、foo.o
が得られる。翻訳結果は:
od -t x1 foo.o | lessでダンプできるが、人が読むようなものではないといえる。 (ただし、objdumpコマンドで見ればもっとわかりやすい表示も得られる。) アセンブラによるアセンブルは基本的には1対1対応の素直な翻訳なので、 人が考えるときはアセンブリ言語プログラムで考えればほとんどの場合は 十分である。
複数のオブジェクトファイルをリンクすることで、実行形式が得られる。
foo.c
とbar.c
に分割してプログラムを開発したとし、
foo.c
から翻訳したfoo.o
と
bar.c
から翻訳したbar.o
を元に
foo
という名前の実行
形式ファイル(ユーザプログラム)を得るには:
gcc foo.o bar.o -o fooとすればよい。コマンドラインでは指定されていないが、実際には、
crt
何とか.o
といったスタートアップ用オブジェクトファ
イルや標準ライブラリに含まれるサブルーチンとともにリンクして実行形式を得
ている。リンクに用いるファイルリストなどを見るにはgcc
とする代わり
にgcc -v
としてやるとよい。リンク時には、分割コンパイルなどの理由
で別のオブジェクトファイルであったために、最終的なアドレスが不明としてシ
ンボルで参照していた部分を結合後の最終的な実行形式中の具体的なアドレスで
置き換える。(ただし、ダイナミックリンクの場合はシンボルが残った実行形式
となり、ユーザプログラムの実行時に必要に応じてリンクされる) これにより、
別のオブジェクトファイルが確保するデータに対してアクセスしたり、別のオブ
ジェクトファイルが持つ機械語命令に制御を移動できるようになる。
最終的に得られた実行形式のユーザプログラムのことも機械語プログラムと呼ぶ ことがある。ユーザプログラムのtextセクションはプロセッサが解釈する機械語 命令から構成されているが、dataセクションなどのデータをプロセッサが解釈実 行することは滅多にない。ユーザプログラムのdataセクションやbssセクション についてはプロセッサに対する指示ではなく、むしろプログラムの実行を管理す るオペレーティングシステムに対して、textセクションの機械語プログラムの実 行開始前に必要なメモリ領域を確保・初期化するように指示していると考えるこ とができる。dataセクションやbssセクションで、データの初期値や領域確保を するためのアセンブリ言語上の指示は、機械語命令とは異なるためしばしば 擬似命令と呼ばれる。
さて、Cプログラムの作成時に、実行時の状態を考える場合は、
call
命令でスタックにpushする形なら取り出すことができる。
また、スタックトップの値をret命令でpopしてcall
命令の直後から実行
を再開(pcにセット)できるようになっている。
上記のようなCプログラムレベル、あるいは、ユーザプログラムレベルの実行時
の状態はgdb
という デバッガでみることができる。
gdb
で、C
レベル(ソースレベル)のデバッグをしたい場合は、gcc
とする代わりに
gcc -g
としてやるとよい。例えば:
gcc -O2 foo.c -o fooの代わりに、
gcc -g -O2 foo.c -o fooとして、デバッグ情報付きのユーザプログラムを得る。デバッグオプション (
-g
)と、最適化オプション(-O2
)は同時に指定できないコンパイラ
も多いが、gcc の場合は同時に指定できるように頑張っている。こうして得た
ユーザプログラムをデバッグするには
gdb ./fooとし、デバッグ対象プログラムを「
-a 500
」
という引数で実行開始するには gdbのプロンプトに対して:
run -a 500とする。gdb のプロンプトに対しては、ブレークポイント設定(
b
)、実行
継続(c
)、
ステップ実行(s
)などを指示することができる。
また、ブレークポイントやエラーにより
一時停止中のプログラムのソースレベルの実行時の状態は次のコマンドで調べられる:
bt
コマンドで
現在の実行場所とリターン後の再開場所が表示される。
プログラムリストはlist
コマンドで表示できる。
info args
で引数、
info locals
で局所変数の値が表示される。
またp
コマンドではCの式の値を表示できる。
info program
で、いまどこの命令を実行中であるかが表示される。
機械語命令列はdisassemble
コマンドでアセンブリ言語として
表示できる。
info reg
コマンドで調べられる。
x
コマンドで調べられる。
実際に動作するのはユーザプログラムであるので、gdbは、ユーザプログラムレ ベルの実行時状態しか情報として取り出せないが、コンパイル時に付加したデバッ グ情報を元にソースレベルの実行時状態のイメージを仮想的に作り出しているこ とになる。場合によっては、ユーザプログラムレベルではソースレベルの情報 (変数の値など)が残っていないことがあるので注意が必要である。上記コマンド を各自試して、ソースレベルの実行時状態とユーザプログラムレベルの実行時状 態について感覚を養うこと。
コンパイラの仕事は、Cプログラムを実行形式ユーザプログラムに翻訳すること であった。高速なプログラムの実行のためには、コンパイラはできるだけ 効率の良い機械語コードを生成する必要がある。中でも重要なのは、できるだ け レジスタを利用して計算を進めることにある。 メモリアクセスは レジスタへのアクセスよりも時間がかかるのが一般的だからである。Cプログラ ムにおける引数や局所変数の値、あるいは部分式の中間結果の値などは、できる だけ(スタック領域などの)メモリに置かず、レジスタ上に保持するようにする ( レジスタ割付)。この他、コンパイラが行う最適化手法には次のものがあ る:
最適化手法は、プログラムの計算内容(意味)を変えない範囲で(安全に)高速化を 行うものなので、その適用には条件を伴う。場合によっては、プログラムの作成 者にとっては安全であることが自明な最適化がなされないことも多い。そのよう な場合は、Cプログラムを修正することで最適化されるようなることも多い。し かし、修正後のプログラムが読みにくく保守しにくいようになるのであれば、そ こには速度と間にトレードオフが存在することになる。本演習ではCプログラム を工夫して計算をより速く実行するにはどうしたらよいかを考えるが、レジスタ 割付や最適化手法を考慮することで達成できる部分もある。
場合によってはC 言語による表現では、意図したユーザプログラムの生成が難し
いこともある。機種依存であるが、C言語ではasm
文を用意し、Cの関数の
本体の文としてアセンブリ言語の命令を埋め込めるようにしてもよいことになっ
ている。asm
文をサポートしないコンパイラもあるが、gcc では拡張した
asm
文が提供されていて、意図どおりのユーザプログラムの生成を行いや
すくなっている。