ライブラリのリンクを忘れずに


迷える子羊さんから、プログラムがコンパイルできないと相談されたので、それをメモとして記述しておく

mathの関数を利用したらコンパイルが出来なかった

平方根を求めるsqrt関数を使うプログラムを書いたのだけど、エラーでコンパイルできないとの話

sqrt.c
#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のプログラムは

  1. ソースコードをオブジェクトコードにコンパイル
  2. オブジェクトコードを他のライブラリとリンク
  • 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が計算できる事があるんだけど

これでアドバイスは終わりかと思っていたが、次の敵が現れた

sqrt_fix.c
#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が上手く処理してくれていたので余計困惑していたようだ