หากฮีปมีการกำหนดค่าเริ่มต้นเป็นศูนย์เพื่อความปลอดภัยแล้วทำไมสแต็กจึงไม่มีการกำหนดค่าเริ่มต้นเพียงอย่างเดียว


15

บนระบบ Debian GNU / Linux 9 ของฉันเมื่อทำการไบนารี่

  • สแต็กถูกกำหนดค่าเริ่มต้น แต่
  • ฮีปจะถูกกำหนดค่าเริ่มต้นเป็นศูนย์

ทำไม?

ฉันสมมติว่า zero-initialization ส่งเสริมความปลอดภัย แต่ถ้าฮีปแล้วทำไมไม่สแต็กล่ะ? สแต็กก็เช่นกันไม่ต้องการความปลอดภัยหรือไม่?

คำถามของฉันไม่เฉพาะเจาะจงกับ Debian เท่าที่ฉันรู้

ตัวอย่างรหัส C:

#include <stddef.h>
#include <stdlib.h>
#include <stdio.h>

const size_t n = 8;

// --------------------------------------------------------------------
// UNINTERESTING CODE
// --------------------------------------------------------------------
static void print_array(
  const int *const p, const size_t size, const char *const name
)
{
    printf("%s at %p: ", name, p);
    for (size_t i = 0; i < size; ++i) printf("%d ", p[i]);
    printf("\n");
}

// --------------------------------------------------------------------
// INTERESTING CODE
// --------------------------------------------------------------------
int main()
{
    int a[n];
    int *const b = malloc(n*sizeof(int));
    print_array(a, n, "a");
    print_array(b, n, "b");
    free(b);
    return 0;
}

เอาท์พุท:

a at 0x7ffe118997e0: 194 0 294230047 32766 294230046 32766 -550453275 32713 
b at 0x561d4bbfe010: 0 0 0 0 0 0 0 0 

มาตรฐาน C ไม่ได้ขอmalloc()ให้ล้างหน่วยความจำก่อนที่จะทำการจัดสรร แต่โปรแกรม C ของฉันเป็นเพียงภาพประกอบเท่านั้น คำถามไม่ใช่คำถามเกี่ยวกับ C หรือเกี่ยวกับไลบรารีมาตรฐานของ C แต่คำถามเป็นคำถามเกี่ยวกับสาเหตุที่เคอร์เนลและ / หรือตัวโหลดรันไทม์กำลัง zeroing ฮีป แต่ไม่ใช่กองซ้อน

ประสบการณ์อื่น ๆ

คำถามของฉันเกี่ยวกับพฤติกรรม GNU / Linux ที่สังเกตเห็นได้มากกว่าความต้องการของเอกสารมาตรฐาน หากไม่แน่ใจในสิ่งที่ฉันหมายถึงให้ลองใช้โค้ดนี้ซึ่งเรียกใช้พฤติกรรมที่ไม่ได้กำหนดเพิ่มเติม ( ไม่ได้กำหนดนั่นคือเท่าที่เกี่ยวข้องกับมาตรฐาน C) เพื่อแสดงให้เห็นถึงจุด:

#include <stddef.h>
#include <stdlib.h>
#include <stdio.h>

const size_t n = 4;

int main()
{
    for (size_t i = n; i; --i) {
        int *const p = malloc(sizeof(int));
        printf("%p %d ", p, *p);
        ++*p;
        printf("%d\n", *p);
        free(p);
    }
    return 0;
}

เอาท์พุทจากเครื่องของฉัน:

0x555e86696010 0 1
0x555e86696010 0 1
0x555e86696010 0 1
0x555e86696010 0 1

เท่าที่เกี่ยวข้องกับมาตรฐาน C พฤติกรรมยังไม่ได้กำหนดดังนั้นคำถามของฉันไม่เกี่ยวกับมาตรฐาน C การเรียกร้องให้malloc()ไม่จำเป็นต้องส่งคืนที่อยู่เดิมทุกครั้ง แต่เนื่องจากการโทรmalloc()นี้เกิดขึ้นเพื่อส่งคืนที่อยู่เดิมทุกครั้งเป็นที่น่าสนใจที่จะสังเกตเห็นว่าหน่วยความจำซึ่งอยู่ในกองนั้นมีค่าเป็นศูนย์ทุกครั้ง

