迷える子羊さんから、プログラムがコンパイルできないと相談されたので、それをメモとして記述しておく
mathの関数を利用したらコンパイルが出来なかった
平方根を求めるsqrt関数を使うプログラムを書いたのだけど、エラーでコンパイルできないとの話
#include <stdio.h>
#include <math.h>
int main(){
double a = 2.0;
printf("sqrt(%f) = %f\n", a, sqrt(a));
}
これをコンパイルすると、こんなエラーになる
$ gcc sqrt.c
/tmp/cctNAE7m.o: In function `main':
test.c:(.text+0x23): undefined reference to `sqrt'
collect2: error: ld returned 1 exit status
ライブラリを指定しよう
-lm オプションを追加して、mathのライブラリをリンクするようにすると大丈夫とアドバイス
$ gcc sqrt.c -lm
$ ./a.out
sqrt(2.000000) = 1.414214
テキストで書かれたcのプログラムは
- ソースコードをオブジェクトコードにコンパイル
- オブジェクトコードを他のライブラリとリンク
- gcc コマンドで1と2が実行されている
- ライブラリをリンクする際に、リンク対象のライブラリを明示しなければならない
この2つがわかっていないと、どうして -lmが必要なのかわからず#include <math.h>
とちゃんと書いているのにどうしてコンパイルできないのか悩んでしまう
さらに言えば、#include <stdio.h>
の方はライブラリの指定が不要で、#include <math.h>
の方はどうしてライブラリの指定が必要なのか?この疑問はもっともだと思う
gccは何も指定しなくてもコンパイル時に標準ライブラリをリンクする設定となっている
printf という関数はcの標準ライブラリの中で定義されているので、-l
でライブラリを明示的に指定しなくても利用できる
簡単に確認してみよう
どのようなライブラリがオブジェクトコードにリンクされているかを見るために lddコマンドを利用してみる
sqrt.c の実行ファイルを ldd で確認する
$ gcc sqrt.c -lm
$ ldd ./a.out
linux-vdso.so.1 (0x00007fff305ba000)
libm.so.6 => /lib/x86_64-linux-gnu/libm.so.6 (0x00007fb1ee760000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007fb1ee36f000)
/lib64/ld-linux-x86-64.so.2 (0x00007fb1eed00000)
lddの出力の2行目のlibm.so.6
がmath ライブラリ
これを、-lm を利用していないものと比較してみる
#include<stdio.h>
int main(){
printf("Test\n");
}
そしてこれをコンパイル
$ gcc test.c
$ ldd ./a.out
linux-vdso.so.1 (0x00007fff80bf0000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007fc9ad348000)
/lib64/ld-linux-x86-64.so.2 (0x00007fc9ad93b000)
libm.so.6
がリンクされていない事がわかる
(0x00007fff80bf0000)等の部分のアドレスが異なる理由は、セキュリティーに関わる問題なのでここでは触れない
おまけとして
gcc
が標準でリンクしてくれるライブラリを利用しないようにすると、printf
も失敗するという事を確認してみる
#include<stdio.h>
int main(){
char code[] = "code";
printf("This is a test %s.\n", code);
}
これを普通にコンパイルして実行して、結果を確認する
$ ./a.out
This is a test code.
次は標準ライブラリをリンクさせずにコンパイルしてみる
そのために-nodefaultlibs
オプションを利用する
$ gcc -nodefaultlibs test.c
/usr/lib/gcc/x86_64-linux-gnu/7/../../../x86_64-linux-gnu/Scrt1.o: In function `_start':
(.text+0x12): undefined reference to `__libc_csu_fini'
(.text+0x19): undefined reference to `__libc_csu_init'
(.text+0x26): undefined reference to `__libc_start_main'
/tmp/cc0GNt3R.o: In function `main':
test.c:(.text+0x36): undefined reference to `printf'
test.c:(.text+0x4f): undefined reference to `__stack_chk_fail'
collect2: error: ld returned 1 exit status
printfが定義されていないとのエラーが表示されるている(他にもエラーが発生しているがここでは触れないでおく)
次にc
のライブラリを-lc
で指定してコンパイルして実行
$ gcc -nodefaultlibs test.c -lc
$ ./a.out
This is a test code.
今度は、エラーが起きずにプログラムのコンパイルと実行ができた
-lm を指定しなくても、sqrtが計算できる事があるんだけど
これでアドバイスは終わりかと思っていたが、次の敵が現れた
#include <stdio.h>
#include <math.h>
int main(){
printf("sqrt(2.0) = %f\n", sqrt(2.0));
}
この時は、-lm
オプションを指定しなくてもちゃんとコンパイルして実行できるのだけどと言う
$ gcc sqrt_fix.c
$ ./a.out
sqrt(2.0) = 1.414214
sqrt関数を利用する場合に、mathのライブラリが必要な場合とそうでは無い場合はどう違うの?運が良いとそのまま実行できるの?との疑問
もっともだと思う
そこで、コンパイルされた後のコードを確認してみよう
$ gcc sqrt.c -o sqrt -lm
$ objdump -S --disassemble ./sqrt
00000000000006ba <main>:
6ba: 55 push %rbp
6bb: 48 89 e5 mov %rsp,%rbp
6be: 48 83 ec 20 sub $0x20,%rsp
6c2: f2 0f 10 05 de 00 00 movsd 0xde(%rip),%xmm0 # 7a8 <_IO_stdin_used+0x18>
6c9: 00
6ca: f2 0f 11 45 f8 movsd %xmm0,-0x8(%rbp)
6cf: 48 8b 45 f8 mov -0x8(%rbp),%rax
6d3: 48 89 45 e8 mov %rax,-0x18(%rbp)
6d7: f2 0f 10 45 e8 movsd -0x18(%rbp),%xmm0
6dc: e8 af fe ff ff callq 590 <sqrt@plt>
6e1: 48 8b 45 f8 mov -0x8(%rbp),%rax
6e5: 66 0f 28 c8 movapd %xmm0,%xmm1
6e9: 48 89 45 e8 mov %rax,-0x18(%rbp)
6ed: f2 0f 10 45 e8 movsd -0x18(%rbp),%xmm0
6f2: 48 8d 3d 9f 00 00 00 lea 0x9f(%rip),%rdi # 798 <_IO_stdin_used+0x8>
6f9: b8 02 00 00 00 mov $0x2,%eax
6fe: e8 7d fe ff ff callq 580 <printf@plt>
703: b8 00 00 00 00 mov $0x0,%eax
708: c9 leaveq
709: c3 retq
70a: 66 0f 1f 44 00 00 nopw 0x0(%rax,%rax,1)
6dcのcallq 590 <sqrt@plt>
部分が sqrtを呼び出している部分
一方で、sqrt_fix.cの場合
$ gcc sqrt_fix.c -o sqrt_fix
$ objdump -S --disassemble ./sqrt_fix
000000000000064a <main>:
64a: 55 push %rbp
64b: 48 89 e5 mov %rsp,%rbp
64e: 48 83 ec 10 sub $0x10,%rsp
652: 48 8b 05 bf 00 00 00 mov 0xbf(%rip),%rax # 718 <_IO_stdin_used+0x18>
659: 48 89 45 f8 mov %rax,-0x8(%rbp)
65d: f2 0f 10 45 f8 movsd -0x8(%rbp),%xmm0
662: 48 8d 3d 9f 00 00 00 lea 0x9f(%rip),%rdi # 708 <_IO_stdin_used+0x8>
669: b8 01 00 00 00 mov $0x1,%eax
66e: e8 ad fe ff ff callq 520 <printf@plt>
673: b8 00 00 00 00 mov $0x0,%eax
678: c9 leaveq
679: c3 retq
67a: 66 0f 1f 44 00 00 nopw 0x0(%rax,%rax,1)
callq 520 <printf@plt>
は有るけれど、<sqrt@plt>
を呼び出すcallq
が存在しない
実は、gcc
コンパイラーがsqrtの計算をコンパイル時に行ってしまっていてsqrt関数を呼ぶ部分が無いのでエラーにならない
(これはcコンパイラーの実装に依存するので、どんな状況でもsqrt(固定値)をコンパイル時に計算するわけでは無い)
お試しのプログラムでsqrt(2.0)で決め打ちした時は、ライブラリの指定をせずにちゃんと動作したのに、sqrtの括弧の中を変数に変えたら動かなくなった事が今回の戸惑いの出発点だったかも
関数の中身が定数の時はgccが上手く処理してくれていたので余計困惑していたようだ