נתחיל מהמונח Secure, כי הוא מבלבל. Secure אינו שם של קובץ ואינו תוצאה של חתימה. Secure הוא מצב חומרתי של המעבד ושל הגישה לזיכרון ולפריפריות. אם קוד רץ כ־Secure, החומרה מאפשרת לו לגעת גם באזורים שמסומנים כמאובטחים. אם קוד רץ כ־Non-Secure, אותה גישה יכולה להיחסם. דוגמה פשוטה: נניח שיש UART מסוים, והרגיסטר שלו יושב בכתובת ידועה. קוד Secure יכול לכתוב לרגיסטר ה־TX שלו ולהוציא תו. קוד Non-Secure שינסה לכתוב לאותו רגיסטר, אם ה־UART נשאר בבעלות Secure, לא יקבל פשוט UART שקט; הוא יכול לקבל fault או התנהגות שנראית כמו דרייבר תקוע. זה TrustZone בפועל: לא סיסמה, אלא גבול חומרה סביב כתובות ופריפריות.
גם Boot ROM צריך להסביר בגובה העיניים. Boot ROM הוא קוד ש־ST צרבה בתוך השבב במפעל. מי מבצע את הפעולה? Boot ROM. על איזה זיכרון הוא משפיע? הוא מחפש תמונת אתחול לפי מצב ה־boot וה־headers ש־ST הגדירה. מה קורה אם האפליקציה שלך מנסה להחליף אותו? היא לא יכולה; זה לא קוד שלך ולא Flash שאתה מוחק. ב־STM32N6, אחרי Reset, המעבד מתחיל במסלול הזה לפני שהוא מגיע לקוד שלך. במודל קצר אפשר לרשום את זה כך: Reset, אחר כך Boot ROM, אחר כך FSBL, אחר כך Secure application, ורק אחר כך Non-Secure application.
FSBL הוא First Stage Boot Loader: טוען האתחול הראשון שאתה כן מספק כחלק מהפרויקט. זה אותו רעיון שאתה כבר מכיר מ־bootloader ב־F4, עם תוספת אחת חשובה: ב־N6 הוא חי בתוך שרשרת שבה יש גם זיכרון חיצוני וגם TrustZone. מי מבצע את הפעולה? ה־FSBL. על איזה זיכרון הוא משפיע? הוא יכול להגדיר את הגישה ל־Flash חיצוני, לטעון image נוסף ל־RAM, או להכין הרצה מ־XIP. XIP, כלומר Execute In Place, הוא מצב שבו הקוד נשאר ב־Flash חיצוני והמעבד קורא ממנו פקודות בזמן ריצה במקום להעתיק את כולו ל־RAM. מה יקרה אם אפליקציה תנסה לרוץ משם לפני שה־FSBL הגדיר את הממשק? ה־program counter יצביע לכתובת שנראית נכונה על הנייר, אבל החומרה עדיין לא יודעת להביא משם פקודות.
חתימה היא נושא אחר, וחשוב לא לערבב אותו עם Secure. החתימה לא קובעת אם הקוד Secure; היא קובעת אם סומכים עליו. מי בודק את החתימה? בשלב boot זה יכול להיות Boot ROM, FSBL או Root of Trust לפי המסלול שבנית. על איזה זיכרון זה משפיע? על image שנשמר ב־Flash או נטען ל־RAM. מה קורה אם קוד חתום רץ אחר כך כ־Non-Secure? הוא עדיין Non-Secure. כלומר הוא יכול להיות אמין מבחינת מקור, ועדיין חסום מלכתוב לרגיסטר שהוגדר Secure. במוצר ייצור נדרש תהליך מסודר של מפתחות, anti-rollback ו־provisioning. בפרויקט שלנו, בשלב שעליו הכתבה מדברת, הדגש הוא קודם להבין את שרשרת הריצה ואת חלוקת ה־Secure/Non-Secure, לא לטעון שכל מדיניות ייצור כבר נסגרה.
בפרויקט שלנו המסלול היה קונקרטי: FSBL, אפליקציה Secure ואפליקציה Non-Secure. זה לא תרגיל מילולי; אלה images נפרדים, וכל אחד מהם יכול להכיל startup משלו, vector table משלו, HAL משלו ומשתני runtime משלו. ה־FSBL קושר ל־RAM באזור 0x34180400, כלומר מרחב Secure. ה־Secure application קושר לאזור 0x34000400, גם הוא Secure. ה־Non-Secure application קושר ל־RAM באזור 0x24100400, כלומר מרחב Non-Secure. לכן כשאנחנו שואלים איפה נמצא משתנה כמו uwTick, לא מספיק להגיד "זה משתנה גלובלי של HAL". צריך לשאול באיזה image הוא קושר ובאיזו כתובת הוא נמצא בפועל.
כאן נכנס HAL Tick, אבל עכשיו הוא יושב במקום הנכון במודל. ב־F4 אתה רגיל לכך ש־HAL_Delay פשוט עובד, ושטיימאאוטים של HAL יודעים מתי לוותר. מתחת לזה יש מונה זמן גלובלי בשם uwTick. פעם בתקופה קבועה, בדרך כלל כל מילישנייה, interrupt קורא ל־HAL_IncTick ומגדיל את המונה. uwTickFreq הוא הערך שמגדיר את תדירות ה־tick. מי מבצע את הפעולה? handler של interrupt. על איזה זיכרון הוא משפיע? על המשתנה uwTick של אותו image. מה קורה אם interrupt לא מגיע? HAL_Delay לא נגמר, ו־timeout שאמור להציל אותך מדרייבר תקוע לא משתחרר.
הבעיה ב־TrustZone היא שאפשר בקלות לטעות ולחשוב שיש "tick של המערכת". בפועל, בפרויקט עם כמה images, לכל image יכול להיות HAL משלו ו־uwTick משלו. ה־Secure image יכול לקבל SysTick או Timer שעובד מצוין. זה לא מוכיח שה־Non-Secure image קיבל interrupt שמקדם את ה־uwTick שלו. זו הנקודה שהייתה קשה לשמוע בגרסה הקודמת של הכתבה, כי היא נאמרה מהר מדי: לא שואלים רק אם הזמן מתקדם איפשהו במיקרו־בקר; שואלים אם הזמן מתקדם בתוך ה־HAL של הקוד שרץ עכשיו כ־Non-Secure.
ב־F4 ברירת המחדל של HAL היא לעיתים SysTick, טיימר פנימי של הליבה. ב־N6 עם RTOS או TrustZone עדיף לא פעם להשתמש ב־Timer נפרד כ־HAL timebase. בפרויקט שלנו, לפי הקבצים, ה־Non-Secure משתמש ב־TIM6 כבסיס הזמן של HAL. זה טוב, כי ה־NS לא אמור להיות תלוי ב־Secure SysTick כדי למדוד timeout. אבל עכשיו צריך לבדוק את כל השרשרת: מי מפעיל את TIM6, מי נותן לו clock, לאיזו טבלת וקטורים מגיעה הפסיקה שלו, ואיזה handler קורא בסוף ל־HAL_IncTick של ה־Non-Secure.
ניקח דוגמה מוחשית. נניח שקוד Non-Secure מפעיל UART ומחכה שדגל TXE או TC ישתחרר. אם ה־UART עובד, הכול נראה תקין. אם הכבל מנותק, clock חסר או פריפריה נתקעה, HAL אמור לצאת אחרי timeout. אבל אם ה־tick של NS קפוא, הלולאה לא יודעת שעבר זמן. מבחינתה עכשיו ועוד חמש שניות הם אותו רגע. לכן מערכת יכולה להיראות טובה בהדגמה, ורק בזמן תקלה אמיתית להיתקע במקום שבו ציפית לקבל שגיאה מסודרת.
עכשיו נחזור לטענה על uwTick ו־uwTickFreq בזיכרון Secure. זו טענה נכונה לבדיקה. אם ה־Non-Secure image ניגש למשתנה שנמצא ב־0x3..., יש בעיית linker או חלוקת זיכרון, כי כתובות שמתחילות ב־0x3... מצביעות בדרך כלל על מרחב Secure. אם המשתנה נמצא ב־0x2..., זה מתאים יותר למרחב Non-Secure. בפרויקט שלנו ה־linker של ה־NS מצביע על RAM באזור 0x24100400, ולכן הטענה שה־uwTickFreq של NS חסום רק כי הוא יושב ב־Secure RAM נחלשת. היא לא נעלמת בלי בדיקה, אבל היא כבר לא הוכחה שהפרויקט שבור.
הבדיקה הראשונה צריכה להיות יבשה ומדויקת: לפתוח את ה־map file של ה־Non-Secure image ולחפש את uwTick ואת uwTickFreq. לא ב־map של ה־FSBL, לא ב־map של Secure, אלא בקובץ שנוצר מה־link של NS. אם הכתובות ב־0x2..., עוברים הלאה. אם אחת מהן ב־0x3..., עוצרים ובודקים Linker Script, sections, startup וקבצי object שהתערבבו בין images. באותה הזדמנות בודקים גם את VTOR של NS: זה הרגיסטר שמצביע לטבלת הווקטורים. אם הוא לא מצביע לטבלה של NS, הפסיקה של TIM6 יכולה ללכת למקום הלא נכון גם אם המשתנים עצמם נמצאים בזיכרון נכון.
הבדיקה השנייה צריכה להיות בזמן ריצה. בתחילת ה־Non-Secure application מדפיסים דרך UART את HAL_GetTick. מחכים פרק זמן קצר בלי להשתמש ב־HAL_Delay, למשל בלולאת CPU פשוטה רק לצורך הבדיקה, ומדפיסים שוב את HAL_GetTick. אם הערך גדל, ה־NS tick חי. אם הערך נשאר קבוע, בודקים את TIM6: האם ה־clock שלו פעיל, האם הפסיקה מאופשרת ב־NVIC של NS, האם עדיפות הפסיקה סבירה, האם RIF או הגדרת האבטחה לא השאירו את המשאב בצד Secure, והאם ה־handler באמת מגיע למסלול שמגדיל את uwTick.
הבדיקה השלישית היא בדיקת תקלה מבוקרת. גורמים לפונקציית HAL ב־NS להגיע ל־timeout בצורה ידועה, למשל מחכים לדגל שלא ישתחרר או מנתקים תגובה מפריפריה בניסוי מוגבל. אם הפונקציה חוזרת עם timeout, יש עדות שה־tick עובד גם ברגע שבו צריך אותו באמת. אם היא נשארת לנצח, זו כבר לא שאלה פילוסופית של TrustZone; זו בעיית timebase או interrupt routing שצריך לפתור לפני שממשיכים לבנות מעליה דרייברים.
חשוב לסמן מה בוצע בפרויקט ומה שייך למוצר ייצור. בפרויקט שלנו הוגדרה שרשרת עם FSBL, Secure ו־Non-Secure, וה־NS קושר ל־RAM לא־מאובטח באזור 0x24100400. בפרויקט שלנו ה־NS משתמש ב־TIM6 כ־HAL timebase, ולכן זה הכיוון הראשון לבדיקה. במוצר ייצור נדרש מעבר נוסף: להגדיר מדיניות אבטחה סופית, להחליט אילו פריפריות נשארות Secure, לנעול מפתחות ו־OTP בתהליך מבוקר, ולוודא ש־timeouts, עדכון קושחה ו־rollback עובדים גם אחרי שהמערכת נעולה. CubeMX יכול לעזור לייצר קוד והגדרות התחלה, אבל הוא לא "עושה את האבטחה" במקום הארכיטקטורה והבדיקות שלך.
השורה התחתונה פשוטה יותר מהגרסה הקודמת: זה עדיין bootloader ואפליקציות נפרדות, כמו רעיון שאתה מכיר מ־F4. TrustZone מוסיף גבול חומרה ביניהן. חתימה קובעת אם סומכים על image, לא אם הוא Secure. Secure הוא מצב ריצה חומרתי, לא שם של קובץ. ו־HAL Tick הוא לא פרט צדדי; הוא השעון שמאפשר ל־Non-Secure לצאת מ־timeout. לכן בפרויקט STM32N6 לא מספיק לראות שהקוד רץ. צריך להוכיח, בכתובות וב־UART, שה־image הלא־מאובטח מחזיק את הזמן שלו בעצמו.
הזווית ההנדסית
למה זה מעניין
STM32N6 לא מבטל את מה שמתכנת STM32F4 כבר מכיר; הוא מוסיף מעליו גבול חומרה שצריך לראות במונחים של כתובות, רגיסטרים ופסיקות. ברגע שמבינים ש־Secure הוא מצב ריצה, שחתימה היא אמון ולא הרשאת גישה, וש־HAL Tick שייך לכל image בנפרד, קל יותר לבדוק את התקלה בלי להיבהל מהמילים TrustZone ו־FSBL. בפרויקט שלנו כתובות ה־NS מצביעות על RAM לא־מאובטח, ולכן החשד ל־uwTickFreq חסום הוא נקודה לבדיקה ולא הוכחה. המדידה הקובעת היא פשוטה: map file, VTOR, TIM6, ו־HAL_GetTick שמתקדם ב־UART.
השיחה ממשיכה
תגובות
שאלות, תיקונים ורעיונות שאפשר לבנות מהם משהו.