Panduan pemula dalam menggunakan linker. penghubung

Tujuan artikel ini adalah untuk membantu pemrogram C dan C++ memahami inti dari apa yang dilakukan linker. Saya telah menjelaskan hal ini kepada banyak rekan selama beberapa tahun terakhir dan akhirnya memutuskan sudah waktunya untuk menuliskan materi ini agar lebih mudah diakses (sehingga saya tidak perlu menjelaskannya lagi). [Pembaruan Maret 2009: Menambahkan informasi lebih lanjut tentang pertimbangan tata letak di Windows, serta detail lebih lanjut tentang aturan satu definisi.

Contoh umum mengapa orang datang kepada saya untuk meminta bantuan adalah kesalahan tata letak berikut:
g++ -o test1 test1a.o test1b.o test1a.o(.text+0x18): Dalam fungsi `main": : referensi tidak terdefinisi ke `findmax(int, int)" collector2: ld mengembalikan 1 status keluar
Jika reaksi Anda adalah “Saya mungkin lupa bagian luar “C””, kemungkinan besar Anda mengetahui semua yang diberikan dalam artikel ini.

Definisi: apa yang ada di file C?

Bab ini adalah pengingat singkat tentang berbagai komponen file C. Jika semuanya masuk akal bagi Anda, kemungkinan besar Anda dapat melewati bab ini dan langsung melanjutkan.

Pertama, Anda perlu memahami perbedaan antara deklarasi dan definisi. Definisi mengaitkan nama dengan implementasi, yang dapat berupa kode atau data:

  • Mendefinisikan variabel menyebabkan kompiler mencadangkan beberapa area memori, mungkin memberinya nilai tertentu.
  • Mendefinisikan suatu fungsi menyebabkan kompiler menghasilkan kode untuk fungsi tersebut
Pengumuman memberi tahu kompiler bahwa definisi suatu fungsi atau variabel (dengan nama tertentu) ada di tempat lain dalam program, mungkin di file C lain. (Perhatikan bahwa definisi juga merupakan deklarasi - pada kenyataannya, ini adalah deklarasi di mana "tempat lain" dari program tersebut sama dengan yang sekarang.)

Ada dua jenis definisi variabel:

  • variabel global, yang ada sepanjang siklus hidup program ("alokasi statis") dan tersedia dalam berbagai fungsi;
  • variabel lokal, yang hanya ada dalam cakupan beberapa fungsi yang menjalankan ("penempatan lokal") dan hanya dapat diakses dalam fungsi tersebut.
Dalam hal ini, istilah “tersedia” harus dipahami sebagai “dapat diakses dengan nama yang terkait dengan variabel pada saat definisi.”

Ada beberapa kasus khusus yang mungkin tidak terlihat jelas pada awalnya:

  • Variabel lokal statis sebenarnya bersifat global karena variabel tersebut ada sepanjang masa program, meskipun variabel tersebut hanya terlihat dalam satu fungsi.
  • Variabel global statis juga bersifat global, dengan satu-satunya perbedaan adalah variabel tersebut hanya tersedia dalam file yang sama tempat variabel tersebut didefinisikan.
Perlu dicatat bahwa dengan mendefinisikan suatu fungsi sebagai statis, Anda cukup mengurangi jumlah tempat di mana Anda dapat merujuk ke fungsi tertentu berdasarkan nama.

Untuk variabel global dan lokal, kita dapat membedakan apakah variabel tersebut diinisialisasi atau tidak, yaitu apakah ruang yang dialokasikan untuk suatu variabel di memori akan diisi dengan nilai tertentu.

Terakhir, kita dapat menyimpan informasi dalam memori yang dialokasikan secara dinamis menggunakan malloc atau new . Dalam hal ini, tidak mungkin untuk mengakses memori yang dialokasikan berdasarkan nama, jadi perlu menggunakan pointer - variabel bernama yang berisi alamat area memori yang tidak disebutkan namanya. Area memori ini juga dapat dikosongkan menggunakan free atau delete . Dalam hal ini kita berhadapan dengan "penempatan dinamis".

Mari kita rangkum:

Mungkin cara yang lebih mudah untuk mempelajarinya adalah dengan melihat contoh program.
/* Definisi variabel global yang tidak diinisialisasi */ int x_global_uninit; /* Definisi variabel global yang diinisialisasi */ int x_global_init = 1; /* Mendefinisikan variabel global yang belum diinisialisasi yang * hanya dapat diakses berdasarkan nama dalam file C ini */ static int y_global_uninit; /* Definisi variabel global yang diinisialisasi yang * hanya dapat diakses berdasarkan nama dalam file C ini */ static int y_global_init = 2; /* Deklarasi variabel global yang didefinisikan di suatu tempat * di tempat lain dalam program */ extern int z_global; /* Mendeklarasikan fungsi yang didefinisikan di tempat lain * dalam program (Anda dapat menambahkan "extern", tapi ini * opsional) */ int fn_a(int x, int y); /* Definisi fungsi. Namun, bila ditandai sebagai statis, * hanya dapat dipanggil berdasarkan nama di dalam file C tersebut. */ static int fn_b(int x) ( return x+1; ) /* Definisi fungsi. */ /* Parameter fungsi dianggap sebagai variabel lokal. */ int fn_c(int x_local) ( /* Definisi variabel lokal yang tidak diinisialisasi */ int y_local_uninit; /* Definisi variabel lokal yang diinisialisasi */ int y_local_init = 3; /* Kode yang mengakses variabel lokal dan global * dan berfungsi dengan nama */ x_global_uninit = fn_a(x_local, x_global_init); y_local_uninit = fn_a(x_local, y_local_init); y_local_uninit += fn_b(z_global); return (x_global_uninit + y_local_uninit); )

Apa yang dilakukan kompiler C?

Tugas kompiler C adalah mengubah teks yang (biasanya) dapat dibaca manusia menjadi sesuatu yang dapat dipahami oleh komputer. Pada output, kompiler menghasilkan berkas objek. Pada platform UNIX, file-file ini biasanya memiliki akhiran .o; di Windows - akhiran.obj. Isi file objek pada dasarnya adalah dua hal:

Kode dan data, dalam hal ini, akan memiliki nama yang terkait dengannya - nama fungsi atau variabel yang terkait dengannya menurut definisi.

Kode objek adalah urutan instruksi mesin (yang disusun dengan tepat) yang sesuai dengan instruksi C yang ditulis oleh pemrogram: semua instruksi if dan while dan bahkan goto. Mantra ini harus memanipulasi informasi dalam bentuk tertentu, dan informasi tersebut harus berada di suatu tempat - itulah mengapa kita memerlukan variabel. Kode juga dapat mereferensikan kode lain (khususnya fungsi C lainnya dalam program).

Dimanapun kode merujuk pada variabel atau fungsi, kompiler hanya mengizinkannya jika ia pernah melihat variabel atau fungsi tersebut sebelumnya. Deklarasi adalah janji bahwa suatu definisi ada di tempat lain dalam program.

Tugas linker adalah memverifikasi janji-janji ini. Namun, apa yang kompiler lakukan dengan semua janji ini ketika menghasilkan file objek?

Intinya kompiler meninggalkan ruang kosong. Ruang kosong (tautan) mempunyai nama, tetapi nilai yang sesuai dengan nama tersebut belum diketahui.

Mengingat hal ini, kita dapat menggambarkan file objek yang sesuai dengan , sebagai berikut:

Mengurai file objek

Hingga saat ini kami telah mempertimbangkan segalanya pada tingkat tinggi. Namun, ada gunanya melihat cara kerjanya dalam praktik. Alat utama bagi kami adalah tim nm, yang memberikan informasi tentang simbol file objek pada platform UNIX. Untuk perintah Windows tempat sampah dengan opsi /symbols adalah perkiraan yang setara. Ada juga alat binutils GNU yang mencakup nm.exe.

Mari kita lihat apa yang dihasilkan nm untuk file objek yang diperoleh dari :
Simbol dari c_parts.o: Nama Nilai Tipe Kelas Ukuran Bagian Garis fn_a | | kamu | BUKAN JENIS| | |*UND* z_global | | kamu | BUKAN JENIS| | |*UND* fn_b |00000000| t | FUNGSI|00000009| |.teks x_global_init |00000000| D | OBYEK|00000004| |.data y_global_uninit |00000000| b | OBYEK|00000004| |.bss x_global_uninit |00000004| C | OBYEK|00000004| |*COM* y_global_init |00000004| d | OBYEK|00000004| |.data fn_c |00000009| T | FUNGSI|00000055| |.teks
Hasilnya mungkin terlihat sedikit berbeda pada platform yang berbeda (lihat halaman manual untuk rinciannya), namun informasi kuncinya adalah kelas dari setiap karakter dan ukurannya (jika ada). Kelas dapat memiliki arti yang berbeda:

  • Kelas kamu menunjukkan tautan yang tidak terdefinisi, “ruang kosong” yang sama yang disebutkan di atas. Ada dua objek untuk kelas ini: fn_a dan z_global. (Beberapa versi nm mungkin menghasilkan output bagian, yang mana *TIDAK* atau UNDEF pada kasus ini.)
  • Kelas T Dan T tunjukkan kode yang ditentukan; perbedaan antara T Dan T apakah fungsinya lokal ( T) dalam file atau tidak ( T), yaitu apakah fungsi tersebut dideklarasikan sebagai static . Sekali lagi pada beberapa sistem, suatu bagian mungkin ditampilkan, mis. .teks.
  • Kelas D Dan D berisi variabel global yang diinisialisasi. Dalam hal ini, variabel statis termasuk dalam kelas D. Jika informasi bagian ada, itu akan ada .data.
  • Untuk variabel global yang tidak diinisialisasi, kita peroleh B, jika statis dan B atau C jika tidak. Bagian dalam hal ini kemungkinan besar adalah .bss atau *COM*.
Anda mungkin juga melihat simbol yang bukan bagian dari kode sumber C. Kami tidak akan memfokuskan perhatian kami pada hal ini, karena ini biasanya merupakan bagian dari mekanisme internal kompiler, sehingga program Anda masih dapat ditautkan nantinya.

Apa yang Dilakukan Linker: Bagian 1

Telah kami katakan sebelumnya bahwa mendeklarasikan suatu fungsi atau variabel merupakan janji kepada compiler bahwa terdapat definisi fungsi atau variabel tersebut di tempat lain dalam program, dan tugas linker adalah memenuhi janji tersebut. Melihat hal ini, kita dapat menggambarkan proses ini sebagai “mengisi kekosongan.”

Mari kita ilustrasikan hal ini dengan sebuah contoh, dengan melihat file C lain selain file .
/* Variabel global diinisialisasi */ int z_global = 11; /* Variabel global kedua bernama y_global_init, tetapi keduanya statis */ static int y_global_init = 2; /* Deklarasi variabel global lain */ extern int x_global_init; int fn_a(int x, int y) ( return(x+y); ) int main(int argc, char *argv) ( const char *message = "Halo, dunia"; return fn_a(11,12); )

Dari kedua diagram tersebut, kita dapat melihat bahwa semua titik dapat dihubungkan (jika tidak, linker akan memunculkan pesan error). Setiap benda mempunyai tempatnya, dan setiap tempat mempunyai bendanya masing-masing. Linker juga dapat mengisi ruang kosong apa pun seperti yang ditunjukkan di sini (pada sistem UNIX, proses penautan biasanya disebut dengan perintah ld).

Untuk C situasinya kurang jelas. Harus ada tepat satu definisi untuk setiap fungsi dan variabel global yang diinisialisasi, namun definisi variabel yang tidak diinisialisasi dapat diperlakukan sebagai penentuan awal. Dengan demikian, bahasa C mengizinkan (atau setidaknya tidak mencegah) file sumber berbeda berisi predefinisi objek yang sama.

Namun, linker juga harus mampu menangani bahasa selain C dan C++, yang mana aturan satu definisi belum tentu berlaku. Misalnya, normal bagi Fortran untuk memiliki salinan setiap variabel global di setiap file yang mereferensikannya. Linker kemudian perlu menghapus duplikat dengan memilih satu salinan (perwakilan terbesar, jika ukurannya berbeda) dan membuang semua salinan lainnya. Model ini terkadang disebut "model umum" tata letak karena kata kunci UMUM dalam bahasa Fortran.

Akibatnya, sangat umum bagi linker UNIX untuk tidak mengeluh tentang simbol duplikat, setidaknya jika simbol tersebut merupakan simbol duplikat dari variabel global yang tidak diinisialisasi (model penghubung ini kadang-kadang disebut "model berpasangan longgar" [ kira-kira. terjemahan Ini adalah terjemahan gratis saya untuk model ref/def yang santai. Saran yang lebih baik diterima]). Jika ini membuat Anda khawatir (dan mungkin memang seharusnya demikian), lihat dokumentasi linker Anda untuk menemukan opsi --work-right yang dapat menjinakkan perilakunya. Misalnya, dalam rantai alat GNU, opsi kompiler -fno-common memaksa variabel yang belum diinisialisasi ditempatkan di segmen BBS alih-alih menghasilkan blok COMMON.

Apa yang dilakukan sistem operasi?

Sekarang linker telah menghasilkan file yang dapat dieksekusi, memberikan setiap referensi simbol definisi yang sesuai, Anda dapat berhenti sejenak untuk memahami apa yang dilakukan sistem operasi saat Anda menjalankan program.

Menjalankan suatu program, tentu saja, memerlukan eksekusi kode mesin, mis. OS jelas perlu mentransfer kode mesin dari file yang dapat dieksekusi dari hard drive ke memori operasi, di mana CPU dapat mengambilnya. Bagian ini disebut segmen kode atau segmen teks.

Kode tanpa data itu sendiri tidak ada gunanya. Oleh karena itu, semua variabel global juga memerlukan ruang di memori komputer. Namun, ada perbedaan antara variabel global yang diinisialisasi dan tidak diinisialisasi. Variabel yang diinisialisasi memiliki nilai awal tertentu, yang juga harus disimpan dalam objek dan file yang dapat dieksekusi. Saat program dimulai, OS menyalin nilai-nilai ini ke dalam ruang virtual program, ke dalam segmen data.

Untuk variabel yang tidak diinisialisasi, OS mungkin berasumsi bahwa semuanya memiliki 0 sebagai nilai awalnya, yaitu. tidak perlu menyalin nilai apa pun. Bagian memori yang diinisialisasi dengan nol dikenal sebagai segmen bss.

Ini berarti bahwa ruang untuk variabel global dapat dialokasikan dalam file yang dapat dieksekusi yang disimpan di disk; Variabel yang diinisialisasi harus mempertahankan nilai awalnya, tetapi variabel yang tidak diinisialisasi hanya perlu mempertahankan ukurannya.

Seperti yang mungkin Anda ketahui, sejauh ini dalam semua diskusi tentang file objek dan linker, kita hanya membicarakan variabel global; Pada saat yang sama, kami tidak menyebutkan variabel lokal dan memori yang digunakan secara dinamis.

Data ini tidak memerlukan intervensi linker apa pun karena masa pakainya dimulai dan berakhir selama eksekusi program—lama setelah linker menyelesaikan tugasnya. Namun, demi kelengkapan, kami akan menunjukkan secara singkat bahwa:

  • variabel lokal terletak di area memori yang disebut tumpukan, yang tumbuh dan berkontraksi seiring pemanggilan dan eksekusi berbagai fungsi.
  • Memori yang dialokasikan secara dinamis diambil dari area memori yang dikenal sebagai banyak, dan fungsi malloc mengontrol akses ke ruang kosong di area ini.
Untuk melengkapi gambarannya, ada baiknya menambahkan seperti apa ruang memori dari proses yang sedang berjalan. Karena heap dan tumpukan dapat mengubah ukurannya secara dinamis, sangat umum bahwa tumpukan tumbuh dalam satu arah dan heap tumbuh dalam arah yang berlawanan. Oleh karena itu, program hanya akan memunculkan error kehabisan memori bebas jika tumpukan dan heap bertemu di tengah-tengah (dalam hal ini, ruang memori program akan benar-benar penuh).

Apa fungsi tautannya? bagian 2

Sekarang setelah kita membahas apa yang dilakukan linker, kita dapat mendalami bagian yang lebih kompleks - kira-kira dalam urutan kronologis bagaimana mereka ditambahkan ke linker.

Pengamatan utama yang mempengaruhi fungsionalitas linker adalah sebagai berikut: jika sejumlah program berbeda melakukan hal yang kira-kira sama (output ke layar, membaca file dari hard drive, dll.), maka jelas masuk akal untuk mengisolasi program ini. kode di tempat tertentu dan memberikannya kepada program lain untuk menggunakannya.

Salah satu solusi yang mungkin adalah dengan menggunakan file objek yang sama, namun akan jauh lebih mudah untuk menyimpan seluruh koleksi... file objek di satu lokasi yang mudah diakses: perpustakaan.

Selain teknis: Bab ini sepenuhnya menghilangkan fitur penting dari linker: pengalihan(relokasi). Program yang berbeda memiliki ukuran yang berbeda, mis. jika perpustakaan bersama dipetakan ke dalam ruang alamat program yang berbeda, maka perpustakaan tersebut akan memiliki alamat yang berbeda. Hal ini berarti semua fungsi dan variabel di perpustakaan akan berada di tempat yang berbeda. Sekarang, jika semua referensi alamat bersifat relatif (“nilai +1020 byte dari sini”) dan bukan absolut (“nilai pada 0x102218BF”), maka hal ini tidak menjadi masalah, tetapi hal ini tidak selalu terjadi. Dalam kasus seperti itu, semua alamat absolut harus ditambahkan dengan offset yang sesuai - ini dia relokasi. Saya tidak akan kembali ke topik ini lagi, tapi saya akan menambahkan bahwa karena ini hampir selalu tersembunyi dari programmer C/C++, sangat jarang masalah tata letak disebabkan oleh kesulitan pengalihan.

Perpustakaan statis

Implementasi perpustakaan yang paling sederhana adalah statis perpustakaan. Pada bab sebelumnya telah disebutkan bahwa Anda dapat berbagi kode hanya dengan menggunakan kembali file objek; inilah inti dari perpustakaan statis.

Pada sistem UNIX, perintah untuk membangun perpustakaan statis biasanya adalah ar, dan file perpustakaan yang dihasilkan memiliki ekstensi *.a. Selain itu, file-file ini biasanya memiliki awalan "lib" di namanya dan diteruskan ke linker dengan opsi "-l" diikuti dengan nama perpustakaan tanpa awalan dan ekstensi (yaitu "-lfred" akan mengambil file " libfred.a").
(Dulu, program bernama ranlib juga diperlukan untuk perpustakaan statis untuk menghasilkan daftar simbol di depan perpustakaan. Saat ini, alat ar melakukan hal ini sendiri.)

Di Windows, perpustakaan statis memiliki ekstensi .LIB dan dibuat oleh alat LIB, tetapi fakta ini bisa menyesatkan, karena ekstensi yang sama digunakan untuk "perpustakaan impor", yang hanya berisi daftar apa yang ada di DLL - melihat

Saat linker melakukan iterasi melalui kumpulan file objek untuk menggabungkannya, ia mempertahankan daftar simbol yang belum dapat diimplementasikan. Setelah semua file objek yang ditentukan secara eksplisit telah diproses, linker sekarang memiliki tempat baru untuk mencari simbol yang tersisa dalam daftar - di perpustakaan. Jika simbol yang belum diimplementasikan didefinisikan di salah satu objek perpustakaan, maka objek tersebut akan ditambahkan, sama seperti jika simbol tersebut telah ditambahkan ke daftar file objek oleh pengguna, dan penautan dilanjutkan.

Perhatikan rincian dari apa yang ditambahkan dari perpustakaan: jika diperlukan definisi beberapa simbol, maka seluruh objek, yang berisi definisi simbol, akan disertakan. Ini berarti bahwa proses ini dapat berupa langkah maju atau mundur - objek yang baru ditambahkan dapat menyelesaikan referensi yang tidak terdefinisi atau memperkenalkan seluruh koleksi referensi baru yang belum terselesaikan.

Detail penting lainnya adalah memesan acara; perpustakaan hanya dipanggil ketika penautan normal selesai dan diproses Oke dari kiri ke kanan. Artinya jika objek terakhir yang diambil dari perpustakaan memerlukan simbol dari perpustakaan sebelumnya di baris perintah link, linker tidak akan menemukannya secara otomatis.

Mari kita beri contoh untuk memperjelas situasi; Katakanlah kita memiliki file objek berikut dan baris perintah link yang berisi a.o, b.o, -lx dan -ly .


Setelah linker memproses a.o dan b.o , referensi ke b2 dan a3 akan diselesaikan, sedangkan x12 dan y22 masih belum terselesaikan. Pada titik ini, linker memeriksa perpustakaan pertama, libx.a, untuk simbol yang hilang dan menemukan bahwa ia dapat menyertakan x1.o untuk mengkompensasi referensi ke x12; namun, dengan melakukan ini, x23 dan y12 ditambahkan ke daftar referensi yang tidak ditentukan (daftar sekarang terlihat seperti y22, x23, y12).

Tautannya masih berhubungan dengan libx.a , jadi referensi ke x23 mudah dikompensasi dengan menyertakan x2.o dari libx.a . Namun, ini menambahkan y11 ke daftar yang tidak ditentukan (yang menjadi y22, y12, y11). Tak satu pun dari tautan ini dapat diselesaikan menggunakan libx.a , sehingga tautannya diasumsikan liby.a .

Hal yang sama terjadi di sini dan tautannya menyertakan y1.o dan y2.o . Objek pertama yang ditambahkan adalah referensi ke y21 , tetapi karena y2.o masih disertakan, referensi ini diselesaikan dengan sederhana. Hasil dari proses ini adalah semua referensi yang tidak terdefinisi diselesaikan dan beberapa (tetapi tidak semua) objek perpustakaan disertakan dalam eksekusi akhir.

Perhatikan bahwa situasinya agak berubah jika kita mengatakan bo juga memiliki tautan ke y32 . Jika ini masalahnya, maka penautan libx.a akan terjadi dengan cara yang sama, tetapi pemrosesan liby.a akan melibatkan penyertaan y3.o . Dengan memasukkan objek ini kita akan menambahkan x31 ke daftar simbol yang belum terselesaikan dan referensi ini akan tetap tidak terselesaikan - pada tahap ini linker telah selesai memproses libx.a dan oleh karena itu tidak lagi menemukan definisi simbol ini (dalam x3.o) .

(Omong-omong, contoh ini memiliki ketergantungan melingkar antara libx.a dan liby.a; ini biasanya merupakan hal yang buruk)

Perpustakaan bersama yang dinamis

Untuk perpustakaan populer seperti perpustakaan standar C (biasanya libc), menjadi perpustakaan statis memiliki kelemahan tersendiri - setiap program yang dapat dieksekusi akan memiliki salinan kode yang sama. Memang benar, jika setiap file yang dapat dieksekusi memiliki salinan printf , fopen dan sejenisnya, maka jumlah ruang disk yang terlalu besar akan terpakai.

Kerugian yang kurang jelas adalah bahwa dalam program yang terhubung secara statis, kode tersebut diperbaiki selamanya. Jika seseorang menemukan dan memperbaiki bug di printf , setiap program harus dihubungkan kembali untuk mendapatkan kode yang benar.

Untuk mengatasi masalah ini dan masalah lainnya, perpustakaan bersama secara dinamis diperkenalkan (biasanya memiliki ekstensi .so atau .dll di Windows dan .dylib di Mac OS X). Untuk perpustakaan jenis ini, linker tidak serta merta menghubungkan semua titik. Sebaliknya, linker mengeluarkan kupon jenis “IOU” (Saya berhutang budi kepada Anda) dan menunda pencairan kupon ini hingga program berjalan.

Intinya adalah jika linker mendeteksi bahwa definisi simbol tertentu ada di perpustakaan bersama, ia tidak menyertakan definisi tersebut dalam eksekusi akhir. Sebagai gantinya, linker menuliskan nama simbol dan perpustakaan dari mana simbol tersebut diharapkan berasal.

Ketika sebuah program dipanggil untuk dieksekusi, OS memastikan bahwa bagian sisa dari proses penautan selesai tepat waktu sebelum program mulai berjalan. Sebelum fungsi utama dipanggil, versi kecil dari linker (sering disebut ld.so) menelusuri daftar janji dan melakukan tindakan terakhir menghubungkan di tempat - memasukkan kode perpustakaan dan menghubungkan titik-titik.

Ini berarti tidak ada file yang dapat dieksekusi yang berisi salinan kode printf. Jika versi baru dari printf tersedia, Anda dapat menggunakannya hanya dengan mengubah libc.so - saat berikutnya Anda menjalankan program, printf baru akan dipanggil.

Ada perbedaan besar lainnya antara cara kerja perpustakaan dinamis dibandingkan dengan perpustakaan statis dan ini muncul dalam bentuk perincian tautan. Jika simbol tertentu diambil dari perpustakaan dinamis tertentu (katakanlah printf dari libc.so), maka seluruh isi perpustakaan ditempatkan di ruang alamat program. Ini adalah perbedaan utama dari perpustakaan statis, di mana hanya objek tertentu yang terkait dengan simbol yang tidak ditentukan yang ditambahkan.

Dengan kata lain, perpustakaan bersama itu sendiri diperoleh sebagai hasil kerja linker (dan bukan sebagai pembentukan tumpukan objek yang besar, seperti yang dilakukan ar), yang berisi referensi antar objek di perpustakaan itu sendiri. Sekali lagi, nm adalah alat yang berguna untuk mengilustrasikan apa yang terjadi: ia akan menghasilkan banyak keluaran untuk setiap file objek individual ketika dijalankan pada versi statis perpustakaan, tetapi untuk versi perpustakaan bersama, liby.so hanya memiliki satu x31 yang tidak ditentukan simbol. Selain itu, dalam contoh dengan urutan menyertakan perpustakaan di akhir, juga tidak akan ada masalah: menambahkan tautan ke y32 di b.c tidak akan menyebabkan perubahan apa pun, karena semua konten y3.o dan x3.o sudah ada telah digunakan.

Jadi, alat lain yang berguna adalah ldd; pada platform Unix, ini menunjukkan semua perpustakaan bersama yang bergantung pada biner yang dapat dieksekusi (atau perpustakaan bersama lainnya), bersama dengan indikasi di mana perpustakaan tersebut dapat ditemukan. Agar program dapat diluncurkan dengan sukses, pemuat perlu menemukan semua perpustakaan ini beserta semua dependensinya. (Biasanya, pemuat mencari perpustakaan dalam daftar direktori yang ditentukan dalam variabel lingkungan LD_LIBRARY_PATH.)
/usr/bin:ldd xeyes linux-gate.so.1 => (0xb7efa000) libXext.so.6 => /usr/lib/libXext.so.6 (0xb7edb000) libXmu.so.6 => /usr/lib /libXmu.so.6 (0xb7ec6000) libXt.so.6 => /usr/lib/libXt.so.6 (0xb7e77000) libX11.so.6 => /usr/lib/libX11.so.6 (0xb7d93000) libSM .so.6 => /usr/lib/libSM.so.6 (0xb7d8b000) libICE.so.6 => /usr/lib/libICE.so.6 (0xb7d74000) libm.so.6 => /lib/libm .so.6 (0xb7d4e000) libc.so.6 => /lib/libc.so.6 (0xb7c05000) libXau.so.6 => /usr/lib/libXau.so.6 (0xb7c01000) libxcb-xlib.so .0 => /usr/lib/libxcb-xlib.so.0 (0xb7bff000) libxcb.so.1 => /usr/lib/libxcb.so.1 (0xb7be8000) libdl.so.2 => /lib/libdl .so.2 (0xb7be4000) /lib/ld-linux.so.2 (0xb7efb000) libXdmcp.so.6 => /usr/lib/libXdmcp.so.6 (0xb7bdf000)
Alasan granularitas yang lebih besar adalah karena sistem operasi modern cukup pintar untuk memungkinkan Anda melakukan lebih dari sekadar menyimpan item duplikat pada disk, sesuatu yang dialami oleh perpustakaan statis. Proses eksekusi berbeda yang menggunakan pustaka bersama yang sama juga dapat berbagi segmen kode (tetapi bukan segmen data atau segmen bss - misalnya, dua proses berbeda dapat berada di tempat berbeda saat menggunakan, katakanlah, strtok). Untuk mencapai hal ini, seluruh perpustakaan harus ditangani dalam satu gerakan sehingga semua tautan internal diselaraskan dengan cara yang unik. Memang, jika satu proses mengambil a.o dan co.o , dan proses lainnya b.o dan co.o , maka OS tidak akan dapat menggunakan kecocokan apa pun.

Windows dll

Meskipun prinsip umum perpustakaan bersama kurang lebih sama pada platform Unix dan Windows, masih ada beberapa detail yang dapat dipahami oleh pemula.

Simbol yang diekspor

Perbedaan terbesarnya adalah di perpustakaan Windows, simbol tidak ada diekspor secara otomatis. Di Unix, semua simbol dari semua file objek yang telah ditautkan ke perpustakaan bersama dapat dilihat oleh pengguna perpustakaan tersebut. Di Windows, pemrogram harus secara eksplisit membuat beberapa karakter terlihat, mis. ekspor mereka.

Ada tiga cara untuk mengekspor simbol dan DLL Windows (dan ketiga metode ini dapat digabungkan dalam perpustakaan yang sama).

  • Dalam kode sumber, deklarasikan simbol sebagai __declspec(dllexport) , seperti ini:
    __declspec(dllexport) int my_exported_function(int x, double y)
  • Saat menjalankan perintah linker, gunakan opsi ekspor LINK.EXE: simbol_ke_ekspor
    LINK.EXE /dll /export:my_exported_function
  • Masukkan file definisi modul (DEF) ke linker (menggunakan opsi /DEF: def_file), dengan menyertakan bagian EKSPOR dalam file ini, yang berisi simbol-simbol yang akan diekspor.
    EKSPOR fungsi_ekspor_saya_fungsi_ekspor_lainnya
Setelah C++ terlibat dalam kekacauan ini, opsi pertama menjadi yang paling sederhana, karena dalam hal ini kompiler bertanggung jawab untuk mengurusnya.

.LIB dan file terkait perpustakaan lainnya

Kita sampai pada kesulitan kedua dengan perpustakaan Windows: informasi tentang simbol yang diekspor yang harus ditautkan oleh linker ke simbol lain tidak terkandung dalam DLL itu sendiri. Sebaliknya, informasi ini terdapat dalam file .LIB yang sesuai.

File LIB yang terkait dengan DLL menjelaskan simbol mana (yang diekspor) yang ada di DLL beserta lokasinya. Biner apa pun yang menggunakan DLL harus mengakses file .LIB untuk menghubungkan simbol dengan benar.

Yang lebih membingungkan lagi, ekstensi .LIB juga digunakan untuk perpustakaan statis.

Faktanya, ada sejumlah file berbeda yang mungkin terkait dengan perpustakaan Windows. Bersama dengan file .LIB, serta file .DEF (opsional), Anda dapat melihat semua file berikut yang terkait dengan perpustakaan Windows Anda.

Ini adalah perbedaan besar pada Unix, di mana hampir semua informasi yang terdapat dalam semua file tambahan ini hanya ditambahkan ke perpustakaan itu sendiri.

Simbol yang diimpor

Selain mengharuskan DLL untuk mendeklarasikan secara eksplisit, Windows juga mengizinkan biner yang menggunakan kode perpustakaan untuk secara eksplisit mendeklarasikan simbol yang akan diimpor. Ini tidak wajib, tetapi memberikan beberapa optimasi kecepatan karena sifat historis dari jendela 16-bit.

Kita dapat melacak daftar ini, sekali lagi menggunakan nm . Pertimbangkan file C++ berikut:
kelas Fred ( pribadi: int x; int y; publik: Fred() : x(1), y(2) () Fred(int z): x(z), y(3) () ); Fred theFred; Fred yang LainFred(55);
Untuk kode ini ( Bukan dihiasi) keluaran nm terlihat seperti ini:
Simbol dari global_obj.o: Nama Nilai Tipe Kelas Ukuran Bagian Garis __gxx_personality_v0| | kamu | BUKAN JENIS| | |*UND* __inisialisasi_statis_dan_penghancuran_0(int, int) |00000000| t | FUNGSI|00000039| |.teks Fred::Fred(int) |00000000| W | FUNGSI|00000017| |.teks._ZN4FredC1Ei Fred::Fred() |00000000| W | FUNGSI|00000018| |.text._ZN4FredC1Ev theFred |00000000| B | OBYEK|00000008| |.bss the OtherFred |00000008| B | OBYEK|00000008| |.bss konstruktor global dengan kunci theFred |0000003a| t | FUNGSI|0000001a| |.teks
Seperti biasa, kita bisa melihat banyak hal berbeda di sini, tapi salah satu yang paling menarik bagi kami adalah postingan kelas W(yang berarti simbol “lemah”) serta entri dengan nama bagian seperti ".gnu.linkonce.t. hal-hal". Ini adalah penanda untuk konstruktor objek global dan kita melihat bahwa bidang “Nama” yang sesuai menunjukkan apa yang sebenarnya dapat kita harapkan di sana - masing-masing dari dua konstruktor terlibat.

Templat

Sebelumnya, kami menyediakan tiga implementasi fungsi max yang berbeda, yang masing-masing menggunakan tipe argumen berbeda. Namun, kita melihat bahwa kode isi fungsi identik dalam ketiga kasus. Dan kita tahu bahwa menduplikasi kode yang sama adalah praktik pemrograman yang buruk.

C++ memperkenalkan konsep templat(templat), yang memungkinkan Anda menggunakan kode di bawah ini untuk semua kasus sekaligus. Kita dapat membuat file header max_template.h hanya dengan satu salinan kode fungsi max:
templat T max(T x, T y) ( jika (x>y) kembalikan x; jika tidak kembalikan y; )
dan sertakan file ini dalam file sumber untuk mencoba fungsi templat:
#include "max_template.h" int main() ( int a=1; int b=2; int c; c = max(a,b); // Kompiler secara otomatis menentukan apa sebenarnya max yang dibutuhkan (int,int) ganda x = 1,1; mengambang y = 2.2; z ganda; z = maks (x,y); // Kompiler tidak dapat menentukan, jadi kami memerlukan maks (ganda, ganda) mengembalikan 0; )
Kode ini, ditulis dalam C++, menggunakan max (int,int) dan maks (ganda, ganda) . Namun, beberapa kode lain dapat menggunakan contoh lain dari pola ini. Katakanlah maks (float,float) atau bahkan maks (KelasFloatingPointSaya,KelasFloatingPointSaya) .

Masing-masing contoh yang berbeda ini menghasilkan kode mesin yang berbeda. Jadi, pada saat program akhirnya ditautkan, compiler dan linker harus memastikan bahwa kode untuk setiap contoh templat yang digunakan disertakan dalam program (dan tidak ada contoh templat yang tidak digunakan yang disertakan, agar tidak memperbesar ukuran program).

Bagaimana hal ini dilakukan? Biasanya ada dua tindakan: menipiskan contoh duplikat atau menunda instantiasi hingga tahap tautan (saya biasanya menyebut pendekatan ini sebagai cara cerdas dan cara Matahari).

Metode menipiskan contoh berulang menyiratkan bahwa setiap file objek berisi kode semua templat yang ditemui. Misalnya untuk file di atas, isi file objeknya terlihat seperti ini:
Simbol dari max_template.o: Nama Nilai Tipe Kelas Ukuran Bagian Garis __gxx_personality_v0 | | kamu | BUKAN JENIS| | |*UND* ganda maks (ganda, ganda) |00000000| W | FUNGSI|00000041| |.teks _Z3maxIdET_S0_S0_ int maks (int, int) |00000000| W | FUNGSI|00000021| |.teks._Z3maxIiET_S0_S0_ utama |00000000| T | FUNGSI|00000073| |.teks
Dan kami melihat keberadaan kedua instance tersebut maks (int,int) dan maks (ganda, ganda) .

Kedua definisi tersebut ditandai sebagai karakter yang lemah, dan ini berarti bahwa linker, saat membuat file akhir yang dapat dieksekusi, dapat membuang semua instance duplikat dari template yang sama dan hanya menyisakan satu (dan jika dianggap perlu, ia dapat memeriksa apakah semua instance duplikat dari template benar-benar dipetakan ke kode yang sama). Kerugian terbesar dari pendekatan ini adalah peningkatan ukuran masing-masing file objek.

Pendekatan lain (yang digunakan dalam Solaris C++) adalah dengan tidak menyertakan definisi template dalam file objek sama sekali, namun menandainya sebagai simbol yang tidak terdefinisi. Ketika sampai pada tahap penautan, linker dapat mengumpulkan semua simbol yang tidak terdefinisi yang sebenarnya milik contoh templat, dan kemudian menghasilkan kode mesin untuk masing-masing simbol tersebut.

Hal ini jelas mengurangi ukuran setiap file objek, namun kelemahan dari pendekatan ini adalah linker harus melacak lokasi kode sumber dan harus dapat menjalankan kompiler C++ pada waktu link (yang dapat memperlambat seluruh proses )

Perpustakaan yang dimuat secara dinamis

Fitur terakhir yang akan kita bahas di sini adalah pemuatan dinamis perpustakaan bersama. Kita melihat bagaimana penggunaan perpustakaan bersama menunda tautan akhir hingga program benar-benar berjalan. Dalam OS modern, hal ini bahkan dimungkinkan pada tahap selanjutnya.

Hal ini dilakukan dengan sepasang panggilan sistem, dlopen dan dlsym (perkiraan setara pada Windows masing-masing disebut LoadLibrary dan GetProcAddress). Yang pertama mengambil nama perpustakaan bersama dan memuatnya ke ruang alamat proses yang sedang berjalan. Tentu saja, perpustakaan ini mungkin juga memiliki simbol yang belum terselesaikan, jadi memanggil dlopen mungkin memerlukan pemuatan perpustakaan bersama lainnya.

Dlopen menawarkan pilihan untuk menghapus semua yang belum terselesaikan segera setelah perpustakaan dimuat (RTLD_NOW) atau menyelesaikan simbol sesuai kebutuhan (RTLD_LAZY). Metode pertama berarti pemanggilan dlopen mungkin memakan waktu cukup lama, tetapi metode kedua menimbulkan risiko tertentu bahwa selama eksekusi program akan ditemukan referensi yang tidak ditentukan yang tidak dapat diselesaikan, yang pada akhirnya program akan dihentikan.

Tentu saja, simbol dari perpustakaan yang dimuat secara dinamis tidak boleh memiliki nama. Namun, hal ini dapat dengan mudah diselesaikan, sama seperti masalah pemrograman lainnya diselesaikan, dengan menambahkan lapisan solusi tambahan. Dalam hal ini, penunjuk ke ruang karakter digunakan. Panggilan ke dlsym mengambil parameter literal yang menentukan nama simbol yang akan ditemukan dan mengembalikan pointer ke lokasinya (atau NULL jika simbol tidak ditemukan).

Interoperabilitas dengan C++

Proses pemuatan dinamis cukup mudah, namun bagaimana cara interaksinya dengan berbagai fitur C++ yang memengaruhi perilaku linker secara keseluruhan?

Pengamatan pertama menyangkut hiasan nama. Saat memanggil dlsym , nama simbol yang akan ditemukan dilewatkan. Ini berarti bahwa ini harus merupakan versi nama yang terlihat oleh linker, mis. nama yang dihias.

Karena proses dekorasi dapat bervariasi dari platform ke platform dan dari kompiler ke kompiler, ini berarti hampir tidak mungkin untuk menemukan simbol C++ secara dinamis menggunakan metode universal. Bahkan jika Anda hanya bekerja dengan satu kompiler dan mempelajari dunia batinnya, ada masalah lain - selain fungsi sederhana seperti C, ada banyak hal lain (tabel metode virtual dan sejenisnya) yang perlu diperhatikan juga .

Untuk meringkas hal di atas, biasanya lebih baik untuk memiliki satu titik masuk "C" eksternal yang dapat ditemukan dengan dlsym ". Titik masuk ini bisa berupa metode pabrik yang mengembalikan pointer ke semua instance kelas C++, memungkinkan akses ke semua kesenangan dari C++.

Kompiler mungkin dapat menangani konstruktor objek global di perpustakaan yang dimuat oleh dlopen , karena ada beberapa simbol khusus yang dapat ditambahkan ke perpustakaan, dan yang akan dipanggil oleh linker (tidak peduli saat memuat atau mengeksekusi waktu) jika perpustakaan dimuat atau dibongkar secara dinamis - maka panggilan yang diperlukan ke konstruktor atau destruktor dapat terjadi di sini. Di Unix, ini adalah fungsi _init dan _fini, atau untuk sistem baru yang menggunakan toolkit GNU, ada fungsi yang diberi label __attribute__((constructor)) atau __attribute__((destructor)) . Di Windows, fungsi terkait adalah DllMain dengan DWORD fdwReason sama dengan DLL_PROCESS_ATTACH atau DLL_PROCESS_DETACH .

Dan sebagai kesimpulan, kami akan menambahkan bahwa pemuatan dinamis melakukan pekerjaan yang sangat baik dalam “menipiskan kejadian berulang” ketika membuat contoh template; dan semuanya tampak ambigu dengan "menunda instantiasi", karena "tahap penautan" terjadi setelah program dijalankan (dan sangat mungkin di komputer lain yang tidak menyimpan sumber). Lihat dokumentasi compiler dan linker untuk menemukan solusi terhadap situasi ini.

Selain itu

Artikel ini sengaja menghilangkan banyak detail tentang cara kerja linker karena saya yakin apa yang ditulis mencakup 95% masalah sehari-hari yang dihadapi seorang programmer saat menghubungkan programnya.

Jika Anda ingin mengetahui lebih lanjut, Anda bisa mendapatkan informasinya pada link di bawah ini:

Terima kasih banyak kepada Mike Capp dan Ed Wilson atas saran berguna tentang halaman ini.

Hak Cipta 2004-2005,2009-2010 David Drysdale

Izin diberikan untuk menyalin, mendistribusikan dan/atau memodifikasi dokumen ini berdasarkan ketentuan Lisensi Dokumentasi Gratis GNU, Versi 1.1 atau versi lebih baru yang diterbitkan oleh Free Software Foundation; tanpa Bagian Invarian, tanpa Teks Sampul Depan, dan tanpa Teks Sampul Belakang. Salinan lisensi tersedia.

Tag: Tambahkan tag

Tag: Linker, linker, file objek, perpustakaan statis, perpustakaan dinamis, eksekusi program, definisi, deklarasi

Panduan pemula untuk linker. Bagian 1

Terjemahan artikel Panduan pemula untuk linker dengan contoh dan tambahan.

Konsep-konsep berikut digunakan secara bergantian: linker dan linker, definisi dan definisi, deklarasi dan deklarasi. Sisipan dengan contoh disorot dengan warna abu-abu.

Penamaan komponen: apa yang ada di dalam file C

Pertama, Anda perlu memahami perbedaan antara deklarasi dan definisi. Definisi mengaitkan nama dengan implementasi nama tersebut, yang dapat berupa data atau kode:

  • Mendefinisikan suatu variabel menyebabkan kompiler mengalokasikan memori untuk variabel tersebut dan mungkin mengisinya dengan beberapa nilai awal
  • Mendefinisikan suatu fungsi menyebabkan kompiler menghasilkan kode untuk fungsi tersebut

Deklarasi tersebut memberi tahu kompiler C bahwa di suatu tempat dalam program, mungkin di file lain, terdapat definisi yang terkait dengan nama ini (perhatikan bahwa definisi tersebut dapat langsung berupa deklarasi yang definisinya ada di tempat yang sama).

Untuk variabel, ada dua jenis definisi

  • Variabel global yang ada selama masa program (alokasi statis) dan biasanya diakses dari banyak fungsi
  • Variabel lokal yang hanya ada selama eksekusi fungsi di mana variabel tersebut dideklarasikan (penempatan lokal) dan hanya dapat diakses di dalamnya

Untuk lebih jelasnya, "dapat diakses" berarti suatu variabel dapat direferensikan dengan nama yang dikaitkan dengan definisinya.

Ada beberapa kasus di mana hal-hal tidak begitu jelas.

  • Variabel lokal statis sebenarnya bersifat global karena variabel tersebut ada sepanjang masa program, meskipun variabel tersebut dapat diakses dalam satu fungsi.
  • Seperti variabel statis, variabel global yang hanya dapat diakses dalam file yang sama tempat variabel tersebut dideklarasikan juga bersifat global.

Perlu segera diingat bahwa mendeklarasikan suatu fungsi statis mengurangi cakupannya ke file yang mendefinisikannya (yaitu, fungsi dari file ini dapat mengaksesnya).

Variabel lokal dan global juga dapat dibagi menjadi tidak diinisialisasi dan diinisialisasi (yang sudah diisi sebelumnya dengan beberapa nilai).

Bagaimanapun, kita dapat bekerja dengan variabel yang dibuat secara dinamis menggunakan fungsi malloc (atau operator baru di C++). Tidak mungkin mengakses area memori berdasarkan nama, jadi kami menggunakan pointer - variabel bernama yang menyimpan alamat area memori yang tidak disebutkan namanya. Area ini juga dapat dikosongkan menggunakan free (atau delete), sehingga memori dianggap dialokasikan secara dinamis.

Mari kita gabungkan semuanya sekarang

Kode Data
Global Lokal Dinamis
Diinisialisasi Tidak diinisialisasi Diinisialisasi Tidak diinisialisasi
Pernyataan int fn(ke dalam x); eksternal int x; eksternal int x; T/A T/A T/A
Definisi ke dalam fn(ke dalam x) ( ... ) ke dalam x = 1;
(pada cakupan file)
ke dalam x;
(pada cakupan file)
ke dalam x = 1;
(pada lingkup fungsi)
ke dalam x;
(pada lingkup fungsi)
(int* p = malloc(ukuran(int));)

Lebih mudah untuk melihat program ini

/* Ini adalah definisi dari variabel global yang tidak diinisialisasi */ int x_global_uninit; /* Ini adalah definisi dari variabel global yang diinisialisasi */ int x_global_init = 1; /* Ini adalah definisi dari variabel global yang tidak diinisialisasi, tetapi hanya dapat diakses berdasarkan nama dari file C yang sama */ static int y_global_uninit; /* Ini adalah definisi variabel global yang diinisialisasi, tetapi hanya dapat diakses berdasarkan nama dari file C yang sama */ static int y_global_init = 2; /* Ini adalah deklarasi variabel global yang ada di tempat lain dalam program */ extern int z_global; /* Ini adalah deklarasi fungsi yang didefinisikan di tempat lain dalam program. Anda dapat menambahkan kata layanan extern. Tapi itu tidak masalah */ int fn_a(int x, int y); /* Ini adalah definisi fungsi, tetapi karena didefinisikan dengan kata static, maka hanya tersedia dalam file C yang sama */ static int fn_b(int x) ( return x+1; ) /* Ini adalah definisi fungsi . Parameternya diperlakukan sebagai variabel lokal */ int fn_c(int x_local) ( /* Ini adalah definisi dari variabel lokal yang tidak diinisialisasi */ int y_local_uninit; /* Ini adalah definisi dari variabel lokal yang diinisialisasi */ int y_local_init = 3; /* Kode ini mengacu pada variabel dan fungsi lokal dan global berdasarkan nama */ x_global_uninit = fn_a(x_local, x_global_init); y_local_uninit = fn_a(x_local, y_local_init); y_local_uninit += fn_b(z_global); return (y_global_uninit + y_local_uninit); )

Biarkan file ini disebut file.c. Mari kita rakit seperti ini

Cc -g -O -c file.c

Mari kita ambil file objek file.o

Apa yang dilakukan kompiler C?

Tugas kompiler C adalah mengubah file kode dari sesuatu yang (terkadang) dapat dipahami oleh manusia menjadi sesuatu yang dapat dipahami oleh komputer. Output dari kompiler adalah file objek, yang biasanya memiliki ekstensi .o pada platform UNIX, dan .obj pada Windows. Isi file objek pada dasarnya adalah dua jenis objek

  • Definisi fungsi pencocokan kode
  • Data yang sesuai dengan variabel global yang ditentukan dalam file (jika sudah diinisialisasi sebelumnya, maka nilainya juga disimpan di sana)

Contoh objek ini akan memiliki nama yang terkait dengannya - nama variabel atau fungsi yang definisinya mengarah pada pembuatannya.

Kode objek adalah urutan instruksi mesin (yang dikodekan dengan tepat) yang sesuai dengan instruksi bahasa C—semuanya jika, sementara, dan bahkan gotos. Semua perintah ini beroperasi pada jenis informasi yang berbeda, dan informasi ini harus disimpan di suatu tempat (ini memerlukan variabel). Selain itu, mereka dapat mengakses potongan kode lain yang ditentukan dalam file.

Setiap kali kode mengakses suatu fungsi atau variabel, kompiler mengizinkannya melakukannya hanya jika ia telah melihat deklarasi variabel atau fungsi tersebut. Deklarasi adalah janji kepada kompiler bahwa suatu definisi ada di suatu tempat dalam program.

Tugas linker adalah memenuhi janji-janji ini, tetapi apa yang harus dilakukan kompiler ketika menemukan entitas yang tidak terdefinisi?

Pada dasarnya, kompiler hanya meninggalkan sebuah rintisan. Stub (tautan) mempunyai nama, namun nilai yang terkait dengannya belum diketahui.

Sekarang kita dapat menggambarkan secara kasar seperti apa program kita nantinya

Analisis file objek

Sejauh ini kami telah bekerja dengan program abstrak; Sekarang penting untuk melihat seperti apa praktiknya. Pada platform UNIX, Anda dapat menggunakan utilitas nm. Di Windows, yang setara adalah dumpbin dengan flag /symbols, meskipun ada juga port GNU binutils yang menyertakan nm.exe.

Mari kita lihat apa yang nm berikan kepada kita untuk program yang ditulis di atas:

00000000 b .bss 00000000 d .data 00000000 N .debug_abbrev 00000000 N .debug_aranges 00000000 N .debug_info 00000000 N .debug_line 00000000 N .debug_loc 00000000 i .drect ve 00000000 r .eh_frame 00000000 r .rdata$zzz 00000000 t .text U_fn_a 00000000 T _fn_c 00000000 D _x_global_init 00000004 C _x_global_uninit U _z_global

Untuk file yang dikompilasi sebelumnya file.o

Nm file.o

Outputnya mungkin berbeda dari satu sistem ke sistem lainnya, namun informasi kuncinya adalah kelas setiap karakter dan ukurannya (jika tersedia). Kelas dapat memiliki nilai-nilai berikut

  • Kelas U artinya tidak diketahui, atau rintisan, seperti disebutkan di atas. Hanya ada dua objek seperti itu: fn_a dan z_global (beberapa versi nm mungkin juga menampilkan bagian, yang dalam hal ini adalah *UND* atau UNDEF)
  • Kelas t atau T menunjukkan bahwa kode tersebut didefinisikan - t adalah lokal atau T adalah fungsi statis. Bagian .text juga dapat menjadi output
  • Kelas d dan D menunjukkan variabel global yang diinisialisasi, d sebagai variabel lokal, D sebagai variabel non-lokal. Segmen untuk data variabel biasanya .data
  • Untuk variabel global yang tidak diinisialisasi, kelas b digunakan jika statis/lokal atau B dan C jika tidak. Biasanya ini adalah segment.bss atau *COM*

Ada kelas jahat lainnya yang mewakili semacam mekanisme internal kompiler.

Apa fungsi tautannya? Bagian 1

Seperti yang telah kami definisikan sebelumnya, mendeklarasikan variabel atau fungsi merupakan janji kepada compiler bahwa terdapat definisi variabel atau fungsi tersebut di suatu tempat, dan tugas linker adalah memenuhi janji tersebut. Dalam diagram file objek kita ini juga dapat disebut "mengisi kekosongan".

Untuk mengilustrasikannya, berikut adalah file C lain selain yang pertama:

/* Variabel global diinisialisasi */ int z_global = 11; /* Variabel global kedua bernama y_global_init, tetapi keduanya statis */ static int y_global_init = 2; /* Deklarasi variabel global lain */ extern int x_global_init; int fn_a(int x, int y) ( return(x+y); ) int main(int argc, char *argv) ( const char *message = "Halo, dunia"; return fn_a(11,12); )

Biarkan file ini diberi nama main.c. Kami mengkompilasinya seperti sebelumnya

Cc –g –O –c utama.c

Dengan dua diagram ini, sekarang kita melihat bahwa semua titik dapat dihubungkan (dan jika tidak, linker akan menghasilkan kesalahan). Setiap benda mempunyai tempatnya, dan setiap tempat mempunyai benda, dan linker dapat menggantikan semua stub seperti yang ditunjukkan pada gambar.


Untuk main.o dan file.o yang telah dikompilasi sebelumnya, buatlah file yang dapat dieksekusi

Cc -o keluar.exe file main.o.o

keluaran nm untuk file yang dapat dieksekusi (out.exe dalam kasus kami):

Simbol dari sample1.exe: Nama Nilai Tipe Kelas Ukuran Bagian Baris _Jv_RegisterClasses | | w | BUKAN JENIS| | |*UND* __gmon_start__ | | w | BUKAN JENIS| | |*UND* __libc_start_main@@GLIBC_2.0| | kamu | FUNGSI|000001iklan| |*UND* _init |08048254| T | FUNGSI| | |.init _mulai |080482c0| T | FUNGSI| | |.teks __do_global_dtors_aux|080482f0| t | FUNGSI| | |.teks frame_dummy |08048320| t | FUNGSI| | |.teks fn_b |08048348| t | FUNGSI|00000009| |.teks fn_c |08048351| T | FUNGSI|00000055| |.teks fn_a |080483a8| T | FUNGSI|0000000b| |.teks utama |080483b3| T | FUNGSI|0000002c| |.teks __libc_csu_fini |080483e0| T | FUNGSI|00000005| |.teks __libc_csu_init |080483f0| T | FUNGSI|00000055| |.teks __do_global_ctors_aux|08048450| t | FUNGSI| | |.text_fini |08048478| T | FUNGSI| | |.fini_fp_hw |08048494| R | OBYEK|00000004| |.rodata_IO_stdin_used |08048498| R | OBYEK|00000004| |.rodata __FRAME_END__ |080484ac| r | OBYEK| | |.eh_frame __CTOR_LIST__ |080494b0| d | OBYEK| | |.ctors __init_array_end |080494b0| d | BUKAN JENIS| | |.ctors __init_array_start |080494b0| d | BUKAN JENIS| | |.ctors __CTOR_END__ |080494b4| d | OBYEK| | |.ctors __DTOR_LIST__ |080494b8| d | OBYEK| | |.dtors __DTOR_END__ |080494bc| d | OBYEK| | |.dtors __JCR_END__ |080494c0| d | OBYEK| | |.jcr __JCR_LIST__ |080494c0| d | OBYEK| | |.jcr_DINAMIS |080494c4| d | OBYEK| | |.dinamis _GLOBAL_OFFSET_TABLE_|08049598| d | OBYEK| | |.got.plt __data_start |080495ac| D | BUKAN JENIS| | |.data data_start |080495ac| W | BUKAN JENIS| | |.data __dso_handle |080495b0| D | OBYEK| | |.data hal.5826 |080495b4| d | OBYEK| | |.data x_global_init |080495b8| D | OBYEK|00000004| |.data y_global_init |080495bc| d | OBYEK|00000004| |.data z_global |080495c0| D | OBYEK|00000004| |.data y_global_init |080495c4| d | OBYEK|00000004| |.data __bss_start |080495c8| SEBUAH | BUKAN JENIS| | |*ABS* _edata |080495c8| SEBUAH | BUKAN JENIS| | |*ABS* selesai.5828 |080495c8| b | OBYEK|00000001| |.bss y_global_uninit |080495cc| b | OBYEK|00000004| |.bss x_global_uninit |080495d0| B | OBYEK|00000004| |.bss_end |080495d4| SEBUAH | BUKAN JENIS| | |*ABS*

Semua simbol dari kedua objek dikumpulkan di sini, dan semua referensi yang tidak terdefinisi telah dibersihkan. Simbol-simbol tersebut juga telah disusun ulang untuk menyatukan kelas-kelas yang serupa, dan beberapa entitas tambahan telah ditambahkan untuk membantu sistem operasi memperlakukan semuanya sebagai program yang dapat dieksekusi.

Untuk membersihkan keluaran pada UNIX, Anda dapat menghapus semua yang dimulai dengan garis bawah.

Karakter duplikat

Pada bagian sebelumnya, kita telah mempelajari bahwa jika linker tidak dapat menemukan definisi untuk simbol yang dideklarasikan, maka akan terjadi error. Apa yang terjadi jika dua definisi ditemukan untuk suatu simbol selama penautan?

Dalam C++, semuanya sederhana - menurut standar, sebuah simbol harus selalu memiliki satu definisi (yang disebut aturan satu definisi) dari bagian 3.2 standar bahasa.

Bagi si, semuanya kurang jelas. Suatu fungsi atau variabel global yang diinisialisasi harus selalu memiliki satu definisi saja. Namun mendefinisikan variabel global yang tidak diinisialisasi dapat dianggap sebagai langkah awal. C dalam hal ini mengizinkan (setidaknya tidak melarang) file kode yang berbeda memiliki definisi awal sendiri untuk objek yang sama.

Namun, linker juga harus berurusan dengan bahasa pemrograman lain yang tidak berlaku aturan satu definisi. Misalnya, wajar jika Fortran memiliki salinan setiap variabel global di setiap file yang diakses. Tautan terpaksa membuang semua salinan, memilih satu (biasanya versi tertinggi, jika ukurannya berbeda) dan membuang sisanya. Model ini sering disebut model rakitan UMUM, karena kata fungsi FORTRAN UMUM.

Akibatnya, linker UNIX biasanya tidak mengeluh tentang definisi simbol duplikat, setidaknya selama simbol duplikat tersebut merupakan variabel global yang tidak diinisialisasi (model ini dikenal sebagai model penghubungan ref/def yang santai). Jika ini mengganggu Anda (dan memang seharusnya demikian!), lihat dokumentasi kompiler Anda untuk mencari kunci yang membuat perilakunya lebih ketat. Misalnya, –fno-common untuk kompiler GNU memaksa variabel yang tidak diinisialisasi ditempatkan di segmen BSS, alih-alih menghasilkan blok umum.

Apa yang dilakukan sistem operasi?

Sekarang, setelah linker menyusun program yang dapat dieksekusi, menghubungkan semua simbol dengan definisi yang diperlukan, kita perlu berhenti sejenak dan memahami apa yang dilakukan sistem operasi saat menjalankan program.

Menjalankan suatu program menyebabkan eksekusi kode mesin, jadi, tentu saja, Anda perlu memindahkan program dari hard drive ke memori operasi, di mana prosesor pusat sudah dapat bekerja dengannya. Sepotong memori untuk suatu program disebut segmen kode atau segmen teks.

Kode tidak ada gunanya tanpa data, jadi semua variabel global juga harus memiliki ruang di RAM sendiri. Di sinilah terdapat perbedaan antara variabel global yang diinisialisasi dan tidak diinisialisasi. Variabel yang diinisialisasi sudah memiliki nilainya sendiri, yang disimpan dalam objek dan file yang dapat dieksekusi. Saat program dimulai, mereka disalin dari hard drive ke memori ke dalam segmen data.

Untuk variabel yang tidak diinisialisasi, OS tidak akan menyalin nilai dari memori (karena tidak ada) dan akan mengisi semuanya dengan nol. Sepotong memori yang diinisialisasi ke 0 disebut segmen bss.

Nilai awal dari variabel yang diinisialisasi disimpan pada disk, dalam file yang dapat dieksekusi; Untuk variabel yang tidak diinisialisasi, ukurannya disimpan.


Perlu diketahui bahwa selama ini kita hanya membicarakan variabel global dan tidak pernah menyebutkan objek lokal atau objek yang dibuat secara dinamis.

Data ini tidak memerlukan linker agar berfungsi karena masa pakainya dimulai saat program dimulai—lama setelah linker selesai dijalankan. Meski demikian, demi kelengkapan, kami tetap akan menunjukkannya sekali lagi

  • Variabel lokal berada di bagian memori yang disebut tumpukan, yang bertambah dan menyusut saat fungsi mulai atau selesai dijalankan.
  • Memori dinamis dialokasikan pada area yang dikenal sebagai heap; pemilihan akan ditangani oleh fungsi malloc

Kita sekarang dapat menambahkan area memori yang hilang ini ke diagram kita. Karena heap dan stack dapat mengubah ukurannya saat program sedang berjalan, untuk memperbaiki masalah, keduanya tumbuh ke arah satu sama lain. Dengan cara ini, pemadaman memori hanya akan terjadi ketika mereka bertemu (dan ini memerlukan penggunaan banyak memori).


Apa fungsi tautannya? Bagian 2

Setelah mempelajari dasar-dasar cara kerja linker, mari kita lanjutkan dan mempelajari kemampuan tambahannya, sesuai urutan kemunculannya dan ditambahkan ke linker secara historis.

Pengamatan pertama yang mengarah pada pengembangan linker: bagian kode yang sama sering digunakan kembali (input/output data, fungsi matematika, pembacaan file, dll.). Oleh karena itu, saya ingin mengalokasikannya di tempat terpisah dan menggunakannya bersama dengan banyak program.

Secara umum, cukup mudah untuk menggunakan file objek yang sama untuk membangun program yang berbeda, namun jauh lebih baik jika mengumpulkan file objek serupa dan membuat perpustakaan.

Perpustakaan statis

Bentuk perpustakaan yang paling sederhana adalah statis. Bagian sebelumnya menyatakan bahwa Anda cukup berbagi satu file objek antar program. Pada kenyataannya, perpustakaan statis tidak lebih dari sekedar file objek.

Pada sistem UNIX, perpustakaan statis biasanya dibuat dengan perintah ar, dan file perpustakaan itu sendiri memiliki ekstensi .a. Selain itu, file-file ini biasanya dimulai dengan awalan lib dan diteruskan ke linker dengan flag –l, diikuti dengan nama perpustakaan tanpa awalan lib dan tanpa ekstensi (misalnya, untuk file libfred.a Anda akan menambahkan -lfred).

Ar rcs libfile.a file.o gcc main.o libfile.a -o out.exe

Contoh yang lebih kompleks, mari kita punya tiga file

A.c int a_f(int a) ( kembalikan a + 1; ) b.c int b_f(int a) ( kembalikan a + 1; ) c.c int c_f(int a) ( kembalikan a + 1; )

Dan file utama

ABC.c #termasuk int main() ( int a = a_f(0); int b = a_f(1); int c = a_f(2); printf("%d %d %d", a, b, c); return 0; )

Mari kita kumpulkan a.c, b.c dan c.c ke dalam perpustakaan libabc.a. Pertama, mari kita kompilasi semua file (Anda bisa melakukannya secara terpisah, bersama-sama lebih cepat)

Gcc –g –O –c a.c b.c c.c abc.c

Kami akan menerima empat file objek. Setelah itu mari kita kumpulkan a, b dan c menjadi satu file

Ar rcs libabc.a a.o b.o c.o

dan sekarang kita dapat mengkompilasi programnya

Gcc -o abc.exe libabc.a abc.o

Harap dicatat bahwa mencoba merakit seperti ini

Gcc -o abc.exe abc.o libabc.a

akan menyebabkan kesalahan - linker akan mulai mengeluh tentang simbol yang belum terselesaikan.

Di Windows, perpustakaan statis biasanya memiliki ekstensi .lib dan dihasilkan oleh utilitas LIB, tetapi yang membuatnya membingungkan adalah ekstensi yang sama juga berlaku untuk perpustakaan impor, yang hanya berisi daftar hal-hal yang tersedia di perpustakaan dinamis (dll) .

Saat linker menelusuri kumpulan file objek untuk menyatukannya, linker mengkompilasi daftar simbol yang belum terselesaikan. Setelah memproses daftar semua objek yang dideklarasikan secara eksplisit, linker kini memiliki satu tempat lagi untuk mencari simbol yang tidak dideklarasikan: perpustakaan. Jika objek yang belum terselesaikan ada di perpustakaan, objek tersebut ditambahkan persis seperti yang ditentukan pengguna pada baris perintah.

Perhatikan tingkat detail objek: jika diperlukan simbol tertentu, maka seluruh objek yang berisi simbol tersebut ditambahkan. Dengan demikian, penghubung mungkin berada dalam situasi di mana ia mengambil satu langkah maju dan dua langkah mundur, karena objek baru, pada gilirannya, mungkin berisi simbol-simbolnya sendiri yang belum terselesaikan.

Detail penting lainnya adalah urutan kejadian. Perpustakaan dikueri hanya setelah penautan normal dilakukan, dan diproses secara berurutan, dari kiri ke kanan. Artinya jika suatu perpustakaan memerlukan simbol yang sebelumnya ada di perpustakaan yang terhubung sebelumnya, linker tidak akan dapat menemukannya secara otomatis.

Sebuah contoh akan membantu untuk memahami hal ini secara lebih rinci. Mari kita memiliki file objek a.o, b.o dan perpustakaan libx.a, liby.b.

Mengajukan a.o bo libx.a Libya
Obyek a.o bo x1.o x2.o x3.o kamu1.o kamu2.o kamu3.o
Definisi a1, a2, a3 b1, b2 x11, x12, x13 x21, x22, x23 x31, x32 y11, y12 y21, y22 y31, y32
Referensi tidak terdefinisi b2, x12 a3, y22 x23, y12 y11 y21 x31

Setelah memproses file a.o dan b.o, linker akan menyelesaikan link b2 dan a3, membiarkan x12 dan y22 tidak terdefinisi. Pada titik ini linker mulai memeriksa perpustakaan libx.a pertama dan menemukan bahwa ia dapat mengeluarkan x1.o, yang mendefinisikan simbol x12; Setelah melakukan ini, linker menerima simbol yang tidak terdefinisi x23 dan y12 yang dideklarasikan di x1.o (yaitu daftar simbol yang tidak terdefinisi mencakup y22, x23 dan y23).

Linker masih memeriksa libx.a, sehingga dapat dengan mudah menyelesaikan simbol x23 dengan menariknya dari perpustakaan x2.o di libx.a. Tapi x2.o ini menambahkan y11 (yang sekarang terdiri dari y11, y22 dan y12). Tak satu pun dari masalah ini dapat diselesaikan lebih lanjut menggunakan libx.a, jadi tautannya menuju ke liby.a.

Hal yang hampir sama terjadi di sini, dan linker mengeluarkan y1.o dan y2.o. Yang pertama menambahkan y21, tetapi mudah diselesaikan karena y2.o sudah terungkap. Hasil dari semua pekerjaan tersebut adalah linker mampu menyelesaikan semua simbol dan mengambil hampir semua file objek yang akan ditempatkan di final executable.

Perhatikan bahwa jika b.o, misalnya, berisi tautan ke y32, maka semuanya akan berjalan sesuai skenario yang berbeda. Memproses libx.a akan sama, tetapi memproses liby.a mengeluarkan y3.o yang berisi referensi x31 yang didefinisikan di libx.a. Karena libx.a telah selesai diproses, linker akan menimbulkan kesalahan.

Ini adalah contoh ketergantungan melingkar antara dua perpustakaan libx dan liby.

Perpustakaan Bersama

Pustaka C standar yang populer (biasanya libc) memiliki kelemahan yang jelas - setiap file yang dapat dieksekusi akan memiliki salinannya sendiri dari file yang sama. Jika setiap program memiliki salinan printf, fopen dan sejenisnya, maka banyak ruang disk yang terbuang.

Kerugian lain yang kurang jelas adalah setelah penautan statis, kode program tidak berubah. Jika seseorang menemukan dan memperbaiki bug di printf, maka semua program yang menggunakan perpustakaan ini perlu dibangun kembali.

Untuk mengatasi masalah ini, perpustakaan bersama telah diperkenalkan (biasanya memiliki ekstensi .so atau .dll di Windows, atau .dylib di Mac OS X). Saat bekerja dengan perpustakaan seperti itu, linker tidak diharuskan untuk menggabungkan semua elemen menjadi satu gambar. Sebaliknya, dia meninggalkan sesuatu seperti surat promes dan menunda pembayarannya sampai program tersebut diluncurkan.

Singkatnya: jika linker mengetahui bahwa simbol yang tidak terdefinisi ada di perpustakaan bersama, ia tidak menambahkan definisi ke simbol yang dapat dieksekusi. Sebaliknya, linker menulis ke program nama simbol dan perpustakaan di mana ia seharusnya didefinisikan.

Selama eksekusi program, sistem operasi menentukan bahwa bit-bit yang hilang ini terhubung “tepat pada waktunya.” Sebelum menjalankan fungsi utama, versi linker yang lebih kecil (seringkali ld.so) menelusuri daftar debitur dan menyelesaikan bagian akhir pekerjaan - ia mengambil kode dari perpustakaan dan menyusun teka-teki.

Ini berarti tidak ada program yang dapat dieksekusi yang memiliki salinan printf. Versi libc baru yang telah diperbaiki dapat dengan mudah menggantikan versi lama, dan versi tersebut akan diambil oleh masing-masing program saat diluncurkan kembali.

Ada perbedaan penting lainnya antara perpustakaan dinamis dan perpustakaan statis, dan ini tercermin dalam tingkat detail tautan. Jika simbol tertentu diambil dari perpustakaan bersama (misalnya, printf dari libc), seluruh konten perpustakaan tersebut dipetakan ke dalam ruang alamat. Ini sangat berbeda dari perpustakaan statis, yang hanya menarik file objek yang berisi definisi simbol yang dideklarasikan.

Dengan kata lain, perpustakaan bersama adalah hasil dari sebuah linker (bukan hanya file objek yang dirakit) dengan referensi yang terselesaikan di dalam objek dalam file tersebut. Sekali lagi: nm berhasil menggambarkan hal ini. Untuk perpustakaan statis, nm akan menampilkan sekumpulan file objek individual. Untuk perpustakaan bersama, liby.so hanya akan menentukan simbol x31 yang tidak ditentukan. Juga, untuk contoh kita dengan urutan tata letak dan referensi melingkar, tidak akan ada masalah, karena semua konten y3.o dan x3.o sudah ditarik keluar.

Ada juga alat berguna lainnya yang disebut ldd. Ini menunjukkan semua perpustakaan bersama tempat executable atau perpustakaan bergantung, dengan informasi tentang di mana ia dapat ditemukan. Agar program dapat berjalan dengan sukses, semua pustaka ini harus ditemukan, beserta semua dependensinya (biasanya, pada sistem UNIX, pemuat mencari pustaka dalam daftar folder, yang disimpan dalam variabel lingkungan LD_LIBRARY_PATH).

/usr/bin:ldd xeyes linux-gate.so.1 => (0xb7efa000) libXext.so.6 => /usr/lib/libXext.so.6 (0xb7edb000) libXmu.so.6 => /usr/lib /libXmu.so.6 (0xb7ec6000) libXt.so.6 => /usr/lib/libXt.so.6 (0xb7e77000) libX11.so.6 => /usr/lib/libX11.so.6 (0xb7d93000) libSM .so.6 => /usr/lib/libSM.so.6 (0xb7d8b000) libICE.so.6 => /usr/lib/libICE.so.6 (0xb7d74000) libm.so.6 => /lib/libm .so.6 (0xb7d4e000) libc.so.6 => /lib/libc.so.6 (0xb7c05000) libXau.so.6 => /usr/lib/libXau.so.6 (0xb7c01000) libxcb-xlib.so .0 => /usr/lib/libxcb-xlib.so.0 (0xb7bff000) libxcb.so.1 => /usr/lib/libxcb.so.1 (0xb7be8000) libdl.so.2 => /lib/libdl .so.2 (0xb7be4000) /lib/ld-linux.so.2 (0xb7efb000) libXdmcp.so.6 => /usr/lib/libXdmcp.so.6 (0xb7bdf000)

Di Windows misalnya

Ldd C:\Windows\System32\rundll32.exe ntdll.dll => /c/WINDOWS/SYSTEM32/ntdll.dll (0x77100000) KERNEL32.DLL => /c/WINDOWS/System32/KERNEL32.DLL (0x763a0000) KERNELBASE.dll => /c/WINDOWS/System32/KERNELBASE.dll (0x73e10000) apphelp.dll => /c/WINDOWS/system32/apphelp.dll (0x71ec0000) AcLayers.DLL => /c/WINDOWS/AppPatch/AcLayers.DLL (0x78830000 ) msvcrt.dll => /c/WINDOWS/System32/msvcrt.dll (0x74ef0000) USER32.dll => /c/WINDOWS/System32/USER32.dll (0x76fb0000) win32u.dll => /c/WINDOWS/System32/win32u .dll (0x74060000) GDI32.dll => /c/WINDOWS/System32/GDI32.dll (0x74b00000) gdi32full.dll => /c/WINDOWS/System32/gdi32full.dll (0x741e0000) SHELL32.dll => /c/WINDOWS /System32/SHELL32.dll (0x74fc0000) cfgmgr32.dll => /c/WINDOWS/System32/cfgmgr32.dll (0x74900000) windows.storage.dll => /c/WINDOWS/System32/windows.storage.dll (0x74390000) digabungkan .dll => /c/WINDOWS/System32/combase.dll (0x76490000) ucrtbase.dll => /c/WINDOWS/System32/ucrtbase.dll (0x74100000) RPCRT4.dll => /c/WINDOWS/System32/RPCRT4.dll (0x76b50000) bcryptPrimitives.dll => /c/WINDOWS/System32/bcryptPrimitives.dll (0x74940000) powrprof.dll => /c/WINDOWS/System32/powrprof.dll (0x73c20000) advapi32.dll => /c/WINDOWS/System 32 /advapi32.dll (0x76ad0000) sechost.dll => /c/WINDOWS/System32/sechost.dll (0x76440000) shlwapi.dll => /c/WINDOWS/System32/shlwapi.dll (0x76d30000) kernel.appcore.dll = > /c/WINDOWS/System32/kernel.appcore.dll (0x73c10000) shcore.dll => /c/WINDOWS/System32/shcore.dll (0x76c20000) profapi.dll => /c/WINDOWS/System32/profapi.dll ( 0x73c70000) ) Oleaut32.dll => /c/windows/system32/oleaut32.dll (0x76e20000) msvcp_win.dll => /c/windows/system32/msvcp_win.dll (0x74080000) setupi.dll =>/C/C/C /C/C/C/C/C/WIINDOM S/System32/Setupapi .dll (0x766c0000) MPR.dll => /c/WINDOWS/SYSTEM32/MPR.dll (0x6cac0000) sfc.dll => /c/WINDOWS/ SYSTEM32/sfc.dll (0x2380000) WINSPOOL.DRV => /c/WINDOWS /SYSTEM32/WINSPOOL.DRV (0x6f2f0000) bcrypt.dll => /c/WINDOWS/SYSTEM32/bcrypt.dll (0x73b70000) sfc_os.DLL => / c/WINDOWS/SYSTEM32/sfc_os.DLL (0x68e00000) IMM32.DLL => /c/WINDOWS/System32/IMM32.DLL (0x76d90000) imagehlp.dll => /c/WINDOWS/System32/imagehlp.dll (0x749a0000)

