ทิศทางของการเติบโตแบบสแต็กในระบบสมัยใหม่ส่วนใหญ่เป็นอย่างไร?


102

ฉันกำลังเตรียมเอกสารการฝึกอบรมบางอย่างในภาษา C และฉันต้องการให้ตัวอย่างของฉันพอดีกับแบบจำลองกองซ้อนทั่วไป

C stack เติบโตไปในทิศทางใดใน Linux, Windows, Mac OSX (PPC และ x86), Solaris และ Unixes ล่าสุด


คำตอบ:


147

การเติบโตของสแต็คไม่ได้ขึ้นอยู่กับระบบปฏิบัติการ แต่ขึ้นอยู่กับโปรเซสเซอร์ที่ทำงานอยู่ ตัวอย่างเช่น Solaris ทำงานบน x86 และ SPARC Mac OSX (ตามที่คุณกล่าวถึง) ทำงานบน PPC และ x86 ลินุกซ์ทำงานบนทุกอย่างจาก Honkin ใหญ่ของฉัน System z ที่ทำงานกับนาฬิกาข้อมืออ่อนแอเล็ก ๆ น้อย ๆ

หากซีพียูมีตัวเลือกแบบใดแบบหนึ่งรูปแบบ ABI / การโทรที่ใช้โดยระบบปฏิบัติการจะระบุว่าคุณต้องเลือกตัวเลือกใดหากคุณต้องการให้รหัสของคุณเรียกรหัสของคนอื่น

โปรเซสเซอร์และทิศทางคือ:

  • x86: ลง
  • SPARC: เลือกได้ ABI มาตรฐานใช้ลดลง
  • PPC: ฉันคิดว่า
  • ระบบ z: ในรายการที่เชื่อมโยงฉันไม่สนใจคุณ (แต่ก็ยังลงอย่างน้อยสำหรับ zLinux)
  • ARM: เลือกได้ แต่ Thumb2 มีการเข้ารหัสแบบกะทัดรัดสำหรับลงเท่านั้น (LDMIA = เพิ่มขึ้นหลัง, STMDB = ลดลงก่อนหน้านี้)
  • 6502: ลง (แต่มีเพียง 256 ไบต์)
  • RCA 1802A: ในแบบที่คุณต้องการโดยขึ้นอยู่กับการใช้งาน SCRT
  • PDP11: ลง
  • 8051: ขึ้น

แสดงอายุของฉันในช่วงไม่กี่คนที่ผ่านมา 1802 เป็นชิปที่ใช้ในการควบคุมรถรับส่งในยุคแรก ๆ (เมื่อตรวจสอบว่าประตูเปิดอยู่ฉันสงสัยว่าขึ้นอยู่กับพลังการประมวลผลที่มี :-) และคอมพิวเตอร์เครื่องที่สองของฉันCOMX-35 ( ตามZX80ของฉัน)

PDP11 รายละเอียดที่รวบรวมได้จากที่นี่ , 8051 รายละเอียดจากที่นี่

สถาปัตยกรรม SPARC ใช้โมเดลการลงทะเบียนหน้าต่างบานเลื่อน รายละเอียดที่มองเห็นได้ทางสถาปัตยกรรมยังรวมถึงบัฟเฟอร์วงกลมของหน้าต่างรีจิสเตอร์ที่ถูกต้องและถูกแคชไว้ภายในพร้อมกับดักเมื่อล้น / ต่ำเกินไป ดูรายละเอียดได้ที่นี่ ตามที่คู่มือ SPARCv8 อธิบายคำแนะนำ SAVE และ RESTORE เป็นเหมือนคำแนะนำ ADD บวกกับการหมุนหน้าต่างลงทะเบียน การใช้ค่าคงที่ที่เป็นบวกแทนที่จะเป็นค่าลบตามปกติจะทำให้กองซ้อนเติบโตขึ้น

เทคนิค SCRT ที่กล่าวถึงข้างต้นเป็นอีกเทคนิคหนึ่ง - 1802 ใช้การลงทะเบียน 16 บิต 16 บิตหรือ 16 บิตสำหรับ SCRT (เทคนิคการโทรและการส่งคืนมาตรฐาน) หนึ่งคือตัวนับโปรแกรมคุณสามารถใช้รีจิสเตอร์เป็นพีซีพร้อมSEP Rnคำสั่ง หนึ่งคือตัวชี้สแต็กและสองตัวถูกตั้งค่าให้ชี้ไปที่ที่อยู่รหัส SCRT เสมอหนึ่งตัวสำหรับการโทรหนึ่งตัวสำหรับการส่งคืน ไม่มีการลงทะเบียนในลักษณะพิเศษ โปรดทราบว่ารายละเอียดเหล่านี้มาจากหน่วยความจำอาจไม่ถูกต้องทั้งหมด

