GNU/Linux >> Belajar Linux >  >> Linux

Jelajahi Proses Penautan GCC Menggunakan LDD, Readelf, dan Objdump

Penautan adalah tahap akhir dari proses kompilasi gcc.

Dalam proses penautan, file objek ditautkan bersama dan semua referensi ke simbol eksternal diselesaikan, alamat akhir ditetapkan ke panggilan fungsi, dll.

Dalam artikel ini, kami terutama akan berfokus pada aspek-aspek berikut dari proses penautan gcc:

  1. File objek dan bagaimana mereka dihubungkan bersama
  2. Relokasi kode


Sebelum Anda membaca artikel ini, pastikan Anda memahami semua 4 tahap yang harus dilalui program C sebelum menjadi executable (pra-pemrosesan, kompilasi, perakitan, dan penautan).

MENAUTKAN FILE OBYEK

Mari kita pahami langkah pertama ini melalui sebuah contoh. Pertama buat program main.c berikut.

$ vi main.c
#include <stdio.h> 

extern void func(void); 

int main(void) 
{ 
    printf("\n Inside main()\n"); 
    func(); 

    return 0; 
}

Selanjutnya buat program func.c berikut. Dalam file main.c kami telah mendeklarasikan fungsi func() melalui kata kunci 'extern' dan telah mendefinisikan fungsi ini dalam file func.c

$ vi func.c
void func(void) 
{ 
    printf("\n Inside func()\n"); 
}

Buat file objek untuk func.c seperti yang ditunjukkan di bawah ini. Ini akan membuat file func.o di direktori saat ini.

$ gcc -c func.c

Demikian pula buat file objek untuk main.c seperti yang ditunjukkan di bawah ini. Ini akan membuat file main.o di direktori saat ini.

$ gcc -c main.c

Sekarang jalankan perintah berikut untuk menautkan kedua file objek ini untuk menghasilkan executable akhir. Ini akan membuat file 'main' di direktori saat ini.

$ gcc func.o main.o -o main

Saat Anda menjalankan program 'utama' ini, Anda akan melihat output berikut.

$ ./main 
Inside main() 
Inside func()

Dari output di atas, jelas bahwa kami berhasil menautkan dua file objek ke dalam executable akhir.

Apa yang kita capai ketika kita memisahkan fungsi func() dari main.c dan menulisnya di func.c?

Jawabannya adalah bahwa di sini mungkin tidak terlalu menjadi masalah jika kita akan menulis fungsi func() dalam file yang sama juga, tetapi pikirkan program yang sangat besar di mana kita mungkin memiliki ribuan baris kode. Perubahan ke satu baris kode dapat mengakibatkan kompilasi ulang seluruh kode sumber yang tidak dapat diterima dalam banyak kasus. Jadi, program yang sangat besar terkadang dibagi menjadi bagian-bagian kecil yang akhirnya dihubungkan bersama untuk menghasilkan yang dapat dieksekusi.

Utilitas make yang berfungsi pada makefile berperan dalam sebagian besar situasi ini karena utilitas ini mengetahui file sumber mana yang telah diubah dan file objek mana yang perlu dikompilasi ulang. File objek yang file sumber terkaitnya belum diubah ditautkan sebagaimana adanya. Ini membuat proses kompilasi menjadi sangat mudah dan mudah dikelola.

Jadi, sekarang kita mengerti bahwa ketika kita menautkan dua file objek func.o dan main.o, linker gcc mampu menyelesaikan panggilan fungsi ke func() dan ketika executable main terakhir dieksekusi, kita melihat printf() di dalam fungsi func() yang sedang dieksekusi.

Di mana tautan menemukan definisi fungsi printf()? Karena Linker tidak memberikan kesalahan apa pun yang pasti berarti bahwa linker menemukan definisi printf(). printf() adalah fungsi yang dideklarasikan di stdio.h dan didefinisikan sebagai bagian dari standar 'C' shared library (libc.so)

Kami tidak menautkan file objek bersama ini ke program kami. Jadi, bagaimana ini berhasil? Gunakan alat ldd untuk mencari tahu, yang mencetak pustaka bersama yang diperlukan oleh setiap program atau pustaka bersama yang ditentukan pada baris perintah.

Jalankan ldd pada executable 'main', yang akan menampilkan output berikut.

$ ldd main 
linux-vdso.so.1 =>  (0x00007fff1c1ff000) 
libc.so.6 => /lib/libc.so.6 (0x00007f32fa6ad000) 
/lib64/ld-linux-x86-64.so.2 (0x00007f32faa4f000)