ในทางกลับกันสแต็คดูเหมือนจะไม่ถูกทำให้เป็นศูนย์

ฉันไม่รู้ว่าโค้ดหลังจะทำอะไรในเครื่องของคุณเนื่องจากฉันไม่รู้ว่าเลเยอร์ของระบบ GNU / Linux ใดที่ทำให้เกิดพฤติกรรมที่สังเกตได้ คุณสามารถลองได้

UPDATE

@ Kusalananda ได้สังเกตในความคิดเห็น:

สำหรับสิ่งที่คุ้มค่ารหัสล่าสุดของคุณจะส่งคืนที่อยู่ที่แตกต่างกันและ (ไม่เป็นศูนย์) ข้อมูล (ไม่เป็นศูนย์) เมื่อเรียกใช้บน OpenBSD สิ่งนี้ไม่ได้พูดอะไรเกี่ยวกับพฤติกรรมที่คุณเห็นบน Linux

ว่าผลลัพธ์ของฉันแตกต่างจากผลลัพธ์ใน OpenBSD แน่นอนน่าสนใจ เห็นได้ชัดว่าการทดลองของฉันไม่ได้ค้นพบโปรโตคอลความปลอดภัยของเคอร์เนล (หรือตัวเชื่อมโยง) ตามที่ฉันคิด แต่เป็นเพียงเครื่องมือในการใช้งานเท่านั้น

ในแง่นี้ฉันเชื่อว่าพร้อมกันคำตอบด้านล่างของ @mosvy, @StephenKitt และ @AndreasGrapentin ชำระคำถามของฉัน

ดูเพิ่มเติมที่ Stack Overflow: ทำไม malloc เริ่มต้นค่าเป็น 0 ใน gcc (เครดิต: @bta)


2
สำหรับสิ่งที่คุ้มค่ารหัสล่าสุดของคุณจะส่งคืนที่อยู่ที่แตกต่างกันและ (ไม่เป็นศูนย์) ข้อมูล (ไม่เป็นศูนย์) เมื่อเรียกใช้บน OpenBSD สิ่งนี้ไม่ได้พูดอะไรเกี่ยวกับพฤติกรรมที่คุณเห็นบน Linux
Kusalananda

โปรดอย่าเปลี่ยนขอบเขตคำถามของคุณและอย่าพยายามแก้ไขเพื่อให้ได้คำตอบและความคิดเห็นซ้ำซ้อน ใน C "heap" ไม่ได้เป็นอะไรนอกจากหน่วยความจำที่ส่งคืนโดย malloc () และ calloc () และมีเพียงอันหลังเท่านั้นที่จะทำให้หน่วยความจำเหลือศูนย์ ตัวnewดำเนินการใน C ++ (เช่นเดียวกับ "heap") อยู่บน Linux ซึ่งเป็นตัวห่อสำหรับ malloc (); เคอร์เนลไม่ทราบและไม่สนใจว่า "กอง" คืออะไร
mosvy

3
ตัวอย่างที่สองของคุณเป็นเพียงการเปิดเผยสิ่งประดิษฐ์ของการใช้งาน malloc ใน glibc; หากคุณทำซ้ำ malloc / free ด้วยบัฟเฟอร์ที่มีขนาดใหญ่กว่า 8 ไบต์คุณจะเห็นได้อย่างชัดเจนว่าเฉพาะ 8 ไบต์แรกเท่านั้นที่เป็นศูนย์
mosvy

