איך לתקן באג בתכנה

מאוד כיף לשבת ולכתוב קוד באופן ספונטני מבלי לתכנן שוב דבר, כשמדובר בסקריפט קצר זה גם לא כזה קריטי. אבל כשהקוד שלנו עושה פעולה מורכבת, חשוב לשבת ולתכנן לפני המימוש. ההבדל בין תכנה טובה, לגרועה מסתכם בארכיטקטורה. אם אתם לא מבינים מה אתם צריכים לממש ומהי הצורה היעילה ביותר לעשות זאת, איך תכתבו את הקוד? כן אתם יכולים לכתוב קוד "שיעשה את העבודה", אבל מה יקרה כשתצטרכו להתמודד עם בעיות שלא חשבתם עליהם? מה יקרה כשבסיס המשתמשים שלכם יגדל, והקוד שלכם לא יעמוד בעומס? מכירים את המשפט "כשנגיע לגשר נחצה אותו"? אז זה מה שאתם לא רוצים להגיד לעצמכם בפיתוח תכנה, מפני שבעיה פותרים פעם אחת. אתם יכולים לחבוש יד שבורה, אך אם לא קיבעתם אותה יהיו לכך השלכות. בעיות פותרים מהשורש.

תמיד תתכננו את האלגוריתם לפני שאתם כותבים את הקוד. ואחרי שאתם עושים את זה, תנסו לשבור את הקוד שלכם, מה יקרה אם האלגוריתם יקבל 1000 נתונים, מיליון או מיליארד? גם אם התכנית שלכם לא תצטרך להתמודד עם כמות עצומה של מידע, חשיבה מעמיקה (באופן תיאורטי) על הקוד שלכם תגרום לכם לכתוב קוד עם פחות באגים, עמיד יותר ושאתם אשכרה יודעים מה הוא עושה. הנקודה האחרונה חשובה מאוד. למדתי לתכנת בגיל מאוד צעיר, ואני זוכר שכהייתי נתקל בבאגים הצורה שבה היתי פותר אותם היתה מאוד לא יעילה: בתקופה ההיא כל מה שרציתי זה "לכתוב קוד שיעשה משהו" - לא באמת היה אכפת לי אם המימוש היה נכון. אם משהו לא היה עובד לי, הייתי משנה ערכים של משתנים ומשחק עם הקוד עד שהוא היה עובד באורח נס. לא באמת הייתי מבין למה הבאג תוקן אבל היי! לפחות הקוד עושה מה שאני רוצה.

אחת מן הטעויות הכי קריטיות שאתם יכולים לעשות כאשר אתם נתקלים בבאג היא לשכתב את הקוד מחדש. כי אז לא באמת למדתם כלום. יש באגים שיקח לכם הרבה מאוד זמן לתקן ולהבין למה הם קרו. כשמדובר בבאג עקשן, קל מאוד להתייאש ולטרוף את הקלפים אך זה לא הפתרון. ברוב המקרים הבאג יווצר בגלל משהו קטן, לפעמים זה יקרה בגלל שאתם לא מבינים את השפה או מערכת ההפעלה שאיתה אתם עובדים, מהסיבה הזאת חשוב לקרוא את המסמכים הרלוונטים, לעצור ולחשוב.

החודש נתקלתי בבאג מעניין בקוד שכתבתי. שדה במבנה נתונים מסוים היה משתנה לערכים לא צפויים במהלך ריצת התכנית. הבאג היה קורה רק במצבים מאוד ספציפים, ההנחות הראשונות שלי היו:

כשאני מנסה להבין למה קוד מסוים לא עובד כשורה, אני מתחיל בלהסיר קוד לא רלוונטי עד שנשאר לי שלד של התכנית המקורית. קוד שלא קשור לבעיה, מוחלף ב - stubs. וכך אני בודק שכבה אחר שכבה עד שאני מגיע לבעיית השורש. אם יש צורך אני גם משתמש ב - gdb ו - valgrind. הדברים הראשונים שבדקתי בקוד שלי, היו: איפה אני משנה את מבנה הנתונים הבעייתי, האם אני דורס את המחסנית? האם יש לי memory leak? כל הבדיקות הנ"ל העלו חרס. אם אני לא משחית את הזכרון, למה השדה במבנה הנתונים מקבל ערך לא צפוי במהלך ריצת התכנית? אני אחסוך מכם את הפרטים הלא מעניינים, ויגיע לעיקר: בסופו של דבר הבנתי מה קרה. הבעיה היתה נעוצה באופן ש - malloc(3) ממומשת במערכת ההפעלה OpenBSD.

malloc(3)

כאשר זכרון מוקצה ומשוחרר עם free(3) הוא לא באמת משוחרר. כלומר הוא מסומן כמשוחרר על ידי malloc(3) ומוחזר ל - pool של זכרון חופשי, כך שבפועל אנחנו לא מבקשים מהקרנל לשחרר את הזכרון והוא מוחזק לשימוש עתידי. הסיבה לכך מאוד פשוטה: הקצאת זכרון כרוכה ב - context switch שנחשב לתהליך יקר, ולכן עדיף פשוט להשאיר זכרון שהוקצה ב - pool ולהקצות אותו מחדש במידת הצורך, במקום לבקש עוד זכרון מהקרנל. זוהי טכניקה שמהווה בסיס ל - slab allocation.

ב - OpenBSD, נכתבים לזכרון המשוחרר "junk bytes" (0xdb בהקצאה, ו - 0xdf בשחרור זכרון) על מנת להבטיח שהנתונים הקודמים שהוקצו, לא יהיו זמינים בהקצאה הבאה. ולכן כל פעם שהייתי קורא לפונקציה free הזכרון שהייתי מקבל בהקצאה הבאה הכיל "junk bytes", ובגלל שלא אתחלתי את מבנה הנתונים לפני השימוש הייתי מקבל התנהגות בלתי צפויה.

מסקנות

אני אוהב שצצות לי בעיות בקוד, כי אני תמיד לומד מזה משהו. יכלתי לאתחל את מבנה הנתונים בהתחלה ולפתור את הבאג מבלי לבזבז זמן, אבל רציתי לדעת למה אני צריך לאתחל אותו, אין צורך לאפס מבנה נתונים כל פעם שאנחנו מקצים זכרון (במיוחד במערכות שדורשות ביצועים גבוהים).

אם הייתי קורא את ה - man page של malloc(3) במלואו, הייתי פותר את הבאג תוך כמה דקות אז אם אתם גם נתקלים בבאג מוזר - RTF. זה יחסוך לכם הרבה מאוד זמן.