Assembly x86
Assembly x86 — רגיסטרים ופקודות בסיס
Assembly היא שכבת הביניים בין C לקוד מכונה. כל מנתח אבטחה, חוקר חולשות ושחקן CTF חייב לקרוא Assembly. זה השיעור שפותח את הדלת.
- שרשרת הקומפילציה: C → Assembly → קוד מכונה
- מחזור CPU: Fetch-Decode-Execute
- 8 רגיסטרים כלליים + EIP + FLAGS
- חלוקת רגיסטרים: 32b / 16b / 8b (EAX → AX → AH/AL)
- פקודות: MOV, ADD, SUB, INC, DEC, MUL, DIV, PUSH, POP
- Addressing Modes: Immediate, Register, Direct, Indirect
- Intel Syntax לעומת AT&T Syntax
- C↔Assembly: if, for, function call — כל תבנית
- שיטת Dry Run — עוקבים שורה-שורה
למה Assembly? — מ-C לקוד מכונה
כשכותבים C, המעבד לא מבין — הוא מבין רק 0 ו-1. התרגום עובר 4 שלבים, וה-Assembly הוא שכבת ביניים טקסטואלית.
| שלב | קלט | פלט | כלי |
|---|---|---|---|
| 1. Preprocessing | code.c | code.i (אחרי #include, #define) | cpp |
| 2. Compilation | code.i | code.s — Assembly טקסטואלי | cc1 |
| 3. Assembly | code.s | code.o — Object File | as |
| 4. Linking | code.o + libs | executable | ld |
| תחום | מה עושים | למה Assembly |
|---|---|---|
| Reverse Engineering | ניתוח תוכנות ללא קוד מקור | קובץ exe הוא Assembly — אין מקור |
| Exploit Development | כתיבת Shellcode | Shellcode הוא Assembly גולמי |
| Malware Analysis | הבנת וירוס/טרויאני | Malware קומפל ל-ASM |
| CTF — Pwn | פיצוח Binary | קריאת ASM של היעד |
| מחשבון קרב גאמא | שאלות Assembly בכנס | קריטי ביותר ← |
2. זיהוי Pattern: מה עושה קטע ה-Assembly? (if/loop/function call)
3. הפיכה: כתבו ב-C את המקבילה לקוד ASM
אין צורך לשנן Opcodes — צריך להבין מה כל פקודה עושה.
רגיסטרים — 8 תאי הזיכרון של המעבד
רגיסטרים הם תאי האחסון המהירים ביותר — ממש בתוך המעבד. ב-x86 יש 8 רגיסטרים כלליים של 32 ביט, בנוסף ל-EIP ו-FLAGS.
| 32b | 64b | שם מלא | תפקיד מסורתי |
|---|---|---|---|
| EAX | RAX | Accumulator | תוצאות חישוב, ערך החזרה מפונקציות |
| EBX | RBX | Base | בסיס לכתובות — General purpose בפועל |
| ECX | RCX | Counter | מונה לולאות ו-REP |
| EDX | RDX | Data | נתונים, זוג עם EAX ב-MUL/DIV |
| ESI | RSI | Source Index | מקור בפעולות מחרוזת |
| EDI | RDI | Destination Index | יעד בפעולות מחרוזת |
| ESP | RSP | Stack Pointer | ראש ה-Stack — אסור לשנות ידנית! |
| EBP | RBP | Base Pointer | בסיס Stack Frame — גישה ל-locals וargs |
חלוקת רגיסטרים: 32b / 16b / 8b
; רגיסטר EAX — ניתן לגשת לחלקים שלו:
; ┌──────────────── EAX (32 bits) ──────────────────┐
; │ ┌──── AX (16 bits) ────┐ │
; │ │ ┌──AH──┐ ┌──AL──┐ │ │
; │ │ │ 8bit │ │ 8bit │ │ │
; └──────────────┴──┴──────┴─┴──────┴──┴────────────┘
mov eax, 0x12345678
; AX = 0x5678 (16 ביטים תחתונים)
; AH = 0x56 (8 ביטים עליונים של AX)
; AL = 0x78 (8 ביטים תחתונים של AX)
; למה זה חשוב?
; כשרואים 'al', 'cx', 'dx' ב-Assembly —
; אלה חלקים מרגיסטרים שכבר מכירים!EIP ו-FLAGS — רגיסטרים מיוחדים
| רגיסטר | שם | מה מכיל | מי משנה |
|---|---|---|---|
| EIP | Instruction Pointer | כתובת הפקודה הבאה | כל פקודה (אוטומטית) + JMP/CALL |
| EFLAGS | Flags Register | ביטים שמתעדים תוצאת פקודות | ADD, SUB, CMP... |
| Flag | שם | מתי מדלק | דוגמה |
|---|---|---|---|
| ZF | Zero Flag | תוצאה = 0 | SUB EAX,EAX → ZF=1 |
| SF | Sign Flag | תוצאה שלילית | SUB 5 מ-3 → SF=1 |
| CF | Carry Flag | Overflow חסר-סימן | ADD גדול מ-FFFFFFFF → CF=1 |
| OF | Overflow Flag | Overflow עם-סימן | 127+1 ב-int8 → OF=1 |
SF דולק כאשר התוצאה שלילית — a < b.
JE בודק ZF. JL בודק SF. אחרי CMP EAX,5 כשEAX=5: ZF=1, SF=0.
mov eax, 0xABCD1234?mov eax, 0?פקודות בסיסיות — MOV, ADD, SUB ועוד
90% מהקוד שתראו משתמש ב-15–20 פקודות בלבד. נלמד כל אחת עם דוגמאות וכשלים נפוצים.
MOV — הפקודה הכי נפוצה
; תחביר Intel: פקודה יעד, מקור (dst תמיד ראשון!)
; MOV dst, src → dst = src
mov eax, 42 ; EAX = 42 (Immediate)
mov ebx, eax ; EBX = EAX (רגיסטר לרגיסטר)
mov ecx, [0x1000] ; ECX = RAM[0x1000] (מהזיכרון)
mov [0x2000], eax ; RAM[0x2000] = EAX (לזיכרון)
mov [ebx], ecx ; RAM[EBX] = ECX (עקיף)
; ❌ אי אפשר:
; mov [0x1000], [0x2000] ; זיכרון→זיכרון ישיר — אסור!
; ⚠️ חשוב: MOV לא משנה FLAGS!
; mov eax, 0 — ZF עדיין לא השתנהADD, SUB, INC, DEC
mov eax, 10
mov ebx, 3
add eax, ebx ; EAX = 13, ZF=0, SF=0
sub eax, 13 ; EAX = 0, ZF=1 (!)
inc eax ; EAX = 1, ZF=0, SF=0
dec eax ; EAX = 0, ZF=1
; ⚠️ INC/DEC לא משנות CF — הבדל מ-ADD/SUB!MUL ו-DIV
; MUL src → EAX = EAX * src
; תוצאה: EDX:EAX (64 ביטים!)
mov eax, 5
mov ebx, 6
mul ebx ; EAX = 30, EDX = 0
; DIV src → חלק EDX:EAX ב-src
; מנה → EAX, שארית → EDX
; ⚠️ חובה לאפס EDX לפני DIV!
mov eax, 17
mov edx, 0 ; חובה!
mov ebx, 5
div ebx ; EAX = 3 (מנה), EDX = 2 (שארית)PUSH ו-POP
; PUSH src → ESP -= 4; [ESP] = src (Stack גדל למטה!)
; POP dst → dst = [ESP]; ESP += 4
push eax ; שמור EAX
push ebx ; שמור EBX
; ... משהו ששינה EAX, EBX ...
pop ebx ; שחזר EBX (LIFO!)
pop eax ; שחזר EAXmov eax,17; mov edx,0; mov ebx,5; div ebx?mov edx,0 לפני DIV?push eax; push ebx; pop eax; pop ebx — מה קרה?Addressing Modes — מאיפה האופרנד?
מצב מיעון מגדיר מאיפה מגיע הערך — מספר ישיר, רגיסטר, כתובת זיכרון, או שילוב. הבנת זה היא הבסיס לקריאת כל קטע Assembly.
| מצב | דוגמה | מה קורה |
|---|---|---|
| Immediate | mov eax, 42 | EAX = 42 (הערך עצמו) |
| Register | mov eax, ebx | EAX = ערך EBX |
| Direct Memory | mov eax, [0x4000] | EAX = RAM[0x4000] |
| Indirect | mov eax, [ebx] | EAX = RAM[EBX] |
| Indirect+Disp | mov eax, [ebp-4] | EAX = RAM[EBP-4] — משתנה מקומי! |
| Indexed*Scale | mov eax, [ebx+ecx*4] | EAX = arr[ecx] כשאיברים = 4B |
; [base + displacement] — הנפוץ ביותר!
mov eax, [ebp - 4] ; ebp-4 = Variable מקומי ראשון
mov eax, [ebp - 8] ; ebp-8 = Variable מקומי שני
mov eax, [ebp + 8] ; ebp+8 = ארגומנט ראשון לפונקציה
; [base + index*scale] — גישה למערך
mov eax, [ebx + ecx*4] ; arr[ecx] כשint=4 bytes
; מה שהקומפיילר מייצר ל: int arr[5]; arr[i] = 10;
mov [ebx + ecx*4], 10 ; EBX=&arr[0], ECX=iיש [] → זו כתובת! לך ל-RAM לכתובת הזו וקרא מה שם
[ebp - 4] = לך לכתובת שהיא EBP-4, קרא מה שם — לא חישוב מספרי!
Intel Syntax לעומת AT&T Syntax
| מה | Intel Syntax | AT&T Syntax | הערה |
|---|---|---|---|
| סדר אופרנדים | dst, src | src, dst | הפוך לחלוטין! |
| Immediate | mov eax, 5 | movl $5, %eax | $ לפני מיידי, % לפני רגיסטר |
| זיכרון | [ebx] | (%ebx) | סוגריים עגולים |
| גודל פקודה | מהקשר | movl/movw/movb | l=32b, w=16b, b=8b |
| כלים | NASM, IDA | gcc -S, GDB | שני הסביבות |
mov eax, [ebx + ecx*4] ?C↔Assembly ו-Dry Run
הדרך הכי טובה להבין Assembly היא לראות מה הקומפיילר מייצר לקוד C שאנחנו מכירים. ו-Dry Run הוא הכלי לענות על שאלות מיון.
if/else ב-Assembly
; C: if (eax > 0) { ebx = 1; } else { ebx = -1; }
cmp eax, 0 ; חישוב eax-0, עדכן FLAGS
jle .else_branch ; אם eax <= 0 → קפוץ ל-else
; ── then: ──
mov ebx, 1
jmp .end
; ── else: ──
.else_branch:
mov ebx, -1
.end:
; ⚠️ הקפיצה היא ה-OPPOSITE של התנאי!
; if (a>b) → JLE (קפוץ לעקוף אם לא גדול)לולאת for
; C: int sum=0; for(int i=0; i<10; i++) { sum += i; }
mov eax, 0 ; sum = 0
mov ecx, 0 ; i = 0
.loop_start:
cmp ecx, 10
jge .loop_end ; אם i >= 10 → יצא
add eax, ecx ; sum += i
inc ecx ; i++
jmp .loop_start
.loop_end:
; EAX = 0+1+2+...+9 = 45שיטת Dry Run
; מה ערך EAX בסוף?
mov eax, 10 ; (1)
mov ebx, 3 ; (2)
sub eax, ebx ; (3)
mov ecx, eax ; (4)
imul ecx, 2 ; (5)
add eax, ecx ; (6)| # | פקודה | EAX | EBX | ECX | הסבר |
|---|---|---|---|---|---|
| 1 | mov eax, 10 | 10 | ? | ? | EAX=10 |
| 2 | mov ebx, 3 | 10 | 3 | ? | EBX=3 |
| 3 | sub eax, ebx | 7 | 3 | ? | 10-3=7 |
| 4 | mov ecx, eax | 7 | 3 | 7 | ECX=7 |
| 5 | imul ecx, 2 | 7 | 3 | 14 | 7×2=14 |
| 6 | add eax, ecx | 21 | 3 | 14 | 7+14=21 ✓ |
mov eax,15; mov ebx,4; mov ecx,0; add ecx,eax; div ebx; sub ecx,edxCMP + JLE(.else) + [code] + JMP(.end) + .else: [code] + .end:?push 7; push 3; call add_func כשadd_func מחשבת a+b?תרגול נוסף — Assembly: אריתמטיקה ו-Stack Frame
שאלות העשרה נוספות לחיזוק החומר של השיעור — באותו פורמט בחירה של הקורס.
EAX אחרי הרצת הקוד?mov eax, 10 mov ebx, 3 sub eax, ebx add eax, ebx imul eax, ebx
xor eax, eax ולמה הוא עדיף על mov eax, 0?[ebp+8] וב-[ebp-4] בתוך פונקציה?אתגר מעשי — Assembly — Dry-Run
שיא השיעור: לרדת לרמת-המכונה — C, אסמבלי, היפוך. ההגשה נבדקת אוטומטית.
סיכום — שיעור 26
- שרשרת קומפילציה: C → Preprocessing → Assembly → Object → Executable
- מחזור CPU: Fetch-Decode-Execute — 3 מיליארד פעמים בשנייה
- 8 רגיסטרים: EAX/EBX/ECX/EDX (נתונים), ESI/EDI, ESP/EBP (Stack)
- EAX → AX → AH/AL — חלקים שונים של אותו רגיסטר
- EIP: מצביע ההוראות. Buffer Overflow = שכתוב EIP
- FLAGS: ZF (=0), SF (שלילי), CF (Unsigned overflow), OF (Signed overflow)
- MOV: לא משנה FLAGS. dst תמיד ראשון ב-Intel Syntax
- MUL: תוצאה ב-EDX:EAX. DIV: מנה ב-EAX, שארית ב-EDX — חובה לאפס EDX!
- Addressing Modes: [], [reg+disp], [reg+reg*scale] — הסוגריים = זיכרון
- if → CMP+Jcc(הפוך), for → init+CMP+Jcc+inc+JMP, CALL → return ב-EAX
- Dry Run: עקיבה שורה-שורה עם טבלת רגיסטרים — הכלי לשאלות מיון
2. Dry Run: מה EAX אחרי
MOV EAX,7 / ADD EAX,EAX / IMUL EAX,3 / SUB EAX,5?3. שיעור 27: FLAGS, קפיצות מותנות, לולאות — הלב של זרימת תוכנית
שאלות תרגול ותשובות (10)
שאלה 1 · איזה שלב בשרשרת הבנייה מתרגם קוד C ל-Assembly טקסטואלי?
- א. הקומפיילר
- ב. האסמבלר
- ג. הלינקר
- ד. הטוען (Loader)
הקומפיילר מייצר Assembly, האסמבלר הופך אותו לקוד מכונה (Object), והלינקר מחבר את הקבצים לתוכנית אחת.
שאלה 2 · מה יהיה ערך AL אחרי הפקודה?
mov eax, 0xABCD1234
- א. 0x34
- ב. 0x12
- ג. 0xAB
- ד. 0x1234
🎯 דגש למבחן — EAX/AX/AL: EAX הוא 32 ביט, AX הם 16 הביטים הנמוכים, AL הבית הנמוך ביותר ו-AH הבית שמעליו. לכן AL=0x34 ו-AH=0x12. שימו לב שזה חיתוך לוגי של הרגיסטר — לא קשור לסדר האחסון בזיכרון, שם נכנס Endianness: אותו ערך שנשמר ל-RAM ב-Little Endian ייכתב כ-34 12 CD AB.
שאלה 3 · מה עושה הפקודה?
lea eax, [ebx + ecx*4 + 8]
- א. מחשבת את הכתובת ומכניסה אותה ל-EAX בלי לגשת לזיכרון
- ב. טוענת את הערך שנמצא בכתובת הזו
- ג. כותבת את EAX לכתובת שחושבה
- ד. מקצה 8 בתים על המחסנית
🎯 דגש למבחן — LEA: הפקודה משתמשת ביחידת חישוב הכתובות (AGU) כדי לבצע כפל וחיבור בפעולה אחת מהירה, ומחזירה את התוצאה עצמה — בלי גישה ל-RAM. לכן קומפיילרים משתמשים בה כקיצור אריתמטי. ההבדל מ-MOV: mov eax, [..] היה טוען את התוכן שבכתובת.
שאלה 4 · מה עושה מצב הכתובוּת הבא?
mov eax, [ebx + ecx*4]
- א. גישה לאיבר במערך של int — EBX בסיס, ECX אינדקס, 4 גודל האיבר
- ב. מחבר את EBX ל-ECX פי 4
- ג. מזיז את ECX שמאלה פי 4
- ד. שומר את EAX למערך
זהו Scaled Index Addressing — התבנית הקלאסית של arr[i] בשפת C, כאשר הסקאלה שווה לגודל האיבר.
שאלה 5 · מדוע רושמים mov edx, 0 לפני DIV?
mov eax, 17 mov edx, 0 mov ebx, 5 div ebx
- א. DIV מחלקת את הזוג EDX:EAX — זבל ב-EDX יגרום לתוצאה שגויה או לחריגה
- ב. זו דרישה תחבירית של האסמבלר בלבד
- ג. EDX מאחסן את התוצאה ולכן צריך לאפס
- ד. כדי לאפס את דגל הקרי
DIV מחלקת מספר בן 64 ביט (EDX:EAX). כאן התוצאה: EAX=3 (מנה) ו-EDX=2 (שארית). EDX מלוכלך עלול לגרום ל-Divide Error.
שאלה 6 · מה מצב הרגיסטרים אחרי הקוד?
push eax push ebx pop eax pop ebx
- א. הערכים של EAX ו-EBX התחלפו
- ב. שניהם מתאפסים
- ג. שום דבר לא השתנה
- ד. המחסנית נשארת לא מאוזנת
המחסנית היא LIFO: הערך האחרון שנדחף (EBX) יוצא ראשון ונכנס ל-EAX. זהו טריק סטנדרטי להחלפת ערכים.
שאלה 7 · למה xor eax, eax עדיף על mov eax, 0?
xor eax, eax
- א. שתי הפקודות מאפסות, אבל XOR קצרה יותר בבתים ומהירה יותר
- ב. XOR מאפס גם את הזיכרון
- ג. MOV לא באמת מאפס את הרגיסטר
- ד. XOR משמר את הדגלים
כל ערך ב-XOR עם עצמו נותן 0. הפקודה תופסת 2 בתים לעומת 5, ולכן זהו האידיום המקובל לאיפוס (הוא כן משנה דגלים, למשל ZF=1).
שאלה 8 · מה מכיל [ebp+8] בתוך פונקציה במוסכמת cdecl?
- א. הפרמטר הראשון שהועבר לפונקציה
- ב. המשתנה המקומי הראשון
- ג. כתובת החזרה
- ד. הערך המוחזר מהפונקציה
ביחס ל-EBP: [ebp] הוא ה-EBP השמור, [ebp+4] כתובת החזרה, [ebp+8] הפרמטר הראשון. המשתנים המקומיים יושבים מתחת — [ebp-4] וכן הלאה.
שאלה 9 · איזה מבנה בשפת C מייצגת התבנית הבאה?
cmp eax, 0 jle .else mov ebx, 1 jmp .end .else: mov ebx, -1 .end:
- א. if / else
- ב. לולאת while
- ג. קריאה לפונקציה
- ד. משפט switch עם טבלת קפיצה
השוואה, קפיצה מותנית לענף השני, ובסוף הענף הראשון קפיצה בלתי-מותנית מעל ה-else — זו החתימה הקלאסית של if/else.
שאלה 10 · Dry Run — מה ערך EAX ו-EDX בסוף?
mov eax, 15 mov ebx, 4 mov edx, 0 div ebx
- א. EAX = 3, EDX = 3
- ב. EAX = 4, EDX = 0
- ג. EAX = 3, EDX = 4
- ד. EAX = 60, EDX = 0
15 חלקי 4 = מנה 3 עם שארית 3. המנה נכנסת ל-EAX והשארית ל-EDX. טיפ ל-Dry Run: נהלו טבלת רגיסטרים ומחקו בקו במקום למחוק ערכים — כך תוכלו לחזור אחורה ולאתר טעות.