こんにちは、ナナです。
本記事では、皆さんが作成したソースコードから「実行ファイル」を製作するための、第3ステップである「リンカ」に焦点を当てます。
第1ステップのプリプロセッサ、第2ステップのコンパイラについて知りたい方は次の記事を見てください。
本記事では次の疑問点を解消する内容となっています。
では、リンカの役割を学んでいきましょう。
リンカ 役割と機能
私が今日呼ばれたということは、今日のテーマは「美しい筋肉を如何に作り出すか」という内容ですよね。是非、聴きたいです。
うん、違うよ。今日は「実行ファイルを如何に作り出すか」をテーマに「リンカ」について学ぶ内容だよ。
何かと何かを結びつけることを「リンクする」と表現します。Webのページもリンクで繋がっていますね。
「リンカ」とは「2つのものを結びつける者」という意味です。
リンカの主な役割は、グローバル変数や関数呼び出しの参照を結合する作業です。リンカが結合する作業のことを「リンク」と呼びます。
製造工程におけるリンカの位置づけ
作家が執筆する「原稿」はもちろん1枚ということはなくて、何枚も執筆しますよね。編集担当や校正・翻訳担当はこれら1枚1枚に対して修正やチェックを行い、次の製本作業に移ります。
書籍出版の製作工程
最後の製本の段階では、ページの順番を調整し、目次などを含めて完成した1冊の本として出版されます。
この「製本」の作業を、プログラムの製作工程で行うのが「リンカ」の仕事です。
プログラムの製作工程
皆さんもこれからC言語のソースファイルを、いくつも作ることになります。
各ソースファイルはコンパイラによってオブジェクトファイルに変換されます。リンカではそれらを結合し1つの「実行ファイル」を作り出します。
皆さんはソースファイルという原稿を作る作家なんです。
いくつものソースファイルに書かれた関数同士が結びついて、最終的に1つの実行ファイルとなるのです。その1つにまとめる作業をするのが「リンカ」なんです。
リンクの仕事内容
リンカが担う大きな仕事は次の2つです。
リンカは関数の呼び出しを実現する非常に大切な機能です。
ここまで関数を呼べることが当たり前のように感じていたと思いますが、それは「リンカ」が活躍していたからなんです。
リンカ 関数と関数を繋ぐもの
リンカが関数同士を結びつけることで、プログラムが動くんですよね。筋肉も筋肉同士がつながり、連動することで体が動くんです。筋肉はリンカってことですか?
なるほど、筋肉が肉体の部品をつないでいると考えたら、確かに筋肉の役割はリンカなのかもしれないね。
リンカが関数の呼び元と呼び出し先を接続することで関数が呼べるようになるよ。その流れを学んでいこうね。
リンカは関数同士を繋いでくれます。関数を繋ぐとはどのようなことなのかを学びましょう。
リンカによる関数のリンク作業
次のプログラムは三角形の面積を取得し、画面に面積を出力するプログラムです。
このようにC言語のソースファイルは、システムの中で複数作られることはよくあることです。また、第3者が作成した関数がライブラリファイルとして提供されることもあるでしょう。
このようにバラバラに定義されている関数を接続し、1つの実行ファイルにまとめるリンカの作業を「リンク」と呼びます。
プログラムを動かしていると当たり前のように思いますが、関数を呼ぶことができるということは関数の呼び出し元と呼び出し先が繋がっているということです。
リンカによって関数呼び出しは可能になります。リンカ先生いつもありがとうございます。
リンクエラー
コンパイラによるコンパイルエラーと同様に、リンカもエラーを出力することがあります。
これを「リンクエラー」と呼びます。
リンクエラーでよくあるパターンは「未解決の外部シンボルがあります」というエラーです。
「シンボル」というのは関数やグローバル変数の名前のことです。
このエラーメッセージは「main関数から呼ばれているtriangre関数が見つからないため、関数呼び出し元と呼び出し先をリンクできませんでした」と指摘しているわけです。
このエラーは多くの場合、関数名の綴りを間違えた時に発生します。
皆さんは、このエラーに遭遇した時は次の2つをチェックしましょう。
C言語は英語の大文字・小文字は別のものとして認識します。このエラーが出た時は、名前が一言一句間違っていないかチェックしましょう。
リンカ シンボルをメモリに配置するもの
リンカのもう一つの仕事の「配置」って何をするんですか?私は全てのトレーニング機器を置く場所を決めてるんですよ。そうすると効率よく順番にトレーニングができるんです。
リンカにはみんなが作った関数やグローバル変数といったシンボルをどこのメモリに配置するかを決める役割があるんだよ。
「関数はここに全部まとめておこう」「変数はこのあたりにまとめておこう」みたいに、まとめることが管理するためには必要なんだね。
リンカにはメモリと密接に関連しており、関数や変数をどこのメモリに配置するかを決定する仕事があります。
皆さんはプログラムをするうえで、関数や変数が最終的にどこのメモリに配置されるのかを意識することはあまりありません。
しかし、プログラムがうまく動かないといった状況で「このメモリ番地で問題が起きているんだが、どこのどいつが使っているメモリなんだ?」みたいなシーンがたまに訪れます。
そんな時に「このメモリはこの関数に使わせてます」ということを決定しているのが「リンカ」なのです。
リンカオプション mapファイルの出力
mapファイルとは、関数やグローバル変数のシンボル情報を一覧表示したファイルのことです。
リンカというツールには、このmapファイルを出力する機能が備わっています。私の知る限りこの出力機能は、自分で有効にしない限り出力しません。
VisualStudioではメニューの[プロジェクト]-[プロパティ…]から設定することができます。
mapファイルの設定を有効にして再度「ビルド」を実行すると、プロジェクトフォルダの中に「(プロジェクト名).map」という拡張子のファイルが生成されます。
mapファイルの出力例
では、具体的にmapファイルがどのような形式で出力されるのかをお見せします。
main.cに次のようにmain関数、sub関数、numグローバル変数の3つを定義しました。
// グローバル変数
long num = 100;
// sub関数
void sub(void)
{
num = 5000;
}
// main関数
int main(void)
{
sub();
return 0;
}
出力されたmapファイルに、どのようなシンボル情報が出力されたかを見てみましょう。量が多いため一部抜粋しています。
Address Publics by Value Rva+Base Lib:Object
・・・
0002:00000760 __JustMyCode_Default 00411760 f i main.obj
0002:00000770 _main 00411770 f main.obj
0002:000007d0 _sub 004117d0 f main.obj
0002:00000830 ___local_stdio_printf_options 00411830 f i bank.obj
・・・
0003:00001dec ___rtc_tzz 00418dec MSVCRTD:initsect.obj
0004:00000000 _num 0041a000 main.obj
0004:00000004 ?_RTC_ErrorLevels@@3PAHA 0041a004 MSVCRTD:error.obj
・・・
「Address」は配置される番地、「Publics by Value」はシンボル名、「Lib:Object」には定義が存在するオブジェクトファイル名が出力されています。
このような情報を解析することで、発生した問題を解くヒントになることがあるのです。
このようなシンボル情報を解析する技術は、高度なものになります。私はベテラン技術者の技術力って、こういったことを知っていることにより支えられているように感じます。
今すぐには初心者では役に立たないと思うかもしれませんが、このような知識の積み重ねが技術力なんです。
Q&A:リンカに関するよくある質問
Q:未解決の外部シンボルのメッセージが出力された際に_mainとか_triangreなど関数名の前にアンダーバーが付いてるけどどうして?
関数名を間違えてリンクエラー出しちゃいました。でもその時気づいたんです。なぜか関数名の前にアンダーバーが付いているんです。なんですかこれは?
許せない余分な肉、それはぜい肉。私は全てを筋肉にしたいんです。
実はアンダーバーはコンパイラ担当の仕業でシンボルの先頭にアンダーバーを付けているんですよ。理由はすいません・・・僕も知りません。なんででしょうね。
これに気が付いた方は、非常によくエラーメッセージを解析している人です。
皆さんの作った関数をコンパイラが翻訳すると、アンダーバーを先頭に付けた名前として翻訳を行います。リンカはその名前を使って関数のリンクを行うため、リンクエラー時にアンダーバー付きの名前で指摘を行うのです。
このようなシンボルを見つけたらアンダーバーを除いた名前で皆さんは問題を解決するとよいでしょう。
mapファイルに出力されているシンボル名も、よく見るとアンダーバーが先頭に付いていますよね。
課題:リンカが学べたかを確認しよう
課題1
課題内容
次のプログラムをビルドするとエラーや警告が発生する。文法エラーを解消し出力期待結果が表示されるように修正せよ。エラーや警告は単純な文法的な問題である。
main.c
#include <stdio.h>
#define D_FRUIT_NAMESIZE (32);
typedef struct
{
char name[D_FRUIT_NAMESIZE];
long price,
} S_FRUIT
void fruitOutput(S_FRUIT fruit)
{
if (fruit.price > 0)
printf("名前:%s\n", fruit.name);
printf("価格:%d\n", fruit.price)
}
return 0;
}
int main(void)
{
S_FRUIT fruit = { "桃" , 400 };
fruit0utput(fruit);
return 0;
}
出力期待結果
名前:桃
価格:400
main.c
#include <stdio.h>
// セミコロンは不要
#define D_FRUIT_NAMESIZE (32)
// priceはカンマではなくセミコロン
// S_FRUITの後ろにセミコロンが必要
typedef struct
{
char name[D_FRUIT_NAMESIZE];
long price;
} S_FRUIT;
void fruitOutput(S_FRUIT fruit)
{
// {が不足している
// セミコロンが必要
if (fruit.price > 0)
{
printf("名前:%s\n", fruit.name);
printf("価格:%d\n", fruit.price);
}
// 戻り値は不要
return;
}
int main(void)
{
S_FRUIT fruit = { "桃" , 400 };
// 0(ゼロ)ではなくO(オー)
fruitOutput(fruit);
return 0;
}
コメントと共に明るくした場所が問題の場所ですね。コンパイルエラーとリンクエラーは上から順番に片っ端から片付けていきます。
この辺は慣れなので数多くこなすことで、対処スピードが上がっていきます。