คอมไพเลอร์จะเริ่มต้นหน่วยความจำเป็น 0xCD, 0xDD และอื่น ๆ บน malloc / free / new / delete เมื่อใดและทำไม


129

ฉันรู้ว่าบางครั้งคอมไพเลอร์จะเริ่มต้นหน่วยความจำด้วยรูปแบบบางอย่างเช่น0xCDและ0xDD. สิ่งที่ฉันอยากรู้คือจะเกิดขึ้นเมื่อไหร่และทำไม

เมื่อไหร่

สิ่งนี้เฉพาะสำหรับคอมไพเลอร์ที่ใช้หรือไม่?

ทำmalloc/newและfree/deleteทำงานในลักษณะเดียวกันในเรื่องนี้หรือไม่?

เป็นแพลตฟอร์มเฉพาะหรือไม่?

มันจะเกิดขึ้นบนระบบปฏิบัติการอื่นเช่นLinuxหรือVxWorks?

ทำไม

ความเข้าใจของฉันคือสิ่งนี้เกิดขึ้นเฉพาะในWin32การกำหนดค่าการดีบักและใช้เพื่อตรวจจับการใช้หน่วยความจำมากเกินไปและเพื่อช่วยคอมไพลเลอร์ตรวจจับข้อยกเว้น

คุณช่วยยกตัวอย่างที่ใช้ได้จริงว่าการเริ่มต้นนี้มีประโยชน์อย่างไร

ฉันจำได้ว่าอ่านบางอย่าง (อาจอยู่ใน Code Complete 2) โดยบอกว่าเป็นการดีที่จะเริ่มต้นหน่วยความจำเป็นรูปแบบที่รู้จักเมื่อจัดสรรและรูปแบบบางอย่างจะทริกเกอร์การขัดจังหวะWin32ซึ่งจะส่งผลให้มีข้อยกเว้นที่แสดงในดีบักเกอร์

พกพาสะดวกขนาดนี้?

คำตอบ:


191

สรุปโดยย่อเกี่ยวกับสิ่งที่คอมไพเลอร์ของ Microsoft ใช้สำหรับบิตต่างๆของหน่วยความจำที่ไม่เป็นเจ้าของ / ไม่ได้กำหนดค่าเริ่มต้นเมื่อคอมไพล์สำหรับโหมดดีบัก (การสนับสนุนอาจแตกต่างกันไปตามเวอร์ชันของคอมไพเลอร์):

Value     Name           Description 
------   --------        -------------------------
0xCD     Clean Memory    Allocated memory via malloc or new but never 
                         written by the application. 

0xDD     Dead Memory     Memory that has been released with delete or free. 
                         It is used to detect writing through dangling pointers. 

0xED or  Aligned Fence   'No man's land' for aligned allocations. Using a 
0xBD                     different value here than 0xFD allows the runtime
                         to detect not only writing outside the allocation,
                         but to also identify mixing alignment-specific
                         allocation/deallocation routines with the regular
                         ones.

0xFD     Fence Memory    Also known as "no mans land." This is used to wrap 
                         the allocated memory (surrounding it with a fence) 
                         and is used to detect indexing arrays out of 
                         bounds or other accesses (especially writes) past
                         the end (or start) of an allocated block.

0xFD or  Buffer slack    Used to fill slack space in some memory buffers 
0xFE                     (unused parts of `std::string` or the user buffer 
                         passed to `fread()`). 0xFD is used in VS 2005 (maybe 
                         some prior versions, too), 0xFE is used in VS 2008 
                         and later.

0xCC                     When the code is compiled with the /GZ option,
                         uninitialized variables are automatically assigned 
                         to this value (at byte level). 


// the following magic values are done by the OS, not the C runtime:

0xAB  (Allocated Block?) Memory allocated by LocalAlloc(). 

0xBAADF00D Bad Food      Memory allocated by LocalAlloc() with LMEM_FIXED,but 
                         not yet written to. 

0xFEEEFEEE               OS fill heap memory, which was marked for usage, 
                         but wasn't allocated by HeapAlloc() or LocalAlloc(). 
                         Or that memory just has been freed by HeapFree(). 

ข้อจำกัดความรับผิดชอบ: ตารางมาจากบันทึกบางส่วนที่ฉันโกหก - อาจไม่ถูกต้อง 100% (หรือสอดคล้องกัน)

ค่าเหล่านี้จำนวนมากถูกกำหนดใน vc / crt / src / dbgheap.c:

