מלכודת ה-R-M-W

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

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

  1. קורא את הערך של x מזיכרון ה-SRAM לרגיסטר פנימי פנוי
  2. מגדיל את הערך של הרגיסטר ב-1
  3. כותב את התוצאה מהרגיסטר בחזרה לזיכרון ה-SRAM

שלושת השלבים האלה, של קריאה, פעולה וכתיבה-בחזרה, מוכרים בשם הרשמי Read-Modify-Write (ובקיצור R-M-W). אי אפשר לברוח מהם: הם נחוצים בגלל הארכיטקטורה הבסיסית של המעבד. לפעמים, בהתאם לנסיבות, הקומפיילר מסוגל לבצע אופטימיזציות שחוסכות כמה פעולות, אך בגדול ככה זה עובד. הרגיסטרים האלה מוגדרים בתוך המעבד עצמו, לא בזיכרון ה-SRAM, ואם אנחנו לא מתכנתים באסמבלי אז בדרך כלל אין לנו שום גישה אליהם – ולמען האמת, עדיף גם שלא ניגע בהם. השיטה הזו עובדת היטב… עד שאנחנו מתחילים לעשות דברים מורכבים עם החומרה, למשל לעבוד עם יותר מפסיקה (interrupt) אחת.

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

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

HWIntReg &= ~1;

היא אכן תנקה את הדגל הרלוונטי: הפקודות בשפת מכונה שהקומפיילר מפיק קוראות את הערך 001 מרגיסטר הפסיקות לרגיסטר פנימי, עושות לפנימי AND עם הערך 110 – התוצאה היא 000 – וכותבות את התשובה הזו לרגיסטר הפסיקות. אבל אם איפשהו במשך התהליך הזה קרה אירוע חיצוני אחר (נניח, הגיע תו ב-UART) והערך של רגיסטר הפסיקות הפך ל-011, תוצאת החישוב שכבר התחיל לא תשתנה בהתאם: רגיסטר הפסיקות יקבל את הערך המחושב 000 – ואיבדנו את האינדיקציה לאירוע החדש!

ה-R-M-W קורה מהר: ברוב התרחישים זה משהו שיתחיל וייגמר תוך מפחות ממיליונית שנייה. הסיכוי שאירוע חיצוני רלוונטי יתרחש במסגרת הזמן הזו הוא בדרך כלל נמוך מאוד, ולכן הבאג יתבטא רק לעתים רחוקות, ולכאורה באקראי. עלול לעבור המון זמן עד שנגלה שיש בכלל באג – כלומר, אולי רק כשעשרות אלפי מוצרים כאלה כבר יהיו בידי לקוחות – וגם אז יהיה לנו מאוד קשה לשחזר אותו כדי לזהות מה מקורו ולתקן אותו.

אז מה אפשר לעשות?

כמו תמיד, בשלב ראשון צריך להיות מודעים לבעיה, ולבחון את הקוד שלנו בהתאם: באילו רגיסטרי חומרה הוא נוגע? האם יש ברגיסטרים האלה דגלים או ביטים אחרים, שרלוונטיים לנו ועלולים להיפגע מ-R-M-W? האם התזמונים של הקוד ושל סביבת החומרה אכן עלולים להתנגש, ואם כן, האם אפשר לשנות אותם כדי לחמוק מלכתחילה מהסכנה?

מעבר לזה, הנושא כולו אינו תגלית חדשה כמובן – יצרני המיקרו-בקרים מודעים לעניין, ופיתחו כל מיני שיטות להתמודד איתו. בדגמי 8-ביט של PIC, למשל, יש פקודות באסמבלי לשינוי של ביט אחד ויחיד ברגיסטר. אנחנו יכולים לשבץ אותן ישירות בקוד שלנו במקומות הקריטיים, או למצוא דרכים לומר לקומפיילר לעשות את זה. במיקרו-בקרים אחרים, חלק מדגלי הפסיקות מנוקים אוטומטית בסיום פונקציית הפסיקה, או כשקוראים את המידע שמשויך להם. לפעמים יש רגיסטרים שהביטים שלהם מגיבים רק ל-1 (או ל-0, תלוי בארכיטקטורה), כך שאנחנו יכולים לכתוב ישירות לרגיסטר "מסכה" של ביטים, נניח 001 בהמשך לדוגמה למעלה, וזה יאפס רק את הביט הנמוך, בלי להשפיע בשום אופן על האחרים. במיקרו-בקרי ARM מתקדמים יש את ה-bit-banding, כתובות בזיכרון שאנחנו ניגשים אליהן כמו אל כתובות רגילות של int, אך למעשה הן "מחוברות" לביטים בודדים ונפרדים ברגיסטרים.

בקיצור, לא חסרים פתרונות, אך אין פתרון אוניברסלי או תקן אחיד. מעל רמת מורכבות מסוימת של הקוד, אין ברירה אלא לקרוא בתשומת לב את ה-datasheet של המיקרו-בקר שבחרנו, ולהבין מתי ואיך צריך לעקוף בו את תופעות הלוואי של ה-R-M-W.

להרשמה
הודע לי על
2 תגובות
מהכי חדשה
מהכי ישנה לפי הצבעות
Inline Feedbacks
הראה את כל התגובות

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