@ Kusalananda ฉันเห็น ว่าผลลัพธ์ของฉันแตกต่างจากผลลัพธ์ใน OpenBSD จริง ๆ น่าสนใจ เห็นได้ชัดว่าคุณและ Mosvy ได้แสดงให้เห็นว่าการทดลองของฉันไม่ได้ค้นพบโปรโตคอลความปลอดภัยของเคอร์เนล (หรือตัวเชื่อมโยง) อย่างที่ฉันคิด แต่เป็นเพียงเครื่องมือในการนำไปใช้
บาท

@ thb ฉันเชื่อว่านี่อาจเป็นการสังเกตที่ถูกต้องใช่
Kusalananda

คำตอบ:


28

หน่วยเก็บที่ส่งคืนโดย malloc () ไม่ได้ถูกเตรียมข้อมูลเบื้องต้นเป็นศูนย์ ไม่เคยคิดมาก่อนเลยว่าจะเป็น

ในโปรแกรมการทดสอบของคุณมันเป็นเพียงความบังเอิญ: ฉันเดาว่าmalloc()เพิ่งจะมีการบล็อกใหม่mmap()แต่ไม่ต้องพึ่งพาสิ่งนั้น

ตัวอย่างเช่นถ้าฉันรันโปรแกรมของคุณบนเครื่องด้วยวิธีนี้:

$ echo 'void __attribute__((constructor)) p(void){
    void *b = malloc(4444); memset(b, 4, 4444); free(b);
}' | cc -include stdlib.h -include string.h -xc - -shared -o pollute.so

$ LD_PRELOAD=./pollute.so ./your_program
a at 0x7ffd40d3aa60: 1256994848 21891 1256994464 21891 1087613792 32765 0 0
b at 0x55834c75d010: 67372036 67372036 67372036 67372036 67372036 67372036 67372036 67372036

ตัวอย่างที่สองของคุณเป็นเพียงการเปิดเผยสิ่งประดิษฐ์ของการmallocใช้งานใน glibc; หากคุณทำซ้ำmalloc/ freeด้วยบัฟเฟอร์ที่มีขนาดใหญ่กว่า 8 ไบต์คุณจะเห็นได้อย่างชัดเจนว่ามีเพียง 8 ไบต์แรกเท่านั้นที่เป็นศูนย์เช่นเดียวกับในตัวอย่างโค้ดต่อไปนี้

#include <stddef.h>
#include <stdlib.h>
#include <stdio.h>

const size_t n = 4;
const size_t m = 0x10;

int main()
{
    for (size_t i = n; i; --i) {
        int *const p = malloc(m*sizeof(int));
        printf("%p ", p);
        for (size_t j = 0; j < m; ++j) {
            printf("%d:", p[j]);
            ++p[j];
            printf("%d ", p[j]);
        }
        free(p);
        printf("\n");
    }
    return 0;
}

เอาท์พุท:

0x55be12864010 0:1 0:1 0:1 0:1 0:1 0:1 0:1 0:1 0:1 0:1 0:1 0:1 0:1 0:1 0:1 0:1 
0x55be12864010 0:1 0:1 1:2 1:2 1:2 1:2 1:2 1:2 1:2 1:2 1:2 1:2 1:2 1:2 1:2 1:2 
0x55be12864010 0:1 0:1 2:3 2:3 2:3 2:3 2:3 2:3 2:3 2:3 2:3 2:3 2:3 2:3 2:3 2:3 
0x55be12864010 0:1 0:1 3:4 3:4 3:4 3:4 3:4 3:4 3:4 3:4 3:4 3:4 3:4 3:4 3:4 3:4

2
ใช่ แต่นี่คือเหตุผลที่ฉันถามคำถามที่นี่มากกว่าใน Stack Overflow คำถามของฉันไม่เกี่ยวกับมาตรฐาน C แต่เกี่ยวกับวิธีการที่ทันสมัยของระบบ GNU / Linux โดยทั่วไปเชื่อมโยงและโหลดไบนารี LD_PRELOAD ของคุณมีอารมณ์ขัน แต่ตอบคำถามอื่นนอกเหนือจากคำถามที่ฉันตั้งใจจะถาม
thb