Alasan fragmentasi yang lebih besar ini adalah karena sistem operasinya cukup pintar sehingga Anda dapat menduplikasi ruang disk dengan lebih dari sekadar perpustakaan statis. Proses eksekusi yang berbeda juga dapat berbagi satu segmen kode (tetapi tidak berbagi segmen data/bss). Untuk melakukan ini, seluruh perpustakaan harus dipetakan dalam satu lintasan, sehingga semua referensi internal berbaris dalam satu baris: jika satu proses mengeluarkan a.o dan c.o, dan proses kedua mengeluarkan bo.o dan co.o, tidak akan ada kecocokan untuk proses tersebut. sistem operasi.


11. Prinsip pengoperasian sistem pemrograman. Fungsi editor teks dalam sistem pemrograman. Kompiler sebagai bagian integral dari sistem pemrograman.

Prinsip pengoperasian sistem pemrograman

Fungsi editor teks dalam sistem pemrograman

Editor teks dalam sistem pemrograman adalah program yang memungkinkan Anda membuat, memodifikasi, dan memproses kode sumber program dalam bahasa tingkat tinggi.

Pada prinsipnya, editor teks muncul tanpa koneksi apa pun dengan alat pengembangan. Mereka memecahkan masalah pembuatan, pengeditan, pemrosesan, dan penyimpanan teks apa pun di media eksternal yang tidak harus berupa teks sumber program dalam bahasa tingkat tinggi. Banyak editor teks yang masih menjalankan fungsi ini hingga saat ini.

