Menjalankan alamat aplikasi, diikuti dengan perluasan tumpukan dan tumpukan

Saya punya m.c:

extern void a(char*);

int main(int ac, char **av){
    static char string [] = "Hello , world!\n";
    a(string);
}

dan a.c:

#include <unistd.h>
#include <string.h>

void a(char* s){
    write(1, s, strlen(s));
}

Saya mengkompilasi dan membangun ini sebagai:

g++ -c -g -std=c++14 -MMD -MP -MF "m.o.d" -o m.o m.c
g++ -c -g -std=c++14 -MMD -MP -MF "a.o.d" -o a.o a.c
g++ -o linux m.o a.o -lm -lpthread -ldl

Kemudian, saya memeriksa executable, linux sebagai berikut:

objdump -drwxCS -Mintel linux

Outputnya di Ubuntu 16.04.6 saya dimulai dengan:

start address 0x0000000000400540

lalu, nanti, adalah bagian init:

00000000004004c8 <_init>:
  4004c8:   48 83 ec 08             sub    rsp,0x8

Terakhir, adalah bagian fini:

0000000000400704 <_fini>:
  400704:   48 83 ec 08             sub    rsp,0x8
  400708:   48 83 c4 08             add    rsp,0x8
  40070c:   c3                      ret 

Program mereferensikan string Hello , world!\n yang ada di bagian .data yang diperoleh dengan perintah:

objdump -sj .data linux

Contents of section .data:
 601030 00000000 00000000 00000000 00000000  ................
 601040 48656c6c 6f202c20 776f726c 64210a00  Hello , world!..

Semua ini memberi tahu saya bahwa executable telah dibuat untuk dimuat di alamat memori aktual mulai dari sekitar 0x0000000000400540 (alamat .init) dan program mengakses data di alamat memori aktual hingga setidaknya 601040 (alamat .data)

Saya mendasarkan ini pada Bab 7 dari Linkers & Loaders oleh John R Levine, di mana dia menyatakan:

Linker menggabungkan sekumpulan file masukan menjadi satu file keluaran yang siap dimuat di alamat tertentu.

Pertanyaan saya adalah tentang baris berikutnya.

Jika, saat program dimuat, penyimpanan di alamat tersebut tidak tersedia, pemuat harus memindahkan program yang dimuat agar sesuai dengan alamat pemuatan sebenarnya.

(1) Misalkan saya memiliki executable lain yang sedang berjalan di mesin saya dan sudah menggunakan ruang memori antara 400540 dan 601040, bagaimana cara memutuskan di mana memulai executable baru saya linux?

(2) Terkait dengan hal tersebut, pada Bab 4 disebutkan:

..Objek ELF...dimuat di sekitar tengah ruang alamat sehingga tumpukan dapat bertambah di bawah segmen teks dan tumpukan dapat bertambah dari akhir data, menjaga total ruang alamat yang digunakan relatif kompak.

Misalkan aplikasi yang berjalan sebelumnya dimulai pada, katakanlah, 200000 dan sekarang linux dimulai sekitar 400540. Tidak ada bentrokan atau tumpang tindih alamat memori. Namun seiring berjalannya program, misalkan tumpukan aplikasi sebelumnya bertambah hingga 300000, sedangkan tumpukan aplikasi baru linux bertambah ke bawah hingga 310000. Segera, akan terjadi bentrokan/tumpang tindih alamat memori. Apa yang terjadi jika bentrokan akhirnya terjadi?


person Tryer    schedule 06.08.2020    source sumber


Jawaban (2)


Jika, saat program dimuat, penyimpanan di alamat tersebut tidak tersedia, pemuat harus memindahkan program yang dimuat agar sesuai dengan alamat pemuatan sebenarnya.

Tidak semua format file mendukung ini:

GCC untuk Windows 32-bit akan menambahkan informasi yang diperlukan untuk pemuat dalam hal perpustakaan dinamis (.dll). Namun, informasi tersebut tidak ditambahkan ke file yang dapat dieksekusi (.exe), sehingga file yang dapat dieksekusi tersebut harus dimuat ke alamat tetap.

Di Linux, ini sedikit lebih rumit; namun, juga tidak mungkin untuk memuat banyak file yang dapat dieksekusi (biasanya 32-bit yang lebih lama) ke alamat yang berbeda sementara perpustakaan dinamis (.so) dapat dimuat ke alamat yang berbeda.

Misalkan saya memiliki executable lain yang sedang berjalan di mesin saya dan sudah menggunakan ruang memori antara 400540 dan 601040 ...

