Union לניתוח פרוטוקולי רשת (TCP Header, IP Address)
Bitwise Operators — AND, OR, XOR, NOT, Shift
Bitmask Idioms — Set/Clear/Toggle/Check
Bit Fields — שדות בגודל ביטים ב-Struct
Endianness — Little vs Big Endian
// פרק 1
Struct — מבנה נתונים מורכב
ב-Python יש Class. ב-C יש Struct — מבנה שאוגד שדות של טיפוסים שונים תחת שם אחד. typedef חוסך את כתיבת struct בכל פעם.
הגדרה ושימוש בסיסי
// הגדרת Struct — תבנית בלבד, לא תופסת זיכרון
struct Point {
int x; // 4 bytes
int y; // 4 bytes
}; // סה"כ: 8 bytes
// יצירת משתנה + גישה עם Dot Operator
struct Point p1;
p1.x = 10;
p1.y = 20;
// אתחול עם שמות שדות (מומלץ!)
struct Point p2 = {.x = 5, .y = 15};
printf("Point: (%d, %d)\n", p2.x, p2.y); // Point: (5, 15)
// typedef — קיצור שם
typedef struct {
int x;
int y;
} Point;
// עכשיו אפשר:
Point p3 = {3, 7}; // במקום: struct Point p3 = {3, 7};
Struct Pointers ו-Arrow Operator (->)
typedef struct {
char name[50];
int age;
} Person;
Person p = {"Bob", 25};
Person *ptr = &p;
// שתי שיטות שקולות לגישה דרך מצביע:
printf("%s\n", (*ptr).name); // שיטה 1 — מסורבלת
printf("%s\n", ptr->name); // שיטה 2 — Arrow Operator ← מועדפת!
ptr->age = 26; // שינוי ערך דרך מצביע
// פונקציה שמקבלת מצביע ל-Struct ומשנה אותו
void birthday(Person *p) {
p->age++; // משנה את ה-Struct המקורי!
}
✏תרגול — Struct ו-typedef3 שאלות
שאלה 1.1
מה שקול לחלוטין לביטוי ptr->field?
✓ תשובה: ג) (*ptr).field — Arrow Operator הוא קיצור: קודם דרפרנס את המצביע (*ptr), אחר כך גש לשדה (.field). ⚠️ *ptr.field שגוי — קדימות: .field רץ לפני * ונותן שגיאה.
שאלה 1.2
מהי השגיאה בקוד הבא? struct Node n; n->id = 5;
✓ תשובה: א. n הוא משתנה מסוג struct (קופסה פיזית), לא מצביע. Arrow Operator (->) משמש רק עם מצביעים לStruct. הנכון: n.id = 5;
שאלה 1.3
מה קורה כאשר מבצעים השמה של Struct אחד לאחר: Point p2 = p1;?
✓ תשובה: א. בשפת C, השמה של Struct מבצעת העתקת זיכרון מלאה (Shallow Copy). שינוי ב-p2 לא ישפיע על p1 ולהפך — הם עצמאיים.
// פרק 2
Struct בזיכרון — Padding ו-Alignment
הקומפיילר לא מניח שדות צמודים. הוא מיישר (Aligns) כל שדה לכתובת שמתחלקת בגודלו — ומוסיף Padding (bytes ריקים) כדי לעמוד בדרישה.
// עצה: סדר שדות חכם = פחות Padding!
typedef struct {
int b; // 4 bytes → כתובת 0
char a; // 1 byte → כתובת 4
char c; // 1 byte → כתובת 5
// Padding: 2 bytes
// סה"כ: 8 bytes במקום 12!
} Compact;
// כלל אצבע: סדר שדות מהגדול לקטן → Padding מינימלי
🔑 חשוב לגאמא — חישוב sizeof
כלל Alignment: כל שדה מיושר לכתובת שמתחלקת בגודלו (char=1, short=2, int=4, double=8). גודל כולל: מתחלק בגודל השדה הגדול ביותר. שאלות מיון: כתבו שדות לפי סדר + הוסיפו Padding לפי הכלל.
✏תרגול — Padding ו-Alignment3 שאלות
שאלה 2.1
מדוע sizeof(struct { char a; int b; }) מחזיר 8 ולא 5?
✓ תשובה: ד. char=1 byte, אז Padding 3 bytes, אז int=4 bytes. סה"כ: 1+3+4 = 8. מעבדים מודרניים קוראים מהר יותר מכתובות "מיושרות".
שאלה 2.2
מהו sizeof של הstruct הבא? typedef struct { char a; double b; int c; char d; } MyStruct;
✓ תשובה: ג) 24 bytes. char a → כתובת 0 (1B) + Padding 7B → double b → כתובת 8 (8B) → int c → כתובת 16 (4B) → char d → כתובת 20 (1B) + Padding 3B → גודל כולל מתחלק ב-8 (double) = 24B.
שאלה 2.3
כיצד ניתן לאלץ את הקומפיילר לא להוסיף Padding (לארוז צמוד)?
✓ תשובה: ג. Packed Struct = ללא Padding. ⚠️ חסרון: ביצועים איטיים יותר + בעיות Alignment בחומרה מסוימת. שימוש: שליחת Structs ברשת בלי bytes ריקים מיותרים.
// פרק 3
Union — שדות שחולקים זיכרון
Union דומה ל-Struct — אבל כל שדות חולקים את אותו מקום בזיכרון. גודל = גדול ביותר. שימושי לניתוח פרוטוקולים וType Punning.
typedef union {
int i; // 4 bytes
float f; // 4 bytes
char bytes[4]; // 4 bytes
} MultiView;
// כל השדות מפנים לאותו מקום בזיכרון!
MultiView u;
u.i = 65; // 0x00000041
printf("As int: %d\n", u.i); // 65
printf("As char: %c\n", u.bytes[0]); // 'A' (ASCII 65)
// כתיבה לשדה אחד "משנה" את כל השאר!
u.f = 3.14;
printf("After f=3.14, i = %d\n", u.i); // ערך int של pattern 3.14 (לא 3!)
Union בפרוטוקולי רשת
// IP Address — נגיש גם כ-uint32_t וגם כ-4 Bytes נפרדים
typedef union {
uint32_t addr; // הכתובת כמספר שלם
uint8_t octets[4]; // 4 Octets נפרדים
} IPAddress;
IPAddress ip;
ip.addr = 0xC0A80001; // 192.168.0.1
// גישה לכל Octet בנפרד:
printf("IP: %d.%d.%d.%d\n",
ip.octets[3], ip.octets[2],
ip.octets[1], ip.octets[0]); // 192.168.0.1
הרחבה Tagged Union — ייצוג Variant Data ›
// JSON value — יכול להיות מספר, מחרוזת, או bool
typedef enum { TYPE_INT, TYPE_FLOAT, TYPE_STRING } ValueType;
typedef struct {
ValueType type; // "tag" — איזה שדה בשימוש
union {
int ival;
float fval;
char sval[64];
} data;
} Value;
Value v1 = {.type = TYPE_INT, .data = {.ival = 42}};
Value v2 = {.type = TYPE_STRING, .data = {.sval = "hello"}};
void printValue(Value *v) {
switch (v->type) {
case TYPE_INT: printf("int: %d\n", v->data.ival); break;
case TYPE_STRING: printf("str: %s\n", v->data.sval); break;
}
}
✏תרגול — Union3 שאלות
שאלה 3.1
הגדרתם union { char a[4]; int b; } u;. כתבתם a[0]='A'; a[1]='B'; ואז קראתם b. מה קרה?
✓ תשובה: ב. ב-Union, כל השדות שותפים לאותו זיכרון. כתיבה ל-a[0] ו-a[1] ממלאת את bytes[0] ו-bytes[1] של ה-int. קריאת b מחזירה את הפרשנות של הביטים האלה כ-int שלם.
שאלה 3.2
מה ההבדל המרכזי בין struct ל-union מבחינת זיכרון?
✓ תשובה: א. struct = סכום גדלי כל השדות (+ Padding). union = גודל השדה הגדול ביותר. כתיבה לשדה אחד ב-union "דורסת" את שאר השדות.
שאלה 3.3
כיצד ניתן לבדוק Endianness של המחשב באמצעות union?
✓ תשובה: ג) x = x ^ x. XOR של מספר עם עצמו = כל הביטים מתאפסים (כל ביט זהה לעצמו → XOR נותן 0). ב-Assembly: XOR EAX, EAX — פעולת האיפוס המהירה ביותר!
// פרק 5
Bitmask Idioms — Set, Clear, Toggle, Check
ארבעת הניתוחים שחוזרים בכל קוד low-level, ב-TCP Flags, בהרשאות קבצים, ובכל Flags. חובה לשנן אותם.
// הגדרת Flags (כל אחד ביט אחד)
#define FLAG_READ (1 << 0) // 0b00000001 = 0x01
#define FLAG_WRITE (1 << 1) // 0b00000010 = 0x02
#define FLAG_EXEC (1 << 2) // 0b00000100 = 0x04
#define FLAG_HIDDEN (1 << 3) // 0b00001000 = 0x08
uint8_t perms = 0; // כל הפלאגים כבויים
// SET BIT — הדלקת ביט (OR עם ה-Mask)
perms |= FLAG_READ; // הדלק READ
perms |= FLAG_WRITE | FLAG_EXEC; // הדלק WRITE וEXEC
// perms עכשיו: 0b00000111 = 0x07
// CHECK BIT — בדיקת ביט (AND עם ה-Mask → != 0 אם דלוק)
if (perms & FLAG_READ) printf("Read!\n"); // ✓
if (perms & FLAG_HIDDEN) printf("Hidden!\n"); // ✗ לא יודפס
// CLEAR BIT — כיבוי ביט (AND עם NOT של ה-Mask)
perms &= ~FLAG_EXEC; // כבה EXEC
// ~FLAG_EXEC = 0b11111011
// TOGGLE BIT — היפוך ביט (XOR עם ה-Mask)
perms ^= FLAG_WRITE; // הפוך WRITE
// READ BITS — חילוץ ערך משדה
uint8_t data = 0b10110100;
uint8_t high = (data >> 4) & 0x0F; // → 0b1011 = 11 (Nibble גבוה)
uint8_t low = data & 0x0F; // → 0b0100 = 4 (Nibble נמוך)
// CHEAT SHEET — Bitwise Idioms
Set bit n: x |= (1 << n)
Clear bit n: x &= ~(1 << n)
Toggle bit n: x ^= (1 << n)
Check bit n: (x >> n) & 1 ← תוצאה: 0 או 1
Check bit n: x & (1 << n) ← תוצאה: 0 או != 0
Extract n bits from pos p: (x >> p) & ((1 << n) - 1)
TCP Flags — דוגמה מהעולם האמיתי
// TCP Flags — כל ביט הוא דגל
#define TCP_FIN (1 << 0) // 0x01 — סיום חיבור
#define TCP_SYN (1 << 1) // 0x02 — בקשת חיבור
#define TCP_RST (1 << 2) // 0x04 — איפוס חיבור
#define TCP_ACK (1 << 4) // 0x10 — Acknowledgment
// ניתוח Packet שקיבלנו
uint8_t flags = 0x12; // SYN-ACK (0x02 | 0x10)
if (flags & TCP_SYN) printf("SYN set\n"); // ✓
if (flags & TCP_ACK) printf("ACK set\n"); // ✓
if (flags & TCP_FIN) printf("FIN set\n"); // ✗
// Three-Way Handshake:
// 1. Client → Server: SYN (0x02)
// 2. Server → Client: SYN + ACK (0x12) ← זה ה-Packet שלנו
// 3. Client → Server: ACK (0x10)
✏תרגול — Bitmask Idioms4 שאלות
שאלה 5.1
כיצד נדליק את ביט 3 (אינדקס 3, ערך 8) ב-flags?
✓ תשובה: ד) flags |= (1 << 3). OR עם מסיכה שבה רק ביט 3 דלוק מבטיח שהביט הזה ידלק, בלי לשנות שאר הביטים.
שאלה 5.2
כיצד נכבה את ביט 0 (הימני ביותר) ב-flags?
✓ תשובה: ג) flags &= ~1. ~1 = 0b11111110. AND עם זה מכבה ביט 0 ושומר את כל השאר. XOR (א) היה מבצע Toggle — לא Clear.
שאלה 5.3
מה הבעיה בקוד: if (flags & 4 == 4)?
✓ תשובה: א. בשפת C, == קודם ל-&. לכן: 4==4 = 1, אז flags & 1 — לא מה שרצינו! תיקון: if ((flags & 4) == 4) עם סוגריים.
שאלה 5.4
לאיזו פעולה לוגית דומה XOR כשפועל על ביטים בודדים?
✓ תשובה: ב) !=. XOR מחזיר 1 אם הביטים שונים, 0 אם זהים — זו בדיוק ההתנהגות של !=. לכן XOR מאפס (0^0=0, 1^1=0) ומדליק לשונים (0^1=1, 1^0=1).
// פרק 6
Bit Fields ו-Endianness
Bit Fields מאפשרים שדות ב-Struct בגודל ביטים. Endianness קובע סדר הbytes בזיכרון — קריטי לניתוח פרוטוקולים.
Bit Fields
// IP Header — כל שדה בגודלו האמיתי
typedef struct {
unsigned int version : 4; // ביטים 0-3
unsigned int ihl : 4; // ביטים 4-7
unsigned int length : 16; // ביטים 16-31
} IPHeader;
printf("sizeof(IPHeader) = %zu\n", sizeof(IPHeader)); // 4 bytes!
// Bit Fields + Union = גישה לשדות + Raw Bytes
typedef union {
struct {
uint8_t fin : 1;
uint8_t syn : 1;
uint8_t rst : 1;
uint8_t ack : 1;
uint8_t res : 4; // Reserved
} flags;
uint8_t raw; // גישה כ-Byte שלם
} TCPFlags;
TCPFlags f;
f.raw = 0x12; // SYN + ACK
printf("SYN: %u\n", f.flags.syn); // 1
printf("ACK: %u\n", f.flags.ack); // 1
⚠️ Bit Fields ו-Portability
סדר הBits בתוך Byte הוא Implementation Defined — הקומפיילר/ארכיטקטורה קובעים. לניתוח פרוטוקולים "אמיתי" עדיף Bitmask ידני. ב-CTF ובקוד קיים — תמצאו גם Bit Fields.
Endianness
Little Endian
Big Endian (Network Order)
הגדרה
Byte הנמוך בכתובת הנמוכה
Byte הגבוה בכתובת הנמוכה
0x12345678
78 56 34 12 (בזיכרון)
12 34 56 78 (בזיכרון)
שימוש
x86, x64 (המחשב שלך)
פרוטוקולי רשת (TCP/IP, DNS)
// המרה בין Network Byte Order ל-Host Byte Order
#include <arpa/inet.h> // Linux
uint32_t host_val = 0x12345678;
uint32_t net_val = htonl(host_val); // Host TO Network Long
uint32_t back = ntohl(net_val); // Network TO Host Long
// לערכי 16 ביט (Ports):
uint16_t port = 80;
uint16_t net_port = htons(port); // 80 → ייצוג רשתי
// לכן: כשקוראים מ-Packet רשת — תמיד ntohl/ntohs לפני שימוש!
✏תרגול — Bit Fields ו-Endianness3 שאלות
שאלה 6.1
union { uint16_t n; uint8_t b[2]; } u; u.n = 0x1234; על מחשב Little Endian — מה הם u.b[0] ו-u.b[1]?
✓ תשובה: ג. Little Endian: LSB (הbyte הנמוך = 0x34) נשמר בכתובת הנמוכה. לכן b[0]=0x34, b[1]=0x12. Big Endian היה הפוך: b[0]=0x12.
שאלה 6.2
מה תפקיד הפונקציה htons()?
✓ תשובה: א. htons = Host TO Network Short. על מחשב Little Endian: הופכת את סדר הbytes. Port 80 → 0x5000 בייצוג הרשתי. ntohl/ntohs = הכיוון ההפוך.
שאלה 6.3
מה הגישה ל-uint8_t b[0] בזיכרון תחזיר אם שמרנו 0x00000001 ב-uint32_t על מחשב Little Endian?
✓ תשובה: ב) 1. Little Endian = LSB בכתובת הנמוכה. 0x00000001: הbyte הנמוך הוא 0x01=1. אם b[0]==1 → המחשב Little Endian. זה הטריק הקלאסי לבדיקת Endianness.
// תרגול נוסף
תרגול נוסף — ביטים: Shift ו-Struct Alignment
שאלות העשרה נוספות לחיזוק החומר של השיעור — באותו פורמט בחירה של הקורס.
✏תרגול נוסף3 שאלות
שאלה 7.1 [בינוני]
מה ערך perms אחרי perms = 0x0F; perms &= ~0x05;?
✓ תשובה נכונה: ב — ~0x05 = ...11111010. 0x0F(1111) & 0xFA = 1010 = 0x0A. תבנית 'ניקוי דגלים': AND עם השלילה של המסכה מכבה ביטים נבחרים.
✓ תשובה: ד) 28 — int(4) + float(4) + char[20](20) = 28 bytes. בדוגמה זו אין Padding כי כל שדה מיושר כבר.
// Structs מעמיק (ממוזג)
Padding ו-Alignment: למה sizeof גדול ממה שציפינו
Alignment הוא הכלל שכתובת של משתנה מסוג T חייבת להיות מחולקת ב-sizeof(T). הקומפיילר מוסיף Padding (bytes ריקים) בין שדות — זה גורם לstruct להיות גדול יותר מסכום שדותיו!
כללי Alignment
טיפוס
Alignment
הסבר
char
1
כל כתובת תקפה
short
2
כתובת חייבת להיות זוגית
int / float
4
כתובת חייבת להיות כפולה של 4
double / pointer (64bit)
8
כתובת חייבת להיות כפולה של 8
// ─── דוגמה עם Padding ─────────────────────────────────
struct Mixed {
char a; // offset 0 (1 byte)
// [Padding: 3 bytes] ← כדי ליישר int ל-offset 4
int b; // offset 4 (4 bytes)
char c; // offset 8 (1 byte)
// [Padding: 3 bytes] ← Trailing Padding (כפולה של 4)
};
// sizeof(struct Mixed) = 12 (לא 6!)
// ─── שינוי סדר שדות חוסך Padding! ──────────────────
struct Bad { struct Good {
char a; // off 0 int b; // off 0
// pad 3 char a; // off 4
int b; // off 4 char c; // off 5
char c; // off 8 // pad 2
// pad 3 }; // sizeof = 8
}; // sizeof = 12
✓ תשובה: ב) 16 — char(0), PAD×3, int(4..7), char(8), PAD×7 (double דורש alignment 8, הבא שמתחלק ב-8 אחרי offset 9 = offset 16), double(16..23).
// Structs מעמיק (ממוזג)
union: שיתוף זיכרון בין שדות
union הוא כמו struct אבל כל השדות חולקים את אותו מקום בזיכרון. גודל union = גודל השדה הגדול ביותר. רק שדה אחד תקף בכל זמן — נפוץ ב-Protocol parsing, Type Punning, ו-Exploitation.
// ─── union קלאסי ─────────────────────────────────────────
union Data {
int i; // 4 bytes
float f; // 4 bytes
char c[4]; // 4 bytes
};
// sizeof(union Data) = 4 (גדול ביניהם = 4)
// כל השדות מתחילים ב-offset 0!
union Data d;
d.i = 0x41424344; // כתוב int
printf("%c\n", d.c[0]); // קרא כ-char[0]
// ב-Little-Endian: d.c[0]='D'(0x44), d.c[1]='C', d.c[2]='B', d.c[3]='A'
// ─── Type Punning: קריאת float כ-int ──────────────────
union FloatInt {
float f;
int i;
};
union FloatInt fi;
fi.f = 3.14f;
printf("%08X\n", fi.i); // הייצוג הבינארי של 3.14f = 0x4048F5C3
// ─── Tagged Union (שכיח ב-C ל-OOP-like) ──────────────
typedef struct {
int type; // 0=int, 1=float, 2=string
union {
int ival;
float fval;
char *sval;
} data;
} Value;
🔑 חשוב לגאמא — Type Confusion ב-CTF
Type Confusion מתרחש כשתוקף גורם לתוכנה לפרש שדה union כסוג שגוי:
◄ תוכנה כותבת ל-data.ival (int), ואחר כך קוראת data.sval (char*) כ-Pointer
◄ תוקף שולט ב-ival → שולט ב-Pointer שdata.sval יצביע עליו → Read/Write לכל כתובת
◄ נפוץ ב-Browser Exploitation: JavaScript engine objects עם Tagged Union
✏תרגול עצמי3 שאלות
שאלה 3.1 — sizeof union
union Big {
char c; // 1 byte
int i; // 4 bytes
double d; // 8 bytes
};
printf("%zu", sizeof(union Big));מה יודפס?
✓ תשובה: א) 8 — sizeof(union) = גודל השדה הגדול ביותר = sizeof(double) = 8. כל השדות חולקים את אותם 8 bytes.
שאלה 3.2 — Type Punning
union { int i; char c[4]; } u;
u.i = 0x12345678;
// Little-Endian x86
printf("%02X %02X", u.c[0], u.c[1]);מה יודפס?
✓ תשובה: א) 78 56 — ב-Little-Endian, ה-LSB (0x78) נמצא בכתובת הנמוכה ביותר (c[0]), הבא (0x56) ב-c[1]. 0x12345678 → c[0]=0x78, c[1]=0x56.
שאלה 3.3 — ההבדל struct/union
מה ההבדל המרכזי בין struct לבין union?
✓ תשובה: א) — struct: גודל = סכום כל השדות (+Padding). union: גודל = שדה הגדול ביותר. כתיבה לשדה אחד ב-union דורסת את האחרים.
// Structs מעמיק (ממוזג)
Bitfields: ניהול ביטים בתוך שדות
Bitfields מאפשרים להגדיר שדות שתופסים מספר ביטים בלבד — לא byte שלם. נפוצים מאוד ב-Network Protocol Headers, Hardware Registers, ו-Flags Fields.
// ─── Bitfield קלאסי: פרוטוקול רשת ──────────────────────
struct IPHeader {
unsigned int version : 4; // 4 ביטים — IP version (4 או 6)
unsigned int ihl : 4; // 4 ביטים — Internet Header Length
unsigned int tos : 8; // 8 ביטים — Type of Service
unsigned int tot_len : 16; // 16 ביטים — Total Length
};
// sizeof(struct IPHeader) = 4 (בסה"כ 32 ביטים = 4 bytes)
struct IPHeader hdr;
hdr.version = 4; // IPv4
hdr.ihl = 5; // Header = 5 * 4 = 20 bytes
hdr.tot_len = 1500;
// ─── Bitfield עם Flags ────────────────────────────────
struct Permissions {
unsigned int read : 1; // ביט 0
unsigned int write : 1; // ביט 1
unsigned int execute : 1; // ביט 2
unsigned int : 29; // 29 ביטים padding
};
struct Permissions perm = {1, 0, 1}; // read + execute
printf("%d\n", perm.read); // 1
printf("%d\n", perm.write); // 0
printf("%d\n", perm.execute); // 1
! מגבלות Bitfield — חשוב! ›
◄ sizeof עם Bitfield — הקומפיילר מעגל לגודל int (4 bytes) גם אם סה"כ הביטים קטן יותר
◄ אי אפשר לקחת כתובת — &hdr.version → שגיאת קומפילציה!
◄ סדר ביטים — האם ביטים ממלאים מ-LSB ל-MSB או הפוך — Implementation Defined! עבור Network Protocols: תמיד בדקו byte order ו-bit order.
◄ לProtocol Parsing אמיתי: השתמשו ב-packed struct + bit manipulation ידני.
✏תרגול עצמי3 שאלות
שאלה 4.1 — sizeof Bitfield
struct Flags {
unsigned int a : 3;
unsigned int b : 5;
unsigned int c : 8;
};
printf("%zu", sizeof(struct Flags));מה יודפס?
✓ תשובה: ב) 4 — סה"כ 3+5+8=16 ביטים = 2 bytes, אבל הקומפיילר מעגל ל-sizeof(unsigned int) = 4 bytes. Bitfields נארזים בתוך יחידת ה-underlying type.
שאלה 4.2 — מגבלת Bitfield
מה קורה אם ניסה לקחת כתובת של שדה Bitfield: &hdr.version?
✓ תשובה: ג) שגיאת קומפילציה — ביטים אינם ניתנים לכתובת. כתובת היא granularity של byte. לא ניתן להגדיר pointer ל-Bitfield.
שאלה 4.3 — שימוש ב-Bitfields
מהו היתרון העיקרי של Bitfields לעומת שימוש ב-#define ו-bit masking ידני?
Network Byte Order = Big-Endian. אם Port מספר 80 נשלח ברשת, איך נראה ב-hex dump של הpacket?
✓ תשובה: ג) 00 50 — Port 80 = 0x0050. ב-Big-Endian (Network): MSB קודם → 00 50. אם היה Little-Endian: 50 00. htons(80) = 0x5000 (בייצוג LE) אבל על הרשת נשלח 00 50.
שאלה 5.3 — Endian ב-RE
ב-IDA/Ghidra, Hex Dump מראה: 78 56 34 12 בכתובת 0x8048000. איזה int מאוחסן שם (x86)?
✓ תשובה: ב) 0x12345678 — ב-Little-Endian x86: 78=LSB בכתובת נמוכה. קוראים הפוך: 78 56 34 12 → 0x12345678. זוהי אחת הבלבלות הנפוצות ב-RE!
// Structs מעמיק (ממוזג)
Struct ב-Assembly: שחזור Layout מ-RE
כשעושים Reverse Engineering לבינארי, שחזור Struct Layout מ-Assembly הוא מיומנות ליבה. כל [reg+N] נותן לנו רמז על offset ב-Struct.
; ─── קוד Assembly לניתוח ──────────────────────────────────
process_obj: ; [ebp+8] = Pointer לObject כלשהו
push ebp
mov ebp, esp
mov eax, [ebp+8] ; eax = Pointer לObject
mov ecx, [eax+0] ; (A) קרא int מ-Offset 0
cmp ecx, 0
jz .skip
mov edx, [eax+4] ; (B) קרא int מ-Offset 4
add edx, 1
mov [eax+4], edx ; (C) כתוב int ל-Offset 4
lea ebx, [eax+8] ; (D) קח כתובת Offset 8
push ebx
call strlen ; (E) strlen → Offset 8 הוא String!
add esp, 4
mov [eax+28], al ; (F) כתוב byte ל-Offset 28
.skip:
pop ebp
ret
הרחבה שחזור ה-Struct — ניתוח מלא ›
(A) offset 0 → int — נקראת ובגדולה (cmp) → כנראה 'type' או 'flags'
(B/C) offset 4 → int — מוגדל ב-1 (counter) → כנראה 'count' או 'refcount'
ממש struct_size(fields) המחשב את sizeof של struct ב-C לפי כללי היישור. fields היא רשימת טיפוסים; היישור הטבעי שווה לגודל: char=1, short=2, int=4, long/double/ptr=8, float=4. כל שדה מיושר לגבול הגודל שלו (padding בין שדות), ובסוף המבנה מרופד לגבול השדה הגדול ביותר (tail padding).
דוגמאות
struct_size(['char','int','char'])→12
struct_size(['char','char','int'])→8
struct_size(['double','char'])→16
הפתרון נשמר מקומית ומשותף עם עמוד „כתיבת קוד”.
פתרון-ייחוס חישוב sizeof עם Padding ›
def struct_size(fields: list) -> int:
sz = {'char':1,'short':2,'int':4,'long':8,'double':8,'ptr':8,'float':4}
off = 0; maxa = 1
for f in fields:
s = sz[f]; maxa = max(maxa, s)
if off % s: off += s - (off % s) # padding ליישור השדה
off += s
if off % maxa: off += maxa - (off % maxa) # tail padding ליישור המבנה
return off
// סיכום
סיכום — שיעור 25
✓מה למדנו בשיעור 25
Struct: הגדרה, Dot Operator, אתחול עם שמות שדות
typedef: קיצורי דרך לשמות Struct — קוד נקי ומקצועי
Padding ו-Alignment: הקומפיילר מוסיף bytes ריקים ליישור שדות בזיכרון
Bit Fields: שדות בגודל ביטים ב-Struct — נוח לקריאה
Endianness: Little (x86) vs Big (רשת) — htonl/ntohl להמרה
לפני שיעור 26 (Assembly x86):
1. כתבו Struct שמייצג DNS Query Header — איזה Bit Fields נחוצים?
2. כתבו פונקציות Set/Clear/Toggle/Check לכל 8 ביטי uint8_t
3. בדקו: sizeof(struct { char a; int b; char c; }) על המחשב שלכם
4. שיעור 26: Assembly x86 — רגיסטרים, MOV, ADD ופקודות בסיסיות
שאלות תרגול ותשובות (10)
שאלה 1 · למה שקול הביטוי ptr->field?
ptr->field
א. (*ptr).field
ב. *(ptr.field)
ג. &ptr.field
ד. ptr + field
תשובה: א
החץ הוא קיצור תחבירי: קודם מפנים את המצביע ואז ניגשים לשדה. נקודה משמשת למשתנה עצמו, חץ למצביע אליו.
שאלה 2 · מה קורה בהשמה של struct שלם למשתנה אחר?
Point p2 = p1;
א. נוצרת העתקה שטחית של כל הבתים — p2 עותק עצמאי
ב. p2 מצביע לאותו זיכרון של p1
ג. הקומפיילר יזרוק שגיאה — אסור להשים structs
ד. רק השדה הראשון מועתק
תשובה: א
C מעתיק את כל הבתים (Shallow copy). זהירות: אם ה-struct מכיל מצביע, שני העותקים יצביעו לאותו בלוק זיכרון.
שאלה 3 · מדוע ה-struct הבא תופס 8 בתים ולא 5?
struct { char a; int b; };
א. הקומפיילר מוסיף Padding כדי ליישר את int לכתובת המתחלקת ב-4
ב. char תופס בפועל 4 בתים
ג. int תופס 7 בתים במערכות 64 ביט
ד. הקומפיילר שומר בית לסיום המחרוזת
תשובה: א
🎯 דגש למבחן — Padding: המעבד קורא זיכרון בבלוקים מיושרים, ולכן הקומפיילר מוסיף בתי ריפוד כדי שכל שדה יתחיל בכתובת המתחלקת בגודלו, וגודל ה-struct הכולל מתחלק ביישור הגדול ביותר. סידור השדות מהגדול לקטן מקטין את הריפוד.
שאלה 4 · מה ההבדל המרכזי בין struct ל-union מבחינת זיכרון?
א. struct מקצה זיכרון לכל השדות יחד; union מקצה בגודל השדה הגדול ביותר וכולם חולקים אותו
ב. union מהיר יותר אבל תופס פי שניים
ג. ב-union כל שדה מקבל עותק משלו
ד. אין הבדל — רק שמות שונים
תשובה: א
ב-union כתיבה לשדה אחד דורסת את האחרים, כי כולם ממופים לאותה כתובת. זה שימושי לחיסכון בזיכרון ולפירוש אותם בתים בשתי דרכים.
שאלה 5 · איך בודקים Endianness של המחשב באמצעות union?
union { int i; char c[4]; } u;
u.i = 1;
if (u.c[0] == 1) { /* ? */ }
א. c[0]==1 מעיד על Little Endian — הבית הפחות משמעותי נשמר ראשון
ב. c[0]==1 מעיד על Big Endian
ג. הבדיקה תמיד נכשלת — union לא תומך במערכים
ד. התוצאה תלויה בגרסת הקומפיילר בלבד
תשובה: א
ב-Little Endian (Intel/AMD) הבית הנמוך נשמר בכתובת הנמוכה, ולכן הערך 1 מופיע ב-c[0]. בפרוטוקולי רשת משתמשים ב-Big Endian — ומכאן הצורך ב-htons/ntohs.
שאלה 6 · מה תוצאת החישוב?
10 | 6 // 1010 | 0110
א. 14
ב. 2
ג. 4
ד. 16
תשובה: א
OR מדליק כל ביט שדלוק לפחות באחד מהצדדים: 1110 = 14. לשם השוואה 10 & 6 = 2, ו-10 ^ 6 = 12.
שאלה 7 · מהי הדרך המהירה להכפיל את x ב-8?
x << 3
א. הזזה שמאלה ב-3 — כל הזזה בסיבית אחת מכפילה ב-2
ב. הזזה ימינה ב-3
ג. x & 8
ד. x ^ 8
תשובה: א
🎯 דגש למבחן — פעולות ביטיות: הזזה שמאלה ב-n שקולה לכפל ב-2ⁿ, והזזה ימינה לחלוקה שלמה ב-2ⁿ. במספרים חיוביים זה בטוח; בשליליים ובגלישה מעבר לרוחב הטיפוס ההתנהגות עלולה להפתיע.
שאלה 8 · מהי הבעיה בקוד הבא?
if (flags & 4 == 4) { ... }
א. אופרטור ההשוואה קודם ל-& , ולכן בפועל מחושב flags & (4==4) — חסרים סוגריים
ב. אין בעיה, הקוד תקין
ג. צריך להשתמש ב-&& במקום &
ד. אי אפשר להשוות מסכות ביטים
תשובה: א
הכתיבה הנכונה היא if ((flags & 4) == 4). זו טעות קלאסית בשאלות מיון — קדימות אופרטורים בבדיקות מסכה.
שאלה 9 · איך מכבים ביט מסוים ב-flags?
flags &= ~(1 << n);
א. AND עם המסכה ההפוכה מאפס רק את הביט הרצוי ומשאיר את השאר
struct TcpFlags {
unsigned int fin : 1;
unsigned int syn : 1;
unsigned int ack : 1;
};
א. הם מאפשרים להקצות מספר סיביות מדויק לשדה, אך סדר האריזה תלוי-קומפיילר
ב. כל שדה עדיין תופס 4 בתים מלאים
ג. אפשר לקחת כתובת של שדה bit field עם &
ד. הם נתמכים רק ב-C++
תשובה: א
Bit Fields חוסכים זיכרון ומתאימים לתיאור דגלי TCP או רגיסטרים, אבל סדר הביטים והריפוד אינם מוגדרים בתקן — ולכן בקוד רשת אמיתי מעדיפים מסכות ביטים מפורשות.