C言語によるプログラミングに関する世界観5(プリプロセッサとインラインアセンブラ)です。
ツール関係はC/C++ツールに移動しました。
システムに依存する部分はLinux(システムコール・API)やWindowsプログラミングも参照のこと。
プリプロセッサは機械的に文字列の変換処理がされるマクロのことで、#defineや#ifdefのように「#」から始まる。これはコンパイル前にプリプロセッサによって処理されて純粋なC言語のソースコードに変換される。
基本的に#define(定義)、#ifdef(定義されているかどうか)、#include(別ファイルを含む)を知っていれば良い。
これらは、主にWindowsとLinux向けのコードを分けて書くことでクロスプラットフォームに対応したり、ある種の定数(変数展開せずマクロで使いたい場合など)のように使うことが多い。
Linuxヘッダファイルも参照のこと。
gccには、定義済みマクロと呼ばれるものがある。自分で#defineしなくても、勝手に#defineされる。
たとえば、OS別にコードを分けるのであれば、#ifdef _WIN32 ~ #endifまたは#ifdef __linux__ ~ #endifとし、ヘッダファイルなどを#includeするようにすれば、Windows環境とLinux環境で、そのコードをビルド時に有効にすることができる。
つまり、
#ifdef _WIN32 #include <windows.h> #endif int main(int argc, char *argv[]) { #ifdef _WIN32 /*Windows向けのコード*/ #endif #ifdef __linux__ /*Linux向けのコード*/ #endif }
これでよし。
詳しくは以下のページが参考になる。
移植性も参照のこと。
printf()デバッグやトレースログを取りたい場合など、デバッグ時のみコードを有効にしたい場合は、
#ifdef DEBUG /*デバッグ時のみ処理する内容*/ #endif
とした上で、gccに-Dオプションをつけてマクロを有効にしてビルドします。
$ gcc -DDEBUG -o hoge hoge.c
詳しくは以下のページが参考になります。
デバッグも参照のこと。
プリプロセッサを上手く使うことで、マクロ的な置換を行うこともできる。C++のテンプレートを使わなくても、マクロを使うことで、全ての型に汎用的な置換を簡単に実現できる。
しかしながら、()の使い方に注意。(T * T)に対してx + 1を使うと、x + 1 * x + 1と置換されてしまい、1 * xが最初に演算されてしまう。注意して使おう。
また、簡単な定数(たとえば円周率をPIとする)のにも使えるし、includeもよく使うが、ほかにはシステムごとに別の定義を行う(SYSVとBSDとMS-DOSで別の定義を使う)などといった用途に使うこともできる。
C++(ジェネリック)も参照のこと。
C/C++ではヘッダファイルなどに関数のプロトタイプ宣言を行うが、この時プリプロセッサを使ったマクロを用いることで、ANSI C以前のK&R Cのプロトタイプ宣言のスタイルにも同時に対応させることができる。
詳しくはオペレーティングシステム―設計と理論およびMINIXによる実装(僕が読んだのは第二版)のMINIXソースコードに書かれているので参考にしてほしい。
プリプロセッサは、このように、ヘッダファイルなどを「環境によって別々に読み込む」際などに使われることが多い。Windowsの場合とUNIXの場合に、別々のコードブロックやヘッダファイルを読み込むためによく使われる。
GCCのインラインアセンブラは、
asm(実際の処理を記述するアセンブリテンプレート : 出力用オペランド : 入力用オペランド : 破壊されるワークレジスタのリスト);
となる。ここで、アセンブリテンプレート(文字列リテラルで書く)以外はオプション。オペランドはアセンブリ言語で使われるデータ、すなわちレジスタのことで、さまざまな制約をかけられる。なので、
int add_asm (int x, int y) { int result = 0; asm ("movq x,%rax"); asm ("movq y,%rbx"); asm ("add %rbx,%rax"); asm ("movq %rax,result"); return result; } int main () { int result = 0; result = add_asm(100, 200); printf("%d\n", result); }
のようにすればよい。GASの標準はAT&T構文なのでIntelとは右と左(ディスティネーションとソース)が逆になる。
ams()は__asm__()と書くこともできる。むしろasm()だといろいろと問題があることがあるため通常は__asm__()を使う。
また、上記の例では一行ごとにasm()を使っているが、以下のように繋げることもできる。
__asm__ ("movq x,%rax\n\t" "movq y,%rbx\n\t" "add %rbx,%rax\n\t" "movq %rax,result\n\t");
オペランド制約として、"r"を使うと、レジスタをC言語の変数の読み書きのために割り当てられる。また"m"を使うと、メモリに直接アクセスする。
また、制約修飾子として無し(読み込み専用)、=(書き込み専用)、+(読み書き)、&(早期破壊オペランド)がある。
__asm__ ("movq %0, %1" : "=r" (y) : "r" (x));
また、出力用・入力用オペランドとして書かれたレジスタ以外のレジスタの用途について、GCCはどのような用途で使われるかを知ることができない。レジスタの退避処理を行うために、ワークレジスタに破壊されるレジスタの一覧を書く必要がある。
そのほか、詳しくは以下が参考になる。以上の内容は以下を参考にして執筆した。
アセンブリ言語やGNUツールチェインを参照のこと。
Visual C++の場合は以下の記事が参考になる。