リンクは、gcc コンパイル プロセスの最終段階です。
リンク プロセスでは、オブジェクト ファイルがリンクされ、外部シンボルへのすべての参照が解決され、最終アドレスが関数呼び出しに割り当てられます。
この記事では、主に gcc リンク プロセスの次の側面に焦点を当てます。
<オール>
この記事を読む前に、C プログラムが実行可能ファイルになる前に通過しなければならない 4 つの段階 (前処理、コンパイル、アセンブリ、およびリンク) をすべて理解していることを確認してください。
オブジェクト ファイルのリンク
例を通して、この最初のステップを理解しましょう。まず、次の main.c プログラムを作成します。
$ vi main.c #include <stdio.h> extern void func(void); int main(void) { printf("\n Inside main()\n"); func(); return 0; }
次に、次の func.c プログラムを作成します。ファイル main.c で、キーワード「extern」を使用して関数 func() を宣言し、この関数を別のファイル func.c で定義しました。
$ vi func.c void func(void) { printf("\n Inside func()\n"); }
以下に示すように、func.c のオブジェクト ファイルを作成します。これにより、現在のディレクトリにファイル funco.o が作成されます。
$ gcc -c func.c
以下に示すように、同様に main.c のオブジェクト ファイルを作成します。これにより、現在のディレクトリにファイル main.o が作成されます。
$ gcc -c main.c
次のコマンドを実行して、これら 2 つのオブジェクト ファイルをリンクし、最終的な実行可能ファイルを生成します。これにより、現在のディレクトリに「main」ファイルが作成されます。
$ gcc func.o main.o -o main
この「メイン」プログラムを実行すると、次の出力が表示されます。
$ ./main Inside main() Inside func()
上記の出力から、2 つのオブジェクト ファイルを最終的な実行可能ファイルに正常にリンクできたことは明らかです。
関数 func() を main.c から分離して func.c に記述した結果、何が得られましたか?
答えは、関数 func() も同じファイルに書いたとしても、ここでは大した問題にはならなかったかもしれませんが、数千行のコードを持つ非常に大きなプログラムを考えてみてください。 1 行のコードを変更すると、ソース コード全体が再コンパイルされる可能性があり、ほとんどの場合、これは受け入れられません。そのため、非常に大きなプログラムは、最終的にリンクされて実行可能ファイルを生成する小さな断片に分割されることがあります。
makefile で機能する make ユーティリティは、どのソース ファイルが変更され、どのオブジェクト ファイルを再コンパイルする必要があるかをこのユーティリティが認識しているため、これらの状況のほとんどで役立ちます。対応するソースファイルが変更されていないオブジェクトファイルはそのままリンクされます。これにより、コンパイル プロセスが非常に簡単で管理しやすくなります。
これで、2 つのオブジェクト ファイル func.o と main.o をリンクすると、gcc リンカが func() への関数呼び出しを解決でき、最終的な実行可能な main が実行されると、printf() が表示されることがわかりました。実行中の関数 func() 内。
リンカーは関数 printf() の定義をどこで見つけましたか?リンカはエラーを出さなかったので、確実にリンカが printf() の定義を見つけたことを意味します。 printf() は、stdio.h で宣言され、標準の「C」共有ライブラリ (libc.so) の一部として定義される関数です。
この共有オブジェクト ファイルをプログラムにリンクしていません。それで、これはどのように機能しましたか? ldd ツールを使用して、各プログラムに必要な共有ライブラリ、またはコマンド ラインで指定された共有ライブラリを表示します。
「main」実行可能ファイルで ldd を実行すると、次の出力が表示されます。
$ ldd main linux-vdso.so.1 => (0x00007fff1c1ff000) libc.so.6 => /lib/libc.so.6 (0x00007f32fa6ad000) /lib64/ld-linux-x86-64.so.2 (0x00007f32faa4f000)
上記の出力は、メインの実行可能ファイルが 3 つのライブラリに依存していることを示しています。上記の出力の 2 行目は「libc.so.6」(標準の「C」ライブラリ) です。これは、gcc リンカが printf() への関数呼び出しを解決できる方法です。
最初のライブラリはシステム コールを作成するために必要であり、3 番目の共有ライブラリは実行可能ファイルに必要な他のすべての共有ライブラリをロードするライブラリです。このライブラリは、実行のために他の共有ライブラリに依存するすべての実行可能ファイルに存在します。
リンク中、gcc が内部的に使用するコマンドは非常に長いですが、ユーザーの観点からは、記述するだけで済みます。
$ gcc <object files> -o <output file name>
コードの再配置
再配置は、リンク時または実行時に埋められるバイナリ内のエントリです。典型的な再配置エントリは次のように述べています:‘z’ の値を見つけて、その値をオフセット ‘x’ の最終的な実行可能ファイルに入れます
この例では、次の reloc.c を作成します。
$ vi reloc.c extern void func(void); void func1(void) { func(); }
上記の reloc.c では、関数 func() を宣言しましたが、その定義はまだ提供されていませんが、その関数を func1() で呼び出しています。
以下に示すように、reloc.c からオブジェクト ファイル reloc.o を作成します。
$ gcc -c reloc.c -o reloc.o
以下に示すように、readelf ユーティリティを使用して、このオブジェクト ファイル内の再配置を確認します。
$ readelf --relocs reloc.o Relocation section '.rela.text' at offset 0x510 contains 1 entries: Offset Info Type Sym. Value Sym. Name + Addend 000000000005 000900000002 R_X86_64_PC32 0000000000000000 func - 4 ...
func() のアドレスは、reloc.o を作成する時点では不明であるため、コンパイラは R_X86_64_PC32 型の再配置を残します。この再配置は、「最終的な実行可能ファイルのオフセット 000000000005 にある関数 func() のアドレスを埋める」ことを間接的に示しています。
上記の再配置は、オブジェクト ファイル reloc.o の .text セクションに対応していました (さまざまなセクションを理解するには、ELF ファイルの構造を理解する必要があります)。それで、objdump ユーティリティを使用して .text セクションを逆アセンブルします。
$ objdump --disassemble reloc.o reloc.o: file format elf64-x86-64 Disassembly of section .text: 0000000000000000 <func1>: 0: 55 push %rbp 1: 48 89 e5 mov %rsp,%rbp 4: e8 00 00 00 00 callq 9 <func1+0x9> 9: c9 leaveq a: c3 retq
上記の出力では、オフセット '5' (開始アドレス 0000000000000000 に対する値 '4' を持つエントリ) には、関数 func() のアドレスで書き込まれるのを待っている 4 バイトがあります。
そのため、関数 func() の保留中の再配置があり、関数 func() の定義を含むオブジェクト ファイルまたはライブラリに reloc.o をリンクすると解決されます。
この再配置が解決されるかどうか試してみましょう。 func() の定義を提供する別のファイル main.c を次に示します。
$ vi main.c #include<stdio.h> void func(void) // Provides the defination { printf("\n Inside func()\n"); } int main(void) { printf("\n Inside main()\n"); func1(); return 0; }
以下に示すように、main.c から main.o オブジェクト ファイルを作成します。
$ gcc -c main.c -o main.o
reloc.o を main.o にリンクし、以下に示すように実行可能ファイルを作成してみてください。
$ gcc reloc.o main.o -o reloc
objdump を再度実行して、再配置が解決されたかどうかを確認します。
$ objdump --disassemble reloc > output.txt
実行可能ファイルには非常に多くの情報が含まれており、stdout で迷子になりたくないため、出力をリダイレクトしました。
output.txt ファイルの内容を表示します。
$ vi output.txt ... 0000000000400524 <func1>: 400524: 55 push %rbp 400525: 48 89 e5 mov %rsp,%rbp 400528: e8 03 00 00 00 callq 400530 <func> 40052d: c9 leaveq 40052e: c3 retq 40052f: 90 nop ...
4 行目で、先ほど見た空のアドレス バイトが関数 func() のアドレスで埋められていることがはっきりとわかります。
結論として、gcc コンパイラーのリンクは 1 回の記事ではカバーしきれないほど大きな海です。それでも、この記事では、リンク プロセスの最初のレイヤーを剥がして、さまざまなオブジェクト ファイルをリンクして実行可能ファイルを生成することを約束する gcc コマンドの下で何が起こるかを理解できるようにしました。