Komputer modern (semua komputer x86 32-bit) memiliki MMU paging yang digunakan oleh sebagian besar sistem operasi modern. Ini adalah beberapa sirkuit (biasanya di CPU) yang menerjemahkan alamat yang dilihat oleh perangkat lunak ke alamat yang dilihat oleh RAM. Dalam contoh Anda, 400540 dapat diterjemahkan ke 1234000, jadi mengakses alamat 400540 sebenarnya akan mengakses alamat 1234000 di RAM.

Intinya adalah: OS modern menggunakan konfigurasi MMU yang berbeda untuk tugas yang berbeda. Jadi jika Anda memulai program Anda lagi, konfigurasi MMU berbeda digunakan yang menerjemahkan alamat 400540 yang dilihat oleh perangkat lunak ke alamat 2345000 di RAM. Kedua program yang menggunakan alamat 400540 dapat dijalankan secara bersamaan karena satu program akan benar-benar mengakses alamat 1234000 dan program lainnya akan mengakses alamat 2345000 di RAM ketika program mengakses alamat 400540.

Ini berarti bahwa beberapa alamat (misalnya 400540) tidak akan pernah digunakan ketika file yang dapat dieksekusi dimuat.

Alamat tersebut mungkin sudah digunakan ketika perpustakaan dinamis (.so/.dll) dimuat karena perpustakaan ini berbagi memori dengan file yang dapat dieksekusi.

... bagaimana memutuskan di mana memulai linux baru saya yang dapat dieksekusi?

Di Linux, file yang dapat dieksekusi akan dimuat ke alamat tetap jika ditautkan sedemikian rupa sehingga tidak dapat dipindahkan ke alamat lain. (Seperti yang telah dikatakan: Ini tipikal untuk file 32-bit lama.) Dalam contoh Anda, string Hello world akan ditempatkan di alamat 0x601040 jika kompiler dan linker Anda membuat file yang dapat dieksekusi dengan cara itu.

Namun, sebagian besar file executable 64-bit dapat dimuat ke alamat yang berbeda. Linux akan memuatnya ke alamat acak tertentu karena alasan keamanan sehingga lebih sulit bagi virus atau malware lain untuk menyerang program.

... sehingga tumpukan dapat bertambah di bawah segmen teks ...

Saya belum pernah melihat tata letak memori ini di sistem operasi mana pun:

Baik di Linux maupun di Solaris, tumpukan terletak di ujung ruang alamat (sekitar 0xBFFFFF00), sedangkan segmen teks dimuat cukup dekat dengan awal memori (mungkin alamat 0x401000).

...dan heap dapat bertambah dari akhir data, ...

misalkan tumpukan aplikasi sebelumnya merayap naik..

Banyak implementasi sejak akhir tahun 1990an tidak menggunakan heap lagi. Sebaliknya, mereka menggunakan mmap() untuk memesan memori baru.

Menurut halaman manual brk(), heap dinyatakan sebagai fitur lawas pada tahun 2001, sehingga tidak boleh digunakan lagi oleh program baru.

(Namun, menurut Peter Cordes malloc() sepertinya masih menggunakan heap dalam beberapa kasus.)

Tidak seperti sistem operasi sederhana seperti MS-DOS, Linux tidak mengizinkan Anda menggunakan heap begitu saja, tetapi Anda harus memanggil fungsi brk() untuk memberi tahu Linux berapa banyak heap yang ingin Anda gunakan.

Jika suatu program menggunakan heap dan menggunakan lebih banyak heap daripada yang tersedia, fungsi brk() mengembalikan beberapa kode kesalahan dan fungsi malloc() hanya mengembalikan NULL.

Namun, situasi ini biasanya terjadi karena tidak ada lagi RAM yang tersedia dan bukan karena heap tersebut tumpang tindih dengan area memori lainnya.

...sementara tumpukan linux yang baru diluncurkan telah berkembang ke bawah menjadi ...

Segera, akan terjadi bentrokan/tumpang tindih alamat memori. Apa yang terjadi jika bentrokan akhirnya terjadi?

Memang ukuran tumpukannya terbatas.

Jika Anda menggunakan terlalu banyak tumpukan, Anda akan mengalami tumpukan yang meluap.

Program ini sengaja menggunakan terlalu banyak tumpukan - hanya untuk melihat apa yang terjadi:

.globl _start
_start:
    sub $0x100000, %rsp
    push %rax
    push %rax
    jmp _start

Dalam kasus sistem operasi dengan MMU (seperti Linux), program Anda akan crash dengan pesan kesalahan:

~$ ./example_program
Segmentation fault (core dumped)
~$

EDIT/TAMBAHAN

Apakah tumpukan untuk semua program yang sedang berjalan terletak di bagian akhir?

