Penautan dengan ld

Berkas objek, titik masuk _start, penautan beberapa berkas, dan executable statik yang dihasilkan.

Daftar isi

Berkas objek belum dapat dijalankan karena alamatnya masih nol dan relokasinya masih kosong. Topik ini membahas langkah penautan dengan ld, yaitu menggabungkan beberapa berkas objek menjadi satu executable, menetapkan titik masuk _start, dan menyelesaikan seluruh relokasi. Program akhir diperiksa dengan file dan readelf, termasuk perilaku ld ketika simbol _start tidak ada.

Dari berkas objek ke executable#

ld membaca daftar berkas objek yang diberikan padanya, menggabungkan section yang bernama sama, lalu mengalokasikan alamat akhir untuk setiap section dan setiap simbol. Setelah alamat final diketahui, seluruh relokasi diisi, dan hasilnya adalah berkas ELF executable yang siap dimuat kernel.

nasm utama.asm
default rel

section .text
global _start
extern kuadrat

_start:
    mov  rdi, 7          ; argumen untuk kuadrat
    call kuadrat
    mov  edi, eax        ; kode keluar = kuadrat(7)
    mov  eax, 60         ; syscall exit
    syscall
nasm kuadrat.asm
default rel

section .text
global kuadrat

kuadrat:
    mov  rax, rdi
    imul rax, rdi        ; rax = rdi * rdi
    ret

utama.asm memanggil kuadrat dengan rdi sebagai argumennya menurut konvensi System V, sedangkan kuadrat.asm menyediakan fungsinya. Penautan dan pelaksanaannya ditunjukkan oleh rangkaian berikut.

bash
# rakit kedua berkas sumber menjadi berkas objek
nasm -f elf64 utama.asm -o utama.o
nasm -f elf64 kuadrat.asm -o kuadrat.o

# tautkan, lalu jalankan program akhirnya
ld utama.o kuadrat.o -o utama
./utama
echo "exit=$?"
text Keluaran
exit=49

Program berhenti dengan kode keluar 49, yaitu hasil kuadrat dari 7 yang dibawa keluar melalui eax. Tidak ada satu byte pun yang diubah setelah perakitan, karena seluruh penggabungan dan pengisian alamat terjadi sepenuhnya di dalam ld.

ld juga tidak menambahkan apa pun yang tidak diminta. Tidak ada runtime, tidak ada pustaka bawaan, dan tidak ada kode inisialisasi yang ikut disisipkan, sehingga executable yang dihasilkan hanya berisi instruksi dan data dari berkas objek yang diberikan. Section .text dari kedua berkas digabungkan menjadi satu, dengan kode kuadrat menyusul kode utama, dan penggabungan yang sama berlaku untuk section lain yang bernama sama. Perintah penautannya pun satu baris saja, yaitu ld diikuti daftar berkas objek dan nama hasilnya.

Titik masuk _start#

Kernel tidak mengeksekusi program dari byte pertamanya, melainkan dari alamat yang tercatat pada header ELF, dan ld menetapkan alamat itu dari simbol bernama _start. Simbol inilah pengganti main pada program yang tidak menggunakan runtime C. Perkakas file menyebut jenis berkasnya, sedangkan readelf -h menampilkan alamat titik masuknya.

bash
# jenis berkas dan alamat titik masuknya
file utama
readelf -h utama | grep Entry
text Keluaran
utama: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, not stripped
  Entry point address:               0x401000

Keluaran file menyebut statically linked karena seluruh kode program berada di dalam berkas itu sendiri tanpa pustaka bersama, sesuai sifat ld yang menautkan berkas objek yang diberikan apa adanya. Penyebut not stripped menandakan tabel simbol masih menyertai berkas, sehingga simbolnya masih dapat didaftar dengan nm, dan strip dapat membuangnya bila ukuran berkas lebih diutamakan. Alamat titik masuk 0x401000 jatuh pada awal section .text, tempat _start ditempatkan oleh penataan bawaan ld.

