GNU/Linux >> Belajar Linux >  >> Linux

Perjalanan Program C ke Linux yang Dapat Dieksekusi dalam 4 Tahap

Anda menulis program C, menggunakan gcc untuk mengompilasinya, dan Anda mendapatkan program yang dapat dieksekusi. Hal ini cukup sederhana. Benar?

Pernahkah Anda bertanya-tanya apa yang terjadi selama proses kompilasi dan bagaimana program C diubah menjadi program yang dapat dieksekusi?

Ada empat tahapan utama yang dilalui sebuah kode sumber untuk akhirnya menjadi sebuah executable.

Empat tahapan agar program C menjadi executable adalah sebagai berikut:

  1. Pra-pemrosesan
  2. Kompilasi
  3. Perakitan
  4. Menautkan

Pada Bagian-I dari seri artikel ini, kita akan membahas langkah-langkah yang dilalui oleh kompiler gcc ketika kode sumber program C dikompilasi menjadi sebuah executable.

Sebelum melangkah lebih jauh, mari kita lihat sekilas cara mengkompilasi dan menjalankan kode ‘C’ menggunakan gcc, menggunakan contoh hello world sederhana.

$ vi print.c
#include <stdio.h>
#define STRING "Hello World"
int main(void)
{
/* Using a macro to print 'Hello World'*/
printf(STRING);
return 0;
}

Sekarang, mari jalankan kompiler gcc di atas kode sumber ini untuk membuat executable.

$ gcc -Wall print.c -o print

Pada perintah di atas:

  • gcc – Memanggil compiler GNU C
  • -Wall – flag gcc yang mengaktifkan semua peringatan. -W adalah singkatan dari warning, dan kami meneruskan "semua" ke -W.
  • print.c – Masukkan program C
  • -o print – Perintahkan compiler C untuk membuat C yang dapat dieksekusi sebagai print. Jika Anda tidak menentukan -o, secara default compiler C akan membuat executable dengan nama a.out

Terakhir, jalankan print yang akan menjalankan program C dan menampilkan hello world.

$ ./print
Hello World

Catatan :Saat Anda mengerjakan proyek besar yang berisi beberapa program C, gunakan utilitas make untuk mengelola kompilasi program C Anda seperti yang telah kita bahas sebelumnya.

Sekarang setelah kita memiliki ide dasar tentang bagaimana gcc digunakan untuk mengubah kode sumber menjadi biner, kita akan meninjau 4 tahap yang harus dilalui program C untuk menjadi sebuah executable.

1. PEMROSESAN PRA

Ini adalah tahap pertama yang dilalui kode sumber. Pada tahap ini tugas-tugas berikut dilakukan:

  1. Pergantian makro
  2. Komentar dihapus
  3. Perluasan file yang disertakan

Untuk memahami prapemrosesan dengan lebih baik, Anda dapat mengkompilasi program 'print.c' di atas menggunakan flag -E, yang akan mencetak keluaran praproses ke stdout.

$ gcc -Wall -E print.c

Lebih baik lagi, Anda dapat menggunakan flag '-save-temps' seperti yang ditunjukkan di bawah ini. Bendera ‘-save-temps’ menginstruksikan kompiler untuk menyimpan file perantara sementara yang digunakan oleh kompiler gcc di direktori saat ini.

$ gcc -Wall -save-temps print.c -o print

Jadi ketika kita mengkompilasi program print.c dengan flag -save-temps kita mendapatkan file perantara berikut di direktori saat ini (bersama dengan print executable)

$ ls
print.i
print.s
print.o

Output praproses disimpan dalam file sementara yang memiliki ekstensi .i (yaitu 'print.i' dalam contoh ini)

Sekarang, mari buka file print.i dan lihat isinya.

$ vi print.i
......
......
......
......
# 846 "/usr/include/stdio.h" 3 4
extern FILE *popen (__const char *__command, __const char *__modes) ;
extern int pclose (FILE *__stream);
extern char *ctermid (char *__s) __attribute__ ((__nothrow__));

# 886 "/usr/include/stdio.h" 3 4
extern void flockfile (FILE *__stream) __attribute__ ((__nothrow__));
extern int ftrylockfile (FILE *__stream) __attribute__ ((__nothrow__)) ;
extern void funlockfile (FILE *__stream) __attribute__ ((__nothrow__));