/*
 * The following values are non-zero, constant, odd, large, and atypical
 *      Non-zero values help find bugs assuming zero filled data.
 *      Constant values are good, so that memory filling is deterministic
 *          (to help make bugs reproducible).  Of course, it is bad if
 *          the constant filling of weird values masks a bug.
 *      Mathematically odd numbers are good for finding bugs assuming a cleared
 *          lower bit.
 *      Large numbers (byte values at least) are less typical and are good
 *          at finding bad addresses.
 *      Atypical values (i.e. not too often) are good since they typically
 *          cause early detection in code.
 *      For the case of no man's land and free blocks, if you store to any
 *          of these locations, the memory integrity checker will detect it.
 *
 *      _bAlignLandFill has been changed from 0xBD to 0xED, to ensure that
 *      4 bytes of that (0xEDEDEDED) would give an inaccessible address under 3gb.
 */

static unsigned char _bNoMansLandFill = 0xFD;   /* fill no-man's land with this */
static unsigned char _bAlignLandFill  = 0xED;   /* fill no-man's land for aligned routines */
static unsigned char _bDeadLandFill   = 0xDD;   /* fill free objects with this */
static unsigned char _bCleanLandFill  = 0xCD;   /* fill new objects with this */

นอกจากนี้ยังมีไม่กี่ครั้งที่รันไทม์การแก้ปัญหาจะเติมบัฟเฟอร์ (หรือบางส่วนของบัฟเฟอร์) มูลค่าที่รู้จักกันเช่น 'หย่อน' พื้นที่ในการstd::stringจัดสรร 's fread()หรือบัฟเฟอร์ที่ผ่านมา กรณีเหล่านี้ใช้ค่าที่ระบุชื่อ_SECURECRT_FILL_BUFFER_PATTERN(กำหนดในcrtdefs.h) ฉันไม่แน่ใจว่ามันถูกนำมาใช้เมื่อใด แต่มันอยู่ในรันไทม์การดีบักอย่างน้อย VS 2005 (VC ++ 8)

ในขั้นต้นค่าที่ใช้เติมบัฟเฟอร์เหล่านี้คือ0xFD- ค่าเดียวกับที่ใช้สำหรับที่ดินที่ไม่มีมนุษย์ อย่างไรก็ตามใน VS 2008 (VC ++ 9) ค่าถูกเปลี่ยนเป็น0xFE. fread()ฉันคิดว่าเพราะอาจจะมีสถานการณ์ที่ดำเนินการเติมจะวิ่งผ่านมาในตอนท้ายของบัฟเฟอร์ตัวอย่างเช่นถ้าโทรผ่านในขนาดบัฟเฟอร์ที่มีขนาดใหญ่เกินไปที่จะ ในกรณีนั้นค่า0xFDอาจไม่ทริกเกอร์การตรวจจับการบุกรุกนี้เนื่องจากหากขนาดของบัฟเฟอร์ใหญ่เกินไปเพียงอันเดียวค่าการเติมจะเหมือนกับมูลค่าที่ดินที่ไม่มีใครใช้ในการเริ่มต้นนกขมิ้นนั้น ไม่มีการเปลี่ยนแปลงในที่ดินของมนุษย์หมายความว่าจะไม่มีใครสังเกตเห็นการบุกรุก

ดังนั้นค่าการเติมจึงเปลี่ยนไปใน VS 2008 ดังนั้นกรณีดังกล่าวจะเปลี่ยนนกขมิ้นที่ไม่มีผู้ใดส่งผลให้รันไทม์ตรวจพบปัญหา

ดังที่คนอื่น ๆ ได้กล่าวไว้คุณสมบัติหลักประการหนึ่งของค่าเหล่านี้คือหากไม่มีการอ้างอิงตัวแปรตัวชี้ที่มีค่าใดค่าหนึ่งเหล่านี้จะทำให้เกิดการละเมิดการเข้าถึงเนื่องจากในการกำหนดค่า Windows 32 บิตมาตรฐานที่อยู่โหมดผู้ใช้ จะไม่สูงกว่า 0x7fffffff


1
ฉันไม่รู้ว่ามันอยู่บน MSDN - ฉันปะติดปะต่อจากที่นี่และที่นั่นหรือบางทีฉันอาจจะได้มาจากเว็บไซต์อื่น
Michael Burr

2
โอ้ใช่ - บางส่วนมาจากแหล่ง CRT ใน DbgHeap.c
Michael Burr

บางส่วนอยู่ใน MSDN ( msdn.microsoft.com/en-us/library/bebs9zyz.aspx ) แต่ไม่ใช่ทั้งหมด รายการที่ดี
ฌอน