ตัวอย่างเช่นถ้า R3 เป็นพีซี R4 คือที่อยู่การโทร SCRT R5 คือที่อยู่สำหรับส่งคืน SCRT และ R2 คือ "สแต็ก" (เครื่องหมายคำพูดที่นำไปใช้ในซอฟต์แวร์) SEP R4จะตั้งค่า R4 ให้เป็นพีซีและเริ่มเรียกใช้ SCRT รหัสโทร.

จากนั้นจะจัดเก็บ R3 บน R2 "สแต็ก" (ฉันคิดว่า R6 ใช้สำหรับการจัดเก็บชั่วคราว) ปรับขึ้นหรือลงจับสองไบต์ตาม R3 โหลดลงใน R3 จากนั้นทำSEP R3และรันที่ที่อยู่ใหม่

ในการส่งคืนมันจะSEP R5ดึงที่อยู่เก่าออกจากสแต็ก R2 เพิ่มสองที่อยู่ (เพื่อข้ามไบต์ที่อยู่ของการโทร) โหลดลงใน R3 และSEP R3เริ่มรันโค้ดก่อนหน้า

ยากมากที่จะห่อหัวของคุณไปรอบ ๆ ในตอนแรกหลังจากรหัสสแต็ก 6502/6809 / z80 ทั้งหมด แต่ยังคงสง่างามในแบบที่หัวชนกับผนัง นอกจากนี้หนึ่งในคุณสมบัติการขายที่ยิ่งใหญ่ของชิปคือชุดการลงทะเบียน 16 16 บิตเต็มรูปแบบแม้ว่าคุณจะสูญเสีย 7 ในทันที (5 สำหรับ SCRT สองตัวสำหรับ DMA และการขัดจังหวะจากหน่วยความจำ) อ่าชัยชนะของการตลาดเหนือความเป็นจริง :-)

จริงๆแล้วระบบ z นั้นค่อนข้างคล้ายกันโดยใช้การลงทะเบียน R14 และ R15 สำหรับการโทร / ส่งคืน


3
ในการเพิ่มลงในรายการ ARM สามารถเติบโตไปในทิศทางใดก็ได้ แต่สามารถตั้งค่าให้เป็นแบบใดแบบหนึ่งโดยใช้ซิลิกอนโดยเฉพาะ (หรือสามารถเลือกได้โดยซอฟต์แวร์) ไม่กี่อย่างที่ฉันเคยรับมือมักจะอยู่ในโหมดเติบโตลง
Michael Burr

1
ในโลก ARM เล็ก ๆ น้อย ๆ ที่ฉันเคยเห็น (ARM7TDMI) สแต็กได้รับการจัดการทั้งหมดในซอฟต์แวร์ ที่อยู่ที่ส่งคืนจะถูกเก็บไว้ในรีจิสเตอร์ซึ่งบันทึกไว้โดยซอฟต์แวร์หากจำเป็นและคำแนะนำก่อน / หลังการเพิ่ม / ลดอนุญาตให้วางและสิ่งอื่น ๆ บนสแต็กในทิศทางใดก็ได้
starblue

1
หนึ่ง HPPA สแต็กเติบโตขึ้น! ค่อนข้างหายากในสถาปัตยกรรมสมัยใหม่ที่สมเหตุสมผล
tml

2
สำหรับผู้ที่อยากรู้อยากเห็นนี่เป็นแหล่งข้อมูลที่ดีเกี่ยวกับการทำงานของสแต็กบน z / OS: www-03.ibm.com/systems/resources/Stack+and+Heap.pdf
Dillon

1
ขอบคุณ @paxdiablo สำหรับความเข้าใจของคุณ บางครั้งผู้คนก็มองว่าเป็นการดูถูกส่วนตัวเมื่อคุณแสดงความคิดเห็นเช่นนี้โดยเฉพาะอย่างยิ่งเมื่อเป็นเรื่องที่เก่ากว่า ฉันรู้แค่ว่ามีความแตกต่างเพราะที่ผ่านมาฉันเคยทำผิดพลาดเหมือนกัน ดูแล.
CasaDeRobison

23

ใน C ++ (ปรับให้เข้ากับ C) stack.cc :