Munculnya lingkungan pengembangan terintegrasi pada tahap tertentu dalam pengembangan alat pengembangan perangkat lunak memungkinkan untuk memasukkan editor teks secara langsung ke dalam alat ini. Awalnya, pendekatan ini mengarah pada fakta bahwa pengguna (pengembang program sumber) hanya bekerja di lingkungan editor teks, tanpa membiarkannya mengkompilasi, menautkan, mengunduh, dan menjalankan program untuk dieksekusi. Untuk ini

Penting untuk membuat alat yang memungkinkan untuk menampilkan kemajuan seluruh proses pengembangan program di lingkungan editor teks, seperti, misalnya, metode untuk menampilkan kesalahan dalam program sumber yang terdeteksi pada tahap kompilasi, dengan posisi di tempatnya. dalam teks program sumber yang mengandung kesalahan.

Kita dapat mengatakan bahwa dengan munculnya lingkungan pengembangan terintegrasi, hari-hari ketika pengembang kode sumber terpaksa menyiapkan teks program di atas kertas dan kemudian memasukkannya ke dalam komputer sudah ketinggalan zaman. Proses penulisan teks dan pembuatan perangkat lunak sebenarnya telah menjadi satu.

Lingkungan pengembangan terintegrasi telah terbukti menjadi alat yang sangat nyaman. Mereka mulai menaklukkan pasar alat pengembangan perangkat lunak. Dan seiring perkembangannya, peluang yang diberikan kepada pengembang di lingkungan editor teks semakin luas. Seiring waktu, alat untuk debugging langkah demi langkah program langsung dari teks sumbernya muncul, menggabungkan kemampuan debugger dan editor teks sumber. Contoh lain adalah alat yang sangat berguna yang memungkinkan Anda menyorot secara grafis semua token bahasa sumber berdasarkan tipenya dalam teks sumber suatu program - alat ini menggabungkan kemampuan editor teks sumber dan penganalisis leksikal kompiler.

