หากเมื่อโหลดโปรแกรมแล้ว พื้นที่เก็บข้อมูลตามที่อยู่นั้นไม่พร้อมใช้งาน ตัวโหลดจะต้องย้ายตำแหน่งโปรแกรมที่โหลดเพื่อให้สะท้อนถึงที่อยู่โหลดจริง
ไฟล์บางรูปแบบไม่รองรับสิ่งนี้:
GCC สำหรับ Windows 32 บิตจะเพิ่มข้อมูลที่จำเป็นสำหรับตัวโหลดในกรณีของไดนามิกไลบรารี (.dll
) อย่างไรก็ตาม ข้อมูลจะไม่ถูกเพิ่มลงในไฟล์ปฏิบัติการ (.exe
) ดังนั้นจึงต้องโหลดไฟล์ปฏิบัติการดังกล่าวไปยังที่อยู่คงที่
ภายใต้ Linux มันจะซับซ้อนกว่าเล็กน้อย อย่างไรก็ตาม ยังไม่สามารถโหลดไฟล์ปฏิบัติการจำนวนมาก (โดยทั่วไปคือ 32 บิตที่เก่ากว่า) ไปยังที่อยู่ที่แตกต่างกันได้ ในขณะที่ไดนามิกไลบรารี (.so
) สามารถโหลดไปยังที่อยู่อื่นได้
สมมติว่าฉันมีไฟล์ปฏิบัติการอื่นที่ทำงานอยู่บนเครื่องของฉันอยู่แล้วโดยใช้พื้นที่หน่วยความจำระหว่าง 400540
ถึง 601040
...
คอมพิวเตอร์สมัยใหม่ (คอมพิวเตอร์ x86 32 บิตทั้งหมด) มี MMU แบบเพจจิ้งซึ่งระบบปฏิบัติการสมัยใหม่ส่วนใหญ่ใช้ นี่คือวงจรบางส่วน (โดยทั่วไปใน CPU) ซึ่งแปลที่อยู่ที่ซอฟต์แวร์เห็นไปเป็นที่อยู่ที่ RAM เห็น ในตัวอย่างของคุณ 400540
สามารถแปลเป็น 1234000
ได้ ดังนั้นการเข้าถึงที่อยู่ 400540
จะเข้าถึงที่อยู่ 1234000
ใน RAM จริงๆ
ประเด็นก็คือ: ระบบปฏิบัติการสมัยใหม่ใช้การกำหนดค่า MMU ที่แตกต่างกันสำหรับงานที่แตกต่างกัน ดังนั้นหากคุณเริ่มโปรแกรมอีกครั้ง ระบบจะใช้การกำหนดค่า MMU อื่นเพื่อแปลที่อยู่ 400540
ที่ซอฟต์แวร์เห็นไปเป็นที่อยู่ 2345000
ใน RAM ทั้งสองโปรแกรมที่ใช้ที่อยู่ 400540
สามารถทำงานได้พร้อมกัน เนื่องจากโปรแกรมหนึ่งจะเข้าถึงที่อยู่ 1234000
จริงๆ และอีกโปรแกรมหนึ่งจะเข้าถึงที่อยู่ 2345000
ใน RAM เมื่อโปรแกรมเข้าถึงที่อยู่ 400540
ซึ่งหมายความว่าที่อยู่บางส่วน (เช่น 400540
) จะไม่ถูกใช้งานเมื่อมีการโหลดไฟล์ปฏิบัติการ
ที่อยู่อาจมีการใช้งานอยู่แล้วเมื่อมีการโหลดไลบรารีแบบไดนามิก (.so
/.dll
) เนื่องจากไลบรารีเหล่านี้แชร์หน่วยความจำกับไฟล์ปฏิบัติการ
... จะตัดสินใจได้อย่างไรว่าจะเริ่ม linux ที่ปฏิบัติการได้ใหม่ของฉันที่ไหน?
ภายใต้ Linux ไฟล์ปฏิบัติการจะถูกโหลดไปยังที่อยู่คงที่หากมีการเชื่อมโยงในลักษณะที่ไม่สามารถย้ายไปยังที่อยู่อื่นได้ (ดังที่ได้กล่าวไปแล้ว: นี่เป็นเรื่องปกติสำหรับไฟล์ 32 บิตรุ่นเก่า) ในตัวอย่างของคุณ สตริง Hello world จะอยู่ที่ที่อยู่ 0x601040
หาก คอมไพเลอร์และตัวเชื่อมโยงของคุณสร้างไฟล์ปฏิบัติการในลักษณะนั้น
อย่างไรก็ตาม ไฟล์ปฏิบัติการ 64 บิตส่วนใหญ่สามารถโหลดไปยังที่อยู่อื่นได้ Linux จะโหลดไปยังที่อยู่ สุ่ม เนื่องจากเหตุผลด้านความปลอดภัย ทำให้ไวรัสหรือมัลแวร์อื่นๆ โจมตีโปรแกรมได้ยากขึ้น
... เพื่อให้สแต็กสามารถขยายลงมาด้านล่างส่วนของข้อความได้ ...
ฉันไม่เคยเห็นเค้าโครงหน่วยความจำนี้ในระบบปฏิบัติการใด ๆ :
ทั้งภายใต้ Linux และภายใต้ Solaris สแต็กจะอยู่ที่ส่วนท้ายของพื้นที่ที่อยู่ (ประมาณ 0xBFFFFF00
) ในขณะที่ส่วนข้อความถูกโหลดค่อนข้างใกล้กับจุดเริ่มต้นของหน่วยความจำ (อาจเป็นที่อยู่ 0x401000
)
... และฮีปสามารถเติบโตได้จากจุดสิ้นสุดของข้อมูล ...
สมมติว่าฮีปของแอปพลิเคชันก่อนหน้าคืบคลานขึ้น ...
การใช้งานหลายอย่างนับตั้งแต่ช่วงปลายทศวรรษ 1990 ไม่ได้ใช้ฮีปอีกต่อไป แต่จะใช้ mmap()
เพื่อสำรองหน่วยความจำใหม่แทน
ตามหน้าคู่มือของ brk()
ฮีปได้รับการประกาศให้เป็นฟีเจอร์ดั้งเดิมในปี 2544 ดังนั้นจึงไม่ควรใช้กับโปรแกรมใหม่อีกต่อไป
(อย่างไรก็ตามตาม Peter Cordes malloc()
ดูเหมือนว่าจะยังคงใช้ฮีปในบางกรณี)
ต่างจากระบบปฏิบัติการทั่วไปอย่าง MS-DOS ตรงที่ Linux ไม่อนุญาตให้คุณใช้ฮีป แต่คุณต้องเรียกใช้ฟังก์ชัน brk()
เพื่อบอก Linux ว่าคุณต้องการใช้ฮีปจำนวนเท่าใด
หากโปรแกรมใช้ฮีปและใช้ฮีปมากกว่าที่มีอยู่ ฟังก์ชัน brk()
จะส่งกลับรหัสข้อผิดพลาดบางส่วน และฟังก์ชัน malloc()
จะส่งคืน NULL
อย่างไรก็ตาม สถานการณ์นี้มักเกิดขึ้นเนื่องจากไม่มี RAM เหลืออยู่ และไม่ใช่เนื่องจากฮีปซ้อนทับกับพื้นที่หน่วยความจำอื่น
... ในขณะที่สแต็คของ linux ที่เพิ่งเปิดตัวกลับลดลงเหลือ ...
ในไม่ช้า จะมีการขัดแย้ง/ทับซ้อนกันของที่อยู่หน่วยความจำ จะเกิดอะไรขึ้นเมื่อการปะทะกันเกิดขึ้นในที่สุด?
แท้จริงแล้วขนาดของสแต็กนั้นมีจำกัด
หากคุณใช้สแต็กมากเกินไป คุณจะมีสแต็กล้น
โปรแกรมนี้ตั้งใจใช้สแต็กมากเกินไป - เพียงเพื่อดูว่าจะเกิดอะไรขึ้น:
.globl _start
_start:
sub $0x100000, %rsp
push %rax
push %rax
jmp _start
ในกรณีของระบบปฏิบัติการที่มี MMU (เช่น Linux) โปรแกรมของคุณจะขัดข้องพร้อมข้อความแสดงข้อผิดพลาด:
~$ ./example_program
Segmentation fault (core dumped)
~$
แก้ไข/เพิ่มเติม
สแต็กสำหรับโปรแกรมที่รันอยู่ทั้งหมดอยู่ที่ส่วนท้ายหรือไม่
ใน Linux เวอร์ชันเก่า สแตกตั้งอยู่ใกล้ (แต่ไม่ตรงทั้งหมด) จุดสิ้นสุดของหน่วยความจำ เสมือน ที่โปรแกรมเข้าถึงได้: โปรแกรมสามารถเข้าถึงช่วงที่อยู่ตั้งแต่ 0
ถึง 0xBFFFFFFF
ใน Linux เวอร์ชันเหล่านั้น ตัวชี้สแต็กเริ่มต้นอยู่ที่ประมาณ 0xBFFFFE00
(อาร์กิวเมนต์บรรทัดคำสั่งและตัวแปรสภาพแวดล้อมอยู่หลังสแต็ก)
และนี่คือจุดสิ้นสุดของหน่วยความจำกายภาพที่แท้จริงหรือไม่? โปรแกรมที่รันอยู่หลายๆ โปรแกรมจะไม่ปะปนกันใช่ไหม? ฉันรู้สึกว่าสแต็กและหน่วยความจำทั้งหมดของโปรแกรมยังคงต่อเนื่องกันในหน่วยความจำกายภาพจริง ...
บนคอมพิวเตอร์ที่ใช้ MMU โปรแกรมจะไม่เห็นหน่วยความจำกายภาพเลย:
เมื่อโหลดโปรแกรมแล้ว ระบบปฏิบัติการจะค้นหาพื้นที่ว่างของ RAM - อาจพบบางส่วนที่ที่อยู่ทางกายภาพ 0xABC000
จากนั้นจะกำหนดค่า MMU ในลักษณะที่ที่อยู่เสมือน 0xBFFFF000-0xBFFFFFFF
ได้รับการแปลเป็นที่อยู่จริง 0xABC000-0xABCFFF
ซึ่งหมายความว่า: เมื่อใดก็ตามที่โปรแกรมเข้าถึงที่อยู่ 0xBFFFFE20
(เช่น การใช้การดำเนินการ push
) ที่อยู่จริง 0xABCE20
ใน RAM ก็จะถูกเข้าถึงจริง
ไม่มีความเป็นไปได้ที่โปรแกรมจะเข้าถึงที่อยู่ทางกายภาพที่แน่นอน
หากคุณมีโปรแกรมอื่นที่ทำงานอยู่ MMU จะได้รับการกำหนดค่าในลักษณะที่ที่อยู่ 0xBFFFF000-0xBFFFFFFF
ได้รับการแปลเป็นที่อยู่ 0x345000-0x345FFF
เมื่อโปรแกรมอื่นกำลังทำงานอยู่
ดังนั้นหากหนึ่งในสองโปรแกรมจะดำเนินการ push
และตัวชี้สแต็กคือ 0xBFFFFE20
ที่อยู่ 0xABCE20
ใน RAM ก็จะสามารถเข้าถึงได้ หากโปรแกรมอื่นดำเนินการ push
(ด้วยค่าตัวชี้สแต็กเดียวกัน) ที่อยู่ 0x345E20
จะถูกเข้าถึง
ดังนั้นสแต็คจะไม่ปะปนกัน
ระบบปฏิบัติการที่ไม่ได้ใช้ MMU แต่รองรับการทำงานหลายอย่างพร้อมกัน (เช่น Amiga 500 หรือ Apple Macintoshes รุ่นแรกๆ) จะไม่ทำงานในลักษณะนี้ ระบบปฏิบัติการดังกล่าวใช้รูปแบบไฟล์พิเศษ (ไม่ใช่ ELF) ซึ่งได้รับการปรับให้เหมาะสมสำหรับการรันหลายโปรแกรมโดยไม่มี MMU การคอมไพล์โปรแกรมสำหรับระบบปฏิบัติการดังกล่าวนั้นซับซ้อนกว่าการคอมไพล์โปรแกรมสำหรับ Linux หรือ Windows มาก และยังมีข้อจำกัดสำหรับนักพัฒนาซอฟต์แวร์ด้วย (ตัวอย่าง: ฟังก์ชันและอาร์เรย์ไม่ควรยาวเกินไป)
แต่ละโปรแกรมมีตัวชี้สแต็ก ตัวชี้ฐาน รีจิสเตอร์ ฯลฯ ของตัวเองหรือไม่ หรือระบบปฏิบัติการมีรีจิสเตอร์เหล่านี้ชุดเดียวที่จะแชร์โดยทุกโปรแกรม?
(สมมติว่าเป็นซีพียูแบบคอร์เดียว) CPU มีรีจิสเตอร์หนึ่งชุด และสามารถทำงานได้เพียงโปรแกรมเดียวเท่านั้น
เมื่อคุณเริ่มหลายโปรแกรม ระบบปฏิบัติการจะสลับระหว่างโปรแกรมต่างๆ ซึ่งหมายความว่าโปรแกรม A จะทำงานเป็นเวลา 1/50 วินาที จากนั้นโปรแกรม B จะทำงานเป็นเวลา 1/50 วินาที จากนั้นโปรแกรม A จะทำงานเป็นเวลา 1/50 วินาทีและอื่นๆ ปรากฏให้คุณเห็นว่าโปรแกรมทำงานในเวลาเดียวกัน
เมื่อระบบปฏิบัติการเปลี่ยนจากโปรแกรม A เป็นโปรแกรม B จะต้องบันทึกค่าของรีจิสเตอร์ (ของโปรแกรม A) ก่อน จากนั้นจะต้องเปลี่ยนการกำหนดค่า MMU สุดท้ายจะต้องคืนค่ารีจิสเตอร์ของโปรแกรม B
person
Martin Rosenau
schedule
06.08.2020