19
ฉันดีใจที่ฉันทำให้คุณหัวเราะ แต่สมมติฐานและอคติของคุณไม่ตลกเลย ในระบบ "GNU / Linux ที่ทันสมัย" โดยทั่วไปแล้วไบนารีจะถูกโหลดโดยตัวเชื่อมโยงแบบไดนามิกซึ่งกำลังเรียกใช้ตัวสร้างจากไลบรารีแบบไดนามิกก่อนที่จะไปยังฟังก์ชัน main () จากโปรแกรมของคุณ ในระบบ Debian GNU / Linux 9 ของคุณทั้ง malloc () และ free () จะถูกเรียกมากกว่าหนึ่งครั้งก่อนหน้าที่ main () จากโปรแกรมของคุณแม้ว่าจะไม่ได้ใช้ไลบรารี่ที่โหลดไว้แล้วก็ตาม
mosvy

23

ไม่ว่าจะมีการเตรียมใช้งานสแต็กอย่างไรคุณไม่เห็นสแต็กที่เก่าแก่เพราะไลบรารี C ทำสิ่งต่าง ๆ ก่อนที่จะโทรmainออกและพวกเขาแตะสแต็ก

กับห้องสมุด GNU C บน x86-64 การดำเนินการเริ่มต้นที่_startจุดเริ่มต้นซึ่งการโทรไปยังชุดสิ่งขึ้นและสิ้นสุดลงหลังค่าการสนทนา__libc_start_main mainแต่ก่อนที่จะเรียกmainมันจะเรียกใช้ฟังก์ชันอื่นจำนวนหนึ่งซึ่งทำให้ข้อมูลส่วนต่าง ๆ ถูกเขียนไปยังสแต็ก เนื้อหาของสแต็กจะไม่ถูกล้างระหว่างการเรียกใช้ฟังก์ชั่นดังนั้นเมื่อคุณเข้าสู่mainสแต็กของคุณจะมีของเหลือจากการเรียกใช้ฟังก์ชันก่อนหน้า

สิ่งนี้จะอธิบายผลลัพธ์ที่คุณได้รับจากสแต็คเท่านั้นดูคำตอบอื่น ๆ เกี่ยวกับวิธีการทั่วไปและข้อสมมติฐานของคุณ


โปรดทราบว่าเมื่อถึงเวลาที่main()เรียกว่ารูทีนการเริ่มต้นอาจมีการปรับเปลี่ยนหน่วยความจำที่ส่งคืนโดยmalloc()- โดยเฉพาะอย่างยิ่งหากมีการลิงก์ไลบรารี C ++ เข้าด้วยกันสมมติว่า "ฮีป" ถูกเตรียมใช้งานกับอะไรก็ตาม
Andrew Henle

คำตอบของคุณพร้อมกับ Mosvy ก็คือการตั้งคำถามของฉัน น่าเสียดายที่ระบบอนุญาตให้ฉันยอมรับเพียงหนึ่งในสอง ฉันจะยอมรับทั้งสองอย่าง
บาท

18

ในทั้งสองกรณีคุณจะได้รับหน่วยความจำที่ไม่ได้กำหนดค่าเริ่มต้นและคุณไม่สามารถทำการสันนิษฐานเกี่ยวกับเนื้อหาได้