Konsekuensi titik masuk ini berkaitan dengan cara program berhenti. _start wajib diakhiri syscall exit, karena proses tidak berhenti dengan sendirinya setelah instruksi terakhir selesai. Program yang instruksi terakhirnya bukan exit akan terus mengeksekusi byte setelahnya sebagai instruksi, dengan akibat yang tidak dapat diprediksi. Seluruh contoh pada topik ini mengakhiri _start dengan rax berisi 60 diikuti instruksi syscall, sehingga proses berhenti secara terkendali beserta kode keluarnya.

Ketika _start tidak ada#

Berkas berikut sengaja tidak mendefinisikan _start, dan kondisi ini dibuat untuk mengamati perilaku ld.

bash
# berkas objek tanpa simbol _start
cat > tanpa-start.asm <<'EOF'
default rel

section .text
global mulai

fungsi:
    ret

mulai:
    mov  edi, 0
    mov  eax, 60
    syscall
EOF
nasm -f elf64 tanpa-start.asm -o tanpa-start.o
ld tanpa-start.o -o tanpa-start
echo "exit=$?"

# dijalankan, executable ini langsung rusak
sh -c ./tanpa-start
echo "exit=$?"
text Keluaran
ld: warning: cannot find entry symbol _start; defaulting to 0000000000401000
exit=0
Segmentation fault
exit=139

ld hanya memberi peringatan dan tetap membuat executable, dengan titik masuk diperkirakan pada 0x401000, byte pertama .text. Angka pada pesan peringatan itu adalah alamat bawaan yang dipilih ld, bukan alamat _start yang ditemukan, karena memang tidak ada simbol tersebut. Pada berkas ini byte pertama milik fungsi yang isinya ret saja, sehingga program mengambil alamat kembali dari stack yang belum pernah diisi dan melompat ke alamat liar. Eksekusi berakhir dengan Segmentation fault dan kode keluar 139, tanpa pernah mencapai instruksi exit yang sebenarnya tersedia. Simbol mulai pada berkas ini sengaja dinyatakan global agar dapat digunakan sebagai titik masuk pengganti pada percobaan berikutnya.

Opsi -e menetapkan simbol titik masuk yang lain, dan berkas ini sudah menyediakan simbol global bernama mulai.

bash
# titik masuk kustom dengan opsi -e
ld -e mulai tanpa-start.o -o tanpa-start
./tanpa-start
echo "exit=$?"
text Keluaran
exit=0

Program kini berhenti dengan kode keluar 0 karena eksekusi dimulai tepat dari label mulai yang memang diperuntukkan sebagai awal eksekusinya.

Skrip penautan bawaan#

Penempatan .text pada 0x401000 dan alamat dasar 0x400000 bukan kebetulan, melainkan hasil skrip penautan (linker script) bawaan yang digunakan ld ketika tidak ada skrip yang diberikan. Skrip itu menetapkan urutan section, alamat dasar setiap segmen, serta simbol bawaan seperti __executable_start, __bss_start, dan _end. Opsi --verbose menampilkan isi skrip bawaan tersebut, dan opsi -T menggantinya dengan skrip buatan sendiri. Penulisan skrip sendiri diperlukan pada pengembangan kernel atau firmware yang alamat memorinya tidak ditentukan oleh Linux, sedangkan program biasa pada jalur ini menggunakan skrip bawaan apa adanya.

Skrip bawaan juga menyuntikkan simbol penanda seperti __bss_start, _edata, dan _end ke dalam executable, dan ketiganya dapat didaftar dengan nm pada utama. Nilainya ditetapkan pada batas antarsection hasil penggabungan, dan runtime C lazim menggunakan simbol semacam ini untuk mengenali area yang harus diisi nol, sedangkan program assembly bebas menggunakan atau mengabaikannya. Perilaku skrip bawaan membuat hasil penautan dapat direproduksi, karena berkas sumber yang sama selalu ditempatkan pada tata letak yang sama pula.

Ringkasan#

  • ld menggabungkan section dari beberapa berkas objek, mengalokasikan alamat akhir, lalu mengisi seluruh relokasi.
  • Titik masuk ditetapkan dari simbol _start dan tercatat pada header ELF yang dapat dibaca dengan readelf -h.
  • ld hanya memperingatkan bila _start tidak ada sehingga executable yang rusak tetap dihasilkan, sedangkan opsi -e menetapkan titik masuk lain.
  • Hasil ld berupa executable statik yang seluruh kodenya berada di dalam berkasnya sendiri.

Referensi#