מבוא לשפת C
מבוא לשפת C — תחביר, טיפוסים ומערכים
ברוכים ליחידה 4! עד כה למדנו Python שמנהלת זיכרון בשבילנו. עכשיו צוללים ל-C — הבסיס לכל מערכות ההפעלה וחולשות האבטחה שנחקור.
- Python לעומת C — שפה מפורשת לעומת מקומפלת
- מבנה תוכנית C: #include, int main(), נקודה-פסיק
- שרשרת קימפול: gcc → קובץ בינארי
- טיפוסים וזיכרון: char, int, unsigned, sizeof
- Integer Overflow — מה קורה כשמספר חורג מטווחו
- מערכים — בלוקים רציפים ונוקשים בזיכרון
- מחרוזות ו-Null Terminator (\0) — הסוד של C
- קלט/פלט: printf, scanf, מעצבי פורמט
- חולשות אבטחה: Buffer Overflow דרך scanf
Python לעומת C — למה C שולטת בסייבר?
ההבדל המרכזי: רמת השליטה על הזיכרון. Python = רכב אוטומטי. C = רכב ספורט ידני ללא בלמי חירום. שליטה = ביצועים + סכנה.
| קריטריון | Python | C |
|---|---|---|
| אופן ריצה | מפורשת (Interpreted) — שורה-שורה | מקומפלת (Compiled) — כל הקוד לפני הריצה |
| ניהול זיכרון | אוטומטי — Garbage Collector | ידני — המתכנת אחראי |
| בדיקת גבולות | Exception בטוח | לא! — Undefined Behavior / Crash |
| מהירות | איטית יותר | מהירה מאוד — קרובה לחומרה |
| חולשות אבטחה | מוגנות ע"י מפרש | Buffer Overflow, Use-After-Free |
רוב מערכות ההפעלה, ה-Kernel, והתוכנות הקריטיות כתובות ב-C. רוב חולשות הזיכרון המודרניות (Buffer Overflow, Format String) נובעות מ-C. כדי לנצל חולשה — חובה להבין איך הקוד נכתב.
מבנה תוכנית C ושרשרת הקימפול
C היא שפה מקומפלת — הקוד חייב לעבור תרגום מלא לפני הריצה. הקומפיילר (gcc) הוא פקיד הגירה קפדני: נקודה-פסיק אחת חסרה — הוא מסרב.
// תוכנית C בסיסית — כל חלק חיוני!
#include <stdio.h> // ייבוא ספריית קלט/פלט (printf, scanf)
int main() { // נקודת הכניסה — חובה בכל תוכנית
printf("Welcome to Gamma Cyber!\n"); // \n = ירידת שורה
return 0; // 0 = הצלחה, כל ערך אחר = שגיאה
}# קומפילציה והרצה
gcc hello.c -o hello # קמפל ל-hello
./hello # הרץ
# שגיאות קומפילציה נפוצות:
# error: expected ';' before '}' — שכחת ;
# error: 'printf' undeclared — שכחת #include <stdio.h>טיפוסים וזיכרון — קופסאות מדויקות
ב-C כל משתנה הוא הזמנה של "קופסה" בזיכרון. חובה להצהיר מראש על הגודל. אי אפשר לשים פיל בקופסת נעליים.
| טיפוס | גודל | טווח (signed) | שימוש |
|---|---|---|---|
| char | 1 byte | -128 עד 127 | תו בודד, byte של זיכרון |
| unsigned char | 1 byte | 0 עד 255 | byte חיובי בלבד |
| int | 4 bytes | -2B עד 2B | מספר שלם רגיל |
| unsigned int | 4 bytes | 0 עד 4B | מספר שלם חיובי |
| size_t | 8 bytes (64-bit) | 0 עד 18EB | גדלים בזיכרון, תוצאת sizeof |
#include <stdio.h>
int main() {
char c = 'A'; // תו בודד — גרש בודד!
int n = 42;
unsigned int u = 300;
// sizeof — כמה bytes המשתנה תופס
printf("char = %zu bytes\n", sizeof(char)); // 1
printf("int = %zu bytes\n", sizeof(int)); // 4
// הצהרת משתנה ללא אתחול = ערך זבל (Garbage Value)!
int x; // x מכיל כל מה שהיה במיקום הזה בזיכרון לפני
printf("x = %d\n", x); // לא מוגדר!
return 0;
}! Integer Overflow — מה קורה כש-255 + 1? ›
unsigned char יכול להכיל 0–255 (8 ביטים). מה קורה אם נוסיף 1 ל-255?
unsigned char x = 255;
x = x + 1;
printf("%d\n", x); // מדפיס 0 !!ה"שעון" מתאפס — חוזר ל-0. זה Integer Overflow. ב-Python: מספרים גדלים אינסוף. ב-C: חורגים מהטווח = גלישה.
מערכים ומחרוזות — הסוד של Null Terminator
מערכים ב-C הם בלוקים רציפים ונוקשים בזיכרון. מחרוזת? אין דבר כזה ב-C — זה רק מערך תווים שמסתיים ב-\0.
#include <stdio.h>
int main() {
// מערך מספרים — 4×4 = 16 bytes ברצף בזיכרון
int numbers[4] = {10, 20, 30, 40};
// גישה: אינדקסים מ-0 עד 3 (לא 1 עד 4!)
for (int i = 0; i < 4; i++) {
printf("Element at index %d is: %d\n", i, numbers[i]);
}
// מחרוזת = מערך תווים + \0 בסוף
// "Hello" = ['H','e','l','l','o','\0'] — 6 תאים!
char word[6] = "Hello";
printf("%s\n", word); // מדפיס עד \0
// מלכודת: char str[5] = "Hello"; — אין מקום ל-\0!
// str[5] = "Hello"; חסר בית אחד — Buffer Overflow!
return 0;
}קלט ופלט — printf ו-scanf
printf ו-scanf עובדות עם מעצבי פורמט — סימנים שמגדירים איזה סוג נתון לפרש ולהדפיס.
| מעצב | סוג נתון | דוגמה |
|---|---|---|
| %d | מספר שלם (int) | printf("%d", 42) → 42 |
| %c | תו בודד (char) | printf("%c", 'A') → A |
| %s | מחרוזת (char[]) | printf("%s", "Hi") → Hi |
| %zu | size_t (גודל) | printf("%zu", sizeof(int)) → 4 |
| %f | מספר עשרוני (float) | printf("%.2f", 3.14) → 3.14 |
#include <stdio.h>
int main() {
int age;
char name[20];
// קלט — חובה & לפני שם משתנה! (כתובת הזיכרון)
printf("Enter your age: ");
scanf("%d", &age); // &age = כתובת הזיכרון של age
printf("Enter your name: ");
scanf("%s", name); // name = כבר כתובת (מערך)!
// פלט
printf("Hello %s, you are %d years old.\n", name, age);
return 0;
}! למה & חובה ב-scanf? ›
scanf צריכה לכתוב את הנתון לתוך המשתנה. לשם כך צריכה את כתובת הזיכרון שלו — לא את הערך.
&age = "כתובת המשתנה age בזיכרון". ללא &, שולחים את הערך הנוכחי (לא מאותחל!) — תוצאה: Segmentation Fault.
char name[10];
scanf("%s", name); // תוקף מזין 20 תווים → CRASH / חולשה!scanf לא בודקת גבולות. תוקף מזין 20 תווים → 10 תווים ראשונים ממלאים את name, 10 נוספים גולשים לזיכרון סמוך ודורסים ערכים קריטיים.
פתרון: scanf("%10s", name); — מגביל ל-10 תווים.
מטלות מעשיות — כתבו קוד C
5 מטלות מדורגות. לחצו על "פתרון" לאחר שניסיתם בעצמכם.
כתבו תוכנית C שלמה שמדפיסה Welcome to Gamma Cyber! וירידת שורה. קמפלו והריצו.
מצאו ותקנו את השגיאות בקוד הבא:
#include <stdio.h>
int main() {
int num
printf("Enter a number: ");
scanf("%d", num);
printf("Double is: %d\n", num * 2);
return 0;
}צרו מערך מספרים שלמים בגודל 5 {10, 20, 30, 40, 50}. כתבו לולאה שסוכמת את כל הערכים ומדפיסה את הסכום.
הגדירו מחרוזת. כתבו לולאת while שעוברת אות אחר אות ומדפיסה כל אות בשורה נפרדת — אך עוצרת כשנתקלת ב-\0.
כתבו תוכנית שקולטת מספר שלם מהמשתמש ומדפיסה האם הוא זוגי או אי-זוגי. השתמשו באופרטור %.
int arr[] = {2, 5, 8, 3};
int sum = 0;
for (int i = 0; i < 4; i++) {
if (arr[i] % 2 == 0) sum += arr[i];
}
printf("Result: %d\n", sum);int i;
for (i = 0; i < 3; i++) { }
printf("%d", i);תרגול נוסף — C: sizeof, מחרוזות ו-Overflow
שאלות העשרה נוספות לחיזוק החומר של השיעור — באותו פורמט בחירה של הקורס.
char s[] = "Cyber";
printf("%zu %zu", strlen(s), sizeof(s));sizeof(int) * 8 ב-x86?char buf[10]; int n = atoi(argv[1]); memset(buf, 0, n); // מאפס n bytes
אתגר מעשי — C — תוספת: צופן קיסר
תוספת מעל 5 מטלות ה-C הקיימות (שלום עולם → זוגיות): צופן קיסר. 5 המטלות הקיימות נשמרות ומקבלות חוזה-בדיקה (קומפילציה+הרצה) בבנק ה-JSON.
סיכום — שיעור 23
הנחנו את יסודות שפת C. יסודות אלו הם הבסיס להבנת Assembly, ניצול חולשות, ו-Buffer Overflow שנלמד בהמשך.
- קומפילציה: C מקומפלת לפני ריצה — gcc hello.c -o hello
- מבנה: #include <stdio.h> + int main() + return 0;
- טיפוסים: char (1B), int (4B), unsigned (חיובי בלבד)
- sizeof: בדיקת גודל משתנה בזמן קומפילציה
- Garbage Value: משתנה לא מאותחל = ערך זבל מהזיכרון
- Integer Overflow: unsigned char 255 + 1 = 0 — השעון מתאפס
- מערכים: בלוקים רציפים, אינדקסים מ-0, גודל קבוע
- Null Terminator: מחרוזת = מערך תווים + \0 — חובה!
- printf/scanf: מעצבי פורמט %d, %s, %c, %zu
- & ב-scanf: כתובת זיכרון — חובה לכל משתנה (לא מערך)
- Buffer Overflow: scanf("%s") ללא הגבלה = חולשת אבטחה
שאלה: "מדוע שפת C נחשבת מסוכנת בעולם הסייבר?"
תשובה מקצועית: שפת C דוגלת ב-"Trust the Programmer" — אין בדיקות גבולות אוטומטיות על מערכים, אין Garbage Collector, ואין הגנות מובנות. כשמתכנת עושה טעות בניהול זיכרון, התוכנית ניגשת לזיכרון שאינו שלה. תוקף חכם מנצל זאת לדריסת Return Address ולהרצת קוד זדוני (Buffer Overflow). ב-Python, אותה טעות הייתה מייצרת Exception בטוח.
- קמפלו והריצו את כל 5 המטלות — וודאו שאתם מבינים כל שורה
- נסו להזין מחרוזת ארוכה מדי ל-scanf ותראו מה קורה
- שיעור 24: פוינטרים — ניהול זיכרון ידני ו-Dynamic Allocation
שאלות תרגול ותשובות (10)
שאלה 1 · מה ההבדל המרכזי בין Python ל-C באופן ההרצה?
- א. שתיהן מתקמפלות לקוד מכונה לפני ההרצה
- ב. Python מפורשת שורה-שורה בזמן ריצה, C מתקמפלת מראש לקובץ הרצה
- ג. C מפורשת ו-Python מתקמפלת
- ד. ההבדל הוא רק בתחביר, לא באופן ההרצה
Python מריצה מפרש (Interpreter) על הקוד בזמן ריצה. C עוברת קימפול מראש ל-Binary של קוד מכונה — ולכן היא מהירה בהרבה אבל פחות סלחנית.
שאלה 2 · מהי שרשרת הקימפול הנכונה ב-C?
gcc main.c -o main
- א. Preprocessor → Compiler → Assembler → Linker
- ב. Compiler → Preprocessor → Linker → Assembler
- ג. Linker → Compiler → Assembler
- ד. Assembler → Linker → Preprocessor → Compiler
קודם ה-Preprocessor מטפל ב-#include ו-#define, אחר כך המהדר יוצר אסמבלי, ה-Assembler הופך אותו ל-Object File, ולבסוף ה-Linker מחבר את הכול לקובץ הרצה אחד. 🎯 דגש למבחן: שגיאת "undefined reference" היא כמעט תמיד שגיאת Linker — הפונקציה הוגדרה אבל המימוש לא נמצא.
שאלה 3 · מה תפקיד השורה #include <stdio.h>?
- א. מייבאת את ההצהרות על פונקציות קלט/פלט כמו printf ו-scanf
- ב. מגדירה כמה זיכרון התוכנית רשאית לתפוס
- ג. מורה למהדר להתעלם משגיאות
- ד. מפעילה בדיקת גבולות מערך אוטומטית
stdio.h הוא Header שמכיל הצהרות (Prototypes) של פונקציות קלט/פלט. בלעדיו המהדר לא מכיר את printf ויתריע.
שאלה 4 · מה יחזיר sizeof בקוד הבא?
int arr[10];
printf("%zu", sizeof(arr));- א. 10
- ב. 40 (בהנחה ש-int תופס 4 בתים)
- ג. 4
- ד. גודל לא ידוע — נקבע רק בזמן ריצה
sizeof מחזיר את הגודל בבתים: 10 איברים × 4 בתים = 40. 🎯 דגש למבחן: sizeof מחושב בזמן קימפול ולא בזמן ריצה. לכן על מצביע (int *p) הוא יחזיר את גודל המצביע (8 בתים ב-64 ביט) ולא את גודל המערך שאליו הוא מצביע.
שאלה 5 · כמה בתים בזיכרון תופסת המחרוזת הבאה?
char name[] = "CYBER";
- א. 5
- ב. 6
- ג. 7
- ד. תלוי במעבד
5 תווים + תו הסיום \0 (Null Terminator) = 6 בתים. ב-C כל מחרוזת מסתיימת ב-\0, וזה מה שמאפשר ל-printf לדעת איפה לעצור.
שאלה 6 · מה יקרה בקוד הבא?
char buf[4]; strcpy(buf, "HELLO");
- א. המהדר יעצור עם שגיאה ויסרב לקמפל
- ב. התוכנית תקצץ אוטומטית ל-4 תווים
- ג. הכתיבה תחרוג מהמאגר ותדרוס זיכרון סמוך — Buffer Overflow
- ד. המערכת תגדיל את buf אוטומטית
C לא בודקת גבולות. strcpy תכתוב 6 בתים לתוך מאגר של 4 ותדרוס את מה שנמצא אחריו במחסנית. 🎯 דגש למבחן — אנטומיה של קריסה: אחרי המשתנים המקומיים במחסנית שמורה כתובת החזרה (Return Address). כשהחריגה דורסת אותה, בסיום הפונקציה המעבד "קופץ" לכתובת שנדרסה — ומכאן Segmentation Fault, או במקרה של תוקף — הרצת קוד זדוני.
שאלה 7 · איזה מהמשתנים הבאים נמצא במחסנית (Stack) ואיזה בערמה (Heap)?
int a = 5; int *p = malloc(sizeof(int) * 100);
- א. שניהם ב-Stack
- ב. a ב-Stack, הזיכרון ש-p מצביע עליו ב-Heap
- ג. a ב-Heap, p ב-Stack
- ד. שניהם ב-Heap
משתנים מקומיים יושבים במחסנית ומשתחררים אוטומטית בסיום הפונקציה. malloc מקצה ב-Heap, והאחריות לשחרר עם free היא של המתכנת — אחרת נוצרת דליפת זיכרון (Memory Leak).
שאלה 8 · מה הבעיה בקוד הבא?
int num;
scanf("%d", num);- א. חסר & לפני num — scanf צריכה כתובת ולא ערך
- ב. צריך להשתמש ב-%s במקום ב-%d
- ג. אסור לקרוא מספרים עם scanf
- ד. אין בעיה, הקוד תקין
scanf כותבת אל תוך כתובת בזיכרון, ולכן חייבים להעביר &num. בלי ה-& מועבר ערך אקראי כאילו היה כתובת — והתוכנית קורסת.
שאלה 9 · מה יודפס?
int i = 0;
while (i < 3) {
printf("%d ", i);
i++;
}- א. 0 1 2
- ב. 1 2 3
- ג. 0 1 2 3
- ד. לולאה אינסופית
הלולאה בודקת את התנאי לפני כל סיבוב ומדפיסה 0, 1, 2. כשi מגיע ל-3 התנאי נכשל והלולאה נעצרת.
שאלה 10 · מדוע C נחשבת מסוכנת יותר מ-Python בעולם הסייבר?
- א. היא שפת קוד פתוח וקל לקרוא אותה
- ב. אין בה בדיקת גבולות וניהול הזיכרון ידני — לכן טעות קטנה הופכת לפרצה
- ג. המהדר שלה מוסיף דלתות אחוריות
- ד. היא מצפינה נתונים בצורה חלשה
ב-C המתכנת אחראי לזיכרון: אין בדיקת גבולות, אין איסוף זבל ואין הגנה מובנית. רוב חולשות ה-Memory Corruption ההיסטוריות נולדו בדיוק מכאן.