# 916 "/usr/include/stdio.h" 3 4
# 2 "print.c" 2

int main(void)
{
printf("Hello World");
return 0;
}

Pada output di atas, Anda dapat melihat bahwa file sumber sekarang dipenuhi dengan banyak informasi, tetapi pada akhirnya kita dapat melihat baris kode yang ditulis oleh kita. Mari kita analisis baris kode ini terlebih dahulu.

  1. Pengamatan pertama adalah bahwa argumen ke printf() sekarang berisi langsung string "Hello World" daripada makro. Sebenarnya definisi dan penggunaan makro telah hilang sama sekali. Ini membuktikan tugas pertama bahwa semua makro diperluas dalam tahap prapemrosesan.
  2. Pengamatan kedua adalah bahwa komentar yang kami tulis dalam kode asli kami tidak ada. Ini membuktikan bahwa semua komentar dihapus.
  3. Pengamatan ketiga adalah bahwa di samping baris '#include' tidak ada dan alih-alih itu kita melihat banyak kode di tempatnya. Jadi aman untuk menyimpulkan bahwa stdio.h telah diperluas dan secara harfiah disertakan dalam file sumber kami. Oleh karena itu, kami memahami bagaimana kompiler dapat melihat deklarasi fungsi printf().

Ketika saya mencari file print.i, saya menemukan, Fungsi printf dideklarasikan sebagai:

extern int printf (__const char *__restrict __format, ...);

Kata kunci 'extern' memberi tahu bahwa fungsi printf() tidak didefinisikan di sini. Ini adalah eksternal untuk file ini. Nanti kita akan melihat bagaimana gcc sampai ke definisi printf().

Anda dapat menggunakan gdb untuk men-debug program c Anda. Sekarang kita memiliki pemahaman yang layak tentang apa yang terjadi selama tahap preprocessing. mari kita lanjutkan ke tahap berikutnya.

2. KOMPILASI

Setelah compiler selesai dengan tahap pre-processor. Langkah selanjutnya adalah mengambil print.i sebagai input, mengkompilasinya dan menghasilkan output terkompilasi antara. File output untuk tahap ini adalah 'print.s'. Output yang ada di print.s adalah instruksi tingkat perakitan.

Buka file print.s di editor dan lihat kontennya.

$ vi print.s
.file "print.c"
.section .rodata
.LC0:
.string "Hello World"
.text
.globl main
.type main, @function
main:
.LFB0:
.cfi_startproc
pushq %rbp
.cfi_def_cfa_offset 16
movq %rsp, %rbp
.cfi_offset 6, -16
.cfi_def_cfa_register 6
movl $.LC0, %eax
movq %rax, %rdi
movl $0, %eax
call printf
movl $0, %eax
leave
ret
.cfi_endproc
.LFE0:
.size main, .-main
.ident "GCC: (Ubuntu 4.4.3-4ubuntu5) 4.4.3"
.section .note.GNU-stack,"",@progbits

Meskipun saya tidak terlalu mendalami pemrograman tingkat perakitan, tetapi pandangan sekilas menyimpulkan bahwa keluaran tingkat perakitan ini dalam beberapa bentuk instruksi yang dapat dipahami oleh assembler dan mengubahnya menjadi bahasa tingkat mesin.

3. PERAKITAN

Pada tahap ini file print.s diambil sebagai input dan file perantara print.o dihasilkan. File ini juga dikenal sebagai file objek.

File ini diproduksi oleh assembler yang memahami dan mengubah file '.s' dengan instruksi perakitan menjadi file objek '.o' yang berisi instruksi level mesin. Pada tahap ini hanya kode yang sudah ada yang diubah menjadi bahasa mesin, pemanggilan fungsi seperti printf() tidak diselesaikan.

Karena output dari tahap ini adalah file level mesin (print.o). Jadi kita tidak bisa melihat isinya. Jika Anda masih mencoba membuka print.o dan melihatnya, Anda akan melihat sesuatu yang sama sekali tidak dapat dibaca.