Akibatnya, dalam sistem pemrograman modern, editor teks telah menjadi komponen penting, yang tidak hanya memungkinkan pengguna menyiapkan kode sumber untuk program, tetapi juga menjalankan semua fungsi antarmuka dan layanan yang disediakan sistem pemrograman kepada pengguna. Dan meskipun pengembang modern masih dapat menggunakan alat apa pun untuk menyiapkan kode sumber program, mereka biasanya masih lebih suka menggunakan editor teks yang disertakan dalam sistem pemrograman tertentu.

Kompiler sebagai bagian integral dari sistem pemrograman

Kompiler, tentu saja, merupakan modul utama dalam sistem pemrograman apa pun. Oleh karena itu, bukan suatu kebetulan jika mereka menjadi salah satu pokok bahasan utama dalam buku teks ini. Tanpa kompiler, tidak ada sistem pemrograman yang masuk akal, dan semua komponen lainnya sebenarnya hanya berfungsi untuk memastikan kompiler bekerja dan menjalankan fungsinya.

Dari tahap pertama pengembangan sistem pemrograman hingga munculnya lingkungan pengembangan terintegrasi, pengguna (pengembang program sumber) selalu berurusan dengan kompiler dalam satu atau lain cara. Mereka berinteraksi langsung dengannya sebagai modul perangkat lunak terpisah.