static int
find_stack_direction ()
{
    static char *addr = 0;
    auto char dummy;
    if (addr == 0)
    {
        addr = &dummy;
        return find_stack_direction ();
    }
    else
    {
        return ((&dummy > addr) ? 1 : -1);
    }
}

14
ว้าวเป็นเวลานานแล้วที่ฉันได้เห็นคำหลัก "อัตโนมัติ"
paxdiablo

9
(& dummy> addr) ไม่ได้กำหนดไว้ ผลลัพธ์ของการป้อนพอยน์เตอร์สองตัวให้กับตัวดำเนินการเชิงสัมพันธ์จะถูกกำหนดก็ต่อเมื่อพอยน์เตอร์สองตัวชี้ภายในอาร์เรย์หรือโครงสร้างเดียวกัน
sigjuice

2
การพยายามตรวจสอบเลย์เอาต์ของสแต็กของคุณเองซึ่งเป็นสิ่งที่ C / C ++ ไม่ได้ระบุเลย - นั้น "ไม่สามารถพกพาได้" มาเริ่มต้นด้วยดังนั้นฉันจึงไม่สนใจเรื่องนั้นมากนัก ดูเหมือนว่าฟังก์ชันนี้จะทำงานได้อย่างถูกต้องเพียงครั้งเดียว
ephemient

9
ไม่จำเป็นต้องใช้ a staticสำหรับสิ่งนี้ แต่คุณสามารถส่งที่อยู่เป็นอาร์กิวเมนต์ไปยังการโทรซ้ำได้
R .. GitHub STOP HELPING ICE

5
บวกโดยใช้ a staticถ้าคุณโทรมากกว่าหนึ่งครั้งการโทรที่ตามมาอาจล้มเหลว ...
Chris Dodd

7

ข้อได้เปรียบของการเติบโตขึ้นคือในระบบเก่าโดยทั่วไปสแต็กจะอยู่ที่ด้านบนสุดของหน่วยความจำ โดยทั่วไปแล้วโปรแกรมจะเติมหน่วยความจำโดยเริ่มจากด้านล่างดังนั้นการจัดการหน่วยความจำประเภทนี้จึงลดความจำเป็นในการวัดและวางด้านล่างของสแต็กไว้ในที่ที่เหมาะสม


3
ไม่ใช่ 'ประโยชน์' tautology จริงๆ
Marquis of Lorne

1
ไม่ใช่ tautology ประเด็นคือการมีพื้นที่หน่วยความจำที่เพิ่มขึ้นสองแห่งไม่รบกวน (เว้นแต่หน่วยความจำจะเต็มอยู่ดี) ตามที่ @valenok ชี้ให้เห็น
YvesgereY

6

สแต็คขยายตัวลงบน x86 (กำหนดโดยสถาปัตยกรรมตัวชี้สแต็กที่เพิ่มขึ้นป๊อปการลดการกด)


5

ใน MIPS และสถาปัตยกรรม RISCสมัยใหม่จำนวนมาก(เช่น PowerPC, RISC-V, SPARC ... ) ไม่มีpushและpopคำแนะนำ การดำเนินการเหล่านั้นทำได้อย่างชัดเจนโดยการปรับตัวชี้สแต็กด้วยตนเองจากนั้นโหลด / จัดเก็บค่าตามตัวชี้ที่ปรับแล้ว การลงทะเบียนทั้งหมด (ยกเว้นการลงทะเบียนศูนย์) เป็นจุดประสงค์ทั่วไปดังนั้นในทางทฤษฎีรีจิสเตอร์ใด ๆก็สามารถเป็นตัวชี้สแต็กได้และสแต็กสามารถเติบโตไปในทิศทางใดก็ได้ที่โปรแกรมเมอร์ต้องการ

ที่กล่าวว่าสแต็กมักจะเติบโตขึ้นตามสถาปัตยกรรมส่วนใหญ่อาจจะเพื่อหลีกเลี่ยงกรณีที่ข้อมูลสแต็กและโปรแกรมหรือข้อมูลฮีปเติบโตขึ้นและปะทะกัน นอกจากนี้ยังมีดีอยู่จากเหตุผลดังกล่าวตอบ SH-ของ ตัวอย่างบางส่วน: MIPS ABI ลดลงและใช้$29(AKA $sp) เป็นตัวชี้สแต็ก RISC-V ABI จะขยายลงด้านล่างและใช้ x2 เป็นตัวชี้สแต็ก