Output di atas menunjukkan bahwa executable utama tergantung pada tiga perpustakaan. Baris kedua pada output di atas adalah 'libc.so.6' (library 'C' standar). Beginilah cara gcc linker menyelesaikan panggilan fungsi ke printf().

Pustaka pertama diperlukan untuk membuat panggilan sistem sedangkan pustaka bersama ketiga adalah pustaka yang memuat semua pustaka bersama lainnya yang diperlukan oleh yang dapat dieksekusi. Pustaka ini akan hadir untuk setiap executable yang bergantung pada pustaka bersama lainnya untuk eksekusinya.

Saat menautkan, perintah yang digunakan secara internal oleh gcc sangat panjang tetapi dari perspektif pengguna, kita hanya perlu menulis.

$ gcc <object files> -o <output file name>

RELOKASI KODE

Relokasi adalah entri dalam biner yang dibiarkan diisi pada waktu tautan atau waktu berjalan. Entri relokasi tipikal mengatakan:Temukan nilai 'z' dan masukkan nilai itu ke dalam executable akhir di offset 'x'

Buat reloc.c berikut untuk contoh ini.

$ vi reloc.c
extern void func(void); 

void func1(void) 
{ 
    func(); 
}

Dalam reloc.c di atas kita mendeklarasikan fungsi func() yang definisinya masih belum tersedia, tetapi kita memanggil fungsi tersebut di func1().

Buat file objek reloc.o dari reloc.c seperti yang ditunjukkan di bawah ini.

$ gcc -c reloc.c -o reloc.o

Gunakan utilitas readelf untuk melihat relokasi dalam file objek ini seperti yang ditunjukkan di bawah ini.

$ 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 
...

Alamat func() tidak diketahui pada saat kita membuat reloc.o sehingga compiler meninggalkan relokasi tipe R_X86_64_PC32. Relokasi ini secara tidak langsung mengatakan bahwa "isi alamat fungsi func() di executable akhir di offset 0000000000005".

Relokasi di atas sesuai dengan bagian .text di file objek reloc.o (sekali lagi kita perlu memahami struktur file ELF untuk memahami berbagai bagian) jadi mari kita bongkar bagian .text menggunakan utilitas objdump:

$ 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

Pada output di atas, offset '5' (entri dengan nilai '4' relatif terhadap alamat awal 0000000000000000) memiliki 4 byte yang menunggu untuk ditulis dengan alamat fungsi func().

Jadi, ada relokasi tertunda untuk fungsi func() yang akan diselesaikan saat kita menautkan reloc.o dengan file objek atau pustaka yang berisi definisi fungsi func().

Mari kita coba dan lihat apakah relokasi ini digulirkan atau tidak. Berikut adalah file main.c lain yang memberikan definisi func() :

$ 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; 
}

Buat file objek main.o dari main.c seperti gambar di bawah ini.

$ gcc -c main.c -o main.o

Tautkan reloc.o dengan main.o dan coba buat executable seperti yang ditunjukkan di bawah ini.

$ gcc reloc.o main.o -o reloc

Jalankan kembali objdump dan lihat apakah relokasi sudah teratasi atau belum:

$ objdump --disassemble reloc > output.txt

Kami mengalihkan output karena executable berisi banyak informasi dan kami tidak ingin tersesat di stdout.
Lihat konten file 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 
...

Pada baris ke-4, kita dapat melihat dengan jelas bahwa byte alamat kosong yang kita lihat sebelumnya sekarang diisi dengan alamat fungsi func().

Sebagai kesimpulan, tautan kompiler gcc adalah lautan yang sangat luas untuk diselami sehingga tidak dapat dicakup dalam satu artikel. Namun, artikel ini mencoba mengupas lapisan pertama dari proses penautan untuk memberi Anda gambaran tentang apa yang terjadi di bawah perintah gcc yang menjanjikan untuk menautkan file objek yang berbeda untuk menghasilkan file yang dapat dieksekusi.


Linux
  1. Komunikasi antar-proses di Linux:Menggunakan pipa dan antrian pesan

  2. Proses Substitusi Dan Pipa?

  3. Systemd Dan Proses Pemijahan:Proses Anak Dibunuh Saat Proses Utama Keluar?

  1. Cara Mengatur Prioritas Proses Linux Menggunakan Perintah Nice dan renice

  2. Bagaimana cara mematikan proses di Linux menggunakan perintah?

  3. Cara Mengubah Prioritas Proses menggunakan Linux Contoh Nice dan Renice

  1. menautkan <iostream.h> di linux menggunakan gcc

  2. Bagaimana cara melakukan peningkatan atom dan mengambil di C?

  3. Mengkompilasi menggunakan arm-none-eabi-gcc dan menautkan pustaka liba.a error