Sekarang, ketika bekerja dengan sistem pemrograman, pengguna biasanya hanya berurusan dengan antarmukanya, yang biasanya berupa editor teks dengan fungsi-fungsi lanjutan. Peluncuran modul kompiler dan semua pekerjaannya terjadi secara otomatis dan tersembunyi dari pengguna - pengembang hanya melihat hasil akhir dari eksekusi kompiler. Meskipun banyak sistem pemrograman modern masih mempertahankan kemampuan sebelumnya untuk interaksi langsung antara pengembang dan kompiler (ini adalah Makefile dan apa yang disebut "antarmuka baris perintah"), hanya segelintir profesional yang menggunakan alat ini. Kebanyakan pengguna sistem pemrograman saat ini sudah jarang berinteraksi langsung dengan compiler.

Faktanya, selain kompiler paling dasar, yang menerjemahkan teks sumber dalam bahasa input ke dalam bahasa instruksi mesin, sebagian besar sistem pemrograman mungkin berisi sejumlah kompiler dan penerjemah lain. Dengan demikian, sebagian besar sistem pemrograman berisi kompiler dari bahasa rakitan dan kompiler (penerjemah) dari bahasa deskripsi sumber daya input. Semuanya jarang berinteraksi langsung dengan pengguna.

Namun, ketika bekerja dengan sistem pemrograman apa pun, Anda harus ingat bahwa modul utamanya selalu berupa kompiler. Karakteristik teknis kompilerlah yang terutama mempengaruhi efisiensi program yang dihasilkan oleh sistem pemrograman.