ใน Intel 8051 สแต็กเติบโตขึ้นอาจเป็นเพราะพื้นที่หน่วยความจำมีขนาดเล็กมาก (128 ไบต์ในเวอร์ชันดั้งเดิม) ซึ่งไม่มีฮีปและคุณไม่จำเป็นต้องวางสแต็กไว้ด้านบนเพื่อที่จะแยกออกจากฮีปที่เพิ่มขึ้น จากด้านล่าง

คุณสามารถค้นหาข้อมูลเพิ่มเติมเกี่ยวกับการใช้สแต็กในสถาปัตยกรรมต่างๆได้ในhttps://en.wikipedia.org/wiki/Calling_convention

ดูสิ่งนี้ด้วย


2

เพียงเล็กน้อยสำหรับคำตอบอื่น ๆ ซึ่งเท่าที่ฉันเห็นยังไม่ได้แตะจุดนี้:

การมีสแต็กเติบโตลงทำให้ที่อยู่ทั้งหมดภายในสแต็กมีค่าชดเชยที่เป็นบวกเมื่อเทียบกับตัวชี้สแต็ก ไม่จำเป็นต้องมีการชดเชยเชิงลบเนื่องจากจะชี้ไปที่พื้นที่สแต็กที่ไม่ได้ใช้เท่านั้น สิ่งนี้ช่วยลดความยุ่งยากในการเข้าถึงตำแหน่งสแต็กเมื่อโปรเซสเซอร์รองรับการกำหนดแอดเดรสแบบสแต็กพอยน์เตอร์

โปรเซสเซอร์หลายตัวมีคำแนะนำที่อนุญาตให้เข้าถึงด้วยค่าออฟเซ็ตบวกอย่างเดียวที่สัมพันธ์กับรีจิสเตอร์บางตัว ซึ่งรวมถึงสถาปัตยกรรมสมัยใหม่จำนวนมากและสถาปัตยกรรมเก่าแก่บางส่วน ตัวอย่างเช่น ARM Thumb ABI จัดเตรียมการเข้าถึงแบบสัมพันธ์กับสแต็กพอยน์เตอร์ด้วยออฟเซ็ตบวกที่เข้ารหัสภายในคำสั่ง 16 บิตเดียว

หากสแต็กโตขึ้นการชดเชยที่เป็นประโยชน์ทั้งหมดเมื่อเทียบกับสแต็กพอยน์เตอร์จะเป็นลบซึ่งใช้งานง่ายน้อยกว่าและสะดวกน้อยกว่า นอกจากนี้ยังขัดแย้งกับแอปพลิเคชันอื่น ๆ ของการระบุแอดเดรสที่สัมพันธ์กับการลงทะเบียนเช่นสำหรับการเข้าถึงฟิลด์ของโครงสร้าง


2

ในระบบส่วนใหญ่สแต็กจะเติบโตลดลงและบทความของฉันที่https://gist.github.com/cpq/8598782อธิบายว่าทำไมมันถึงเติบโตลง เป็นเรื่องง่าย: จะจัดวางบล็อกหน่วยความจำที่เพิ่มขึ้นสองบล็อก (ฮีปและสแต็ก) ในหน่วยความจำคงที่ได้อย่างไร? ทางออกที่ดีที่สุดคือวางไว้ที่ด้านตรงข้ามและปล่อยให้เติบโตเข้าหากัน


สรุปสาระสำคัญที่ดูเหมือนว่าจะตายในขณะนี้ :(
Ven

@ เวน - ฉันไปถึงมันได้
Brett Holman

1

มันเติบโตลงเนื่องจากหน่วยความจำที่จัดสรรให้กับโปรแกรมมีรหัส "ข้อมูลถาวร" สำหรับโปรแกรมเองที่ด้านล่างจากนั้นฮีปที่อยู่ตรงกลาง คุณต้องการจุดคงที่อีกจุดเพื่ออ้างอิงสแต็กเพื่อที่จะทำให้คุณอยู่ในจุดสูงสุด ซึ่งหมายความว่าสแต็กจะขยายตัวลดลงจนกว่าจะอยู่ติดกับวัตถุบนฮีป


0

มาโครนี้ควรตรวจพบที่รันไทม์โดยไม่มี UB:

#define stk_grows_up_eh() stk_grows_up__(&(char){0})
_Bool stk_grows_up__(char *ParentsLocal);

__attribute((__noinline__))
_Bool stk_grows_up__(char *ParentsLocal) { 
    return (uintptr_t)ParentsLocal < (uintptr_t)&ParentsLocal;
}
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.