Assembly Jumps ולולאות
זרימת תוכנית ב-Assembly — Jumps, FLAGS ולולאות
בשיעור הקודם למדנו פקודות. עכשיו — הלב של כל תוכנית: איך CPU מקבל החלטות, קופץ, ויוצר לולאות. זה מה שמופיע בכל שאלת Dry Run ו-Reverse Engineering.
- FLAGS לעומק: ZF, SF, CF, OF — מתי דולקים ומה משתמש בהם
- CMP ו-TEST — שתי פקודות ההחלטה
- JMP — קפיצה ללא תנאי
- משפחת Jcc: JE, JNE, JL, JG, JLE, JGE, JB, JA... Signed vs Unsigned
- Patterns: if, while, for, do-while ב-Assembly
- פקודת LOOP — ECX כמונה מובנה
- CALL ו-RET — Stack Frame לעומק
- NOP, INT3, LEA — פקודות מיוחדות
- Dry Run מלא עם לולאה ותנאי — שאלות מיון
FLAGS לעומק — זיכרון התוצאה
כל פקודת קפיצה מותנית מסתמכת על ביטים ב-FLAGS. ללא הבנה מוצקה של FLAGS, אי אפשר להבין קפיצות.
| Flag | שם | מתי = 1 | Jcc שמשתמש |
|---|---|---|---|
| ZF | Zero | תוצאה = 0 | JE, JZ, JNE, JNZ |
| SF | Sign | תוצאה < 0 (ביט גבוה=1) | JL, JG, JS, JNS |
| CF | Carry | Carry/Borrow (unsigned) | JB, JA, JNC, JC |
| OF | Overflow | Signed Overflow | JO, JNO |
טבלת עדכון FLAGS לפי פקודה
| פקודה | ZF | SF | CF | OF | הערה |
|---|---|---|---|---|---|
| ADD | ✓ | ✓ | ✓ | ✓ | משנה הכל |
| SUB | ✓ | ✓ | ✓ | ✓ | משנה הכל |
| CMP | ✓ | ✓ | ✓ | ✓ | כמו SUB ללא שמירת תוצאה |
| INC | ✓ | ✓ | ✗ | ✓ | לא משנה CF — הבדל מ-ADD! |
| DEC | ✓ | ✓ | ✗ | ✓ | לא משנה CF |
| MOV | ✗ | ✗ | ✗ | ✗ | לא משנה FLAGS בכלל! |
| TEST | ✓ | ✓ | 0 | 0 | כמו AND ללא שמירה |
| AND/OR/XOR | ✓ | ✓ | 0 | 0 | מאפסות CF ו-OF |
CMP ו-TEST — שתי פקודות ההחלטה
; CMP a, b → חישוב a-b, עדכן FLAGS, לא שומרת תוצאה
mov eax, 5
cmp eax, 5 ; 5-5=0 → ZF=1, SF=0, CF=0
; JE יקפוץ! (בודק ZF=1)
cmp eax, 10 ; 5-10=-5 → ZF=0, SF=1, CF=1
; JL יקפוץ! (בודק SF≠OF)
; ⚠️ CMP לא שינה EAX! הוא עדיין 5.
; TEST a, b → חישוב a AND b, עדכן FLAGS, לא שומרת
; שימוש 1: האם הרגיסטר הוא 0?
test eax, eax ; EAX AND EAX = 0 רק אם EAX=0
jz .is_zero ; קפוץ אם EAX=0
; שימוש 2: האם ביט ספציפי דלוק?
test eax, 0x80 ; בדוק ביט 7
jnz .bit7_set ; קפוץ אם ביט 7 דלוק
; TEST EAX, EAX vs CMP EAX, 0:
; שניהם בודקים ==0, אבל TEST מהיר יותר!
; קומפיילרים תמיד מייצרים TEST לבדיקת if(x==0)MOV EAX,15; MOV EBX,15; CMP EAX,EBX — מה ZF ומה EAX?TEST EAX, EAX?if(x==0).mov eax,5; sub eax,5; dec eax; inc eax?JMP ומשפחת Jcc — קפיצות
JMP = goto ב-Assembly. Jcc = קפיצה מותנית לפי FLAGS. הקומפיילר מתרגם כל if ולולאה ל-CMP+Jcc.
| פקודה | Flag שנבדק | שקולה ל-C | Signed/Unsigned |
|---|---|---|---|
| JE / JZ | ZF=1 | == | שניהם |
| JNE / JNZ | ZF=0 | != | שניהם |
| JL / JNGE | SF≠OF | < | Signed |
| JLE / JNG | ZF=1 OR SF≠OF | <= | Signed |
| JG / JNLE | ZF=0 AND SF=OF | > | Signed |
| JGE / JNL | SF=OF | >= | Signed |
| JB / JNAE | CF=1 | < | Unsigned |
| JA / JNBE | CF=0 AND ZF=0 | > | Unsigned |
int a = -1; int b = 5; if (a < b) → JL (כי -1 < 5 כ-Signed)unsigned a = 0xFFFFFFFF; → 4,294,967,295 כ-Unsigned — גדול מ-5! לא JBקומפיילר: int → JL/JG. unsigned int → JB/JA. בRE: JB/JA רומז unsigned.
Patterns: if ו-if-else
; ══ תבנית 1: if בלי else ══════════════════════
; C: if (a == b) { do_something(); }
cmp eax, ebx
jne .skip ; אם a ≠ b → דלג על הגוף
call do_something
.skip:
; ⚠️ הקפיצה היא ה-OPPOSITE של התנאי!
; if (a==b) → JNE (קפוץ כשלא שווים → עוקף הגוף)
; ══ תבנית 2: if-else ══════════════════════════
; C: if (a > b) { ecx=1; } else { ecx=-1; }
cmp eax, ebx
jle .else_part ; אם a ≤ b → קפוץ ל-else
mov ecx, 1 ; then
jmp .end_if ; דלג על else
.else_part:
mov ecx, -1 ; else
.end_if:
; 🔍 Pattern לזיהוי if-else:
; CMP → Jcc(.else) → [then] → JMP(.end) → .else: [else] → .end:MOV EAX,1; MOV EBX,2; CMP EAX,EBX; JG label_1; JMP label_2; label_1: ADD EAX,10; label_2: ADD EAX,5 — מה EAX הסופי?CALL check_password; CMP EAX,0x1337; JNE Access_Denied; MOV EBX,1. מה תנאי לגישת מנהל?if (x != 5) { y=10; } else { y=20; } — x ב-EAX, y ב-EBX?לולאות ב-Assembly — while, for, do-while
לולאות הן שילוב של Label, CMP/TEST, Jcc ו-JMP. כלל: קפיצה אחורה = לולאה. קפיצה קדימה = if.
while, for, do-while
; ══ while: בדיקה בראש ══════════════════════════
; C: while (ecx > 0) { eax += 2; ecx--; }
.while_start:
cmp ecx, 0
jle .while_end ; ecx <= 0 → יצא
add eax, 2
dec ecx
jmp .while_start
.while_end:
; ══ for: init + while + increment ══════════════
; C: for(int i=0; i<10; i++) { sum += i; }
mov ecx, 0 ; i=0 (init)
mov eax, 0 ; sum=0
.for_start:
cmp ecx, 10
jge .for_end ; i>=10 → יצא
add eax, ecx ; sum += i
inc ecx ; i++ (increment)
jmp .for_start
.for_end:
; EAX = 45
; ══ do-while: גוף לפני בדיקה ══════════════════
; C: do { eax *= 2; } while (eax < 100);
.do_start:
imul eax, 2 ; גוף (רץ לפחות פעם!)
cmp eax, 100
jl .do_start ; ← קפיצה אחורה בסוף!פקודת LOOP — ECX כמונה
; LOOP target → ECX--; אם ECX != 0 → קפוץ
; ⚠️ LOOP לא משנה FLAGS!
mov ecx, 4 ; מונה
mov eax, 1
factorial_loop:
imul eax, ecx ; eax *= ecx
loop factorial_loop ; ECX--; אם != 0 → חזור
; EAX = 1*4*3*2*1 = 24, ECX = 0for: [init] → [label]: CMP → Jcc(exit) → [body] → [incr] → JMP(label)
do-while: [label]: [body] → CMP → Jcc(label) ← קפיצה אחורה בסוף!
LOOP: [label]: [body] → LOOP(label) ← ECX כמונה
כלל: קפיצה אחורה = לולאה | קפיצה קדימה = if
MOV EAX,1; MOV ECX,4; my_loop: ADD EAX,EAX; SUB ECX,1; JNZ my_loop — מה EAX הסופי?loop_start: CMP EAX,0; _____ loop_end; DEC EAX; JMP loop_start; loop_end: — ממש while(EAX>0)mov eax,0; .loop: test ecx,1; jz .even; add eax,ecx; .even: dec ecx; test ecx,ecx; jnz .loop — מה EAX?MOV EAX,1; MOV ECX,4; loop: IMUL EAX,ECX; LOOP loop — מה EAX?CALL, RET ופקודות מיוחדות
CALL = PUSH כתובת חזרה + JMP. RET = POP + JMP. NOP ו-INT3 נפוצים ב-Exploit Development ו-Debugging.
CALL ו-RET — מה קורה ב-Stack
; CALL target → 1. PUSH EIP+N (כתובת חזרה)
; 2. JMP target
; RET → 1. POP EIP (שחזר כתובת חזרה)
; 2. JMP EIP
main:
push 7 ; arg2
push 3 ; arg1 (הפוך!)
call my_add ; PUSH [כתובת חזרה]; JMP my_add
add esp, 8 ; נקה 2 args * 4B (cdecl!)
; EAX = 10 (return value)
my_add:
push ebp
mov ebp, esp ; הגדר Stack Frame
mov eax, [ebp+8] ; arg1=3 [EBP+8]
add eax, [ebp+12] ; arg2=7 [EBP+12]
pop ebp
ret ; POP כתובת חזרה → JMP| כתובת ב-Stack | תוכן | הסבר |
|---|---|---|
| [EBP+0] | EBP ישן | PUSH EBP שמר אותו |
| [EBP+4] | כתובת חזרה | CALL שמר אותה |
| [EBP+8] | arg1 | ארגומנט ראשון |
| [EBP+12] | arg2 | ארגומנט שני |
| [EBP-4] | local var 1 | משתנה מקומי ראשון |
NOP, INT3 ו-LEA
; NOP (0x90): No Operation — EIP += 1, שום דבר אחר
; שימושים: Alignment, NOP Sled לExploits, Patching
; 90 90 90 90 [SHELLCODE] = NOP Sled
; INT 3 (0xCC): Breakpoint Exception
; Debugger מחליף byte ב-0xCC → CPU מגיע → עצירה
; כשמגדירים Breakpoint ב-IDA/GDB — זה מה שקורה
; LEA dst, [expr] → dst = כתובת (לא תוכן!)
mov eax, [ebx + 4] ; EAX = memory[EBX+4] ← קורא מזיכרון
lea eax, [ebx + 4] ; EAX = EBX+4 ← חישוב כתובת!
; LEA לחישוב מהיר:
lea eax, [ebx + ebx*4] ; EAX = EBX*5 (מהיר מ-IMUL!)mov eax, [ebx+4] ל-lea eax, [ebx+4]?Reverse Engineering — זיהוי Patterns
ברגע שמזהים "זה if", "זה while", "זה factorial" — קריאת Assembly הופכת לתרגום של C. המטרה: לזהות מה פונקציה עושה מבלי קוד מקור.
ניתוח קטע: מה הפונקציה עושה?
; ניתחו: מה קטע ה-Assembly הזה עושה?
mov eax, 1
mov ecx, 2
.start:
cmp ecx, [ebp+8] ; [ebp+8] = ארגומנט n
jg .done
imul eax, ecx
inc ecx
jmp .start
.done:
; EAX = תוצאה סופיתEAX = 1 × 2 × 3 × ... × n = n! (עצרת)
ב-C:
int factorial(int n) { int r=1; for(int i=2; i<=n; i++) r*=i; return r; }
| Pattern ב-Assembly | מה זה ב-C | סימן לזיהוי |
|---|---|---|
| CMP + Jcc(skip) + body | if (cond) { body } | קפיצה קדימה, לא JMP אחרי body |
| CMP + Jcc(else) + body + JMP(end) + else | if-else | שני labels, JMP בין גופים |
| label + CMP + Jcc(exit) + body + JMP(label) | while | JMP אחורה בסוף |
| init + label + CMP + body + inc + JMP | for | init לפני, inc בסוף |
| label + body + CMP + Jcc(label) | do-while | הבדיקה אחרי הגוף |
| CMP + JA + JMP [table + reg*4] | switch-case | Jump Table |
| PUSH args (הפוך) + CALL + ADD ESP,N | function call | PUSH לפני, ADD אחרי |
.lp: add ecx,eax; sub eax,3; cmp eax,0; jg .lp — מה ECX?סיכום — שיעור 27
- FLAGS: ZF (=0), SF (שלילי), CF (Unsigned), OF (Signed Overflow)
- MOV לא משנה FLAGS. INC/DEC לא משנות CF!
- CMP = SUB ללא שמירה. TEST = AND ללא שמירה. TEST EAX,EAX = בדיקת ==0
- Jcc: JE/JNE, JL/JG/JLE/JGE (Signed), JB/JA (Unsigned)
- Pattern if: CMP + Jcc(הפוך) + body — קפיצה OPPOSITE של התנאי
- Pattern while: label + CMP + Jcc(exit) + body + JMP(label)
- Pattern for: init + label + CMP + body + increment + JMP
- Pattern do-while: label + body + CMP + Jcc(label) — קפיצה אחורה!
- LOOP: ECX-- + JNZ בפקודה אחת. ECX=מונה מובנה
- CALL = PUSH כתובת חזרה + JMP. RET = POP + JMP
- [EBP+4]=ret addr, [EBP+8]=arg1, [EBP+12]=arg2, [EBP-4]=local1
- NOP=0x90 (NOP Sled לExploits), INT3=0xCC (Breakpoint)
- LEA: מחשב כתובת (לא קורא זיכרון). Switch → Jump Table
2. כתבו Assembly ל-fibonacci(n)
3. חשבו: CALL → PUSH כתובת חזרה. מה קורה ב-Buffer Overflow?
4. שיעור 28: Stack Frames מלאים, cdecl vs stdcall
תרגול נוסף — Assembly: לולאות ותנאים
שאלות העשרה נוספות לחיזוק החומר של השיעור — באותו פורמט בחירה של הקורס.
EAX אחרי הרצת הקוד?mov eax, 0
mov ecx, 5
.l: add eax, ecx
dec ecx
jnz .lcmp eax, 10 jge .skip add eax, 5 .skip:
אתגר מעשי — Reverse Engineering — Crackme ו-XOR
שיא השיעור: לרדת לרמת-המכונה — C, אסמבלי, היפוך. ההגשה נבדקת אוטומטית.
שאלות תרגול ותשובות (10)
שאלה 1 · מה קורה לדגלים ולרגיסטרים אחרי הקוד?
mov eax, 15 mov ebx, 15 cmp eax, ebx
- א. ZF=1 ו-EAX נשאר 15 — CMP מחסרת רק כדי לעדכן דגלים
- ב. ZF=1 ו-EAX מתאפס
- ג. ZF=0 ו-EAX נשאר 15
- ד. EAX נהיה 30
CMP מבצעת חיסור "וירטואלי": התוצאה נזרקת ורק הדגלים מתעדכנים. שני ערכים שווים ⇒ תוצאה 0 ⇒ ZF=1.
שאלה 2 · למה משמשת הפקודה הבאה, ובמה היא שונה מ-CMP?
test eax, eax
- א. היא מבצעת AND לוגי ובודקת אם EAX שווה לאפס — הדרך המהירה למימוש if (x == 0)
- ב. היא משווה בין EAX לערך קבוע 1
- ג. היא מאפסת את EAX
- ד. היא זהה לחלוטין ל-CMP EAX, EAX
🎯 דגש למבחן — TEST מול CMP: CMP מבוססת חיסור ומיועדת להשוואה בין שני ערכים (גדול/קטן/שווה). TEST מבוססת AND ומיועדת לבדיקת ביטים; test eax,eax נותנת 0 רק אם EAX אפס, כלומר ZF=1 ⇒ x==0. היא קצרה ומהירה יותר מ-cmp eax,0 ולכן זה האידיום שתראו בכל דיסאסמבלי.
שאלה 3 · מדוע חשוב להבחין בין JG ל-JA?
- א. JG לערכים מסומנים (בודקת SF/OF) ו-JA לערכים לא-מסומנים (בודקת CF) — טעות תהפוך -1 למספר ענק
- ב. JG מהירה יותר
- ג. JA עובדת רק בלולאות
- ד. אין הבדל, רק שמות נרדפים
0xFFFFFFFF הוא -1 כמסומן, אבל 4,294,967,295 כלא-מסומן. בחירה שגויה בקפיצה היא באג אבטחה קלאסי בבדיקות אורך.
שאלה 4 · ניתוח Crackme — מתי תתקבל גישת מנהל?
call check_password cmp eax, 0x1337 jne Access_Denied mov ebx, 1
- א. כאשר הפונקציה מחזירה ב-EAX בדיוק את הערך 0x1337
- ב. כאשר EAX שונה מ-0x1337
- ג. תמיד — הקפיצה לא מתבצעת
- ד. רק אם EBX כבר שווה 1
JNE קופצת החוצה כשהערכים שונים; רק שוויון מדויק ממשיך לשורה הבאה. בפריצה טיפוסית משנים JNE ל-NOP או ל-JE כדי לעקוף את הבדיקה.
שאלה 5 · Dry Run — מה יהיה ערך EAX בסוף?
mov eax, 1 mov ecx, 4 .loop: add eax, eax sub ecx, 1 jnz .loop
- א. 16
- ב. 8
- ג. 4
- ד. 32
כל סיבוב מכפיל את EAX ב-2, וארבעה סיבובים נותנים 1·2⁴ = 16. SUB מעדכנת את ZF, ולכן JNZ ממשיכה עד ש-ECX מתאפס.
שאלה 6 · איזו פקודה חסרה כדי לקבל לולאת while (eax != 0)?
.loop_start: cmp eax, 0 ____ .loop_end dec eax jmp .loop_start .loop_end:
- א. je
- ב. jne
- ג. jg
- ד. jmp
תנאי הכניסה ללולאה הוא "שונה מאפס", ולכן היציאה מתבצעת כשהוא כן שווה לאפס — JE. שימו לב שהתנאי בקוד מכונה תמיד הפוך מהתנאי בשפת C.
שאלה 7 · מה מסוכן בקוד הבא כאשר ECX מתחיל ב-0?
mov ecx, 0 .lp: ; ... loop .lp
- א. LOOP מקטינה תחילה את ECX — 0 יגלוש ל-0xFFFFFFFF והלולאה תרוץ כ-4 מיליארד פעם
- ב. הקוד לא יתקמפל
- ג. הלולאה לא תרוץ אפילו פעם אחת
- ד. ECX יישאר 0 והלולאה תעצור מיד
🎯 דגש למבחן — LOOP ו-Off-by-One: הפקודה מבצעת DEC ECX ואז קופצת אם ECX≠0. משמעות הדבר: ECX=n נותן n חזרות, אבל ECX=0 גורם ל-Underflow ולולאה כמעט אינסופית. לכן בקוד אמיתי בודקים ECX לפני, או משתמשים ב-JECXZ.
שאלה 8 · מה מכיל [ebp+4] בתוך פונקציה שנקראה עם CALL?
- א. כתובת החזרה שנדחפה למחסנית
- ב. הפרמטר הראשון
- ג. המשתנה המקומי הראשון
- ד. ה-EBP של הפונקציה הקוראת
[ebp] מחזיק את ה-EBP השמור, [ebp+4] את כתובת החזרה ו-[ebp+8] את הפרמטר הראשון. דריסת [ebp+4] היא בדיוק מה שקורה ב-Buffer Overflow.
שאלה 9 · איזה קוד C מקביל לקטע הבא?
cmp eax, 10 jge .skip add eax, 5 .skip:
- א. if (eax < 10) eax += 5;
- ב. if (eax >= 10) eax += 5;
- ג. while (eax < 10) eax += 5;
- ד. eax = 10;
הקפיצה מדלגת על הגוף כשהתנאי ההפוך מתקיים, ולכן הגוף רץ רק כאשר EAX קטן מ-10 — זהו if ללא else.
שאלה 10 · Dry Run — מה ערך ECX בסוף?
mov eax, 10 mov ecx, 0 .lp: add ecx, eax sub eax, 3 cmp eax, 0 jg .lp
- א. 22
- ב. 30
- ג. 25
- ד. 10
הסיבובים: EAX=10 (ECX=10) → 7 (17) → 4 (21) → 1 (22) → -2 ואז JG לא קופצת. סה"כ 10+7+4+1 = 22. טיפ ל-Dry Run: נהלו טבלה של EAX/ECX והדגלים, ומחקו בקו במקום למחוק — כך אפשר לחזור אחורה ולאתר טעות.