penghubung. Tujuan dan fungsi linker.

12. Penghubung. Tujuan dan fungsi linker

Linker (atau editor tautan) dirancang untuk menghubungkan file objek yang dihasilkan oleh kompiler, serta file perpustakaan yang termasuk dalam sistem pemrograman 1 .

File objek (atau sekumpulan file objek) tidak dapat dieksekusi sampai semua modul dan bagian di dalamnya terhubung satu sama lain. Inilah yang dilakukan editor tautan (linker). Hasil pengoperasiannya adalah satu file (sering disebut "file yang dapat dieksekusi") yang berisi semua teks program bahasa kode mesin yang dihasilkan. Linker mungkin menghasilkan pesan kesalahan jika gagal mendeteksi komponen apa pun yang diperlukan saat mencoba merakit file objek menjadi satu kesatuan.

Fungsi linkernya cukup sederhana. Ia memulai pekerjaannya dengan memilih bagian program dari modul objek pertama dan memberinya alamat awal. Bagian program dari modul objek yang tersisa menerima alamat relatif terhadap alamat awal dalam urutan berikut. Dalam hal ini, fungsi menyelaraskan alamat awal bagian program juga dapat dilakukan. Bersamaan dengan penggabungan teks bagian program, bagian data, tabel pengidentifikasi, dan nama eksternal digabungkan. Tautan lintas bagian diperbolehkan.

Prosedur untuk menyelesaikan tautan direduksi menjadi menghitung nilai konstanta alamat prosedur, fungsi dan variabel, dengan mempertimbangkan pergerakan bagian relatif terhadap awal modul program yang dirakit. Jika referensi ke variabel eksternal yang tidak ada dalam daftar modul objek terdeteksi, editor tautan mengatur pencariannya di perpustakaan yang tersedia dalam sistem pemrograman. Jika komponen yang diperlukan tidak dapat ditemukan di perpustakaan, pesan kesalahan akan dihasilkan.

Biasanya, linker membuat modul perangkat lunak sederhana yang dibuat sebagai satu kesatuan. Namun, dalam kasus yang lebih kompleks, linker dapat membuat modul lain: modul program dengan struktur overlay, modul objek perpustakaan, dan modul perpustakaan yang terhubung secara dinamis (bekerja dengan modul overlay dan modul yang terhubung secara dinamis di OS dijelaskan di bagian pertama dari ini panduan).

13. Loader dan debugger. Fungsi pemuat boot

Kebanyakan modul objek dalam sistem pemrograman modern dibangun berdasarkan apa yang disebut alamat relatif. Kompiler, yang menghasilkan file objek, dan kemudian linker, yang menggabungkannya menjadi satu kesatuan, tidak dapat mengetahui secara pasti di area memori komputer mana program tersebut akan ditempatkan pada saat dijalankan. Oleh karena itu, mereka tidak bekerja dengan alamat sel RAM yang sebenarnya, tetapi dengan beberapa alamat relatif. Alamat tersebut dihitung dari titik konvensional tertentu, yang diambil sebagai awal dari area memori yang ditempati oleh program yang dihasilkan (biasanya ini adalah titik awal dari modul program pertama).

Tentu saja, tidak ada program yang dapat dijalankan pada alamat relatif ini. Oleh karena itu, diperlukan modul yang dapat mengubah alamat relatif menjadi alamat nyata (absolut) segera pada saat program diluncurkan untuk dieksekusi. Proses ini disebut terjemahan alamat dan dilakukan oleh modul khusus yang disebut loader.

Namun, boot loader tidak selalu merupakan bagian integral dari sistem pemrograman, karena fungsi yang dijalankannya sangat bergantung pada arsitektur sistem komputer target di mana program yang dihasilkan oleh sistem pemrograman tersebut dijalankan. Pada tahap pertama pengembangan OS, boot loader ada dalam bentuk modul terpisah yang melakukan terjemahan alamat dan menyiapkan program untuk dieksekusi - menciptakan apa yang disebut "gambar tugas". Skema ini umum untuk banyak sistem operasi (misalnya, RTOS pada komputer tipe SM-1, OS RSX/11 atau RAFOS pada komputer tipe SM-4, dll.). Gambar tugas dapat disimpan di media eksternal atau dibuat kembali setiap kali program disiapkan untuk dijalankan.

Dengan berkembangnya arsitektur komputasi komputer, penerjemahan alamat dapat dilakukan secara langsung pada saat program diluncurkan untuk dieksekusi. Untuk melakukan ini, perlu menyertakan tabel terkait dalam file yang dapat dieksekusi yang berisi daftar tautan ke alamat yang perlu diterjemahkan. Pada saat file yang dapat dieksekusi diluncurkan, OS memproses tabel ini dan mengubah alamat relatif menjadi alamat absolut. Skema ini, misalnya, khas untuk sistem operasi seperti MS-DOS, yang tersebar luas di kalangan komputer pribadi. Dalam skema ini, tidak ada modul bootloader (sebenarnya, ini adalah bagian dari OS), dan sistem pemrograman hanya bertanggung jawab untuk menyiapkan tabel terjemahan alamat - fungsi ini dilakukan oleh linker.

Dalam sistem operasi modern, terdapat metode konversi alamat kompleks yang bekerja secara langsung selama eksekusi program. Metode-metode ini didasarkan pada kemampuan yang dibangun ke dalam perangkat keras arsitektur sistem komputasi. Metode penerjemahan alamat dapat didasarkan pada organisasi memori segmen, halaman, dan halaman segmen (semua metode ini dibahas di bagian pertama manual ini). Kemudian, untuk melakukan penerjemahan alamat, tabel sistem yang sesuai harus disiapkan pada saat program diluncurkan. Fungsi-fungsi ini sepenuhnya berada pada modul OS, sehingga tidak dijalankan dalam sistem pemrograman.

Modul lain dari sistem pemrograman yang fungsinya erat kaitannya dengan eksekusi program adalah debugger.

Debugger adalah modul perangkat lunak yang memungkinkan Anda melakukan tugas-tugas dasar yang berkaitan dengan pemantauan proses eksekusi program aplikasi yang dihasilkan. Proses ini disebut debugging dan mencakup fitur-fitur utama berikut:


  • eksekusi langkah demi langkah berurutan dari program yang dihasilkan pada OS
    langkah-langkah baru pada perintah mesin atau operator bahasa input;

  • eksekusi program yang dihasilkan hingga mencapai salah satu yang ditentukan
    breakpoint lokal (alamat break);

  • eksekusi program yang dihasilkan sebelum terjadinya hal tertentu yang ditentukan
    kondisi yang terkait dengan data dan alamat yang diproses oleh program ini
    ku;

  • melihat isi area memori yang ditempati oleh perintah atau data
    program yang dihasilkan.
Awalnya, debugger adalah modul perangkat lunak terpisah yang dapat memproses program yang dihasilkan dalam bahasa perintah mesin. Kemampuan mereka terutama terbatas pada memodelkan eksekusi program yang dihasilkan dalam arsitektur sistem komputer yang bersangkutan. Eksekusi dapat dilakukan terus menerus atau bertahap. Pengembangan lebih lanjut dari debugger dikaitkan dengan poin-poin mendasar berikut:

  • munculnya lingkungan pembangunan yang terintegrasi;

  • munculnya dukungan perangkat keras untuk alat debugging di banyak tempat
    sistem komputasi.
