ในสิ่งที่ควรจะเป็นการรันครั้งสุดท้ายของลูปคุณเขียนถึงarray[10]
แต่มีเพียง 10 องค์ประกอบในอาเรย์ซึ่งมีหมายเลข 0 ถึง 9 ข้อมูลจำเพาะภาษา C บอกว่านี่คือ "พฤติกรรมที่ไม่ได้กำหนด" สิ่งนี้หมายความว่าในทางปฏิบัติคือโปรแกรมของคุณจะพยายามเขียนลงในint
หน่วยความจำขนาดที่อยู่array
ในหน่วยความจำทันที สิ่งที่เกิดขึ้นนั้นขึ้นอยู่กับสิ่งที่เกิดขึ้นจริงแล้วนอนอยู่ที่นั่นและสิ่งนี้ไม่เพียง แต่ขึ้นอยู่กับระบบปฏิบัติการเท่านั้น เป็นต้นมันอาจแตกต่างกันไปจากการดำเนินการจนถึงการกระทำเช่นเนื่องจากการสุ่มพื้นที่ที่อยู่ (อาจไม่ได้อยู่ในตัวอย่างของเล่นนี้ แต่มันเกิดขึ้นในชีวิตจริง) ความเป็นไปได้บางอย่างรวมถึง:
- ไม่ได้ใช้ตำแหน่ง การวนซ้ำยุติลงตามปกติ
- ตำแหน่งถูกใช้สำหรับบางสิ่งที่เกิดขึ้นที่มีค่า 0 การวนซ้ำจะสิ้นสุดลงตามปกติ
- ที่ตั้งมีที่อยู่ผู้ส่งของฟังก์ชั่น การวนซ้ำจะสิ้นสุดลงตามปกติ แต่แล้วโปรแกรมก็ขัดข้องเนื่องจากพยายามข้ามไปยังที่อยู่ 0
i
สถานที่มีตัวแปร การวนซ้ำไม่สิ้นสุดเนื่องจากi
รีสตาร์ทที่ 0
- สถานที่มีตัวแปรอื่น ๆ การวนซ้ำยุติลงตามปกติ แต่แล้วสิ่งที่“ น่าสนใจ” ก็เกิดขึ้น
- ตำแหน่งนี้เป็นที่อยู่หน่วยความจำที่ไม่ถูกต้องเช่นเนื่องจาก
array
อยู่ด้านท้ายของหน้าหน่วยความจำเสมือนและหน้าถัดไปจะไม่ถูกแมป
- ปีศาจบินออกจากจมูกของคุณ โชคดีที่คอมพิวเตอร์ส่วนใหญ่ไม่มีฮาร์ดแวร์ที่จำเป็น
สิ่งที่คุณสังเกตเห็นบน Windows ก็คือว่าคอมไพเลอร์ตัดสินใจที่จะวางตัวแปรi
ทันทีหลังจากอาร์เรย์ในหน่วยความจำเพื่อให้จบลงด้วยการมอบหมายให้array[10] = 0
i
บน Ubuntu และ CentOS คอมไพเลอร์ไม่ได้อยู่ที่i
นั่น การใช้งาน C เกือบทั้งหมดทำกลุ่มตัวแปรโลคอลในหน่วยความจำบนสแต็คหน่วยความจำโดยมีข้อยกเว้นหลักประการหนึ่ง: ตัวแปรโลคอลบางตัวสามารถวางได้ทั้งหมดในรีจิสเตอร์ได้ทั้งหมด แม้ว่าตัวแปรอยู่ในสแต็กลำดับของตัวแปรจะถูกกำหนดโดยคอมไพเลอร์และอาจขึ้นอยู่กับลำดับในไฟล์ต้นฉบับเท่านั้น แต่ยังอยู่ในประเภทของไฟล์เหล่านั้นด้วย ในชื่อของพวกเขาในค่าแฮชบางอย่างที่ใช้ในโครงสร้างข้อมูลภายในของคอมไพเลอร์ ฯลฯ
หากคุณต้องการทราบว่าคอมไพเลอร์ของคุณตัดสินใจทำอะไรคุณสามารถบอกให้คอมไพเลอร์แสดงรหัส โอ้และเรียนรู้ที่จะถอดรหัสแอสเซมเบลอร์ (ง่ายกว่าการเขียน) ด้วย GCC (และคอมไพเลอร์อื่น ๆ โดยเฉพาะในโลกของ Unix) ให้ส่งตัวเลือก-S
ในการสร้างรหัสแอสเซมเบลอร์แทนไบนารี ตัวอย่างเช่นนี่คือตัวอย่างข้อมูลประกอบของลูปจากการรวบรวมกับ GCC ใน amd64 ด้วยตัวเลือกการเพิ่มประสิทธิภาพ-O0
(ไม่มีการเพิ่มประสิทธิภาพ) โดยมีการเพิ่มความคิดเห็นด้วยตนเอง:
.L3:
movl -52(%rbp), %eax ; load i to register eax
cltq
movl $0, -48(%rbp,%rax,4) ; set array[i] to 0
movl $.LC0, %edi
call puts ; printf of a constant string was optimized to puts
addl $1, -52(%rbp) ; add 1 to i
.L2:
cmpl $10, -52(%rbp) ; compare i to 10
jle .L3
นี่คือตัวแปรที่i
52 ไบต์ด้านล่างด้านบนของสแต็กในขณะที่อาร์เรย์เริ่ม 48 ไบต์ด้านล่างด้านบนของสแต็ก ดังนั้นคอมไพเลอร์นี้เกิดขึ้นi
ก่อนที่อาร์เรย์; คุณต้องการเขียนทับถ้าคุณเกิดขึ้นที่จะเขียนถึงi
array[-1]
ถ้าคุณเปลี่ยนarray[i]=0
ไปarray[9-i]=0
คุณจะได้รับห่วงอนันต์บนแพลตฟอร์มนี้โดยเฉพาะอย่างยิ่งกับตัวเลือกคอมไพเลอร์เหล่านี้โดยเฉพาะอย่างยิ่ง
gcc -O1
ตอนนี้ขอรวบรวมโปรแกรมของคุณด้วย
movl $11, %ebx
.L3:
movl $.LC0, %edi
call puts
subl $1, %ebx
jne .L3
นั่นสั้นกว่า! คอมไพเลอร์ไม่เพียง แต่ปฏิเสธที่จะจัดสรรตำแหน่งสแต็กสำหรับi
- มันเคยถูกเก็บไว้ในรีจิสเตอร์ebx
เท่านั้น แต่มันไม่ได้ใส่ใจที่จะจัดสรรหน่วยความจำใด ๆarray
หรือสร้างรหัสเพื่อตั้งองค์ประกอบเนื่องจากมันสังเกตว่าไม่มีองค์ประกอบใด ๆ เคยใช้
เพื่อให้ตัวอย่างนี้บอกได้มากขึ้นให้แน่ใจว่าการกำหนดอาเรย์จะดำเนินการโดยการให้คอมไพเลอร์กับสิ่งที่มันไม่สามารถเพิ่มประสิทธิภาพออกไป วิธีง่ายๆในการทำเช่นนั้นคือการใช้อาร์เรย์จากไฟล์อื่น - เนื่องจากการรวบรวมแยกกันคอมไพเลอร์ไม่ทราบว่าเกิดอะไรขึ้นในไฟล์อื่น (เว้นแต่ว่ามันจะปรับให้เหมาะสมในเวลาลิงค์gcc -O0
หรือgcc -O1
ไม่) สร้างไฟล์ต้นฉบับuse_array.c
ที่มี
void use_array(int *array) {}
และเปลี่ยนซอร์สโค้ดของคุณเป็น
#include <stdio.h>
void use_array(int *array);
int main()
{
int array[10],i;
for (i = 0; i <=10 ; i++)
{
array[i]=0; /*code should never terminate*/
printf("test \n");
}
printf("%zd \n", sizeof(array)/sizeof(int));
use_array(array);
return 0;
}
รวบรวมกับ
gcc -c use_array.c
gcc -O1 -S -o with_use_array1.c with_use_array.c use_array.o
คราวนี้โค้ดแอสเซมเบลอร์จะเป็นดังนี้:
movq %rsp, %rbx
leaq 44(%rsp), %rbp
.L3:
movl $0, (%rbx)
movl $.LC0, %edi
call puts
addq $4, %rbx
cmpq %rbp, %rbx
jne .L3
ตอนนี้อาร์เรย์อยู่บนสแต็ก 44 ไบต์จากด้านบน เกี่ยวกับi
อะไร มันจะไม่ปรากฏที่ใดก็ได้! rbx
แต่วงเคาน์เตอร์จะถูกเก็บไว้ในการลงทะเบียน มันไม่ตรงแต่ที่อยู่ของi
array[i]
คอมไพเลอร์ตัดสินใจว่าเนื่องจากi
ไม่เคยใช้ค่าของโดยตรงจึงไม่มีประเด็นในการคำนวณทางคณิตศาสตร์เพื่อคำนวณตำแหน่งที่จะเก็บ 0 ระหว่างการรันลูปแต่ละครั้ง แต่ที่อยู่นั้นเป็นตัวแปรลูปและการคำนวณทางคณิตศาสตร์เพื่อกำหนดขอบเขตได้ดำเนินการบางส่วน ณ เวลารวบรวม (คูณ 11 ซ้ำคูณด้วย 4 ไบต์ต่อองค์ประกอบอาเรย์เพื่อรับ 44) และบางส่วนในเวลาดำเนินการ แต่เพียงครั้งเดียว ทำการลบเพื่อรับค่าเริ่มต้น)
แม้ในตัวอย่างง่ายๆนี้เราได้เห็นว่าการเปลี่ยนตัวเลือกคอมไพเลอร์ (เปิดการปรับให้เหมาะสม) หรือเปลี่ยนบางสิ่งเล็กน้อย ( array[i]
เป็นarray[9-i]
) หรือแม้กระทั่งการเปลี่ยนแปลงบางสิ่งที่ไม่เกี่ยวข้องอย่างเห็นได้ชัด (เพิ่มการเรียกไปยังuse_array
) สามารถสร้างความแตกต่าง โดยคอมไพเลอร์ทำ การเพิ่มประสิทธิภาพคอมไพเลอร์สามารถทำหลายสิ่งหลายอย่างที่อาจปรากฏ unintuitive ในโปรแกรมที่เรียกพฤติกรรมที่ไม่ได้กำหนด นั่นเป็นสาเหตุที่พฤติกรรมที่ไม่ได้กำหนดนั้นไม่ได้กำหนดอย่างสมบูรณ์ เมื่อคุณเบี่ยงเบนไปจากแทร็กเล็กน้อยในโปรแกรมโลกแห่งความจริงมันยากที่จะเข้าใจความสัมพันธ์ระหว่างสิ่งที่รหัสทำกับสิ่งที่ควรทำแม้กระทั่งโปรแกรมเมอร์ที่มีประสบการณ์