דליפת זיכרון היא לא זיכרון שנעלם. זה זיכרון שעדיין שלכם, עדיין שמור, ושאיבדתם את היכולת להחזיר אותו, כי המצביע האחרון אליו נעלם. שום דבר לא קורס. התוכנית ממשיכה לרוץ, קצת יותר כבדה בכל פעם, עד שבסוף משהו נכשל במקום שנראה לא קשור.
העמוד הזה מכסה איך דליפות נוצרות, את המשמעת שמונעת את רובן, ואת שני הכלים שמוצאים את השאר.
איך נראית דליפה
כל איטרציה דורסת את block עם מצביע חדש. הבלוק הקודם עדיין מוקצה; אף משתנה לא מחזיק את הכתובת שלו; אי אפשר לשחרר אותו לעולם. שלוש איטרציות מאבדות שנים עשר קילובייט. שרת שעושה את זה פעם בכל בקשה מאבד את הזיכרון לתמיד, בכל קצב שבו הבקשות מגיעות.
התיקון הוא שורה אחת, free(block); בסוף הגוף, אבל לזהות איפה היא צריכה להיות זו המיומנות האמיתית.
איך דליפות קורות
1. המצביע האבוד
כל השמה למצביע שעדיין מחזיק את ההפניה היחידה לבלוק חי מדליפה אותו.
char *name = malloc(32);
name = malloc(64); /* 32 הבתים הראשונים כבר לא נגישים */
הלולאה למעלה היא אותו באג בתחפושת של לולאה. כך גם השמה מחדש לשדה של struct, וכך גם הקיצור של realloc מהעמוד על calloc ו-realloc:
p = realloc(p, n); /* בכישלון: p הופך ל-NULL, הבלוק הישן נשאר יתום */
2. היציאה המוקדמת
כל מסלול החוצה מפונקציה חייב לשחרר את מה שהפונקציה כבר לקחה. זה שנשכח הוא תמיד מסלול שגיאה.
המסלול השמח נכון ומסלול השגיאה דולף, ולכן זה שורד בדיקות: הענף שנכשל כמעט אף פעם לא רץ בזמן הפיתוח. התיקון הוא אזור ניקוי אחד שכל מסלול קופץ אליו:
זה השימוש היחיד ב-goto שמתכנתי C מנוסים ממליצים עליו באופן פעיל. זה עובד כי כל מצביע מתחיל ב-NULL ו-free(NULL) לא עושה כלום, כך שבלוק יציאה אחד נכון לא משנה עד כמה רחוק הפונקציה הגיעה.
3. בעלות לא ברורה
הדליפות העדינות ביותר הן בכלל לא שגיאות קוד: אלה שתי פונקציות שלא מסכימות על מי התפקיד.
char *build_message(void); /* האם הקוראת משחררת את זה? */
void store(char *text); /* האם store לוקחת בעלות? */
אם build_message מחזירה זיכרון מוקצה ו-store מעתיקה אותו, הקוראת חייבת לשחרר. אם store שומרת את המצביע, הקוראת אסור לה לשחרר. שום דבר בקוד לא אומר מה נכון, כך שאחת משתי ההנחות נעשית פעמיים, ומקבלים או דליפה או double free.
התרופה היא מוסכמה, שנכתבת בהערה ליד כל פונקציה שמקצה:
/* מחזירה מחרוזת חדשה שהוקצתה; הקוראת חייבת לשחרר אותה. */
char *build_message(void);
/* לוקחת בעלות על 'text'; היא תשוחרר על ידי store_free(). */
void store(char *text);
כתבו את הכלל ליד הפונקציה, לא במסמך עיצוב. זה ההרגל בעל הערך הגבוה ביותר בניהול זיכרון ב-C.
משמעת הבעלות
ארבעה כללים מכסים כמעט הכול:
- לכל הקצאה יש בעלים אחד בדיוק: קטע קוד אחד שאחראי לשחרר אותה.
- צמדו לכל פונקציה שמקצה פונקציה שמשחררת.
vec_init/vec_free,config_load/config_free. הסימטריה הופכת קריאה חסרה לגלויה. - שחררו באותה שכבה שהקצתה, אלא אם ההערה של הפונקציה מעבירה בעלות במפורש.
- הציבו
NULLבמצביע אחרי ששחררתם אותו, כך ששימוש מקרי מאוחר יותר יקרוס במקום התקלה במקום להשחית את ה-heap בשקט.
מציאת דליפות: valgrind
ב-Linux, valgrind לא דורש קומפילציה מחדש, אם כי סמלי debug הופכים את הדוח לקריא:
gcc -g -O0 program.c -o program
valgrind --leak-check=full --show-leak-kinds=all ./program
עבור הלולאה הדולפת בראש העמוד, הדוח מסתיים במשהו כמו:
==12345== HEAP SUMMARY:
==12345== in use at exit: 12,000 bytes in 3 blocks
==12345== total heap usage: 3 allocs, 0 frees, 12,000 bytes allocated
==12345==
==12345== 12,000 bytes in 3 blocks are definitely lost in loss record 1 of 1
==12345== at 0x4C2FB0F: malloc (vg_replace_malloc.c:299)
==12345== by 0x108671: main (program.c:6)
==12345==
==12345== LEAK SUMMARY:
==12345== definitely lost: 12,000 bytes in 3 blocks
קראו אותו מלמטה. "definitely lost" פירושו שלא היה שום מצביע לבלוק ביציאה: דליפה אמיתית. ה-stack trace מציין את השורה של ה-malloc שיצר אותו, לא את השורה שבה הוא אבד, ובדרך כלל זה מספיק כדי למצוא את ה-free החסר.
מופיעות עוד שתי קטגוריות:
- indirectly lost: בלוקים שאפשר להגיע אליהם רק דרך בלוק שבעצמו אבד, כמו האיברים של רשימה מקושרת שדלפה. תקנו את ה-"definitely lost" והם ייעלמו.
- still reachable: מוקצים ביציאה אבל עם מצביע חי, בדרך כלל cache גלובלי. לא דליפה במובן המסוכן, אבל שווה לשחרר אותם כדי שהדוח יישאר ריק.
valgrind תופס גם קריאות של זיכרון לא מאותחל וכתיבות מעבר לסוף של בלוק, ולעתים קרובות כך מגלים את הבאג שמאחורי הדליפה.
מציאת דליפות: AddressSanitizer
AddressSanitizer מובנה ב-GCC וב-Clang, רץ הרבה יותר מהר מ-valgrind ועובד במקומות שבהם valgrind לא עובד (כולל macOS עדכני):
gcc -g -fsanitize=address -fno-omit-frame-pointer program.c -o program
./program
דוח הדליפות מודפס אוטומטית ביציאה:
=================================================================
==12345==ERROR: LeakSanitizer: detected memory leaks
Direct leak of 12000 byte(s) in 3 object(s) allocated from:
#0 0x7f... in malloc
#1 0x1086... in main program.c:6
SUMMARY: AddressSanitizer: 12000 byte(s) leaked in 3 allocation(s).
ASan גם הופך use-after-free ו-heap buffer overflow לעצירות מיידיות עם תיאור ברור, במקום השחתה מסתורית מאוחר יותר. בנו את הרצות הבדיקה איתו; בנו גרסאות release בלעדיו, כי הוא עולה בזיכרון ובמהירות.
אם זיהוי הדליפות לא מופעל בפלטפורמה שלכם, הגדירו ASAN_OPTIONS=detect_leaks=1 בסביבה לפני ההרצה.
תיקון תוכנית דולפת צעד אחר צעד
הנה תוכנית קטנה עם שלוש דליפות נפרדות:
valgrind מדווח על שלוש רשומות "definitely lost" עם שלושה מספרי שורות שונים. מתוקנות אחת אחת:
תיקון 1 הוא הערת הבעלות שהפכה למציאות: shout מקצה, main משחררת. תיקון 2 מסיר את ההקצאה הכפולה לגמרי במקום לשחרר את הראשונה: הקוד הפשוט יותר הוא גם הקוד הנכון. תיקון 3 מוסיף את ה-free החסר ביציאה המוקדמת; כשיש יותר הקצאות, התווית היחידה cleanup: שהוצגה קודם מתרחבת טוב יותר מחזרה על ה-free.
הרגלים שמונעים דליפות
- כתבו את ה-
freeמיד אחרי שכתבתם את ה-malloc, ואז מלאו את הקוד שביניהם. - תנו לכל פונקציה שמקצה פונקציית שחרור תואמת.
- ציינו בעלות בהערה על כל פונקציה שמחזירה או מקבלת מצביע שהיא הקצתה.
- השתמשו בבלוק יציאה אחד
cleanup:בפונקציות שמחזיקות כמה הקצאות. - הריצו את הבדיקות שלכם תחת
-fsanitize=addressכדבר שבשגרה, לא רק כשמשהו נראה לא בסדר. - התייחסו ל-"definitely lost: 0 bytes" כחלק מהרצת בדיקות שעוברת.
שאלות נפוצות
מה זו דליפת זיכרון ב-C?
זיכרון שהקציתם עם malloc ושאתם כבר לא יכולים לשחרר, כי שום דבר בתוכנית כבר לא מצביע עליו. הבלוק נשאר שמור לכל אורך חיי התהליך. זו לא קריסה: התוכנית ממשיכה לעבוד, רק משתמשת ביותר זיכרון בכל סבב עד שבסוף הוא נגמר.
איך מוצאים דליפות זיכרון ב-C?
הריצו את התוכנית תחת valgrind: valgrind --leak-check=full ./program. הוא מדווח על כל בלוק שעדיין מוקצה ביציאה, עם ה-stack trace של ה-malloc שיצר אותו. ב-macOS או במקום שבו valgrind לא זמין, קמפלו עם -fsanitize=address ואותו דוח יודפס ביציאה.
מה גורם לדליפות זיכרון ב-C?
שלושה דפוסים מכסים כמעט את כולן: דריסה של המצביע היחיד לבלוק (כולל p = realloc(p, n) בכישלון), יציאה מוקדמת מפונקציה שכבר הקצתה, ובעלות לא ברורה: שתי פונקציות שכל אחת מהן מניחה שהשנייה משחררת, כך שאף אחת לא משחררת.
האם דליפות זיכרון חשובות אם התוכנית יוצאת בכל מקרה?
בתוכנית שרצה פעם אחת ויוצאת, מערכת ההפעלה מחזירה הכול, כך שההשפעה המעשית אפסית. זה חשוב בכל דבר שרץ לאורך זמן: שרת, לולאת משחק, daemon, שבהם דליפה בכל בקשה גדלה בלי גבול. שחררו בעקביות בכל זאת: דוח דליפות מלא ברעש מסתיר את הדליפות שבאמת חשובות.