Langkah pertama memungkinkan pengembang program untuk bekerja bukan dalam hal instruksi mesin, tetapi dalam hal bahasa pemrograman sumber, yang secara signifikan mengurangi biaya tenaga kerja untuk men-debug perangkat lunak. Pada saat yang sama, debugger tidak lagi menjadi modul terpisah dan menjadi bagian terintegrasi dari sistem pemrograman, karena mereka sekarang harus mendukung pekerjaan dengan tabel pengidentifikasi (lihat bagian “Tabel pengidentifikasi. Organisasi tabel pengidentifikasi,” Bab 15) dan Anda meningkatkan tugas kebalikan dari identifikasi unit leksikal bahasa (lihat bagian “Analisis semantik dan persiapan pembuatan kode”, Bab 14). Hal ini disebabkan oleh fakta bahwa dalam lingkungan seperti itu, debugging program terjadi berdasarkan nama yang diberikan oleh pengguna, dan bukan berdasarkan nama internal yang diberikan oleh kompiler. Perubahan yang sesuai juga diperlukan dalam fungsi kompiler linker, karena mereka harus menyertakan tabel nama dalam komposisi objek dan file yang dapat dieksekusi agar dapat diproses oleh debugger.

Langkah kedua memungkinkan untuk memperluas kemampuan alat debugging secara signifikan, karena sekarang mereka tidak memerlukan pemodelan operasi dan arsitektur sistem komputer yang sesuai. Eksekusi program yang dihasilkan dalam mode debug menjadi mungkin di lingkungan yang sama seperti dalam mode normal. DI DALAM zg Debugger hanya menyertakan fungsi mentransfer sistem komputer ke mode yang sesuai sebelum meluncurkan program yang dihasilkan untuk debugging. Dalam banyak hal, fungsi-fungsi ini merupakan prioritas, karena sering kali memerlukan pengaturan tabel sistem dan flag prosesor sistem komputer.
Debugger dalam sistem pemrograman modern adalah modul dengan antarmuka pengguna yang dikembangkan yang bekerja langsung dengan teks dan modul program sumber. Banyak fungsinya yang terintegrasi: dengan fungsi editor teks sumber yang disertakan dalam sistem pemrograman.

14. Perpustakaan subrutin sebagai bagian integral dari sistem pemrograman

Perpustakaan rutinitas merupakan bagian penting dari sistem pemrograman.
Perpustakaan rutin telah menjadi bagian dari alat pengembangan sejak tahap awal pengembangannya. Bahkan ketika kompiler masih menyediakan modul program mereka sendiri yang terpisah, mereka sudah dikaitkan dengan perpustakaan yang sesuai, karena kompilasi melibatkan menghubungkan program dengan fungsi standar bahasa sumber.Fungsi-fungsi ini harus menjadi bagian dari perpustakaan.

Dari sudut pandang sistem pemrograman, perpustakaan subrutin terdiri dari dua komponen utama. Ini adalah file itu sendiri (atau sekumpulan file perpustakaan) yang berisi kode objek, dan sekumpulan file yang menjelaskan fungsi, perangkat lunak, program, konstanta, dan variabel yang membentuk perpustakaan.

15. Analisis leksikal “on the fly”. Sistem petunjuk dan referensi.

Fitur tambahan dari sistem pemrograman

Analisis leksikal dengan cepat. Sistem petunjuk dan bantuan

Analisis leksikal sambil jalan adalah fungsi editor teks sebagai bagian dari sistem pemrograman. Ini terdiri dari pencarian dan penyorotan token bahasa input dalam teks program secara langsung selama proses pembuatannya oleh pengembang.

Hal ini diterapkan sebagai berikut: pengembang membuat teks sumber program (mengetiknya atau menerimanya dari sumber lain), dan pada saat yang sama, sistem pemrograman secara bersamaan mencari leksem dalam teks ini.

Dalam kasus paling sederhana, leksem yang terdeteksi hanya disorot dalam teks menggunakan antarmuka grafis editor teks - warna, font, dll. Hal ini membuat pekerjaan pengembang program lebih mudah, membuat teks sumber lebih visual dan membantu mendeteksi kesalahan di a tahap yang sangat awal – pada tahap penyiapan kode sumber.

Dalam sistem pemrograman yang lebih maju, leksem yang ditemukan tidak hanya dialokasikan selama persiapan teks sumber, tetapi juga ditempatkan dalam tabel pengenal kompiler yang termasuk dalam sistem pemrograman. Pendekatan ini memungkinkan Anda menghemat waktu pada tahap kompilasi, karena tahap pertama - analisis leksikal - telah selesai pada tahap penyiapan teks sumber program.

Fitur layanan berikutnya yang diberikan kepada pengembang oleh sistem pemrograman karena analisis leksikal on-the-fly adalah kemampuan pengembang untuk mengakses tabel pengidentifikasi selama persiapan teks sumber program. Pengembang dapat menginstruksikan kompiler untuk menemukan token yang dibutuhkannya dalam tabel. Pencarian dapat dilakukan berdasarkan jenis atau beberapa bagian informasi token (misalnya, berdasarkan beberapa huruf pertama). Selain itu, pencarian dapat peka terhadap konteks - sistem pemrograman akan memberikan kesempatan kepada pengembang untuk menemukan jenis leksem yang persis seperti yang dapat digunakan di tempat tertentu dalam teks sumber. Selain leksem itu sendiri, pengembang dapat diberikan beberapa informasi tentangnya - misalnya, tipe dan komposisi parameter formal untuk suatu fungsi, daftar metode yang tersedia untuk suatu tipe atau instance kelas. Hal ini sekali lagi membuat pekerjaan pengembang lebih mudah, karena menghilangkan kebutuhannya untuk mengingat komposisi fungsi dan jenis banyak modul (terutama modul perpustakaan) atau merujuk sekali lagi ke dokumentasi dan informasi referensi.

Analisis leksikal langsung adalah fungsi canggih yang sangat menyederhanakan pekerjaan yang terkait dengan penyiapan teks sumber. Ini disertakan tidak hanya di banyak sistem pemrograman, tetapi juga di banyak editor teks yang disediakan secara terpisah dari sistem pemrograman (dalam kasus terakhir, ini memungkinkan Anda untuk menyesuaikan kosakata bahasa tertentu).

Fungsi layanan lain yang nyaman dalam sistem pemrograman modern adalah sistem petunjuk dan bantuan. Biasanya, ini berisi tiga bagian utama:


  • bantuan tentang semantik dan sintaksis bahasa input yang digunakan;

  • petunjuk tentang bekerja dengan sistem pemrograman itu sendiri;

  • informasi tentang fungsi perpustakaan yang termasuk dalam sistem pemrograman
    Nia.
Sistem petunjuk dan bantuan saat ini merupakan bagian integral dari banyak aplikasi dan program sistem. Biasanya, ini didukung oleh utilitas OS yang sesuai. Oleh karena itu, antara lain, banyak sistem pemrograman menyertakan fungsi layanan yang memungkinkan Anda membuat dan melengkapi sistem petunjuk dan bantuan. Hal ini dilakukan agar pengembang dapat membuat dan mendistribusikan petunjuk dan bantuan yang relevan beserta program aplikasinya.

16.Pengembangan program dalam arsitektur client-server

Struktur aplikasi dibangun dalam arsitektur client-server.

Perkembangan perpustakaan dan sumber daya program aplikasi yang terhubung secara dinamis telah menyebabkan situasi di mana sebagian besar program aplikasi tidak lagi berupa modul perangkat lunak tunggal, namun sekumpulan komponen yang saling berhubungan secara kompleks. Banyak dari komponen ini merupakan bagian dari OS atau memerlukan pengiriman dan instalasi dari pengembang lain, yang sering kali tidak memiliki hubungan dengan pengembang program aplikasi itu sendiri.

Selain itu, di antara seluruh rangkaian komponen program aplikasi, dua komponen yang terintegrasi secara logis dapat dibedakan: yang pertama - menyediakan aplikasi "tingkat bawah", yang bertanggung jawab atas metode penyimpanan, akses, dan berbagi data; yang kedua mengatur “tingkat atas” aplikasi, yang mencakup logika pemrosesan data dan antarmuka pengguna.

Komponen pertama, biasanya, adalah sekumpulan komponen pihak ketiga. Seringkali hal ini entah bagaimana terhubung dengan akses ke database, yang dapat menyediakan organisasi yang cukup kompleks. Untuk mengoperasikan komponen-komponennya, diperlukan sistem komputasi berkinerja tinggi.

Komponen kedua mencakup algoritma aktual, logika dan seluruh antarmuka yang dibuat oleh pengembang program. Untuk itu diperlukan koneksi dengan metode untuk mengakses data yang terdapat pada komponen pertama. Persyaratan sistem komputasi yang diperlukan untuk mengimplementasikan komponen-komponennya biasanya jauh lebih rendah dibandingkan komponen pertama.

Kemudian muncullah konsep aplikasi yang dibangun dengan arsitektur client-server. Komponen (server) pertama dari aplikasi tersebut mencakup semua metode yang terkait dengan akses data. Paling sering mereka diimplementasikan oleh ser-

Ver DB (server data) dari DBMS (sistem manajemen basis data) terkait lengkap dengan driver untuk mengaksesnya. Bagian kedua (klien) dari aplikasi mencakup semua metode untuk memproses data dan menyajikannya kepada pengguna. Bagian klien berinteraksi, di satu sisi, dengan server, menerima data darinya, dan di sisi lain, dengan pengguna, sumber daya aplikasi dan OS, memproses data dan menampilkan hasilnya. Klien dapat kembali menyimpan hasil pengolahannya ke database menggunakan fungsi bagian server.

Selain itu, seiring berjalannya waktu, beberapa perusahaan manufaktur paling terkenal mulai mendominasi pasar DBMS. Mereka menawarkan antarmuka standar untuk mengakses DBMS yang mereka buat. Pengembang program aplikasi, pada gilirannya, mulai fokus pada mereka. Situasi ini juga mempengaruhi struktur sistem pemrograman. Banyak dari mereka mulai menawarkan alat yang ditujukan untuk membuat aplikasi dalam arsitektur client-server. Biasanya, alat-alat ini disediakan sebagai bagian dari sistem pemrograman dan mendukung kemampuan untuk bekerja dengan berbagai server data yang dikenal melalui satu atau lebih antarmuka pertukaran data yang tersedia. Pengembang program aplikasi memilih salah satu alat yang tersedia ditambah kemungkinan jenis server (atau beberapa jenis kemungkinan), dan kemudian tugasnya dikurangi hanya untuk membuat bagian klien; aplikasi yang dibangun berdasarkan antarmuka yang dipilih. Setelah membuat bagian klien, pengembang kemudian dapat menggunakan dan mendistribusikannya hanya bersama dengan alat terkait dari sistem pemrograman. Antarmuka komunikasi biasanya disertakan dalam sistem pemrograman. Kebanyakan sistem pemrograman menyediakan kemampuan untuk mendistribusikan sarana akses ke sisi server tanpa batasan tambahan.

Sedangkan untuk bagian server, ada dua cara yang mungkin: server database paling sederhana mengharuskan pengembang untuk membeli lisensi alat untuk membuat dan men-debug database, tetapi sering kali mengizinkan hasil pekerjaan mereka untuk didistribusikan tanpa batasan tambahan; server database yang kuat, berorientasi pada pekerjaan puluhan dan ratusan pengguna, memerlukan perolehan lisensi untuk pembuatan dan distribusi bagian server aplikasi. Dalam hal ini, pengguna akhir aplikasi menerima berbagai macam produk perangkat lunak dari banyak pengembang.

Informasi lebih lanjut tentang pengorganisasian aplikasi berdasarkan arsitektur client-server dapat ditemukan di.

Saya ingin memahami dengan tepat bagian mana dari kompiler program yang dilihat dan direferensikan oleh linker. Jadi saya menulis kode berikut:

#termasuk menggunakan namespace std; #termasuk void FunctionTemplate (paramType val) ( i = val ) ); void Test::DefinedCorrectFunction(int val) ( i = val; ) void Test::DefinedIntrueFunction(int val) ( i = val ) void main() ( Test testObject(1); //testObject.NonDefinedFunction(2); / /testObject.FunctionTemplate (2); }

Saya memiliki tiga fungsi:

  • DefinedCorrectFunction adalah fungsi normal, dideklarasikan dan didefinisikan dengan benar.
  • DefinedIntrueFunction - fungsi ini dideklarasikan dengan benar, tetapi implementasinya salah (hilang;)
  • NonDefinedFunction hanya deklarasi. Tidak ada definisi.
  • FunctionTemplate - templat fungsi.

    Sekarang jika saya mengkompilasi kode ini saya mendapatkan kesalahan kompiler karena ";" yang hilang di DefinedIntrueFunction.
    Katakanlah saya memperbaikinya dan kemudian mengomentari testObject.NonDefinedFunction(2). Sekarang saya mendapatkan kesalahan tautan. Sekarang beri komentar pada testObject.FunctionTemplate(2). Sekarang saya mendapatkan kesalahan kompiler karena ";" hilang.

Untuk templat fungsi, pemahaman saya adalah bahwa templat tersebut tidak disentuh oleh kompiler kecuali jika dipanggil dalam kode. Jadi, ";" yang hilang Kompiler tidak mengeluh sampai saya memanggil testObject.FunctionTemplate(2).

Untuk testObject.NonDefinedFunction(2), kompiler tidak mengeluh, tetapi linker yang mengeluh. Sejauh yang saya pahami, seluruh kompiler seharusnya mengetahui bahwa fungsi NonDefinedFunction telah dideklarasikan. Dia tidak peduli dengan implementasinya. Linker kemudian mengeluh karena dia tidak dapat menemukan implementasinya. Sejauh ini bagus.

Jadi, saya tidak begitu mengerti apa sebenarnya yang dilakukan kompiler dan apa yang dilakukan linker. Pemahaman saya tentang komponen pembuat tautan dengan panggilannya. Jadi ketika NonDefinedFunction dipanggil, ia mencari implementasi NonDefinedFunction yang dikompilasi dan mengajukan keluhan. Namun kompiler tidak peduli tentang penerapan NonDefinedFunction, tetapi kompiler melakukannya untuk DefinedIntrueFunction.

Saya akan sangat menghargai jika ada yang bisa menjelaskan hal ini atau memberikan referensi.

8 jawaban

Fungsi compiler adalah mengkompilasi kode yang Anda tulis dan mengubahnya menjadi file objek. Jadi jika Anda melewatkannya; atau menggunakan variabel yang tidak ditentukan, kompiler akan mengeluh karena ini adalah kesalahan sintaksis.

Jika kompilasi berhasil tanpa kegagalan apa pun, file .object akan dibuat. File objek memiliki struktur yang kompleks, tetapi pada dasarnya berisi lima hal

  • Header - informasi tentang file
  • Kode objek - kode bahasa mesin (dalam banyak kasus kode ini tidak dapat berjalan sendiri)
  • Memindahkan informasi. Bagian kode mana yang perlu diubah alamatnya saat benar-benar dijalankan.
  • tabel simbol. Karakter yang direferensikan oleh kode. Mereka dapat ditentukan dalam kode ini, diimpor dari modul lain, atau ditentukan oleh linker
  • Informasi debug - digunakan oleh debugger

Kompiler mengkompilasi kode dan mengisi tabel simbol dengan setiap simbol yang ditemuinya. Simbol mengacu pada variabel dan fungsi. Jawaban atas pertanyaan ini menjelaskan tabel simbol.