3
@seane - FYI ลิงค์ของคุณดูเหมือนจะตาย ข้อความใหม่ (ปรับปรุงข้อความแล้ว) มีอยู่ที่นี่: msdn.microsoft.com/en-us/library/974tc9t1.aspx
Simon Mourier

ชื่อของบล็อกเหล่านี้คืออะไร? มันเป็นอุปสรรคความจำเมมบาร์รั้วหน่วยความจำหรือคำแนะนำรั้ว ( en.wikipedia.org/wiki/Memory_barrier )
kr85

36

คุณสมบัติที่ดีอย่างหนึ่งเกี่ยวกับค่าเติม 0xCCCCCCCC คือในแอสเซมบลี x86 opcode 0xCC คือopcode int3ซึ่งเป็นตัวขัดจังหวะของซอฟต์แวร์ ดังนั้นหากคุณเคยพยายามรันโค้ดในหน่วยความจำที่ไม่ได้กำหนดค่าเริ่มต้นซึ่งเต็มไปด้วยค่าการเติมนั้นคุณจะเข้าสู่จุดพักทันทีและระบบปฏิบัติการจะให้คุณแนบดีบักเกอร์ (หรือฆ่ากระบวนการ)


6
และ 0xCD คือintคำสั่งดังนั้นการรัน 0xCD 0xCD จะสร้าง an int CDซึ่งจะดักจับด้วย
Tad Marshall

2
ในโลกปัจจุบัน Data Execution Prevention ไม่อนุญาตให้ CPU ดึงคำสั่งจากฮีปด้วยซ้ำ คำตอบนี้ล้าสมัยตั้งแต่ XP SP2
MSalters

2
@MSalters: ใช่เป็นความจริงที่โดยค่าเริ่มต้นหน่วยความจำที่จัดสรรใหม่จะไม่สามารถเรียกใช้งานได้ แต่บางคนสามารถใช้VirtualProtect()หรือmprotect()ทำให้หน่วยความจำทำงานได้อย่างง่ายดาย
Adam Rosenfield

คุณไม่สามารถรันโค้ดจากบล็อกข้อมูลได้ เคย. เดาอีกครั้ง.
แดน

9

เป็นคอมไพเลอร์และระบบปฏิบัติการเฉพาะ Visual studio ตั้งค่าหน่วยความจำประเภทต่างๆเป็นค่าที่แตกต่างกันเพื่อให้ในดีบักเกอร์คุณสามารถดูได้อย่างง่ายดายว่าคุณมีหน่วยความจำมากเกินไปหรือไม่อาร์เรย์คงที่หรือวัตถุที่ไม่ได้เริ่มต้น ใครบางคนจะโพสต์รายละเอียดในขณะที่ฉันกำลัง googling พวกเขา ...

http://msdn.microsoft.com/en-us/library/974tc9t1.aspx


ฉันเดาว่ามันใช้เพื่อตรวจสอบว่าคุณลืมยกเลิกสตริงของคุณอย่างถูกต้องด้วยหรือไม่ (เนื่องจากมีการพิมพ์ 0xCD หรือ 0xDD)
strager

0xCC = ตัวแปร local (stack) ที่ไม่ได้กำหนดค่าเริ่มต้น 0xCD = คลาสที่ไม่ได้กำหนดค่าเริ่มต้น (heap?) ตัวแปร 0xDD = ตัวแปรที่ถูกลบ
FryGuy

@FryGuy มีเหตุผลในทางปฏิบัติซึ่งคำสั่ง (บางแห่ง) ค่าเหล่านี้ที่ผมอธิบายที่นี่
Glenn Slayden

4

ไม่ใช่ระบบปฏิบัติการ - เป็นคอมไพเลอร์ คุณสามารถปรับเปลี่ยนพฤติกรรมได้เช่นกัน - ดูด้านล่างของโพสต์นี้

Microsoft Visual Studio สร้าง (ในโหมด Debug) ไบนารีที่เติมหน่วยความจำสแตกล่วงหน้าด้วย 0xCC นอกจากนี้ยังแทรกช่องว่างระหว่างทุกสแต็กเฟรมเพื่อตรวจจับการล้นของบัฟเฟอร์ ตัวอย่างง่ายๆของสิ่งนี้มีประโยชน์อยู่ที่นี่ (ในทางปฏิบัติ Visual Studio จะพบปัญหานี้และออกคำเตือน):

...
   bool error; // uninitialised value
   if(something)
   {
      error = true;
   }
   return error;