เมื่อระบบปฏิบัติการมีการแบ่งปันหน้าใหม่ให้กับกระบวนการของคุณ (ไม่ว่าจะเป็นของสแต็กหรือเวทีที่ใช้malloc()) จะรับประกันว่าจะไม่เปิดเผยข้อมูลจากกระบวนการอื่น วิธีปกติในการตรวจสอบให้แน่ใจว่าเติมเต็มด้วยศูนย์ (แต่ก็ใช้ได้เหมือนกันกับการเขียนทับสิ่งอื่นรวมถึงมูลค่าของหน้า/dev/urandom- ในความเป็นจริงmalloc()การใช้งานการแก้ไขข้อบกพร่องบางอย่างเขียนรูปแบบที่ไม่เป็นศูนย์

หากmalloc()สามารถตอบสนองคำขอจากหน่วยความจำที่ใช้แล้วและนำออกใช้โดยกระบวนการนี้เนื้อหาของมันจะไม่ถูกล้างออก (อันที่จริงแล้วการล้างจะไม่เกี่ยวข้องกับอะไรmalloc()และไม่สามารถทำได้ - ต้องเกิดขึ้นก่อนที่หน่วยความจำจะถูกแมป พื้นที่ที่อยู่ของคุณ) คุณอาจได้รับหน่วยความจำที่เคยเขียนโดยกระบวนการ / โปรแกรมของคุณ (เช่นก่อนหน้านี้main())

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

หากคุณต้องการที่จะเข้าใจสิ่งที่เกิดขึ้นในระดับระบบปฏิบัติการฉันขอแนะนำให้คุณหลีกเลี่ยงเลเยอร์ C Library และโต้ตอบโดยใช้การโทรของระบบเช่นbrk()และmmap()แทน


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

1
@thb บางทีฉันอาจจะไม่ชัดเจน - การใช้งานส่วนใหญ่malloc()ไม่ได้ทำอะไรกับหน่วยความจำที่พวกเขามอบให้คุณ - มันถูกใช้ก่อนหน้านี้หรือได้รับมอบหมายสดใหม่ ในการทดสอบของคุณคุณจะได้รับอย่างชัดเจน ในทำนองเดียวกันหน่วยความจำสแต็กจะถูกกำหนดให้กับกระบวนการของคุณในสถานะเคลียร์ แต่คุณไม่ได้ตรวจสอบมันมากพอที่จะเห็นส่วนที่กระบวนการของคุณยังไม่ได้สัมผัส หน่วยความจำสแต็กของคุณจะถูกล้างก่อนที่จะได้รับกระบวนการของคุณ
Toby Speight

2
@TobySpeight: brk และ sbrk ล้าสมัยโดย mmap pubs.opengroup.org/onlinepubs/7908799/xsh/brk.htmlพูดว่า LEGACY อยู่ด้านบนสุด
Joshua

2
หากคุณต้องการใช้หน่วยความจำที่เริ่มต้นใช้งานcallocอาจเป็นตัวเลือก (แทนmemset)
เครื่องหมาย

2
@thb และ Toby: ความจริงแล้วสนุก: หน้าใหม่จากเคอร์เนลมักจะถูกจัดสรรอย่างเฉื่อยชา สิ่งนี้เกิดขึ้นได้mmap(MAP_ANONYMOUS)เว้นแต่คุณจะใช้MAP_POPULATEเช่นกัน หวังว่าสแต็กหน้าใหม่จะได้รับการสนับสนุนจากเพจฟิสิคัลที่สดใหม่และการต่อสาย (แมปในตารางหน้าฮาร์ดแวร์รวมถึงรายการตัวชี้ / ความยาวของเคอร์เนลของการแมป) เมื่อมีการเติบโตเนื่องจากโดยปกติแล้ว . แต่ใช่เคอร์เนลจะต้องหลีกเลี่ยงการรั่วไหลของข้อมูลอย่างใดและการ zeroing นั้นถูกที่สุดและมีประโยชน์มากที่สุด
ปีเตอร์

9

หลักฐานของคุณผิด

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

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

สิ่งนี้ทำให้มั่นใจได้ว่าคุณจะเห็นเฉพาะศูนย์หรือขยะของคุณเองดังนั้นความลับจึงมั่นใจได้ว่าทั้งฮีปและสแต็คนั้น 'ปลอดภัย' แม้ว่าจะไม่จำเป็นต้องเริ่มต้นก็ตาม (ศูนย์ -)

คุณกำลังอ่านค่าการวัดของคุณมากเกินไป


1
ตอนนี้ส่วนอัปเดตของคำถามจะอ้างอิงคำตอบส่องสว่างของคุณอย่างชัดเจน
บาท
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.