Pada mode 64-bit, rujukan data di dalam instruksi pada umumnya dihitung relatif terhadap RIP, yaitu register yang selalu menunjuk instruksi berikutnya. Topik ini membahas makna direktif default rel pada NASM, bentuk pengalamatan [rel label] yang dihasilkannya, serta cara menelusuri perhitungan tersebut pada berkas objek dan executable. Pemahaman ini menjadi dasar kode bebas posisi, yaitu kode yang dapat ditempatkan pada alamat memori mana pun tanpa diubah.
Direktif default rel dan [rel label]#
NASM menyediakan dua mode bawaan untuk operand memori yang ditulis dengan nama label. Dalam mode abs, operand [pesan] dihitung sebagai alamat absolut, sedangkan dalam mode rel operand yang sama dihitung sebagai [rel pesan], yaitu alamat relatif terhadap RIP. Baris default rel pada awal berkas memilih mode rel untuk seluruh berkas, sehingga setiap operand [label] berperilaku seperti [rel label] tanpa kata rel yang ditulis eksplisit. Apabila rujukan absolut benar-benar diperlukan, NASM menyediakan bentuk [abs label] untuk keluar dari mode rel pada satu instruksi saja.
Rumus yang digunakan prosesor selalu sama, yaitu alamat efektif sama dengan RIP ditambah displacement.
RIP yang dirujuk adalah alamat instruksi berikutnya, bukan alamat instruksi yang sedang membaca operandnya. Displacement yang tersedia berukuran 32 bit bertanda, sehingga jaraknya mencakup sekitar dua gigabyte ke dua arah, jauh melebihi jarak antara kode dan data pada program biasa.
Mode ini dipilih desainer x86-64 sebagai mode bawaan compiler karena hemat dan aman. Operan absolut 32 bit tidak dapat menyimpan alamat penuh 64 bit, sedangkan operan absolut 64 bit membuat instruksi membengkak sekaligus mengikat data pada satu alamat tetap. Pengalamatan relatif RIP menggunakan displacement 32 bit yang sama dengan pengalamatan basis biasa, hanya saja basisnya dipatok ke RIP sehingga tidak memerlukan register tambahan.
Menelusuri LEA pada berkas objek#
Instruksi lea memperlihatkan bentuk pengalamatan ini dengan paling jelas karena alamatnya dihitung tanpa mengakses memori. Program berikut mencetak pesan dari .rodata dengan memuat alamatnya melalui lea.
default rel
section .rodata
pesan: db "kode bebas posisi", 10
len: equ $ - pesan
section .text
global _start
_start:
lea rsi, [pesan] ; alamat pesan relatif terhadap RIP
mov edx, len ; panjang pesan
mov edi, 1 ; stdout
mov eax, 1 ; syscall write
syscall
mov edi, 0
mov eax, 60 ; syscall exit
syscallBaris lea rsi, [pesan] tidak menulis kata rel sama sekali, tetapi direktif default rel menjadikannya setara dengan lea rsi, [rel pesan]. Disassembly berikut menunjukkan encoding yang benar-benar dihasilkan assembler.
# rakit berkas objek, lalu amati instruksi LEA-nya
nasm -f elf64 pesan.asm -o pesan.o
objdump -d -M intel pesan.o
pesan.o: file format elf64-x86-64
Disassembly of section .text:
0000000000000000 <_start>:
0: 48 8d 35 00 00 00 00 lea rsi,[rip+0x0] # 7 <_start+0x7>
7: ba 12 00 00 00 mov edx,0x12
c: bf 01 00 00 00 mov edi,0x1
11: b8 01 00 00 00 mov eax,0x1
16: 0f 05 syscall
18: bf 00 00 00 00 mov edi,0x0
1d: b8 3c 00 00 00 mov eax,0x3c
22: 0f 05 syscallKeluaran memperlihatkan baris lea rsi,[rip+0x0] # 7 <_start+0x7>, yang menegaskan bahwa operandnya benar-benar berbasis RIP. Displacement-nya masih 0x0 karena assembler tidak mengetahui alamat akhir pesan, dan byte-nya dibiarkan kosong untuk diisi kemudian. Panjang seluruh instruksi lea ini tujuh byte, terdiri atas opcode, byte ModRM, dan displacement 4 byte yang menunggu pengisian, dan kekosongan itu tampak sebagai empat pasang 00 di kolom byte heksadesimalnya. Baris instruksi lain pada keluaran yang sama tidak membawa relokasi karena operannya berupa immediate dan register yang sudah lengkap. Komentar di ujung baris menunjukkan alamat yang dirujuk apabila displacement bernilai nol, yaitu 0x7, alamat instruksi berikutnya. Dari sini rumus pada bagian sebelumnya dapat diuji secara langsung pada executable.
Relokasi PC32 dan pengisiannya#
Byte displacement yang kosong tadi tercatat sebagai relokasi pada berkas objek.
# relokasi yang menunggu penyelesaian pada berkas objek
readelf -rW pesan.oRelocation section '.rela.text' at offset 0x360 contains 1 entry:
Offset Info Type Symbol's Value Symbol's Name + Addend
0000000000000003 0000000200000002 R_X86_64_PC32 0000000000000000 .rodata - 4Relokasi bertipe R_X86_64_PC32 dan merujuk simbol section .rodata dengan addend -4. Tipe PC32 menerangkan bahwa linker harus mengisi displacement 32 bit berdasarkan selisih antara alamat simbol dan alamat relokasi, sedangkan addend -4 menyesuaikan hasil agar dihitung dari akhir ruang displacement yang berukuran 4 byte. Satu-satunya relokasi pada berkas ini menegaskan bahwa perakitan sudah menyelesaikan segalanya kecuali satu jarak, dan kolom Info memadukan jenis relokasi dengan nomor entri tabel simbolnya dalam satu nilai heksadesimal. Setelah pola ini dikenali, keluaran readelf pada berkas objek apa pun dapat dibaca dengan cara yang sama. Proses pengisian dapat dilihat setelah penautan.
# tautkan, jalankan, lalu bandingkan instruksi LEA-nya
ld pesan.o -o pesan
./pesan
objdump -d -M intel pesan | grep -A 1 '<_start>:'kode bebas posisi
0000000000401000 <_start>:
401000: 48 8d 35 f9 0f 00 00 lea rsi,[rip+0xff9] # 402000 <pesan>Program mencetak pesannya dengan benar, dan disassembly pada executable menunjukkan displacement yang tadinya kosong kini terisi 0xff9. Alamat instruksi berikutnya adalah 0x401007, sehingga 0x401007 ditambah 0xff9 menghasilkan 0x402000, tepat pada alamat pesan. Linker menghitung nilai itu dari jarak antara instruksi dan datanya, bukan dari alamat absolut keduanya.
Kode bebas posisi dan PIE#
Tidak ada satu byte pun pada displacement yang menyimpan alamat absolut program, karena seluruh nilai hanya menyatakan jarak antara instruksi dan datanya. Segmen kode dan data karenanya dapat digeser ke alamat mana saja asal jaraknya tidak berubah. Sifat inilah yang disebut position independent code atau PIC, kode yang bebas dari alamat muat. Sifat tersebut menjadi dasar PIE (position independent executable), yaitu executable yang dapat ditempatkan oleh kernel pada alamat berbeda setiap kali dijalankan untuk mempersulit serangan yang bergantung pada alamat. Sifat yang sama digunakan pustaka bersama, yaitu kode yang dimuat ke ruang alamat proses yang sudah terisi sehingga alamat muatnya tidak mungkin ditetapkan pada saat penautan. Executable statik hasil ld pada topik ini dimuat pada alamat tetap 0x400000, tetapi mode pengalamatannya sendiri tidak bergantung pada alamat itu.
Diagram berikut merangkum alur dari berkas sumber sampai executable, beserta saat relokasi selesai diisi.
Ringkasan#
default relmenjadikan setiap operand memori berlabel dihitung relatif terhadap RIP tanpa kata rel yang eksplisit.- Alamat efektif selalu RIP ditambah displacement, dengan RIP menunjuk instruksi berikutnya, dan displacementnya berukuran 32 bit.
- Berkas objek menyimpan displacement kosong yang diisi linker berdasarkan relokasi R_X86_64_PC32.
- Karena hanya jarak yang tersimpan, kode menjadi bebas posisi dan dapat digunakan sebagai PIE.
Referensi#
- Assembler Directives — NASM — Direktif DEFAULT beserta subbab REL dan ABS tentang pengalamatan RIP-relatif.
- Intel 64 and IA-32 Software Developer Manuals — Intel — Encoding RIP-relative pada volume instruksi dan perilaku register RIP.
- readelf — GNU Binutils — Opsi -r untuk menampilkan relokasi yang menunggu penyelesaian.