หาก Visual Studio ไม่ได้กำหนดค่าตัวแปรล่วงหน้าให้เป็นค่าที่ทราบข้อบกพร่องนี้อาจหาได้ยาก ด้วยตัวแปรที่กำหนดค่าเริ่มต้น (หรือมากกว่าหน่วยความจำสแต็กที่กำหนดค่าเริ่มต้นล่วงหน้า) ปัญหาจะเกิดขึ้นซ้ำได้ในทุกครั้งที่รัน

อย่างไรก็ตามมีปัญหาเล็กน้อย ค่าที่ Visual Studio ใช้คือ TRUE - ทุกอย่างยกเว้น 0 จะเป็น เป็นไปได้มากว่าเมื่อคุณรันโค้ดของคุณในโหมดรีลีสตัวแปรที่แปลงเป็นหน่วยอาจถูกจัดสรรให้กับหน่วยความจำสแต็กที่มีค่า 0 ซึ่งหมายความว่าคุณสามารถมีบั๊กตัวแปรแบบหน่วยซึ่งจะปรากฏในโหมดรีลีสเท่านั้น

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


1
VS ไม่ออกคำเตือนเมื่อใช้ค่าก่อนที่จะเริ่มต้นเช่น GCC?
strager

3
ใช่ แต่ไม่เสมอไปเนื่องจากขึ้นอยู่กับการวิเคราะห์แบบคงที่ ดังนั้นจึงค่อนข้างง่ายที่จะสับสนกับเลขคณิตของตัวชี้
Airsource Ltd

3
"ไม่ใช่ระบบปฏิบัติการ แต่เป็นคอมไพเลอร์" จริงๆแล้วมันไม่ใช่คอมไพเลอร์ แต่เป็นไลบรารีรันไทม์
Adrian McCarthy

เมื่อการแก้จุดบกพร่อง, Visual Studio ดีบักจะแสดงค่าของบูลในกรณีที่ไม่ได้ 0 หรือ 1 กับสิ่งที่ต้องการที่แท้จริง (204) ดังนั้นจึงค่อนข้างง่ายที่จะเห็นข้อผิดพลาดประเภทนั้นหากคุณติดตามโค้ด
Phil1970

4

สิ่งนี้เฉพาะสำหรับคอมไพเลอร์ที่ใช้หรือไม่?

จริงๆแล้วมันมักจะเป็นคุณลักษณะของไลบรารีรันไทม์ (เช่นไลบรารีรันไทม์ C) รันไทม์มักจะมีความสัมพันธ์อย่างมากกับคอมไพเลอร์ แต่มีชุดค่าผสมบางอย่างที่คุณสามารถสลับได้

ฉันเชื่อว่าบน Windows ฮีปดีบัก (HeapAlloc ฯลฯ ) ยังใช้รูปแบบการเติมพิเศษซึ่งแตกต่างจากรูปแบบที่มาจาก malloc และการใช้งานฟรีในไลบรารีรันไทม์ดีบัก C ดังนั้นอาจเป็นคุณลักษณะของระบบปฏิบัติการ แต่โดยส่วนใหญ่แล้วจะเป็นเพียงไลบรารีรันไทม์ของภาษา

malloc / ใหม่และฟรี / ลบทำงานในลักษณะเดียวกันกับเรื่องนี้หรือไม่?

ส่วนการจัดการหน่วยความจำของใหม่และลบมักจะใช้กับ malloc และฟรีดังนั้นหน่วยความจำที่จัดสรรใหม่และลบมักจะมีคุณสมบัติเหมือนกัน

เป็นแพลตฟอร์มเฉพาะหรือไม่?

รายละเอียดเป็นเฉพาะรันไทม์ ค่าจริงที่ใช้มักจะถูกเลือกเพื่อไม่เพียง แต่ดูผิดปกติและชัดเจนเมื่อดูการถ่ายโอนข้อมูลฐานสิบหก แต่ได้รับการออกแบบให้มีคุณสมบัติบางอย่างที่อาจใช้ประโยชน์จากคุณสมบัติของโปรเซสเซอร์ ตัวอย่างเช่นมักใช้ค่าคี่เนื่องจากอาจทำให้เกิดข้อผิดพลาดในการจัดตำแหน่ง ใช้ค่าขนาดใหญ่ (ตรงข้ามกับ 0) เนื่องจากทำให้เกิดความล่าช้าอย่างน่าประหลาดใจหากคุณวนลูปไปยังตัวนับที่ไม่ได้เริ่มต้น บน x86 0xCC คือint 3คำสั่งดังนั้นหากคุณเรียกใช้หน่วยความจำที่ไม่ได้เริ่มต้นระบบจะดักจับ

