שגיאת סגמנטציה

בתחום המחשוב, שגיאת סגמנטציה (באנגלית: Segmentation fault, לעיתים נקרא גם בקיצור segfault) או הפרת גישה (באנגלית: access violation) היא פסיקה או תרחיש שגיאה המורם על ידי חומרה בעלת מנגנון הגנת זיכרון, המודיעה למערכת ההפעלה כי התוכנית ניסתה לגשת למקטע זיכרון אשר אין לה גישה אליו (הפרת גישה לזיכרון). במעבדי x86, זוהי שגיאת הגנה כללית. בתגובה לפסיקה, ליבת מערכת ההפעלה תנסה לטפל בשגיאה, לרוב על ידי שליחת סיגנל (אנ') לתוכנית אשר גרמה לפסיקה. לעיתים קרובות, תוכניות יכולות לקבוע פונקציית טיפול בפסיקה כזו, שמאפשר לתוכנית להמשיך לרוץ. אם הפסיקה לא מטופלת ברמת התוכנית, לרוב התוכנית עצמה קורסת ולעיתים היא מייצרת גם פלט ליבה (אנ'). שגיאות סגמנטציה הן שגיאות נפוצות בשפות תכנות המאפשרות גישה ישירה לזיכרון ולא מבצעות בדיקות בטיחות, כמו שפת C. שגיאות אלו נובעות משימוש שגוי במצביעים לזיכרון הווירטואלי של התוכנית, בייחוד גישות לדפים שאין אליהם גישה, או ניסיון לכתוב בדף המוגדר לקריאה בלבד. סיבות מרכזיות הגורמות לשגיאת סגמנטציה כוללות: ניסיון לגשת לכתובת זיכרון שלא קיימת (כתובת אשר נמצאת מחוץ לטווח הכתובות שהוקצו לתוכנית). ניסיון לגשת למקטע זיכרון אשר לתוכנית אין גישה אליו (לדוגמה, מבני קרנל בקונטקסט של התוכנית). ניסיון לכתוב למקטע זיכרון המסומן לקריאה בלבד (לדוגמה, מקטע קוד (אנ')). אלו שגיאות התכנות הנפוצות הגורמות לגישה לא חוקית לזיכרון: גישה (dereference) למצביע האפס, אשר בדרך כלל מצביע לכתובת אשר נמצאת מחוץ לטווח הכתובות שהוקצו לתוכנית. התייחסות או השמה למצביע שלא מאותחל (מצביעים "פראיים", דהי מצביעים שמצביעים לכתובת אקראית בזיכרון). התייחסות או השמה למצביע משוחרר (מצביע מתנדנד (אנ'), אשר מצביע לכתובת זיכרון אשר כבר שוחררה ואיננה בשימוש). גלישת חוצץ גלישת מחסנית ניסיון להריץ תוכנית שלא הודרה כראוי. ניסיון לכתוב למקטע זיכרון המוגדר לקריאה בלבד יגרום לשגיאת סגמנטציה. ברמת הכתיבת קוד, הפסיקה נגרמת כאשר תוכנית מנסה לכתוב לחלק ממקטע הקוד שלה עצמה, או החלק המוגדר לקריאה בלבד של מקטע הנתונים (אנ'), שכן אלו מוגדרים על ידי מערכת ההפעלה כמקטעי זיכרון לקריאה בלבד. להלן דוגמה לתוכנית ב-C אשר כתובה בסטנדרט C89, שברוב המקרים תגרום לשגיאת סגמנטציה בפלטפורמות עם הגנת זיכרון. התוכנית מנסה לשנות ליטרל מחרוזת, אשר מוגדרת כהתנהגות בלתי צפויה על פי התקן. בפועל, רוב המהדרים לא יצליחו לתפוס את השגיאה הזאת בזמן הקימפול. int main(void) { char *s = "hello world"; *s = 'H'; } כאשר ננסה לקמפל את התוכנית, התוכנית ברוב המקרים אכן תתקמפל, אך כאשר ננסה להריץ אותה, היא תקרוס ישר: $ gcc segfault.c -g -o segfault $ ./segfault Segmentation fault (core dump) על ידי שימוש בכלי לניפוי שגיאות כמו GDB, נוכל למצוא את השורה הבעייתית: Program received signal SIGSEGV, Segmentation fault. 0x1c0005c2 in main () at segfault.c:6 6 *s = 'H'; תיקון אפשרי לתוכנית הוא על ידי הקצאת מערך במקום מצביע לתו, שכן המערך יוקצה בסופו של דבר לזיכרון המחסנית, שאליה ניתן לכתוב ללא כל בעיה: char s[] = "hello world"; *s = 'H'; בשפת C ושפות הדומות ל-C, מצביעי אפס הם מצביעים אשר מקבלים את הערך NULL ומשמעותם "מצביעים שלא מצביעים לאף אובייקט" ולכן הם משמשים גם לבדיקת שגיאות, שכן גישה למצביע האפס (ניסיון של קריאת הערך או כתיבה לערך שאליו המצביע מצביע) היא שגיאה תכנותית נפוצה. התקן של שפת C לא קובע שמצביע האפס יצביע דווקא על כתובת 0 בזיכרון, אף על פי שברוב המקרים זהו אכן המצב. רוב מערכות ההפעלה ממפות את הכתובת שאליה מצביע האפס מצביע ככה שגישה לכתובת זו תוביל לשגיאת סגמנטציה. התקן של C קובע כי ניסיון לגשת למצביע האפס היא התנהגות בלתי צפויה. הדוגמה הבאה יוצרת מצביע אפס ואז מנסה לגשת לתוכן שהוא מצביע אליו (לקרוא את הערך). ניסיון להריץ את הקטע קוד הזה יגרום לשגיאת סגמנטציה ברוב מערכות ההפעלה: int *ptr = NULL; printf("%d", *ptr); גישה למצביע האפס ואז כתיבת ערך לתוכו, גם כן תוביל לשגיאת סגמנטציה: int *ptr = NULL; *ptr = 1; הקוד שלמטה כולל גם שורת קוד המנסה לגשת למצביע האפס, אך כאשר התוכנית תקומפל ותרוץ, ברוב המקרים לא תהיה שגיאת סגמנטציה עקב אופטימיזציה של המהדר עצמו שמוחק קטעי קוד לא שמישים: int *ptr = NULL; *ptr; ערך מורחב – גלישת חוצץ הקוד להלן מנסה לגשת למערך התווים s מעבר לגודלו, כלומר לתא שנמצא מחוץ למערך. בהתאם למהדר עצמו ולמעבד, תיתכן שגיאת סגמנטציה: char s[] = "hello world"; char c = s[20]; ערך מורחב – גלישת מחסנית דוגמה נוספת לרקורסיה ללא מקרה בסיס: int main(void) { return main(); } הקוד יגרום למחסנית הקריאות לגלוש, דבר אשר יוביל לשגיאת סגמנטציה. בהתאם לשפה, לאופטימיזציות שונות שמבוצעות על ידי המהדר וכן למבנה הקוד עצמו, לעיתים לא נדרשת רקורסיה אינסופית כדי לגרום לגלישת המחסנית. במקרה זה, ההתנהגות של הקוד שלא ניתן להשגה (פקודת החזרה) לא מוגדרת, לכן המהדר יכול למחוק אותה ולהשתמש במקום ברקורסיית זנב שלעיתים לא משתמשת בכלל במחסנית. אופטימיזציות אחרות כוללות המרת הרקורסיה ללולאה, שעל פי הקוד בדוגמה יגרום לתוכנית להיכנס ללולאה אינסופית, אבל לא לגלישת מחסנית. כשכותבים תוכנית ישירות באסמבלי, מערכות הפעלה מודרניות דורשות מהתוכנית לצאת בסיום ריצתה. מהדרים בשפות עיליות מוסיפים פקודות אלו באופן אוטומטי בעת הקימפול, אך אם תוכנית מראש נכתבה באסמבלי, באחריות המתכנת לבצע זאת. להלן תוכנית פשוטה באסמבלי nasm, אשר תואמת למעבדי x86-64 ולמשפחת מערכות ההפעלה של לינוקס. section .data hello: db "Hi Mom!", 10 ;string to print helloLen: equ $-hello ;length of string section .text global _start ;entry point for linker _start: mov rax,1 ; sys_write in linux mov rdi,1 ; stdout mov rsi,hello ; message to write mov rdx,helloLen ; message length syscall ; call kernel בניסיון להריץ את התוכנית, נקבל שגיאת סגמנטציה בסופה: $ ./a.out Hi Mom! Segmentation fault (core dump) השגיאה נובעת מכך שהמעבד אינו יודע מראש מתי התוכנית אמורה להסתיים, והוא מנסה לבצע את הפקודה הבאה בתור, שכבר אינה חלק מזיכרון התוכנית (מקטע הקוד, text). על מנת לתקן זאת, עלינו להוסיף רוטינת יציאה בסוף התוכנית: ; end program mov rax,60 ; sys_exit in linux mov rdi,0 ; error code 0 (success) syscall ; call kernel