Informasi tentang topik ini di web, atau di tempat lain, jarang terjadi. Sumber daya terpenting yang tersedia adalah ABI x64 resmi, Anda dapat mengunduhnya di sini (selanjutnya akan disebut sebagai "ABI"). Beberapa informasi juga dapat ditemukan di
man
halamangcc
... Tujuan artikel ini adalah untuk memberikan rekomendasi yang dapat diakses tentang topik tersebut, membahas masalah yang terkait dengannya, serta menunjukkan beberapa konsep melalui kode yang digunakan dalam pekerjaan.
Catatan Penting: Artikel ini tidak dimaksudkan sebagai tutorial untuk pemula. Sebelum berkenalan, disarankan untuk memiliki perintah C dan assembler yang kuat, serta kenalan dasar dengan arsitektur x64.
Lihat juga posting kami sebelumnya tentang topik serupa: Bagaimana x86_x64 menangani memori
Model Kode. Bagian motivasi
Dalam arsitektur x64, baik kode dan data dikirim melalui model pengalamatan relatif-perintah (atau, menggunakan jargon x64, relatif-RIP). Dalam perintah ini, pergeseran dari RIP terbatas pada 32 bit, namun, mungkin ada kasus ketika perintah, ketika mencoba menangani bagian dari memori atau data, sama sekali tidak memiliki cukup pergeseran 32 bit, misalnya, ketika bekerja dengan program lebih dari dua gigabyte.
Salah satu cara untuk mengatasi masalah ini adalah dengan sepenuhnya meninggalkan mode pengalamatan relatif-RIP demi perubahan 64-bit penuh untuk semua data dan referensi kode. Namun, langkah ini akan sangat mahal: untuk menutupi kasus (agak jarang) dari program dan pustaka yang sangat besar, bahkan operasi yang paling sederhana dalam seluruh kode akan memerlukan lebih banyak perintah.
Dengan demikian, model kode menjadi trade-off. [1] Model kode adalah perjanjian formal antara programmer dan kompiler di mana programmer menentukan niatnya tentang ukuran program yang diharapkan (atau program) yang akan dimasukkan ke dalam modul objek yang dikompilasi. [2] Diperlukan model kode agar pemrogram dapat memberi tahu kompiler: "jangan khawatir, modul objek ini hanya akan menuju ke program kecil, sehingga Anda dapat menggunakan mode pengalamatan relatif RIP yang cepat". Di sisi lain, ia dapat memberi tahu kompiler hal berikut: "kita akan mengkompilasi modul ini ke dalam program besar, jadi silakan gunakan mode pengalamatan absolut yang aman dan aman dengan pergeseran 64-bit penuh."
Apa yang akan diceritakan artikel ini
Kita akan berbicara tentang dua skenario yang dijelaskan di atas, model kode kecil dan model kode besar: model pertama memberi tahu kompiler bahwa offset relatif 32-bit harus cukup untuk semua kode dan referensi data dalam unit objek; yang kedua menegaskan bahwa kompiler menggunakan mode pengalamatan 64-bit absolut. Selain itu, ada juga versi menengah, yang disebut model kode tengah .
Masing-masing model kode disajikan dalam variasi PIC dan non-PIC independen, dan kita akan membicarakan masing-masing dari keenam model tersebut.
Contoh asli dalam C
Untuk menunjukkan konsep-konsep yang dibahas dalam artikel ini, saya akan menggunakan program C berikut dan mengompilasinya dengan berbagai model kode. Seperti yang Anda lihat, fungsi tersebut
main
mendapatkan akses ke empat array global yang berbeda dan satu fungsi global. Array berbeda dalam dua parameter: ukuran dan visibilitas. Ukuran penting untuk menjelaskan model kode rata-rata dan tidak akan diperlukan ketika bekerja dengan model kecil dan besar. Visibilitas penting untuk pengoperasian model kode PIC dan bersifat statis (hanya terlihat di file sumber) atau global (visibilitas ke semua objek yang ditautkan ke dalam program).
int global_arr[100] = {2, 3};
static int static_arr[100] = {9, 7};
int global_arr_big[50000] = {5, 6};
static int static_arr_big[50000] = {10, 20};
int global_func(int param)
{
return param * 10;
}
int main(int argc, const char* argv[])
{
int t = global_func(argc);
t += global_arr[7];
t += static_arr[7];
t += global_arr_big[7];
t += static_arr_big[7];
return t;
}
gcc
menggunakan model kode sebagai nilai opsi -mcmodel
. Selain itu, -fpic
kompilasi PIC dapat diatur dengan bendera .
Contoh kompilasi ke modul objek melalui model kode besar menggunakan PIC:
> gcc -g -O0 -c codemodel1.c -fpic -mcmodel=large -o codemodel1_large_pic.o
Model kode kecil
Terjemahan kutipan dari man gcc dengan subjek model kode kecil:
-mcmodel = small
Generasi kode untuk model kecil: program dan simbol-simbolnya harus diatur dalam dua gigabytes ruang alamat yang lebih rendah. Ukuran pointer adalah 64 bit. Program dapat dibangun baik secara statis maupun dinamis. Ini adalah model kode dasar.
Dengan kata lain, kompiler dapat dengan aman mengasumsikan bahwa kode dan data dapat diakses melalui offset relatif RIP 32-bit dari perintah apa pun dalam kode. Mari kita lihat contoh dibongkar dari program C yang kami susun melalui model kode kecil non-PIC:
> objdump -dS codemodel1_small.o
[...]
int main(int argc, const char* argv[])
{
15: 55 push %rbp
16: 48 89 e5 mov %rsp,%rbp
19: 48 83 ec 20 sub $0x20,%rsp
1d: 89 7d ec mov %edi,-0x14(%rbp)
20: 48 89 75 e0 mov %rsi,-0x20(%rbp)
int t = global_func(argc);
24: 8b 45 ec mov -0x14(%rbp),%eax
27: 89 c7 mov %eax,%edi
29: b8 00 00 00 00 mov $0x0,%eax
2e: e8 00 00 00 00 callq 33 <main+0x1e>
33: 89 45 fc mov %eax,-0x4(%rbp)
t += global_arr[7];
36: 8b 05 00 00 00 00 mov 0x0(%rip),%eax
3c: 01 45 fc add %eax,-0x4(%rbp)
t += static_arr[7];
3f: 8b 05 00 00 00 00 mov 0x0(%rip),%eax
45: 01 45 fc add %eax,-0x4(%rbp)
t += global_arr_big[7];
48: 8b 05 00 00 00 00 mov 0x0(%rip),%eax
4e: 01 45 fc add %eax,-0x4(%rbp)
t += static_arr_big[7];
51: 8b 05 00 00 00 00 mov 0x0(%rip),%eax
57: 01 45 fc add %eax,-0x4(%rbp)
return t;
5a: 8b 45 fc mov -0x4(%rbp),%eax
}
5d: c9 leaveq
5e: c3 retq
Seperti yang Anda lihat, akses ke semua array diatur dengan cara yang sama - menggunakan pergeseran relatif RIP. Namun, pergeseran dalam kode adalah 0, karena kompiler tidak tahu di mana segmen data akan ditempatkan, oleh karena itu, untuk setiap akses tersebut, itu menciptakan relokasi:
> readelf -r codemodel1_small.o
Relocation section '.rela.text' at offset 0x62bd8 contains 5 entries:
Offset Info Type Sym. Value Sym. Name + Addend
00000000002f 001500000002 R_X86_64_PC32 0000000000000000 global_func - 4
000000000038 001100000002 R_X86_64_PC32 0000000000000000 global_arr + 18
000000000041 000300000002 R_X86_64_PC32 0000000000000000 .data + 1b8
00000000004a 001200000002 R_X86_64_PC32 0000000000000340 global_arr_big + 18
000000000053 000300000002 R_X86_64_PC32 0000000000000000 .data + 31098
Mari kita, sebagai contoh, mengakses sepenuhnya sandi
global_arr
. Segmen yang dibongkar yang kami minati adalah:
t += global_arr[7];
36: 8b 05 00 00 00 00 mov 0x0(%rip),%eax
3c: 01 45 fc add %eax,-0x4(%rbp)
Pengalamatan RIP-relatif adalah relatif ke perintah berikutnya, jadi shift harus ditambal ke dalam perintah
mov
sehingga sesuai dengan 0x3s. Kami tertarik pada relokasi kedua, R_X86_64_PC32
itu menunjuk ke operan mov
di alamat 0x38
dan berarti yang berikut: kami mengambil nilai simbol, menambahkan istilah dan mengurangi pergeseran yang ditunjukkan oleh relokasi. Jika Anda menghitung semuanya dengan benar, Anda akan melihat bagaimana hasilnya menempatkan pergeseran relatif antara tim berikutnya dan global_arr
, plus 01
. Karena ini 01
berarti "int ketujuh dalam array" (dalam arsitektur x64 ukuran masing-masing int
adalah 4 byte), maka kita perlu pergeseran relatif ini. Dengan demikian, menggunakan pengalamatan RIP-relatif, perintah dengan benar referensi global_arr[7]
.
Sangat menarik untuk mencatat hal-hal berikut: walaupun perintah akses di
static_arr
sini mirip, pengalihannya menggunakan simbol yang berbeda, dengan demikian menunjuk ke bagian bukan simbol tertentu .data
. Hal ini disebabkan oleh tindakan penghubung, itu menempatkan array statis di lokasi yang diketahui di bagian, dan dengan demikian array tidak dapat dibagi dengan perpustakaan bersama lainnya. Akibatnya, penghubung akan menyelesaikan situasi dengan relokasi ini. Di sisi lain, karena global_arr
dapat digunakan (atau ditimpa) oleh pustaka bersama lainnya, pemuat dinamis yang sudah ada harus berurusan dengan tautan ke global_arr
. [3]
Akhirnya, mari kita lihat referensi untuk
global_func
:
int t = global_func(argc);
24: 8b 45 ec mov -0x14(%rbp),%eax
27: 89 c7 mov %eax,%edi
29: b8 00 00 00 00 mov $0x0,%eax
2e: e8 00 00 00 00 callq 33 <main+0x1e>
33: 89 45 fc mov %eax,-0x4(%rbp)
Karena operan
callq
juga relatif RIP, relokasi R_X86_64_PC32
bekerja di sini mirip dengan menempatkan pergeseran relatif aktual ke global_func di operan.
Sebagai kesimpulan, kami mencatat bahwa karena model kode kecil, kompilator menganggap semua data dan kode program masa depan sebagai dapat diakses melalui pergeseran 32-bit, dan dengan demikian menciptakan kode sederhana dan efisien untuk mengakses semua jenis objek.
Model kode besar
Terjemahan kutipan dari
man
gcc
pada topik model kode besar:
-mcmodel = besar
Menghasilkan kode untuk model besar: Model ini tidak membuat asumsi tentang alamat dan ukuran bagian.
Contoh kode bongkar yang
main
dikompilasi dengan model besar non-PIC:
int main(int argc, const char* argv[])
{
15: 55 push %rbp
16: 48 89 e5 mov %rsp,%rbp
19: 48 83 ec 20 sub $0x20,%rsp
1d: 89 7d ec mov %edi,-0x14(%rbp)
20: 48 89 75 e0 mov %rsi,-0x20(%rbp)
int t = global_func(argc);
24: 8b 45 ec mov -0x14(%rbp),%eax
27: 89 c7 mov %eax,%edi
29: b8 00 00 00 00 mov $0x0,%eax
2e: 48 ba 00 00 00 00 00 movabs $0x0,%rdx
35: 00 00 00
38: ff d2 callq *%rdx
3a: 89 45 fc mov %eax,-0x4(%rbp)
t += global_arr[7];
3d: 48 b8 00 00 00 00 00 movabs $0x0,%rax
44: 00 00 00
47: 8b 40 1c mov 0x1c(%rax),%eax
4a: 01 45 fc add %eax,-0x4(%rbp)
t += static_arr[7];
4d: 48 b8 00 00 00 00 00 movabs $0x0,%rax
54: 00 00 00
57: 8b 40 1c mov 0x1c(%rax),%eax
5a: 01 45 fc add %eax,-0x4(%rbp)
t += global_arr_big[7];
5d: 48 b8 00 00 00 00 00 movabs $0x0,%rax
64: 00 00 00
67: 8b 40 1c mov 0x1c(%rax),%eax
6a: 01 45 fc add %eax,-0x4(%rbp)
t += static_arr_big[7];
6d: 48 b8 00 00 00 00 00 movabs $0x0,%rax
74: 00 00 00
77: 8b 40 1c mov 0x1c(%rax),%eax
7a: 01 45 fc add %eax,-0x4(%rbp)
return t;
7d: 8b 45 fc mov -0x4(%rbp),%eax
}
80: c9 leaveq
81: c3 retq
Sekali lagi, penting untuk melihat relokasi:
Relocation section '.rela.text' at offset 0x62c18 contains 5 entries:
Offset Info Type Sym. Value Sym. Name + Addend
000000000030 001500000001 R_X86_64_64 0000000000000000 global_func + 0
00000000003f 001100000001 R_X86_64_64 0000000000000000 global_arr + 0
00000000004f 000300000001 R_X86_64_64 0000000000000000 .data + 1a0
00000000005f 001200000001 R_X86_64_64 0000000000000340 global_arr_big + 0
00000000006f 000300000001 R_X86_64_64 0000000000000000 .data + 31080
Karena tidak perlu membuat asumsi tentang ukuran kode dan bagian data, model kode besar cukup seragam dan mendefinisikan akses ke semua data dengan cara yang sama. Mari kita lihat lagi
global_arr
:
t += global_arr[7];
3d: 48 b8 00 00 00 00 00 movabs $0x0,%rax
44: 00 00 00
47: 8b 40 1c mov 0x1c(%rax),%eax
4a: 01 45 fc add %eax,-0x4(%rbp)
Dua tim perlu mendapatkan nilai yang diinginkan dari array. Perintah pertama menempatkan alamat 64-bit absolut
rax
, yang, seperti akan kita lihat nanti, ternyata menjadi alamat global_arr
, sedangkan perintah kedua memuat satu kata dari (rax) + 01
ke eax
.
Jadi mari kita fokus pada tim di
0x3d
, movabs
versi 64-bit absolut mov
dalam arsitektur x64. Ini dapat menjatuhkan konstanta 64-bit penuh langsung ke register, dan karena dalam kode kami yang dibongkar nilai konstanta ini adalah nol, kita harus merujuk ke tabel relokasi untuk jawabannya. Di dalamnya kita akan menemukan relokasi absolut R_X86_64_64
untuk operan di alamat 0x3f
, dengan nilai berikut: penempatan nilai simbol ditambah istilah kembali ke shift. Dengan kata lain,rax
akan berisi alamat absolut global_arr
.
Bagaimana dengan fungsi panggilan?
int t = global_func(argc);
24: 8b 45 ec mov -0x14(%rbp),%eax
27: 89 c7 mov %eax,%edi
29: b8 00 00 00 00 mov $0x0,%eax
2e: 48 ba 00 00 00 00 00 movabs $0x0,%rdx
35: 00 00 00
38: ff d2 callq *%rdx
3a: 89 45 fc mov %eax,-0x4(%rbp)
Kami sudah terbiasa dengan
movabs
perintah call
yang memanggil fungsi di alamat di rdx
. Cukup untuk melihat relokasi yang sesuai untuk memahami betapa miripnya dengan akses data.
Seperti yang Anda lihat, model kode besar tidak membuat asumsi tentang ukuran kode dan bagian data, serta tentang lokasi akhir karakter, itu hanya merujuk pada karakter melalui langkah-langkah mutlak 64-bit, semacam "jalur aman". Namun, perhatikan bagaimana, dibandingkan dengan model kode kecil, model besar dipaksa untuk menggunakan perintah tambahan untuk setiap karakter. Ini adalah harga keamanan.
Jadi, kami bertemu dua model yang benar-benar berlawanan: sementara model kode kecil mengasumsikan bahwa semuanya sesuai dengan dua gigabytes memori terbawah, model besar mengasumsikan bahwa tidak ada yang mustahil dan karakter apa pun dapat berada di mana saja secara keseluruhan. ruang alamat bit. Pertukaran antara keduanya adalah model kode tengah.
Model kode rata-rata
Seperti sebelumnya, mari kita lihat terjemahan kutipan dari
man
gcc
:
-mcmodel=medium
: . . , -mlarge-data-threshold, bss . , .
Mirip dengan model kode kecil, model menengah mengasumsikan semua kode dikompilasi di dua gigabytes bawah. Namun demikian, data dibagi menjadi seharusnya diatur dalam dua gigabytes bawah "data kecil" dan ruang memori "data besar" tanpa batas. Data termasuk dalam kategori besar ketika mereka melebihi batas, menurut definisi, sama dengan 64 kilobyte.
Penting juga untuk dicatat bahwa ketika bekerja dengan model kode rata-rata untuk data besar, dengan analogi dengan bagian
.data
dan .bss
, bagian khusus dibuat: .ldata
dan .lbss
. Ini tidak begitu penting dalam prisma topik artikel saat ini, tetapi saya akan sedikit menyimpang darinya. Rincian lebih lanjut tentang masalah ini dapat ditemukan di ABI.
Sekarang menjadi jelas mengapa array tersebut muncul dalam contoh
_big
: mereka dibutuhkan oleh model tengah untuk menafsirkan "data besar" yang mereka miliki, masing-masing berukuran 200 kilobyte. Di bawah ini Anda dapat melihat hasil pembongkaran:
int main(int argc, const char* argv[])
{
15: 55 push %rbp
16: 48 89 e5 mov %rsp,%rbp
19: 48 83 ec 20 sub $0x20,%rsp
1d: 89 7d ec mov %edi,-0x14(%rbp)
20: 48 89 75 e0 mov %rsi,-0x20(%rbp)
int t = global_func(argc);
24: 8b 45 ec mov -0x14(%rbp),%eax
27: 89 c7 mov %eax,%edi
29: b8 00 00 00 00 mov $0x0,%eax
2e: e8 00 00 00 00 callq 33 <main+0x1e>
33: 89 45 fc mov %eax,-0x4(%rbp)
t += global_arr[7];
36: 8b 05 00 00 00 00 mov 0x0(%rip),%eax
3c: 01 45 fc add %eax,-0x4(%rbp)
t += static_arr[7];
3f: 8b 05 00 00 00 00 mov 0x0(%rip),%eax
45: 01 45 fc add %eax,-0x4(%rbp)
t += global_arr_big[7];
48: 48 b8 00 00 00 00 00 movabs $0x0,%rax
4f: 00 00 00
52: 8b 40 1c mov 0x1c(%rax),%eax
55: 01 45 fc add %eax,-0x4(%rbp)
t += static_arr_big[7];
58: 48 b8 00 00 00 00 00 movabs $0x0,%rax
5f: 00 00 00
62: 8b 40 1c mov 0x1c(%rax),%eax
65: 01 45 fc add %eax,-0x4(%rbp)
return t;
68: 8b 45 fc mov -0x4(%rbp),%eax
}
6b: c9 leaveq
6c: c3 retq
Perhatikan bagaimana array diakses: array
_big
diakses melalui metode model kode besar, sedangkan array lainnya diakses melalui metode model kecil. Fungsi ini juga disebut menggunakan metode model kode kecil, dan relokasi sangat mirip dengan contoh sebelumnya sehingga saya bahkan tidak akan menunjukkannya.
Model kode tengah adalah kompromi terampil antara model besar dan kecil. Tidak mungkin kode program berubah menjadi terlalu besar [4], jadi hanya potongan besar data yang terhubung secara statis ke dalamnya dapat memindahkannya melampaui batas dua gigabyte, mungkin sebagai bagian dari semacam pencarian tabel yang banyak. Karena model kode tengah memfilter sejumlah besar data dan memprosesnya dengan cara khusus, panggilan dengan kode fungsi dan simbol kecil akan seefisien dalam model kode kecil. Hanya akses ke simbol besar, dengan analogi dengan model besar, akan memerlukan kode untuk menggunakan metode 64-bit penuh dari model besar.
Model Kode PIC kecil
Sekarang mari kita lihat varian PIC dari model kode, dan seperti sebelumnya kita mulai dengan model kecil. [5] Di bawah ini Anda dapat melihat contoh kode yang dikompilasi melalui model PIC kecil:
int main(int argc, const char* argv[])
{
15: 55 push %rbp
16: 48 89 e5 mov %rsp,%rbp
19: 48 83 ec 20 sub $0x20,%rsp
1d: 89 7d ec mov %edi,-0x14(%rbp)
20: 48 89 75 e0 mov %rsi,-0x20(%rbp)
int t = global_func(argc);
24: 8b 45 ec mov -0x14(%rbp),%eax
27: 89 c7 mov %eax,%edi
29: b8 00 00 00 00 mov $0x0,%eax
2e: e8 00 00 00 00 callq 33 <main+0x1e>
33: 89 45 fc mov %eax,-0x4(%rbp)
t += global_arr[7];
36: 48 8b 05 00 00 00 00 mov 0x0(%rip),%rax
3d: 8b 40 1c mov 0x1c(%rax),%eax
40: 01 45 fc add %eax,-0x4(%rbp)
t += static_arr[7];
43: 8b 05 00 00 00 00 mov 0x0(%rip),%eax
49: 01 45 fc add %eax,-0x4(%rbp)
t += global_arr_big[7];
4c: 48 8b 05 00 00 00 00 mov 0x0(%rip),%rax
53: 8b 40 1c mov 0x1c(%rax),%eax
56: 01 45 fc add %eax,-0x4(%rbp)
t += static_arr_big[7];
59: 8b 05 00 00 00 00 mov 0x0(%rip),%eax
5f: 01 45 fc add %eax,-0x4(%rbp)
return t;
62: 8b 45 fc mov -0x4(%rbp),%eax
}
65: c9 leaveq
66: c3 retq
Relokasi:
Relocation section '.rela.text' at offset 0x62ce8 contains 5 entries:
Offset Info Type Sym. Value Sym. Name + Addend
00000000002f 001600000004 R_X86_64_PLT32 0000000000000000 global_func - 4
000000000039 001100000009 R_X86_64_GOTPCREL 0000000000000000 global_arr - 4
000000000045 000300000002 R_X86_64_PC32 0000000000000000 .data + 1b8
00000000004f 001200000009 R_X86_64_GOTPCREL 0000000000000340 global_arr_big - 4
00000000005b 000300000002 R_X86_64_PC32 0000000000000000 .data + 31098
Karena perbedaan antara data besar dan kecil tidak memainkan peran apa pun dalam model kode kecil, kami akan fokus pada poin-poin penting ketika menghasilkan kode melalui PIC: perbedaan antara simbol lokal (statis) dan global.
Seperti yang Anda lihat, tidak ada perbedaan antara kode yang dihasilkan untuk array statis dan kode dalam kasus non-PIC. Ini adalah salah satu kelebihan arsitektur x64: berkat akses IP-relatif ke data, kami mendapatkan PIC sebagai bonus, setidaknya sampai akses eksternal ke simbol diperlukan. Semua perintah dan relokasi tetap sama, jadi tidak perlu memprosesnya lagi.
Sangat menarik untuk memperhatikan array global: perlu diingat bahwa dalam PIC data global harus melewati GOT, karena pada titik tertentu mereka dapat disimpan, atau dibagikan, oleh perpustakaan bersama [6]. Di bawah ini Anda dapat melihat kode untuk diakses
global_arr
:
t += global_arr[7];
36: 48 8b 05 00 00 00 00 mov 0x0(%rip),%rax
3d: 8b 40 1c mov 0x1c(%rax),%eax
40: 01 45 fc add %eax,-0x4(%rbp)
Relokasi yang kami minati adalah
R_X86_64_GOTPCREL
: posisi entri simbol ke dalam GOT plus istilah, minus pergeseran untuk menerapkan relokasi. Dengan kata lain, pergeseran relatif antara RIP (instruksi berikutnya) dan global_arr
slot yang disediakan untuk di GOT ditambal ke dalam perintah . Dengan demikian, alamat aktual ditempatkan rax
dalam perintah berdasarkan 0x36
alamat global_arr
. Mengikuti langkah ini adalah mengatur ulang tautan ke alamat global_arr
plus pergeseran ke elemen ketujuh di eax
.
Sekarang mari kita lihat pemanggilan fungsi:
int t = global_func(argc);
24: 8b 45 ec mov -0x14(%rbp),%eax
27: 89 c7 mov %eax,%edi
29: b8 00 00 00 00 mov $0x0,%eax
2e: e8 00 00 00 00 callq 33 <main+0x1e>
33: 89 45 fc mov %eax,-0x4(%rbp)
Memiliki relokasi operan
callq
alamat 0x2e
, R_X86_64_PLT32
: Alamat entri PLT untuk simbol ditambah jangka pergeseran negatif bagi penerapan relokasi. Dengan kata lain, ia callq
harus memanggil papan loncatan PLT dengan benar global_func
.
Perhatikan asumsi implisit apa yang dibuat oleh kompiler: bahwa GOT dan PLT dapat diakses melalui pengalamatan relatif-RIP. Ini akan menjadi penting ketika membandingkan model ini dengan varian model kode PIC lainnya.
Model Kode PIC Besar
Pembongkaran:
int main(int argc, const char* argv[])
{
15: 55 push %rbp
16: 48 89 e5 mov %rsp,%rbp
19: 53 push %rbx
1a: 48 83 ec 28 sub $0x28,%rsp
1e: 48 8d 1d f9 ff ff ff lea -0x7(%rip),%rbx
25: 49 bb 00 00 00 00 00 movabs $0x0,%r11
2c: 00 00 00
2f: 4c 01 db add %r11,%rbx
32: 89 7d dc mov %edi,-0x24(%rbp)
35: 48 89 75 d0 mov %rsi,-0x30(%rbp)
int t = global_func(argc);
39: 8b 45 dc mov -0x24(%rbp),%eax
3c: 89 c7 mov %eax,%edi
3e: b8 00 00 00 00 mov $0x0,%eax
43: 48 ba 00 00 00 00 00 movabs $0x0,%rdx
4a: 00 00 00
4d: 48 01 da add %rbx,%rdx
50: ff d2 callq *%rdx
52: 89 45 ec mov %eax,-0x14(%rbp)
t += global_arr[7];
55: 48 b8 00 00 00 00 00 movabs $0x0,%rax
5c: 00 00 00
5f: 48 8b 04 03 mov (%rbx,%rax,1),%rax
63: 8b 40 1c mov 0x1c(%rax),%eax
66: 01 45 ec add %eax,-0x14(%rbp)
t += static_arr[7];
69: 48 b8 00 00 00 00 00 movabs $0x0,%rax
70: 00 00 00
73: 8b 44 03 1c mov 0x1c(%rbx,%rax,1),%eax
77: 01 45 ec add %eax,-0x14(%rbp)
t += global_arr_big[7];
7a: 48 b8 00 00 00 00 00 movabs $0x0,%rax
81: 00 00 00
84: 48 8b 04 03 mov (%rbx,%rax,1),%rax
88: 8b 40 1c mov 0x1c(%rax),%eax
8b: 01 45 ec add %eax,-0x14(%rbp)
t += static_arr_big[7];
8e: 48 b8 00 00 00 00 00 movabs $0x0,%rax
95: 00 00 00
98: 8b 44 03 1c mov 0x1c(%rbx,%rax,1),%eax
9c: 01 45 ec add %eax,-0x14(%rbp)
return t;
9f: 8b 45 ec mov -0x14(%rbp),%eax
}
a2: 48 83 c4 28 add $0x28,%rsp
a6: 5b pop %rbx
a7: c9 leaveq
a8: c3 retq
Relokasi: Kali ini , perbedaan antara data besar dan kecil masih tidak penting, jadi kita akan fokus pada dan . Tetapi pertama-tama Anda harus memperhatikan prolog dalam kode ini, sebelumnya kami tidak menemukan ini:
Relocation section '.rela.text' at offset 0x62c70 contains 6 entries:
Offset Info Type Sym. Value Sym. Name + Addend
000000000027 00150000001d R_X86_64_GOTPC64 0000000000000000 _GLOBAL_OFFSET_TABLE_ + 9
000000000045 00160000001f R_X86_64_PLTOFF64 0000000000000000 global_func + 0
000000000057 00110000001b R_X86_64_GOT64 0000000000000000 global_arr + 0
00000000006b 000800000019 R_X86_64_GOTOFF64 00000000000001a0 static_arr + 0
00000000007c 00120000001b R_X86_64_GOT64 0000000000000340 global_arr_big + 0
000000000090 000900000019 R_X86_64_GOTOFF64 0000000000031080 static_arr_big + 0
static_arr
global_arr
1e: 48 8d 1d f9 ff ff ff lea -0x7(%rip),%rbx
25: 49 bb 00 00 00 00 00 movabs $0x0,%r11
2c: 00 00 00
2f: 4c 01 db add %r11,%rbx
Di bawah ini Anda dapat membaca terjemahan dari kutipan terkait dari ABI:
( GOT) AMD64 IP- . GOT . GOT , AMD64 ISA 32 .
Mari kita lihat bagaimana prolog yang dijelaskan di atas menghitung alamat GOT. Pertama, tim alamat
0x1e
memuat alamatnya sendiri di rbx
. Kemudian, bersama dengan relokasi, R_X86_64_GOTPC64
langkah 64-bit mutlak dilakukan r11
. Relokasi ini berarti yang berikut: ambil alamat GOT, kurangi shift yang digeser dan tambahkan istilah. Akhirnya, perintah di alamat 0x2f
menambahkan kedua hasil bersama. Hasilnya adalah alamat absolut GOT rbx
. [7]
Mengapa repot-repot menghitung alamat GOT? Pertama, seperti yang disebutkan dalam kutipan, dalam model kode besar, kita tidak dapat mengasumsikan bahwa pergeseran relatif RIP 32-bit akan cukup untuk pengalamatan GOT, itulah sebabnya kita membutuhkan alamat 64-bit penuh. Kedua, kami masih ingin bekerja dengan variasi PIC, jadi kami tidak bisa begitu saja memasukkan alamat absolut ke dalam register. Sebaliknya, alamat itu sendiri harus dihitung relatif terhadap RIP. Itulah gunanya prolog: ia melakukan komputasi RIP-relatif 64-bit.
Bagaimanapun, karena kita
rbx
sekarang memiliki alamat GOT, mari kita lihat cara mengakses static_arr
:
t += static_arr[7];
69: 48 b8 00 00 00 00 00 movabs $0x0,%rax
70: 00 00 00
73: 8b 44 03 1c mov 0x1c(%rbx,%rax,1),%eax
77: 01 45 ec add %eax,-0x14(%rbp)
Relokasi perintah pertama adalah
R_X86_64_GOTOFF64
: simbol plus istilah minus GOT. Dalam kasus kami, ini adalah offset relatif antara alamat static_arr
dan alamat GOT. Pernyataan berikut menambahkan hasilnya ke rbx
(alamat GOT absolut) dan mengatur ulang tautan dengan shift by 0x1c
. Untuk memudahkan visualisasi perhitungan seperti itu, contoh pseudo-C dapat ditemukan di bawah:
// char* static_arr
// char* GOT
rax = static_arr + 0 - GOT; // rax now contains an offset
eax = *(rbx + rax + 0x1c); // rbx == GOT, so eax now contains
// *(GOT + static_arr - GOT + 0x1c) or
// *(static_arr + 0x1c)
Perhatikan hal yang menarik: alamat GOT digunakan sebagai pengikat
static_arr
. Biasanya GOT tidak mengandung alamat simbol, dan karena itu static_arr
bukan simbol eksternal, tidak ada alasan untuk menyimpannya di dalam GOT. Namun, dalam kasus ini, GOT digunakan sebagai pengikatan ke alamat simbol relatif dari bagian data. Alamat ini, yang, antara lain, adalah lokasi independen, dapat ditemukan dengan pergeseran 64-bit penuh. Linker dapat menangani relokasi ini, sehingga tidak perlu mengubah bagian kode pada waktu buka.
Tapi bagaimana dengan itu
global_arr
?
t += global_arr[7];
55: 48 b8 00 00 00 00 00 movabs $0x0,%rax
5c: 00 00 00
5f: 48 8b 04 03 mov (%rbx,%rax,1),%rax
63: 8b 40 1c mov 0x1c(%rax),%eax
66: 01 45 ec add %eax,-0x14(%rbp)
Kode ini agak lebih lama, dan relokasi berbeda dari yang biasa. Faktanya, GOT digunakan di sini dengan cara yang lebih tradisional: relokasi
R_X86_64_GOT64
untuk movabs
hanya memberi tahu fungsi untuk menempatkan offset di GOT di mana rax
alamat berada global_arr
. Perintah di alamat 0x5f
mengambil alamat global_arr
dari GOT dan memasukkannya ke dalam rax
. Perintah berikut akan mengatur ulang referensi ke global_arr[7]
dan memasukkan nilainya eax
.
Sekarang mari kita lihat tautan kode untuk
global_func
. Ingatlah bahwa dalam model kode besar, kita tidak dapat membuat asumsi tentang ukuran bagian kode, jadi kita harus berasumsi bahwa bahkan untuk akses ke PLT kita memerlukan alamat 64-bit mutlak:
int t = global_func(argc);
39: 8b 45 dc mov -0x24(%rbp),%eax
3c: 89 c7 mov %eax,%edi
3e: b8 00 00 00 00 mov $0x0,%eax
43: 48 ba 00 00 00 00 00 movabs $0x0,%rdx
4a: 00 00 00
4d: 48 01 da add %rbx,%rdx
50: ff d2 callq *%rdx
52: 89 45 ec mov %eax,-0x14(%rbp)
Relokasi yang kami minati adalah
R_X86_64_PLTOFF64
: global_func
alamat input PLT untuk minus alamat GOT. Hasilnya ditempatkan di rdx
, di mana ia kemudian ditempatkan rbx
(alamat absolut dari GOT). Akibatnya, kami mendapatkan alamat PLT input untuk global_func
di rdx
.
Perhatikan bahwa sekali lagi GOT digunakan sebagai pengikat, kali ini untuk memberikan referensi yang tidak tergantung alamat pada pergeseran input PLT.
Model Kode PIC rata-rata
Akhirnya, kami akan memecah kode yang dihasilkan untuk model PIC rata-rata:
int main(int argc, const char* argv[])
{
15: 55 push %rbp
16: 48 89 e5 mov %rsp,%rbp
19: 53 push %rbx
1a: 48 83 ec 28 sub $0x28,%rsp
1e: 48 8d 1d 00 00 00 00 lea 0x0(%rip),%rbx
25: 89 7d dc mov %edi,-0x24(%rbp)
28: 48 89 75 d0 mov %rsi,-0x30(%rbp)
int t = global_func(argc);
2c: 8b 45 dc mov -0x24(%rbp),%eax
2f: 89 c7 mov %eax,%edi
31: b8 00 00 00 00 mov $0x0,%eax
36: e8 00 00 00 00 callq 3b <main+0x26>
3b: 89 45 ec mov %eax,-0x14(%rbp)
t += global_arr[7];
3e: 48 8b 05 00 00 00 00 mov 0x0(%rip),%rax
45: 8b 40 1c mov 0x1c(%rax),%eax
48: 01 45 ec add %eax,-0x14(%rbp)
t += static_arr[7];
4b: 8b 05 00 00 00 00 mov 0x0(%rip),%eax
51: 01 45 ec add %eax,-0x14(%rbp)
t += global_arr_big[7];
54: 48 8b 05 00 00 00 00 mov 0x0(%rip),%rax
5b: 8b 40 1c mov 0x1c(%rax),%eax
5e: 01 45 ec add %eax,-0x14(%rbp)
t += static_arr_big[7];
61: 48 b8 00 00 00 00 00 movabs $0x0,%rax
68: 00 00 00
6b: 8b 44 03 1c mov 0x1c(%rbx,%rax,1),%eax
6f: 01 45 ec add %eax,-0x14(%rbp)
return t;
72: 8b 45 ec mov -0x14(%rbp),%eax
}
75: 48 83 c4 28 add $0x28,%rsp
79: 5b pop %rbx
7a: c9 leaveq
7b: c3 retq
Relokasi:
Relocation section '.rela.text' at offset 0x62d60 contains 6 entries:
Offset Info Type Sym. Value Sym. Name + Addend
000000000021 00160000001a R_X86_64_GOTPC32 0000000000000000 _GLOBAL_OFFSET_TABLE_ - 4
000000000037 001700000004 R_X86_64_PLT32 0000000000000000 global_func - 4
000000000041 001200000009 R_X86_64_GOTPCREL 0000000000000000 global_arr - 4
00000000004d 000300000002 R_X86_64_PC32 0000000000000000 .data + 1b8
000000000057 001300000009 R_X86_64_GOTPCREL 0000000000000000 global_arr_big - 4
000000000063 000a00000019 R_X86_64_GOTOFF64 0000000000030d40 static_arr_big + 0
Pertama, mari kita hapus pemanggilan fungsi. Demikian pula dengan model kecil, di model tengah kami mengasumsikan bahwa referensi kode tidak melebihi batas dari pergeseran RIP 32-bit, oleh karena itu, kode untuk panggilan
global_func
benar - benar mirip dengan kode yang sama dalam model PIC kecil, serta untuk kasus array data kecil static_arr
dan global_arr
. Oleh karena itu, kami akan fokus pada array data besar, tetapi pertama-tama mari kita bicara tentang prolog: di sini berbeda dari prolog model data besar.
1e: 48 8d 1d 00 00 00 00 lea 0x0(%rip),%rbx
Ini adalah keseluruhan prolog: hanya butuh satu perintah untuk memindahkan
R_X86_64_GOTPC32
alamat GOT rbx
(dibandingkan dengan tiga dalam model besar). Apa bedanya? Intinya adalah bahwa karena dalam model tengah GOT bukan bagian dari "partisi data besar", kami menganggap ketersediaannya dalam pergeseran 32-bit. Dalam model besar, kami tidak dapat membuat asumsi seperti itu, dan terpaksa menggunakan pergeseran 64-bit penuh.
Yang menarik adalah kenyataan bahwa kode akses
global_arr_big
mirip dengan kode yang sama dalam model PIC kecil. Ini terjadi karena alasan yang sama bahwa prolog model tengah lebih pendek dari prolog model besar: kami menganggap ketersediaan GOT sebagai bagian dari pengalamatan RIP 32-bit. Memang, sangatglobal_arr_big
Anda tidak bisa mendapatkan akses seperti itu, tetapi kasing ini masih mencakup GOT, karena pada kenyataannya global_arr_big
terletak di dalamnya, apalagi, dalam bentuk alamat 64-bit lengkap.
Namun, situasinya berbeda untuk
static_arr_big
:
t += static_arr_big[7];
61: 48 b8 00 00 00 00 00 movabs $0x0,%rax
68: 00 00 00
6b: 8b 44 03 1c mov 0x1c(%rbx,%rax,1),%eax
6f: 01 45 ec add %eax,-0x14(%rbp)
Kasus ini mirip dengan model kode PIC besar, karena di sini kita masih mendapatkan alamat absolut dari simbol, yang tidak ada di GOT itu sendiri. Karena ini adalah simbol besar, yang tidak dapat diasumsikan berada di dua gigabita yang lebih rendah, kami, seperti dalam model besar, memerlukan pergeseran PIC 64-bit.
Catatan:
[1] Jangan bingung antara model kode dengan model data 64-bit dan model memori Intel , ini semua adalah topik yang berbeda.
[2] Penting untuk diingat: perintah aktual dibuat oleh kompiler, dan mode pengalamatan diperbaiki tepat pada langkah ini. Kompiler tidak dapat mengetahui program mana atau pustaka bersama yang akan jatuh ke modul objek, beberapa mungkin kecil, sementara yang lain mungkin besar. Linker mengetahui ukuran program akhir, tetapi sudah terlambat: linker hanya dapat menambal perpindahan perintah dengan relokasi, dan tidak mengubah perintah itu sendiri. Dengan demikian, "konvensi" model kode harus "ditandatangani" oleh programmer pada waktu kompilasi.
[3] Jika ada sesuatu yang tidak jelas, lihat artikel berikutnya .
[4] Namun, volume meningkat secara bertahap. Terakhir kali saya mengecek build Clang dari Debug + Asserts, hampir mencapai satu gigabyte, yang banyak terima kasih kepada kode yang dibuat secara otomatis.
[5] Jika Anda masih tidak tahu cara kerja PIC (baik secara umum dan khusus untuk arsitektur x64), inilah saatnya untuk membaca artikel berikut tentang topik: satu dan dua .
[6] Dengan demikian, tautan tidak dapat menyelesaikan tautannya sendiri, dan harus mengalihkan pemrosesan GOT ke loader dinamis.
[7] 0x25 - 0x7 + GOT - 0x27 + 0x9 = GOT