จะเกิดขึ้นกับระบบปฏิบัติการอื่นเช่น Linux หรือ VxWorks หรือไม่?

ส่วนใหญ่ขึ้นอยู่กับไลบรารีรันไทม์ที่คุณใช้

คุณช่วยยกตัวอย่างที่ใช้ได้จริงว่าการเริ่มต้นนี้มีประโยชน์อย่างไร

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

ฉันจำได้ว่าอ่านบางอย่าง (อาจจะอยู่ใน Code Complete 2) ว่าเป็นการดีที่จะเริ่มต้นหน่วยความจำเป็นรูปแบบที่รู้จักเมื่อจัดสรรและรูปแบบบางอย่างจะทริกเกอร์การขัดจังหวะใน Win32 ซึ่งจะส่งผลให้มีข้อยกเว้นที่แสดงในดีบักเกอร์

พกพาสะดวกขนาดนี้?

การเขียน Solid Code (และอาจจะเป็นCode Complete ) พูดถึงสิ่งที่ต้องพิจารณาเมื่อเลือกรูปแบบการเติม ฉันได้กล่าวถึงบางส่วนที่นี่และบทความ Wikipedia เกี่ยวกับMagic Number (การเขียนโปรแกรม)ก็สรุปไว้ด้วย เทคนิคบางอย่างขึ้นอยู่กับลักษณะเฉพาะของโปรเซสเซอร์ที่คุณใช้ (เช่นต้องมีการอ่านและเขียนที่สอดคล้องกันหรือไม่และค่าใดที่จับคู่กับคำแนะนำที่จะดักจับ) เทคนิคอื่น ๆ เช่นการใช้ค่าขนาดใหญ่และค่าผิดปกติที่โดดเด่นในการถ่ายโอนข้อมูลหน่วยความจำนั้นพกพาได้ง่ายกว่า



2

เหตุผลที่ชัดเจนสำหรับ "ทำไม" คือสมมติว่าคุณมีคลาสเช่นนี้:

class Foo
{
public:
    void SomeFunction()
    {
        cout << _obj->value << endl;
    }

private:
    SomeObject *_obj;
}

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

AFAIK สิ่งนี้ใช้ได้เฉพาะกับคอมไพเลอร์ Visual Studio เมื่ออยู่ในโหมดดีบัก (ตรงข้ามกับรีลีส)


คำอธิบายของคุณไม่เป็นไปตามเนื่องจากคุณจะได้รับการละเมิดการเข้าถึงที่พยายามอ่าน0x00000000ซึ่งจะมีประโยชน์เช่นเดียวกับที่อยู่ที่ไม่ถูกต้อง ดังที่ฉันได้ชี้ให้เห็นในความคิดเห็นอื่นในหน้านี้เหตุผลที่แท้จริงของ0xCD(และ0xCC) ก็คือพวกมันเป็นรหัส x86 ที่ตีความได้ซึ่งทำให้เกิดการขัดจังหวะซอฟต์แวร์และสิ่งนี้ช่วยให้สามารถกู้คืนอย่างสง่างามไปยังดีบักเกอร์ด้วยข้อผิดพลาดประเภทเดียวที่เฉพาะเจาะจงและหายาก กล่าวคือเมื่อ CPU พยายามเรียกใช้งานไบต์ในพื้นที่ที่ไม่ใช่รหัสโดยไม่ถูกต้อง นอกเหนือจากการใช้งานฟังก์ชันนี้ค่าการเติมเป็นเพียงคำแนะนำคำแนะนำตามที่คุณทราบ
Glenn Slayden

2

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

ไม่ใช่แค่หน่วยความจำเท่านั้นดีบักเกอร์จำนวนมากจะตั้งค่ารีจิสเตอร์เนื้อหาเป็นค่าเซนทิเนลเมื่อกระบวนการเริ่มต้น (AIX บางเวอร์ชันจะตั้งค่ารีจิสเตอร์บางตัว0xdeadbeefที่มีอารมณ์ขันเล็กน้อย)


1

คอมไพลเลอร์ IBM XLC มีอ็อพชัน "initauto" ที่จะกำหนดตัวแปรอัตโนมัติเป็นค่าที่คุณระบุ ฉันใช้สิ่งต่อไปนี้สำหรับบิวด์ดีบักของฉัน:

-Wc,'initauto(deadbeef,word)'

ถ้าฉันดูที่การจัดเก็บของตัวแปรที่ไม่ได้เริ่มต้นมันจะถูกตั้งค่าเป็น 0xdeadbeef

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