Pada versi Linux yang lebih lama, tumpukan terletak di dekat (tetapi tidak persis pada) ujung memori virtual yang dapat diakses oleh program: Program dapat mengakses rentang alamat dari 0 hingga 0xBFFFFFFF pada versi Linux tersebut. Penunjuk tumpukan awal terletak di sekitar 0xBFFFFE00. (Argumen baris perintah dan variabel lingkungan muncul setelah tumpukan.)

Dan apakah ini akhir dari memori fisik yang sebenarnya? Bukankah tumpukan program yang sedang berjalan akan tercampur? Saya mendapat kesan bahwa semua tumpukan dan memori suatu program tetap berdekatan dalam memori fisik sebenarnya, ...

Pada komputer yang menggunakan MMU, program tidak pernah melihat memori fisik:

Saat program dimuat, OS akan mencari area bebas pada RAM - mungkin menemukan beberapa di alamat fisik 0xABC000. Kemudian mengkonfigurasi MMU sedemikian rupa sehingga alamat virtual 0xBFFFF000-0xBFFFFFFF diterjemahkan ke alamat fisik 0xABC000-0xABCFFF.

Artinya: Setiap kali program mengakses alamat 0xBFFFFE20 (misalnya menggunakan operasi push), alamat fisik 0xABCE20 di RAM sebenarnya diakses.

Tidak ada kemungkinan sama sekali bagi suatu program untuk mengakses alamat fisik tertentu.

Jika Anda menjalankan program lain, MMU dikonfigurasi sedemikian rupa sehingga alamat 0xBFFFF000-0xBFFFFFFF diterjemahkan ke alamat 0x345000-0x345FFF saat program lain sedang berjalan.

Jadi jika salah satu dari dua program akan melakukan operasi push dan penunjuk tumpukan adalah 0xBFFFFE20, alamat 0xABCE20 di RAM akan diakses; jika program lain melakukan operasi push (dengan nilai penunjuk tumpukan yang sama), alamat 0x345E20 akan diakses.

Oleh karena itu, tumpukannya tidak akan tercampur.

OS yang tidak menggunakan MMU namun mendukung multitasking (contohnya adalah Amiga 500 atau Apple Macintosh versi awal) tentu saja tidak akan berfungsi dengan cara ini. OS tersebut menggunakan format file khusus (dan bukan ELF) yang dioptimalkan untuk menjalankan banyak program tanpa MMU. Mengkompilasi program untuk OS semacam itu jauh lebih rumit daripada mengkompilasi program untuk Linux atau Windows. Dan bahkan ada batasan bagi pengembang perangkat lunak (contoh: fungsi dan array tidak boleh terlalu panjang).

Selain itu, apakah setiap program memiliki penunjuk tumpukan, penunjuk dasar, register, dll.? Atau apakah OS hanya memiliki satu set register ini untuk digunakan bersama oleh semua program?

(Dengan asumsi CPU single-core), CPU memiliki satu set register; dan hanya satu program yang dapat dijalankan pada waktu yang bersamaan.

Saat Anda memulai beberapa program, OS akan beralih antar program. Artinya program A berjalan (misalnya) 1/50 detik, kemudian program B berjalan 1/50 detik, kemudian program A berjalan 1/50 detik dan seterusnya. Tampaknya bagi Anda seolah-olah program tersebut berjalan pada waktu yang sama.

Ketika OS beralih dari program A ke program B, ia harus terlebih dahulu menyimpan nilai register (program A). Maka harus mengubah konfigurasi MMU. Akhirnya ia harus mengembalikan nilai register program B.

