- コンパイルって一体どんな処理をしているの?
- コンパイルの途中で出てくるオブジェクトファイルって何?
- コンパイルに必要なGCCやCygwin、MinGWって何をするもの?
コンパイルとは一言で言えば「(複数の)Cソースからマイコンに書き込むための実行ソフトを作るもの」になります。
C言語ファイルはそのままマイコンに書き込んで動作をさせることが不可能なので、マイコンに書き込めるように実行ソフト(.hexや.motファイルなど)に変換する必要があります。そこで変換をする作業がコンパイルになるのです。
コンパイルには必要となるソフトや、実施している過程で生成されるファイルがいくつかあります。どの組み込みの現場でもコンパイルは実施するものの、意外と全体を通しての解説はなかったりします。
今回はコンパイルについて初心者向けに詳しく解説していきます。
コンパイル概要
早速ですが「コンパイルとはCソースからマイコンで動作する実行ファイルを作成するものである」というのは不正確です。
- コンパイル:Cソースをアセンブリに変換する
- ビルド:Cソースをプリプロセスやコンパイルを経て実行ファイルを作成する
コンパイルはあくまでもCソースからアセンブリに変換する作業を指すもので、実行ファイルを作成するのであればビルドと呼ぶのが正しいです。とはいえ昔からの慣例でコンパイルと言えば実質的にビルドのことを指すことが多く、ソフト開発の現場ではあまり意識して使い分けはされていません。Cソースからアセンブリを出力したい場面なんてほとんどないですからねぇ
オブジェクト/アセンブリ/Cソースの関係性について
コンパイル(ビルド)の一連の流れを説明する前にこれらのファイルの関係性について説明しておかなくてはなりません。
オブジェクトファイル(.o)
マイコンやCPUが実行できる、数値だけで記述されたプログラムファイル。
基本的には人間の目で理解できない。
アセンブリファイル(.s)
オブジェクトファイルに近い、人間でもある程度理解できるプログラムファイル。
昔はアセンブリを使ってプログラミングをしていた。このアセンブリファイルをアセンブルすることでオブジェクトファイルを生成することができる。
Cソースファイル(.c)
人間が読み書き可能な水準で書かれたプログラムファイル。
このCソースをコンパイルすることでアセンブリファイルを生成することができる。
プリプロセスの役割
Cソースコードをコンパイルする前の最初の段階で実行される処理です。
#include、#define、#if などのプリプロ命令を展開し、最終的に純粋なCコードを生成します。たとえば、#include "***.h" はそのヘッダファイルの中身をソースコードにそのまま挿入し、#define MAX 100 はコード中の MAX を 100 に置き換えます。また、条件付きコンパイル(例:#ifdef DEBUG)により、環境に応じたコードの有効・無効を制御することもできます。
以下に実例を示します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 |
#include "fancontrol.h" // 内部define定義 #define HEATVAL (int)40 //高温判定値 #define AFAN 0 //AFAN設定値 #define BFAN 1 //BFAN設定値 #define FANSEL AFAN //FAN選択(今回はAFANを選択) // グローバル変数の定義 unsigned char fanstate; //FAN状態 // スタティック変数の定義 static unsigned long fandrvtim; //FAN駆動時間 // 関数 // ============================== // main(メイン関数) // 引数:void // 戻り値: int // 機能: main関数 // ============================== int main(void){ if(roomtemp >= HEATVAL){ fandrive(); fanstate = DRIVE; fandrvtim++; } else{ fanstop(); fanstate = STOP; fandrvtim = 0; } #if FANSEL == AFAN statedisp_afan(); #elif FANSEL == BFAN statedisp_bfan(); #endif return 0; } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
#ifndef FANCONTROL_H #define FANCONTROL_H // 構造体定義 // 列挙体定義 enum state { DRIVE, STOP, PENDING }; // グローバル変数のextern宣言 extern int roomtemp; // 室温 // 関数プロトタイプの宣言 void fandrive(void); //FAN駆動 void fanstop(void); //FAN停止 void statedisp_afan(void); //AFAN状態表示 void statedisp_bfan(void); //BFAN状態表示 #endif // FANCONTROL_H // 多重インクルード防止のためのマクロ定義(終了) |
ここにmain.cとfancotrol.hというファイルがあります。
これをプリプロセスによって#includeや#defineを展開すると、以下のようなファイルが出来上がります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
enum state { DRIVE, STOP, PENDING }; extern int roomtemp; unsigned char fanstate; static unsigned long fandrvtim; int main(void){ if(roomtemp >= (int)40){ fandrive(); fanstate = DRIVE; fandrvtim++; } else{ fanstop(); fanstate = STOP; fandrvtim = 0; } statedisp_afan(); return 0; } |
プリプロセスが行われると#includeで指定されたヘッダファイルはCソースに取り込まれ展開されます。#defineで指定された値はコード中に置き換えられ、定義自体は削除されます。また#ifで有効/無効化された箇所は指示に従って残置/削除されます。
コンパイル
コンパイルはプリプロセスが終わったCソースを元に、アセンブリファイルを生成することです。
実際にコンパイルで出来上がるアセンブリファイルの例を以下に示します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 |
.file "main.c" .text .globl fanstate .bss .type fanstate, @object .size fanstate, 1 fanstate: .zero 1 .local fandrvtim .comm fandrvtim,8,8 .text .globl main .type main, @function main: .LFB0: .cfi_startproc pushq %rbp .cfi_def_cfa_offset 16 .cfi_offset 6, -16 movq %rsp, %rbp .cfi_def_cfa_register 6 movl roomtemp(%rip), %eax cmpl $39, %eax jle .L2 call fandrive@PLT movb $0, fanstate(%rip) movq fandrvtim(%rip), %rax addq $1, %rax movq %rax, fandrvtim(%rip) jmp .L3 .L2: call fanstop@PLT movb $1, fanstate(%rip) movq $0, fandrvtim(%rip) .L3: call statedisp_afan@PLT movl $0, %eax popq %rbp .cfi_def_cfa 7, 8 ret .cfi_endproc .LFE0: .size main, .-main .ident "GCC: (Debian 12.2.0-14) 12.2.0" .section .note.GNU-stack,"",@progbits |
アセンブル
アセンブルはコンパイルで出来上がったアセンブリファイルを元に、オブジェクトファイルを生成することです。
実際にアセンブルで出来上がるオブジェクトファイルの例を以下に示します。
1 2 3 4 |
7F 45 4C 46 02 01 01 00 00 00 00 00 00 00 00 00 01 00 3E 00 01 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 98 03 00 00 00 00 00 00 00 00 00 40 00 00 00 00 00 40 00 0C 00 0B 00 |
このオブジェクトを元に、このあとリンクを行うことで実行ファイルを作成していきます。Cソースを修正してもプリプロなどの加減でオブジェクトファイルが変わらないということは往々にしてあって、よく組み込みの現場でも「Cソースの修正をしましたがオブジェクトファイルは変わっていなかったので追加のテストは不要です」など言われたりします。
リンク
各Cソースから生成されたオブジェクトファイルを、このリンクという作業によりひとつにまとめ上げ、デバッグファイル(.elf)を作成します。
リンクにはリンカ(.ld)ファイルを使用して、どのようにオブジェクトファイルを結合させていくか指示を出します。
リンクで出来上がるデバッグファイルは、デバッグをするために余計な情報が色々と付加されているので、特別なソフトや書き込みツールがないとマイコンに書き込みができません。
ファイル変換
デバッグファイル(.elf)から余分な情報をすべて削ぎ落とし、最終的な実行ファイルを作り上げるのがこのファイル変換です。
使うマイコンによって実行ファイルは異なり、.hexファイルであったり.motファイルであったりします。
コンパイルに必要なGCC
- GCC:Cソースコンパイルをするためのソフト。GNUというプロジェクトチームにより開発された。
- Cygwin:GCCはUNIX環境でしか動かないので、UNIX環境を提供するもの①
- MinGW:UNIX環境を提供するもの②
- MSYS2:UNIX環境を提供するもの③
組み込みの現場でコンパイルを行うためにGCCやらCygwinをインストールさせられますが、それぞれの役割を解説します。
CソースのコンパイルはGCCというソフトで行うことが多いのですが、GCCはUNIX環境でしか動きません。そこでWindowsでもコンパイルができるようCygwinなどの仮想UNIX環境を提供するソフトを使って、そこでGCCを稼働させます。
Cygwinは古いソフトなので最近ではMinGWやMSYS2などが使われることも多いです。
なお組み込みの現場でよく「Cソースの改行コードはLFに統一しておくこと」と言われたりしますが、それはUNIXがLFの改行コードにしか対応しておらず、CソースがLFになっていないとコンパイル時にエラーが発生することがあるからなのです。最近はLFにしていなかったからといってエラーになることはほとんどないそうですけどね
以上がコンパイルの役割の解説となります。
コンパイルの流れを理解することは、単なる知識の習得にとどまらず、トラブルシューティングやコード最適化にも役立つ視点を得ることにつながります。
プリプロセッサやアセンブラ、リンカの働きを意識できるようになれば、ビルドエラーの原因にも迅速に対応できるでしょう。今回の解説が、より深いプログラミング理解の第一歩となれば幸いです。