Memahami model kode arsitektur x64

"Model kode mana yang harus saya gunakan?" - pertanyaan yang sering muncul, tetapi jarang ditangani ketika menulis kode untuk arsitektur x64. Namun demikian, ini adalah masalah yang agak menarik, dan berguna untuk memiliki gagasan tentang model kode untuk memahami kode mesin x64 yang dihasilkan oleh kompiler. Selain itu, bagi mereka yang khawatir tentang kinerja hingga ke instruksi terkecil, pilihan model kode juga mempengaruhi pengoptimalan.



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 manhalamangcc... 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 mainmendapatkan 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;
}


gccmenggunakan model kode sebagai nilai opsi -mcmodel. Selain itu, -fpickompilasi 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 movsehingga sesuai dengan 0x3s. Kami tertarik pada relokasi kedua, R_X86_64_PC32itu menunjuk ke operan movdi alamat 0x38dan 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 01berarti "int ketujuh dalam array" (dalam arsitektur x64 ukuran masing-masing intadalah 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_arrsini 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_arrdapat 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 callqjuga relatif RIP, relokasi R_X86_64_PC32bekerja 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 gccpada 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 maindikompilasi 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) + 01ke eax.



Jadi mari kita fokus pada tim di 0x3d, movabsversi 64-bit absolut movdalam 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_64untuk operan di alamat 0x3f, dengan nilai berikut: penempatan nilai simbol ditambah istilah kembali ke shift. Dengan kata lain,raxakan 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 movabsperintah callyang 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 .datadan .bss, bagian khusus dibuat: .ldatadan .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 _bigdiakses 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_arrslot yang disediakan untuk di GOT ditambal ke dalam perintah . Dengan demikian, alamat aktual ditempatkan raxdalam perintah berdasarkan 0x36alamat global_arr. Mengikuti langkah ini adalah mengatur ulang tautan ke alamat global_arrplus 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 callqalamat 0x2e, R_X86_64_PLT32: Alamat entri PLT untuk simbol ditambah jangka pergeseran negatif bagi penerapan relokasi. Dengan kata lain, ia callqharus 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_arrglobal_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 0x1ememuat alamatnya sendiri di rbx. Kemudian, bersama dengan relokasi, R_X86_64_GOTPC64langkah 64-bit mutlak dilakukan r11. Relokasi ini berarti yang berikut: ambil alamat GOT, kurangi shift yang digeser dan tambahkan istilah. Akhirnya, perintah di alamat 0x2fmenambahkan 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 rbxsekarang 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_arrdan 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_arrbukan 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_GOT64untuk movabshanya memberi tahu fungsi untuk menempatkan offset di GOT di mana raxalamat berada global_arr. Perintah di alamat 0x5fmengambil alamat global_arrdari 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_funcalamat 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_funcdi 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_funcbenar - benar mirip dengan kode yang sama dalam model PIC kecil, serta untuk kasus array data kecil static_arrdan 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_GOTPC32alamat 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_bigmirip 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_bigAnda tidak bisa mendapatkan akses seperti itu, tetapi kasing ini masih mencakup GOT, karena pada kenyataannya global_arr_bigterletak 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









All Articles