person Martin Rosenau    schedule 06.08.2020
comment
Distro Linux modern membuat PIE 32-bit. Anda mengatakan bahwa executable Linux 64-bit biasanya dapat direlokasi, tetapi sebagian besar executable 32-bit tidak. Apakah itu hanya mempertimbangkan beban sejarah? x86-64 sudah tersebar luas selama bertahun-tahun sebelum executable PIE mulai menjadi populer; misalnya Ubuntu 16.04 OP membuat non-PIE dapat dieksekusi secara default; itu tidak bisa di ASLR. GCC akan menggunakan instruksi seperti mov edi, offset .LC0 untuk memasukkan alamat statis ke dalam register, karena model kode non-PIE default menjamin bahwa kode/data statis berada dalam ruang alamat 31 bit yang rendah. - person Peter Cordes; 06.08.2020
comment
Anda juga berbicara tentang program OP di paragraf yang sama dengan 32-bit. Ini 64-bit, seperti yang dapat kami pastikan dari pembongkarannya. Selain itu, binutils ld memiliki alamat dasar default yang berbeda untuk .text dalam mode 32-bit. Program OP adalah non-PIE (ELF type EXEC) x86-64 yang dapat dieksekusi. Tidak dapat direlokasi: tidak ada metadata relokasi untuk menerapkan perbaikan pada data atau kode statis, dan tidak ada persyaratan yang tidak bergantung pada posisi. - person Peter Cordes; 06.08.2020
comment
Glibc malloc saat ini masih menggunakan brk untuk alokasi kecil, mmap untuk alokasi besar (sehingga pasti dapat mengembalikan halaman tersebut ke OS, tidak terjebak di daftar gratis). Ada heuristik penyetelan, IIRC batasnya beberapa halaman atau bahkan mungkin 64k. strace ls dan melihatnya menggunakan beberapa brk syscall. (poin keseluruhan dari jawaban Anda tentu saja benar; memori virtual menjadikannya bukan masalah. Namun sayangnya beberapa detail spesifiknya tidak tepat.) - person Peter Cordes; 06.08.2020
comment
@PeterCordes Saya berasumsi bahwa sebagian besar distro Linux x86 modern adalah 64-bit, bahkan seringkali tidak mendukung program 32-bit tanpa menginstal paket tambahan. Jadi ketika menulis tentang program 32-bit, yang saya maksud adalah program lama. Oleh karena itu, kata tipikal mengacu pada rata-rata program pada tahun 1995-2018, bukan pada rata-rata program di salah satu dari sedikit distro 32-bit yang masih ada. - person Martin Rosenau; 06.08.2020
comment
@PeterCordes Saya memperbarui kalimat tentang file yang dapat dieksekusi 32-bit dalam jawaban saya. Saya juga menambahkan penjelasan tentang bagaimana heap digunakan di Linux dan apa yang terjadi jika tidak ada lagi heap. - person Martin Rosenau; 06.08.2020
comment
@MartinRosenau, apakah tumpukan untuk semua program yang berjalan terletak di akhir? Dan apakah ini akhir dari memori fisik yang sebenarnya? Bukankah tumpukan program yang sedang berjalan akan tercampur? Saya mendapat kesan bahwa semua tumpukan dan memori suatu program tetap berdekatan dalam memori fisik aktual, bertambah dan berkurang jika diperlukan, namun tetap berdekatan. Selain itu, apakah setiap program memiliki penunjuk tumpukan, penunjuk dasar, register, dll.? Atau apakah OS hanya memiliki satu set register ini untuk digunakan bersama oleh semua program? - person Tryer; 07.08.2020
comment
@Tryer Silakan lihat bagian EDIT saya di jawaban saya. - person Martin Rosenau; 07.08.2020

Ya, objdump pada executable ini menunjukkan alamat di mana segmennya akan dipetakan. (Menautkan mengumpulkan bagian menjadi segmen: Apa bedanya bagian dan segmen dalam format file ELF) .data dan .text ditautkan ke bagian berbeda dengan izin berbeda (baca+tulis vs. baca+eksekutif).

Jika, saat program dimuat, penyimpanan di alamat tersebut tidak tersedia

Itu hanya bisa terjadi ketika memuat perpustakaan dinamis, bukan perpustakaan yang dapat dieksekusi itu sendiri. Memori virtual berarti bahwa setiap proses memiliki ruang alamat virtual pribadinya sendiri, meskipun proses tersebut dimulai dari executable yang sama. (Ini juga mengapa ld selalu dapat memilih alamat dasar default yang sama untuk segmen text dan data, tidak mencoba menempatkan setiap executable dan pustaka pada sistem ke tempat berbeda dalam satu ruang alamat.)

Eksekusi adalah hal pertama yang mengklaim bagian dari ruang alamat tersebut, ketika dimuat/dipetakan oleh pemuat program ELF OS. Itu sebabnya executable ELF tradisional (non-PIE) tidak dapat direlokasi, tidak seperti objek bersama ELF seperti /lib/libc.so.6

Jika Anda melakukan satu langkah program dengan debugger, atau menyertakan sleep, Anda akan punya waktu untuk melihat less /proc/<PID>/maps. Atau cat /proc/self/maps agar kucing menunjukkan petanya sendiri. (Juga /proc/self/smaps untuk info lebih detail tentang setiap pemetaan, seperti seberapa banyak yang kotor, penggunaan halaman besar, dll.)

(Distro GNU/Linux yang lebih baru mengonfigurasi GCC untuk membuat PIE dapat dieksekusi secara default: Alamat absolut 32-bit tidak lagi diperbolehkan di Linux x86-64?. Dalam hal ini objdump hanya akan melihat alamat relatif terhadap basis 0 atau 1000 atau semacamnya. Dan asm yang dihasilkan kompiler akan memilikinya menggunakan pengalamatan relatif PC, bukan absolut.)

person Peter Cordes    schedule 06.08.2020