Memory leak กับโค้ดบรรทัดเดียว ที่หาอยู่สองวัน

มาบ่นเรื่อง Memory Leak ที่หายากสุดๆ ครับ

ก่อนอื่นเลย ผมเขียนโปรแกรม C++ และมีกฏว่า ระหว่าง test normal operation และ Graceful Exit ห้ามมี Memory Leak ขึ้นใน Output เด็ดขาด

ซึ่งมันก็ทำงานได้ดี แต่อยู่ๆ ก็เจอปัญหานี้

Detected Memory Leaks
{126241} normal block at 0x04279DC0, 8 bytes long.
Data: < l > 88 D8 6C 03 00 00 00 00
{126240} normal block at 0x0427A028, 8 bytes long.
Data: <l l > 6C D8 6C 03 00 00 00 00

จริงๆ มีเยอะกว่านี้ แต่ตัดมา ยกตัวอย่างเฉยๆ

ผมก็อุทาน เชี่ยอะไรวะ ใล่หาทั้งโปรแกรม ทั้งทำ CRT_DBG_ALLOC ส่วนอื่นๆ ขึ้น source line หมด แต่บรรทัดนี้กลับแปลก ไม่ขึ้น source line เลย แถมเลข block number / allocation number ดันเปลี่ยนไปเรื่อยๆ ทำให้ BreakAlloc เป็นไปไม่ได้

เลยใช้วิธี scope แคบลงมาเรื่อยๆ จนรู้ว่ามันมี function หนึ่งถ้ากดให้มันทำงาน จะ leak แน่นอน เลยโฟกัสแค่จุดนั้น และดึง code มาเทสในส่วน Test Form

ผลคือไม่มี Memory leak…..

Me: เชี่ยอะไรอีกวะเนี่ย สรุปใล่หาเพิ่มอีกราวๆ 7 ชั่วโมงแต่ก็ยังไม่เจออะไร

แต่พอทดสอบไปเรื่อยๆ เลยเจอ pattern leak ว่ามันมาจาก struct เพราะตอนสร้าง leak test ทุกแบบ pattern มันตรงกับตัวแปรประเภท struct พอดี และจำนวนจะเท่ากับ struct member ด้วย

เลยใล่ๆ นับ struct ทั้งโปรแกรม เลยเจอ candidate ที่น่าจะ leak อยู่สองตัว

เลยเพิ่ม member เข้าไปใน struct ด้วย concept ที่ว่า ถ้าจำนวน Block Leak เพิ่ม เช่น struct A เพิ่ม member ไป 1 ตัว / struct B เพิ่ม member ไปสองตัว
ถ้าจำนวน leak block ใหนเพิ่ม 1 หรือ 2 นั่นแหละผู้ร้าย!!

หลังจากเพิ่ม struct member ไป ผลคือ

{126241} normal block at 0x04279DC0, 8 bytes long.
Data: < l > 88 D8 6C 03 00 00 00 00
{126240} normal block at 0x0427A028, 8 bytes long.
Data: <l l > 6C D8 6C 03 00 00 00 00
{126239} normal block at 0x0427A0D0, 8 bytes long.
Data: <P l > 50 D8 6C 03 00 00 00 00
{126238} normal block at 0x0427A178, 8 bytes long.
Data: <4 l > 34 D8 6C 03 00 00 00 00

ชัดแล้ว struct B แน่นอน เพื่อความชัวร์เลยเพิ่ม member ไปอีก 1 ตัว > leak เพิ่มมาอีก 1 block จริงๆ

เลยไปใล่ๆ หาว่า struct นี้ใช้ตรงจุดใหนบ้าง พบว่ามันใช้งานอยู่จุดเดียว และ code เขียนแบบนี้

MyData tmpData;
ZeroMemory(&tmpData, sizeof(tmpData));

ชัดละ นี่แหละตัวการ เลยลบ ZeroMemory ออก > Leak ทั้งหมดที่เจอหายหมดทันที!

เกิดอะไรขึ้น

เดิมที struct MyData เขียนไว้แบบนี้

struct MyData
{
    wchar_t data1[256];
    wchar_t data2[256];
}
 
แต่ปัญหาคือพอ use case จริง data1 และ data2 กลับมีขนาดไม่คงที่ และมีบางเคสขนาดเกิน 256 และอาจยาวกว่านี้ ทางทีมเลยตัดสินใจเปลี่ยนเป็น
 
struct MyData
{
    std::wstring data1;
    std::wstring data2;
}
 
แต่ไม่มีใครไปสนใจ Code ZeroMemory ที่เขียนไว้ตั้งแต่ยุคที่ struct ยังเป็น wchar_t ผลคือ เกิด memory leak ขึ้นเงียบๆ เพราะ RAII ไม่ทำลายตัวแปรของ wstring ตอนออกจากโปรแกรม (ไปเคลียร์ state มันทิ้ง) กว่าจะรู้ตัวกัน ก็พัฒนาเพิ่มไปหลาย feature / หลาย commit แล้ว การมาใล่ย้อน commit เลยเสียเวลา เลยต้อง debug กันสดๆ นี่แหละ
 

Comments are closed.