$ vi print.o
^?ELF^B^A^A^@^@^@^@^@^@^@^@^@^A^@>^@^A^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@0^
^@UH<89>å¸^@^@^@^@H<89>ǸHello World^@^@GCC: (Ubuntu 4.4.3-4ubuntu5) 4.4.3^@^
T^@^@^@^@^@^@^@^AzR^@^Ax^P^A^[^L^G^H<90>^A^@^@^\^@^@]^@^@^@^@A^N^PC<86>^B^M^F
^@^@^@^@^@^@^@^@.symtab^@.strtab^@.shstrtab^@.rela.text^@.data^@.bss^@.rodata
^@.comment^@.note.GNU-stack^@.rela.eh_frame^@^@^@^@^@^@^@^@^@^@^@^
...
...
…

Satu-satunya hal yang dapat kami jelaskan dengan melihat file print.o adalah tentang string ELF.

ELF adalah singkatan dari executable and linkable format.

Ini adalah format yang relatif baru untuk file objek tingkat mesin dan dapat dieksekusi yang diproduksi oleh gcc. Sebelum ini, format yang dikenal sebagai a.out digunakan. ELF dikatakan format yang lebih canggih daripada a.out (Kita mungkin menggali lebih dalam format ELF di beberapa artikel mendatang lainnya).

Catatan:Jika Anda mengkompilasi kode Anda tanpa menentukan nama file output, file output yang dihasilkan memiliki nama 'a.out' tetapi formatnya sekarang telah berubah menjadi ELF. Hanya saja nama file executable default tetap sama.

4. MENGHUBUNGKAN

Ini adalah tahap terakhir di mana semua penautan panggilan fungsi dengan definisinya selesai. Seperti yang telah dibahas sebelumnya, hingga tahap ini gcc belum tahu tentang definisi fungsi seperti printf(). Sampai kompiler tahu persis di mana semua fungsi ini diimplementasikan, itu hanya menggunakan place-holder untuk pemanggilan fungsi. Pada tahap ini, definisi printf() diselesaikan dan alamat sebenarnya dari fungsi printf() dicolokkan.

Linker beraksi pada tahap ini dan melakukan tugas ini.

Linker juga melakukan beberapa pekerjaan ekstra; itu menggabungkan beberapa kode tambahan untuk program kami yang diperlukan saat program dimulai dan saat program berakhir. Misalnya, ada kode yang merupakan standar untuk menyiapkan lingkungan yang sedang berjalan seperti meneruskan argumen baris perintah, meneruskan variabel lingkungan ke setiap program. Demikian pula beberapa kode standar yang diperlukan untuk mengembalikan nilai kembalian program ke sistem.

Tugas kompiler di atas dapat diverifikasi dengan eksperimen kecil. Karena sekarang kita sudah mengetahui bahwa linker mengonversi file .o (print.o) menjadi file yang dapat dieksekusi (print).

Jadi jika kita membandingkan ukuran file print.o dan file print, kita akan melihat perbedaannya.

$ size print.o
   text	   data	    bss	    dec	    hex	filename
     97	      0	      0	     97	     61	print.o 

$ size print
   text	   data	    bss	    dec	    hex	filename
   1181	    520	     16	   1717	    6b5	print

Melalui perintah size kita mendapatkan gambaran kasar tentang bagaimana ukuran file output meningkat dari file objek ke file executable. Ini semua karena kode standar ekstra yang digabungkan oleh penaut dengan program kami.

Sekarang Anda tahu apa yang terjadi pada program C sebelum menjadi program yang dapat dieksekusi. Anda tahu tentang tahap Prapemrosesan, Kompilasi, Perakitan, dan Penautan Masih banyak lagi tahap penautan, yang akan kami bahas di artikel berikutnya dalam seri ini.


Linux
  1. Contoh Perintah awk di Linux

  2. Menyematkan ikon di executable Linux

  3. Menginstal program Python di Linux

  1. Contoh Perintah lpr di Linux

  2. Bagaimana cara mengetahui di mana suatu program macet di linux?

  3. Bisakah Anda mendapatkan program apa pun di Linux untuk mencetak jejak tumpukan jika itu segfault?

  1. Apa yang saya gunakan di linux untuk membuat program python dapat dieksekusi

  2. gdb tampaknya mengabaikan kemampuan yang dapat dieksekusi

  3. Tata letak memori program di linux