Ini berisi kumpulan kode dan data yang dapat dieksekusi yang dapat diproses oleh linker dalam aplikasi produksi atau pustaka bersama. File objek memiliki struktur data yang disebut tabel simbol di dalamnya, yang memetakan berbagai elemen dalam file objek ke nama yang dapat dipahami oleh linker.

Poin catatan

Jika Anda memanggil suatu fungsi dari kode Anda, kompiler tidak memasukkan alamat akhir rutin dalam file objek. Sebaliknya, ia menempatkan nilai placeholder dalam kode dan menambahkan catatan yang memberitahu linker untuk mencari referensi dalam berbagai tabel simbol dari semua file objek yang diprosesnya, dan memasukkan lokasi akhir di sana.

File objek yang dihasilkan diproses oleh linker, yang mengisi kekosongan dalam tabel simbol, menghubungkan satu modul ke modul lainnya, dan akhirnya menghasilkan kode yang dapat dieksekusi yang dapat dimuat oleh loader.

Jadi dalam kasus spesifik Anda -

  • DefinedIntrueFunction() - Kompiler menerima definisi fungsi dan mulai mengkompilasinya untuk menghasilkan kode objek dan memasukkan referensi yang sesuai ke dalam tabel simbol. Kompilasi gagal karena kesalahan sintaksis, sehingga kompiler dibatalkan karena kesalahan.
  • NonDefinedFunction() - Kompiler menerima deklarasi tetapi tidak memiliki definisi, sehingga menambahkan entri ke tabel simbol dan menempatkan linker untuk menambahkan nilai yang sesuai (karena linker memproses banyak file objek, ada kemungkinan bahwa definisi ini hadir di beberapa file objek lain). Dalam kasus Anda, Anda tidak menentukan file lain, sehingga linker gagal dengan kesalahan referensi yang tidak ditentukan ke NonDefinedFunction karena tidak dapat menemukan referensi ke entri tabel simbol yang sesuai.

Untuk memahami hal ini, katakanlah lagi bahwa kode Anda terstruktur seperti ini

#termasuk #termasuk kelas Tes ( pribadi: int i; publik: Tes(int val) (i=val ;) void DefinedCorrectFunction(int val); void DefinedIntrueFunction(int val); void NonDefinedFunction(int val); templat void FunctionTemplate (paramType val) ( i = val; ) );

coba.cpp file

#include "try.h" void Test::DefinedCorrectFunction(int val) ( i = val; ) void Test::DefinedIntrueFunction(int val) ( i = val; ) int main() ( Test testObject(1); testObject. NonDefinedFunction(2); //objek tes.FunctionTemplate (2); kembali 0; )

Mari kita salin dan susun kodenya terlebih dahulu, tetapi jangan menautkannya

$g++ -c coba.cpp -o coba.o $

Langkah ini berjalan tanpa masalah. Jadi, Anda memiliki kode objek di try.o. Cobalah dan sambungkan.

$g++ try.o try.o: Dalam fungsi `main": try.cpp:(.text+0x52): referensi tidak terdefinisi ke `Test::NonDefinedFunction(int)" collector2: ld mengembalikan 1 status keluar

Anda lupa mendefinisikan Test::NonDefinedFunction. Mari kita definisikan dalam file terpisah.

File-coba1.cpp

#termasuk "try.h" void Test::NonDefinedFunction(int val) ( i = val; )

Mari kita kompilasi menjadi kode objek

$ g++ -c coba1.cpp -o coba1.o $

Sekali lagi ini berhasil. Mari kita coba menautkan file ini saja

$ g++ try1.o /usr/lib/gcc/x86_64-redhat-linux/4.4.5/../../../../lib64/crt1.o: Dalam fungsi `_start": (.text+ 0x20 ): referensi tidak terdefinisi ke `main" collector2: ld mengembalikan 1 status keluar

Tidak ada kemenangan utama; tidak terhubung!!

Anda sekarang memiliki dua kode objek terpisah yang memiliki semua komponen yang diperlukan. Cukup berikan KEDUAnya ke linker dan biarkan sisanya melakukannya

$g++ coba.o coba1.o $

Tidak ada kesalahan! Hal ini karena linker menemukan definisi semua fungsi (walaupun tersebar di file objek berbeda) dan mengisi spasi di kode objek dengan nilai yang sesuai.

Katakanlah Anda ingin makan sup, jadi pergilah ke restoran.

Anda sedang mencari menu sup. Jika Anda tidak menemukannya di menu, tinggalkan restoran. (seperti kompiler yang mengeluh tidak dapat menemukan fungsi). Jika Anda menemukannya, apa yang Anda lakukan?

Anda memanggil pelayan untuk datang membawakan sup Anda. Namun, hanya karena ada di menu bukan berarti mereka juga menyediakannya di dapur. Mungkin menunya sudah ketinggalan jaman, mungkin ada yang lupa memberi tahu chefnya agar membuat sup. Jadi sekali lagi kamu pergi. (misalnya error dari linker sehingga tidak dapat menemukan simbolnya)

Saya yakin ini adalah pertanyaan Anda:

Yang membuat saya bingung adalah ketika kompiler mengeluh tentang DefinedIntrueFunction. Itu tidak mencari implementasi NonDefinedFunction, tetapi melewati DefinedIntrueFunction.

Kompiler mencoba menguraikan DefinedIntrueFunction (karena Anda memberikan definisi dalam file sumber itu) dan terjadi kesalahan sintaksis (tidak ada titik koma). Di sisi lain, kompiler tidak pernah melihat definisi untuk NonDefinedFunction karena tidak ada kode dalam modul tersebut. Anda mungkin telah menentukan definisi NonDefinedFunction di file sumber lain, tetapi kompiler tidak mengetahui hal ini. Kompiler hanya melihat satu file sumber (dan file header yang disertakan) pada satu waktu.

Kompilator memeriksa apakah kode sumber sesuai untuk bahasa tersebut dan mengikuti semantik bahasa tersebut. Output kompiler adalah kode objek.

Linker menghubungkan berbagai modul objek bersama-sama untuk membentuk sebuah exe. Definisi fungsi ditempatkan pada fase ini, dan kode yang sesuai untuk memanggilnya ditambahkan pada fase ini.

Kompiler mengkompilasi kode ke dalam unit terjemahan. Ini akan mengkompilasi semua kode yang disertakan dalam file .cpp sumber.
DefinedIntrueFunction() didefinisikan dalam file sumber Anda, sehingga kompiler memeriksa kebenaran bahasanya.
NonDefinedFunction() memiliki beberapa definisi dalam file sumber sehingga kompiler tidak perlu mengkompilasinya, jika definisi tersebut ada di beberapa file sumber lain, fungsinya akan dikompilasi sebagai bagian dari unit terjemahan itu dan kemudian linker akan menautkannya jika pada tahap penautan, definisi tidak ditemukan oleh penaut, maka akan terjadi kesalahan penautan.

Apa yang dilakukan compiler, dan apa yang dilakukan linker, bergantung pada implementasinya: implementasi legal mungkin hanya menyimpan sumber yang diberi token di "compiler" dan melakukan semuanya di linker. Implementasi modern semakin menekankan pada linker, untuk pengoptimalan yang lebih baik. Dan banyak implementasi template awal bahkan tidak melihat kode template sampai waktu referensi, selain kurung kurawal yang cocok sudah cukup untuk mengetahui di mana template berakhir. Dari sudut pandang pengguna, Anda lebih tertarik pada apakah kesalahan tersebut memerlukan "diagnosis" (yang dapat dipilih oleh kompiler atau tautan) atau tidak ditentukan.

Dalam kasus DefinedIntrueFunction, Anda memberikan teks sumber yang diperlukan untuk analisis. Teks ini berisi kesalahan yang memerlukan diagnostik. Dalam kasus NonDefinedFunction: jika fungsi digunakan, kegagalan untuk memberikan definisi (atau memberikan lebih dari satu definisi) dalam program lengkap merupakan pelanggaran terhadap satu aturan definisi, yaitu perilaku tidak terdefinisi. Tidak diperlukan diagnostik (tetapi saya tidak dapat membayangkan mana yang tidak menyertakan beberapa definisi fungsi yang digunakan yang hilang).

Dalam praktiknya, kesalahan yang dapat dengan mudah dideteksi hanya dengan memeriksa masukan teks dari satu unit terjemahan ditentukan oleh standar "diagnostik yang diperlukan" dan akan dideteksi oleh kompiler. Kesalahan yang tidak dapat dideteksi dengan memeriksa satu unit terjemahan (misalnya, definisi yang hilang yang mungkin ada di unit terjemahan lain) memiliki perilaku yang secara formal tidak terdefinisi; dalam banyak kasus kesalahan dapat dideteksi oleh penghubung, dan dalam kasus seperti itu implementasinya sebenarnya menimbulkan kesalahan.

Ini agak dimodifikasi dalam kasus seperti fungsi sebaris, di mana Anda diperbolehkan mengulangi definisi di setiap unit terjemahan, dan dimodifikasi oleh templat, karena banyak kesalahan tidak dapat dideteksi hingga dipakai. Dalam hal templat, lembar implementasi standar memiliki banyak kebebasan: paling tidak, kompiler harus mengurai templat secukupnya untuk menentukan di mana templat berakhir. menambahkan hal-hal standar seperti typename, namun memungkinkan penguraian yang jauh lebih banyak sebelum pembuatan. Namun, dalam konteks dependen, beberapa kesalahan mungkin tidak terdeteksi hingga instance dibuat, yang mungkin terjadi pada waktu kompilasi atau waktu tautan; implementasi awal tata letak waktu tautan yang disukai; waktu kompilasi adalah hari ini, dan VC++ dan g++ digunakan.

Linker (atau editor tautan) dirancang untuk menghubungkan file objek yang dihasilkan oleh kompiler, serta file perpustakaan yang disertakan dalam sistem pemrograman.

File objek (atau sekumpulan file objek) tidak dapat dieksekusi sampai semua modul dan bagian di dalamnya terhubung satu sama lain. Inilah yang dilakukan editor tautan (linker). Hasil kerjanya adalah satu file yang disebut modul boot.

Modul beban adalah modul perangkat lunak yang cocok untuk memuat dan mengeksekusi, diperoleh dari modul objek saat mengedit tautan dan mewakili program dalam bentuk urutan perintah mesin.

Linker mungkin menghasilkan pesan kesalahan jika gagal mendeteksi komponen apa pun yang diperlukan saat mencoba merakit file objek menjadi satu kesatuan.

Fungsi linkernya cukup sederhana. Ia memulai pekerjaannya dengan memilih bagian program dari modul objek pertama dan memberinya alamat awal. Bagian program dari modul objek yang tersisa menerima alamat relatif terhadap alamat awal dalam urutan berikut. Dalam hal ini, fungsi menyelaraskan alamat awal bagian program juga dapat dilakukan. Bersamaan dengan penggabungan teks bagian program, bagian data, tabel pengidentifikasi, dan nama eksternal digabungkan. Tautan lintas bagian diperbolehkan.

Prosedur untuk menyelesaikan tautan direduksi menjadi menghitung nilai konstanta alamat prosedur, fungsi dan variabel, dengan mempertimbangkan pergerakan bagian relatif terhadap awal modul program yang dirakit. Jika referensi ke variabel eksternal yang tidak ada dalam daftar modul objek terdeteksi, editor tautan mengatur pencariannya di perpustakaan yang tersedia dalam sistem pemrograman. Jika komponen yang diperlukan tidak dapat ditemukan di perpustakaan, pesan kesalahan akan dihasilkan.

Biasanya, linker membuat modul perangkat lunak sederhana yang dibuat sebagai satu kesatuan. Namun, dalam kasus yang lebih kompleks, linker dapat membuat modul lain: modul program terstruktur overlay, modul objek perpustakaan, dan modul perpustakaan tautan dinamis.

Kebanyakan modul objek dalam sistem pemrograman modern dibangun berdasarkan apa yang disebut alamat relatif. Kompiler, yang menghasilkan file objek, dan kemudian linker, yang menggabungkannya menjadi satu kesatuan, tidak dapat mengetahui secara pasti di area memori komputer mana program tersebut akan ditempatkan pada saat dijalankan. Oleh karena itu, mereka tidak bekerja dengan alamat sel RAM yang sebenarnya, tetapi dengan beberapa alamat relatif. Alamat tersebut dihitung dari titik konvensional tertentu, yang diambil sebagai awal dari area memori yang ditempati oleh program yang dihasilkan (biasanya ini adalah titik awal dari modul program pertama).

Tentu saja, tidak ada program yang dapat dijalankan pada alamat relatif ini. Oleh karena itu, diperlukan modul yang dapat mengubah alamat relatif menjadi alamat nyata (absolut) segera pada saat program diluncurkan untuk dieksekusi. Proses ini disebut terjemahan alamat dan dilakukan oleh modul khusus yang disebut loader.

Namun, boot loader tidak selalu merupakan bagian integral dari sistem pemrograman, karena fungsi yang dijalankannya sangat bergantung pada arsitektur sistem komputer target di mana program yang dihasilkan oleh sistem pemrograman tersebut dijalankan. Pada tahap pertama pengembangan OS, boot loader ada dalam bentuk modul terpisah yang melakukan terjemahan alamat dan menyiapkan program untuk dieksekusi - menciptakan apa yang disebut "gambar tugas". Skema ini umum untuk banyak sistem operasi (misalnya, RTOS pada komputer tipe SM-1, OS RSX/11 atau RAFOS pada komputer tipe SM-4, dll.). Gambar tugas dapat disimpan di media eksternal atau dibuat kembali setiap kali program disiapkan untuk dijalankan.

Dengan berkembangnya arsitektur komputasi komputer, penerjemahan alamat dapat dilakukan secara langsung pada saat program diluncurkan untuk dieksekusi. Untuk melakukan ini, perlu menyertakan tabel terkait dalam file yang dapat dieksekusi yang berisi daftar tautan ke alamat yang perlu diterjemahkan. Pada saat file yang dapat dieksekusi diluncurkan, OS memproses tabel ini dan mengubah alamat relatif menjadi alamat absolut. Skema ini, misalnya, khas untuk sistem operasi seperti MS-DOS. Dalam skema ini, tidak ada modul bootloader (sebenarnya, ini adalah bagian dari OS), dan sistem pemrograman hanya bertanggung jawab untuk menyiapkan tabel terjemahan alamat - fungsi ini dilakukan oleh linker.

Dalam sistem operasi modern, terdapat metode konversi alamat kompleks yang bekerja secara langsung selama eksekusi program. Metode-metode ini didasarkan pada kemampuan yang dibangun ke dalam perangkat keras arsitektur sistem komputasi. Metode penerjemahan alamat dapat didasarkan pada organisasi memori segmen, halaman, dan halaman segmen. Kemudian, untuk melakukan penerjemahan alamat, tabel sistem yang sesuai harus disiapkan pada saat program diluncurkan. Fungsi-fungsi ini sepenuhnya berada pada modul OS, sehingga tidak dijalankan dalam sistem pemrograman.

Artikel untuk dibaca:

Bagaimana cara meningkatkan ke Miui 9 Stable Global dari firmware Cina? Membuka kunci bootloader