من اهم المواضيع الذي يجب على اي مهندس عكسي فهمها

الباب .0 مقدمة

0.1 ليش الـ case-switch تحديداً مهم في الـ reversing ?

لو بدنا نختار شكل واحد بلغة C بيستاهل مقال كامل في الـ reverse engineering، فالـ switch-case راح يكون من أوائل المرشحين لكي نقوم بشرحه وبشراسة ايضا مش ألنه د معق بحد ذاته، بل ألنه العمود الفقري للـ flow control في فئة ّ كاملة من البرامج اللي بتشتغل عليها يومياً كـ reverser لا يوجد برنامج حقيقي الا ويستخدم منطق من الـ case-switch.

خلّينا نمشي على الأماكن اللي بتلاقيه فيها بكثافة:

الـ Parsers و الـ Lexers. أي برنامج بيقرأ صيغة معينة — file format، أو network protocol، أو لغة scripting — لازم بمرحلة ما يصنّف البايت أو الـ token اللي قدامه ويقرر شو يعمل فيه. القرار هذا بطبيعته switch على نوع الـ token أو على الـ opcode. لما تفتح parser مثلا لـ PE header، راح تلاقي قلب الـ logic عبارة عن switch بيوزّع على الـ chunk types.

الـ State Machines. أي شي بيشتغل بمنطق الحالة الحالية تحدّد ما هي ردة الفعل — TCP stack، game loop، UI flow — بينعمل عادةً كـ

C
 switch (current_state).

لما مثلا تقوم بعكس firmware أو driver، فهمك لشكل الـ state machine في الـ ASM بيختصر عليك وقت كبير.

الـ Dispatchers. مثلا من أهم السياقات على بالنسبة لعالم الـ Windows. مثلاً الـ WndProc — دالة بتستقبل رسائل النظام (WM_PAINT, WM_CLOSE, ...) وبتوزّع عليها بـ switch ضخم. نفس الشي في أي command handler أو RPC dispatcher أو syscall table منطقياً. لما تشوف دالة بتاخد رقم وبتقفز لمكان مختلف حسب الرقم — هذا dispatcher، وغالباً تحته switch.

الـ VM Handlers (من المواضيع المهمة لتعتيم). هون بصير الموضوع مختلف. أغلب الـ protectors والـ packers الحديثة (مثل VMProtect / Themida وما شابه) بتشتغل بفكرة الـ virtualization: بدل ما ينفّذوا كودك الأصلي مباشرة، بيترجموه لـ bytecode خاص فيهم، وبيحطوا interpreter جوّا البرنامج بينفّذ هذا الـ bytecode. وقلب أي interpreter زي هيك؟ dispatch loop — يعني حلقة بتقرأ الـ opcode الحالي وبتعمله switch على عشرات أو مئات الـ handlers. يعني فهمك لكيفية ترجمة الـ switch لـ jump table — هي بالضبط المهارة اللي بتحتاجها لما تحاول تفكك VM-based obfuscation. (ويعتمد بناء على التعتيم لكن المنطق نفسه)

لما تتعلّم تقرأ الـ switch بشكل صح، إنت فعلياً بتتعلّم تقرأ آلية اتخاذ القرار في البرامج. وأي indirect jump بتصادفه مثل

Assembly
jmp eax 
jmp [table + idx*4]

أول سؤال لازم يخطر ببالك هو: "هل هذا dispatch لـ switch?"

0.2 الفكرة: الـ switch مش بناء واحد ثابت لكل الـ Builds

هون لازم نوقف عند المفهوم اللي بيغيّر طريقة تفكيرك بالكامل، والمقال كله مبني عليه:

الـ switch في الكود المصدري هو نية ، مش تنفيذ (implementation).اختيار الاستراتيجية نفسه دالة في مستوى التحسين

اختيار الاستراتيجية نفسه دالة في مستوى

لما تكتب

C
 switch (x)

إنت مش بتقول للكومبايلر اعمل jump table. إنت بتقول له: "عندي قيمة، وبدي أنفّذ كود مختلف حسبها، مع احترام الـ fall-through والـ default". كيف بيحقّق النية هذه — هذا قراره هو، وهو حر فيه تماماً طالما النتيجة (الـ semantics) محفوظة.

ونفس الـ switch بالضبط ممكن يتحوّل لأشكال مختلفة :

  • سلسلة if-else (cmp / je متتالية) لو الحالات قليلة ومتفرقة.

  • شجرة binary search لو الحالات متفرقة بس عددها كبير.

  • Jump Table لو الحالات كثيفة ومتقاربة.

  • اختبار bitmap / bit-test لو اكثر من حالة بتشترك بنفس الكود (نادر).

  • حتى حساب رياضي مباشر لو القيم منتظمة جداً.

  • أو hybrid بيجمع اكثر من أسلوب بنفس الدالة.

والعوامل اللي بتحدّد الاختيار (وهذا الباب لحاله راح نفصّله): كثافة الحالات (density)، عددها (count)، المدى بين أصغر وأكبر case (range)، مستوى التحسين (-O0 vs /O2).

النتيجة العملية لهذا الكلام: إنت ما بتلاقي الـ switch في الـ binary — إنت بتعيد بناءه (reconstruct). يعني وظيفتك مش تدوّر على شكل واحد محفوظ، بل تتعرّف على أي واحد من عدة أشكال محتملة، وترجع منه للنية الأصلية. وهذا بفسّر ليش binary اثنين متطابقين بالـ source ممكن يطلعوا بأشكال ASM مختلفة كلياً لمجرد إنهم اتبنوا بـ flags مختلفة او بالعامية اكثر حتى لو يوجد Flags بناء على مزاج الكومبايلر.

التحوّل المطلوب منك: لا تقرأ الـ ASM حرفياً (في cmp، وفي jump، وفي table) — اقرأه كـ نية اريد اعادة بناؤه .

0.3 ماذا ستتعلمة بنهاية المقال

بنهاية المقال، المفروض تكون قادر — بشكل عملي، مش نظري — على إنك:

  • تتعرّف بصرياً (من الـ ASM أو من الـ Graph View) على أي واحدة من الاستراتيجيات الثالث الرئيسية: if-else chain، binary tree، jump table.

  • تفهم ليش اختار الكومبايلر هاي الاستراتيجية تحديداً، بالرجوع لكثافة/عدد/مدى الحالات.

  • تشرّح الـ jump table idiom خطوة-بخطوة: الـ normalization، الـ bounds check، الـ scaling، والـ indirect jump.

  • تتعامل مع الـ double-indirection الخاص بـ MSVC (جدول bytes + جدول addresses).

  • تحسب يدوياً عنوان الهدف من جدول بيخزّن offsets نسبية بدل عناوين مطلقة (حالة x64).

  • تستخرج وتعيد بناء الـ default case والـ "holes" في الجدول.

  • تصلّح يدوياً switch ما تعرّف عليه IDA، أو تعرّف عليه غلط، عبر Specify switch idiom.

  • تقرأ ناتج الـ Hex-Rays decompiler للـ switch، وتعرف وين بيفشل ويطلّعلك JUMPOUT / goto بشكل سيء.

  • تكتب سكربت IDAPython بسيط (get_switch_info) يعدّ ويستخرج كل الـ jump tables من binary ويعملك mapping من case value لـ target.

0.4 البيئة المستخدمة في الأمثلة

هاي البيئة اللي راح نشتغل عليها:

الكومبايلر: MSVC (الـ cl.exe) او يمكنك استخدام الـ GUI هو الاختيار المنطقي لأن أغلب اللي ستقوم بعكسه على Windows مبنى فيه، وله أسلوب ثابت بترتيب الـ jump tables (خصوصاً في الـ double-indirection اللي سنفصّلها بباب لحالها). معظم الأمثلة على target x64 مع تحسين /O2، ورح نوقف عند الفرق لما نحوّل لـ نطفّي التحسين بـ /Od لأنه بيغيّر شكل الكود كلياً.

ملاحظة: لو بدك تشوف الـ ASM اللي بيطلّعه الكومبايلر مباشرة بدون ما تفتح disassembler، استخدم flag الـ listing:

cl /O2 /FA swtich.c

بيطلّعلك ملف .asm فيه الـ assembly مع رموز مقروءة — مرجع ممتاز تقارن فيه ناتج الـ disassembler.

أداة التحليل: IDA Pro مع الـ Hex-Rays decompiler. راح نعتمد عليه أساساً للـ disassembly، الـ Graph View، الكشف التلقائي عن الـ switch، والـ decompilation. بنهاية المقال.

ملاحظة: الـ offsets، أسماء الـ registers المستخدمة، وحتى اختيار الاستراتيجية نفسها — كلها بتختلف حسب نسخة الكومبايلر بالضبط والـ flags. يعني لو شغّلت نفس المثال وطلع عندك العنوان أو الـ register مختلف، هذا طبيعي. اللي بدنا نركّز عليه طول المقال هو النمط (pattern) والـ idiom، مش الأرقام بشكل ثابت لأن النمط هو اللي بينتقل معك لأي binary، والأرقام بتتغير من حالة لحالة.

الباب 1: كيف تتحول لغة السي إلى أسمبلي؟ (الأساس قبل الـ switch)

1.1 خط الإنتاج: من الـ Source للـ ASM (Compilation Pipeline)

لما تكتب cl switch.c وتضغط Enter، إنت مش بتشغّل برنامج واحد — إنت بتشغّل driver اسمه cl.exe بيدير سلسلة مراحل، كل وحدة بتسلّم اللي بعدها. وفهمك لهاي السلسلة هو اللي بيخلّيك تعرف وين بالضبط بيتقرّر شكل الـ switch.

المراحل بالترتيب:

[صورة]

خلّينا نمشي عليها بسرعة، وبالأخص بالنسبة لـ MSVC:

الـ Preprocessor. بوخذ الكود النصّي، بيفكّ الـ #include، بيوسّع الـ macros، وبيشيل الـ comments. الناتج لسا C عادي، بس موسع. (بتقدر تشوفه بـ cl /P — بيطلّعلك ملف .i). هاي المرحلة ما إلها أي علاقة بقرار الـ switch، بس مهم تعرف إنها موجودة.

الـ Front-end. هون بيصير الـ parsing والـ semantic analysis — يعني الكومبايلر بيتأكد إن الكود سليم نحوياً ومنطقياً، وبيبني منه تمثيل داخلي. في MSVC هذا بيتم عبر c1.dll (للسي) أو c1xx.dll (للسي بلس بلس). نقطة مهمة: الـ front-end بيفهم إن عندك switch، بس لسا ما قرّر شكله النهائي.

الـ IR الداخلي. الـ front-end بيطلّع تمثيل وسيط (Intermediate Representation) — تمثيل أبسط وأقرب للمشين من الـ C، بس لسا مش assembly. هون راح نوقف بالتفصيل بالنقطة الجاية لأنها مهمة جداً مع MSVC تحديداً.

الـ Optimizer + CodeGen (الـ Back-end). هون الفارق في المترجمات الحديثة. في MSVC، الجزء هذا كله بصير جوّا c2.dll (مشترك بين السي والسي بلس بلس). هون بالضبط — جوّا الـ back-end — بيتقرّر: هل الـ switchراحيصير if-else chain? ولا jump table? ولا binary tree? المُحسّن (Optimizer) بيحلّل الحالات (عددها، كثافتها، مداها) وبياخذ القرار، وبعدها الـ CodeGen بيترجم القرار لتعليمات x64 فعلية.

اللي بدي تطلع فيها من هاي النقطة: قرار شكل الـ switch مش قرار كتبته إنت، وهو مش قرار الـ front-end — هو قرار الـ optimizer في c2.dll. وعشان هيك نفس الكود بيطلع أشكال مختلفة لما تغيّر مستوى التحسين، لأنك عملياً عم بتغيّر مزاج الـ back-end.

الـ Back-end (c2.dll) هو black box بمعنى غير قابل للتفريغ (Not Dumpable)

لكن على الناحية الاخرى مثل LLVM/CLANG يمكنك تفريغ الـ IR .

1.2 دور مستويات التحسين في تغيير شكل الكود كلياً

قبل ما نكمّل، لازم نحكي بموضوع مهم: الـ flags اللي حطّيتها في الـ index (-O0, -O1, -O2, -O3, -Os) هي flags تبع GCC/Clang.

إحنا شغّالين على MSVC، سنتحدث عنها فقط راح تستخدمها مع cl.exe:

الـ flag في MSVCالمعنىالتأثير على شكل الـ switch
/Odإيقاف التحسين (debug)كود الحرفي كل سطر بينعكس مباشرة، لكن سيء
/O1تصغير الحجميميل لاختيارات أوفر بالمساحة، ممكن يتجنب جداول كبيرة
/O2أهمية السرعة (الافتراضي للـ release)اختيار الأفضل لـ jump tables لما تستاهل
/Oxتحسين كامل (subset)قريب من /O2
/Osتفضيل صغر الحجم (modifier)بيرجّح المقارنات على الجداول لو الجدول كبير
/Otتفضيل السرعة (modifier)بيرجّح الجداول السريعة

طيب ليش هذا كله مهم؟ لأن نفس الكود المصدري بالضبط بيطلع بأشكال مختلفة جذرياً حسب الـ flag:

  • مع /Od: المتغيرات كلها على الـ stack، في mov من وإلى الذاكرة قبل وبعد كل عملية، والـ switch ممكن يطلع بشكل واضح لكن يمكننا القول انه محاط بنوع من انواع الفوضى الواضحة. الميزة الوحيدة: سهل تربط كل سطر assembly بسطر C مقابل (لأنه ما في إعادة ترتيب ولا دمج).

  • مع /O2: المتغيرات بالـ registers، الكود مضغوط جداً، والـ (Optimizer) بياخد حرية كاملة بقرار الاستراتيجية في التحسين — هون راح تشوف الـ jump tables والـ binary trees على اغلب الـ Builds (وصعوبتها).

اللي بربط هذا الكلام بكل المقال:

اختيار الاستراتيجية نفسه دالة في مستوى التحسين. ممكن نفس الـ switch ممكن يطلع if-else chain تحت /Od و jump table تحت /O2، بدون ما تغيّر حرف واحد بالكود.

لذالك أول سؤال لازم تسأله لما تحلّل binary مش معروف: "بأي مستوى تحسين متبني؟" — لأن الإجابة بتحدّد شو راح تشوف. وفي المقال راح نركّز على /O2 (لأنه الأغلب في الـ release binaries اللي راح تعكسها)، مع وقفات عند /Od و /Os لما يكون الفرق مهم.

1.3 مفهوم الـ "Lowering": كيف ينزل HighLevel الى LowLevel

Lowering (التخفيض / الأسفل/ نزول | لا يهم التسمية).

الفكرة بسيطة وعميقة ومهمة بنفس الوقت: الكومبايلر ما بيترجم الـ C لـ assembly مرة واحدة واحدة. هو بنزّل من عالي المستوى الى أدنى على مراحل، كل مرحلة أقرب للمشين من اللي قبلها. مثل ما تنزل درج لا تقفز مرة وحدة ، خطوة خطوة.

أمثلة:

  • الـ for loop العالي المستوى بينزل لـ: تهيئة + label + شرط + جسم + زيادة + jmp رجوع. ما في "for" في الـ assembly — في بس قفزات وشروط.

  • الـ struct member access (p->field) بينزل لـ: حساب عنوان القاعدة (Base Address) + إضافة offset ثابت + mov.

  • و — اللي يهمنا — الـ switch بينزل لواحدة من الاستراتيجيات اللي ذكرناها الباب صفر (if-else / tree / jump table / bitmap).

مهم لإلنا لأن الـ reverse engineering هو عملية lowering لكن معكوسة (un-lowering). الكومبايلر نزّل من نية عالية لتعليمات منخفضة؛ إنت بتاخذ التعليمات المنخفضة وبتطلع فيها رجوع للنية العالية. مثلا لما تشوف :

sub + cmp + ja + jmp [table+idx*4]

إنت بتعمل un-lowering: بترجّع الأربع تعليمات لكلمة واحدة — switch.

وهون بتكمن الصعوبة الحقيقية: الـ lowering مش دالة عكسية بشكل نظيف. اكثر من بناء C مختلف ممكن ينزّلوا لنفس الـ assembly، واكثر من شكل assembly ممكن يرجعوا لنفس الـ switch. لذالك هيك إنت ما بتفك تشفير إنت بتستنتج النية الأرجح بناءً على التعرّف على الأنماط (idioms). وهذا بالضبط اللي راح نبنيه طول المقال: انماط تخليك تعمل un-lowering بسرعة.

1.4 الفرق بين قراءة الـ ASM حرفياً وقراءتها كـ إعادة بناء نية

هون بنجمع كل اللي فات بمثال صغير، ونثبّت العقلية اللي راح ترافقك للآخر.

تخيّل إنك فتحت دالة في IDA ولقيت المقطع (مبسّط):

Assembly
movzx   eax, byte ptr [rcx]
sub     eax, 1
cmp     eax, 5
ja      short loc_default
mov     eax, eax ; rax = 0 
jmp     off_jumptable[rax*8]

القراءة الحرفية

في حركة لبايت من الذاكرة لـ eax، بعدها طرح واحد، بعدها مقارنة مع 5، بعدها قفزة مشروطة لو أكبر، بعدها mov، بعدها قفزة غير مشروطة وغير مباشرة عبر جدول."

كل كلمة صح... وما فهمت إشي. هاي ترجمة، مش فهم.

القراءة كإعادة بناء نية :

هذا switch.

الـ movzx بيجيب قيمة الـ selector (بايت).

الـ sub eax, 1 معناه إن أصغر case هو 1 (normalization).

الـ cmp eax, 5 + ja معناهم إن في 6 حالات (من 1 لـ 6)، وأي شي بَرّا المدى بيروح للـ default.

الـ jmp ...[rax*8] معناه جدول عناوين، كل entry 8 بايت (يعني x64). يبقى الكود الأصلي تقريباً

switch(x)

حالاته من 1 لـ 6، وفي default.

شوف الفرق؟ نفس الست تعليمات بس واحد ترجمها، والثاني رجّعها للنية. والثاني هذا عرف يطلع منها: عدد الحالات، أصغر case، نوع المعمارية، ووجود الـ default — كلها بدون ما يفتح الكود المصدري.

لا تقرأ الـ assembly سطر-سطر كأنها كتاب. اقرأها بالكتل (idioms). كل idiom بحكي نية معينة. وكل اللي راح نعمله بباقي المقال هو غياب الـ jmp بين حالتين هو بالضبط فهم الـ idioms — حتى يصير sub/cmp/ja/jmp[table] يقفز ببالك مباشرة كـ "switch" زي ما اسمك.

الباب 2. بنية الـ switch-case في C من الداخل

2.1 سيمانتيك الـ switch

قبل ما نحكي كيف الكومبايلر بيترجم الـ switch، لازم نتفق بالضبط شو هو ملزَم يحقّقه. لأن الكومبايلر حر يختار أي شكل assembly براسو — بس بشرط واحد: يحافظ على السيمانتيك (المعنى) كما عرّفته اللغة. فهمك للسيمانتيك هو اللي بيخليك تعرف وين الكومبايلر مقيّد ووين حر.

خلّينا نفكّك المعنى:

أولاً: الـ controlling expression لازم يكون integer. الـ

C
switch (x)

بيشتغل بس على قيمة عددية صحيحة (int، char، enum، إلخ) — مش float، مش string (في C/C++). هاي نقطة مهمة لأنها بتفسّر ليش الكومبايلر بيقدر يستخدم القيمة مباشرة كـ index في جدول: لأنها رقم.

ثانياً: كل case لازم تكون قيمة ثابتة وقت الترجمة (compile-time constant)، وفريدة. ما بتقدر تعمل case x: لو x متغير. وما بتقدر تكرّر نفس القيمة بحالتين. هاي الخاصية — إن القيم معروفة وثابتة وقت الترجمة — هي اللي بتسمح للكومبايلر يبني جدول جاهز بالذاكرة. لو كانت القيم متغيرة، ما كان في طريقة لبناء jump table أصلاً.

ثالثاً، والأهم: الـ Fall-through. هاي الخاصية اللي بتميّز الـ switch عن أي تنسيق متدفق ثاني ولازم تفهمها كويس:

C
switch (x) {
    case 1:
        do_a();

        // ما في break بيكمل تحت

    case 2:
        do_b();
        break;
    case 3:
        do_c();
        break;
}

لما x == 1، البرنامج بينفّذ do_a() وبعدها بيكمّل لـ do_b() (لأنه ما في break بعد case 1)، وبس عند الـ break تبع case 2 بيوقف. يعني الـ case مش بلوكات منفصلة — هي نقاط دخول (entry points) في تسلسل واحد متصل من الكود، والتنفيذ بيكمّل خطّي لحد ما يصادف break (أو نهاية الـ switch).

هاي النقطة لها أثر مباشر ضخم على الـ reversing: لما تشوف في الـ assembly إن حالتين بيوصلوا لنفس الكود بدون قفزة بينهم — هذا fall-through مقصود، مش غلط. والكومبايلر بيترجم الـ break لـ jmp لنهاية الـ switch؛ يعني غياب الـ jmp بين حالتين هو بالضبط علامة الـ fall-through.

Image

ابعاً: الـ default. هي الحالة اللي بتمسك أي قيمة ما طابقت ولا case. مش لازم تكون بالآخر (ممكن تكون بالنص)، وإذا ما كانت موجودالكثافة = عدد الحالاتة وما طابقت ولا قيمة، الـ switch كله بينسى. بالـ assembly، الـ default هي وجهة الـ "خارج المدى" — يعني الـ ja بعد الـ bounds check بيقفز عليها، وكمان هي اللي بتملّي الفجوات (holes) في الجدول. (رح نفصّل الجزئية هاي كثير بباب الـ jump table).

Image

خامساً: نطاق الـ case (الـ scope). كل الـ case بتشترك بنفس النطاق (block scope) تبع الـ switch. هذا بيخلق حالة غريبة شوي مع تعريف المتغيرات داخل الحالات، لكن الأهم لإلنا: لأنهم بنفس النطاق، الكومبايلر بيقدر يشاركهم نفس prologue/epilogue ونفس الـ stack frame، ولذالك هيك الكتل بتطلع متجاورة بالـ assembly.

بابلدي : الكومبايلر مقيّد بإنه يحافظ على: أي case بيوصل لأي كود، الـ fall-through، والـ default. وهو حر بكل شي ثاني — كيف يوصل للكود (مقارنة؟ جدول؟)، ترتيب الفحوصات، إلخ.

2.2 ليش الـ switch مش مجرد if-else متسلسلة (حتى لو طلع كذلك أحياناً)

هون بتلخبط كتير ناس، وبدي أوضّحها على مستويين: مستوى اللغة و مستوى التنفيذ.

على مستوى التنفيذ: نعم، أحياناً الكومبايلر بيترجم الـ switch لسلسلة cmp/je متطابقة بالمثل مع اللي كان راح يطلّعه لو كتبت if-else. وبالحالة، من ناحية الـ assembly الناتج، ما في فرق. فممكن تقول "إذن switch = if-else".

بس على مستوى اللغة، لأ وفي فروقات بتأثّر على التنفيذ نفسه أحياناً:

الفرق الأول: الـ fall-through. هذا الفرق. في if-else، كل فرع منفصل بالكامل؛ ما في تسرّب من فرع لفرع. في switch، الـ fall-through جزء من المعنى. لو حاولت تكتب نفس منطق الـ fall-through بـ if-else،راح تحتاج تكرّر كود أو تستخدم goto. يعني الـ switch بيعبّر عن شيء ما بتقدر تعبّر عنه بـ if-else نظيفة.

الفرق الثاني: التقييم مرة واحدة. فيswitch (function())

C
switch (function())

، الـ function() بتتقيّم مرة وحدة، وبعدها القيمة بتتقارن مع الحالات. في سلسلة :

C
if (function() == 1) ...
 
else if (function() == 2)

نظرياً بتنادي الدالة اكثر من مرة (إلا إذا الـ (Optimizer) لاحظ إنها pure وحفظ النتيجة). فالـ switch بيضمن لك تقييم واحد للـ selector.

الفرق الثالث، وهو الأهم لإلنا كـ reversers: الـ switch بيعطي الكومبايلر معلومة بنيوية أغنى. لما تكتب switch، إنت بتقول للكومبايلر: هاي كل القيم اللي بهمّني، وكلها تقارن مع نفس المتغير، وكلها ثوابت. هاي معلومة كانها 100 دولار لـ Optimizer— لأنها بتسمحله يبني jump table أو binary tree. أما سلسلة if-else، فالكومبايلر لازم يكتشف إنها فعلياً بتقارن نفس المتغير مع ثوابت قبل ما يفكّر يحسّنها لجدول (وغالباً ما بعملها، أو بعملها بشروط أصعب).

النتيجة : مش كل if-else chain هي switch، ومش كل switch بتطلع jump table. لما تشوف سلسلة cmp/je في IDA، ما تفترض دغري إنها switch — ممكن تكون if-else حقيقية كتبها المبرمج. الطريقة الوحيدة للتمييز أحياناً: هل كل المقارنات على نفس المتغير مع ثوابت؟ إذا نعم، غالباً switch (أو if-else بيقلّد switch). إذا المقارنات على متغيرات مختلفة أو شروط مركّبة، فهي if-else حقيقية. (رح نرجع لهذه المشكلة بتفصيل بباب الـ if-else chain).

2.3 القيم الممكنة: dense vs sparse، العدد، والمدى

وصلنا للثلاثة مفاهيم الل يراح يبني عليها الكومبايلر كل قراره في الباب الجاي. افمهم كويس لأنهم العملة الصعبة اللي راح نتعامل فيها للآخر:

1. عدد الحالات (Case Count): كم case عندك. switch بـ 3 حالات غير switch بـ 200 حالة. العدد لحاله بيأثر: حالات قليلة جداً ما بتستاهل عبء بناء جدول، حالات كثيرة بتخلي سلسلة المقارنات بطيئة (O(n)) فيلجأ لحل أذكى.

2. المدى (Range / Span): الفرق بين أكبر وأصغر قيمة case. يعني

maxcasemincasemaxcasemincase

هذا رقم مختلف تماماً عن العدد:

  • حالات (Cases) 1, 2, 3, 4, 5 عدد = 5، مدى = 4.

  • حالات (Cases) 1, 1000, 50000 → عدد = 3، مدى = 49999.

المدى مهم لأن الـ jump table حجمه بيتناسب مع المدى، مش مع العدد. جدول للحالة الثانية (مدى 49999)راح يكون مصفوفة فيها 50000 entry معظمها فاضي — مشكلة بالذاكرة.

3. الكثافة (Density): وهي النسبة بين العدد والمدى:

الكثافة = عدد الحالات ÷ (المدى + 1)

  • حالات (Cases) 1..5

كثافة = 5 ÷ 5 = 100% (كثيف جداً — مثالي للجدول).

  • حالة (Case) 1, 1000, 50000

كثافة = 3 ÷ 50000 = 0.00006% (متفرق جداً — جدول = غير منطقي وسيء).

هون بنوصل للتمييز :

كـDense (كثيف): القيم متقاربة ومتلاصقة، الكثافة عالية. → الكومبايلر بيحب الـ jump table (مساحة معقولة، سرعة O(1)).

كـSparse (متفرق): القيم متباعدة، فجوات كبيرة، الكثافة واطية. الجدول بيصير مهدر للذاكرة، فالكومبايلر بيلجأ لـ if-else chain أو binary search tree.

النقطة اللي لازم تطلع فيها: لما تشوف switch في الـ assembly وتحاول تفهم ليش اختار الكومبايلر شكل معين، أول إشي قدّر الكثافة — هي مفتاح جيد جدا.

الباب 3. الأسس: كيف يقرّر الكومبايلر استراتيجية الترجمة؟

3.1 الـ Decision Matrix الذي يستخدمه الكومبايلر

قبل، خلّيني أحط قدامك الثلاث نتائج الحقيقية، لأنها بتلخّص القرار كله:

الحالة المتفرقة (sparse) :

case 1, 1000, 50000

اختار سلسلة مقارنات:

C
short state = 0;
short x ;
switch(state)
{
case 1 :
x = 50;
break;
case 1000 :
x = 100;
break;
case 50000 :
x = 500;
break;
}
Image

ما في ولا جدول — فقط cmp/je متتالية، لأن المدى 49999 بيخلّي أي جدول مشكلة.

je = jz

الحالة الكثيفة (dense) — case 0..7 بـ control flow مختلف اختار jump table حقيقي:

C
  switch (state)
   {
   case 0 : 

       x = 11;
       break;

   case 1 : 

       x = 22;
       break;

   case 2 :

       x = 33;
       break;

   case 3 : 

       x = 44;
       break;

   case 4 :

       x = 55;
       break;

   case 5 :

       x = 66;
       break;

   case 6 :

       x = 77;
       break;

   case 7 : 

       x = 88;
       break;

   }
Image

هل ترى الاختلاف ؟ واضح جدا والسبب ؟ هو الكثافة .

3.2 معيار الكثافة (Density Heuristic): متى الجدول "يستحق" مساحته؟

هذا الأهم. الكومبايلر عنده مقايضة (trade-off) بسيطة: الـ jump table بيكلّف مساحة بالذاكرة (بايتات للجدول)، بس بيعطي سرعة ثابتة O(1) — قفزة وحدة بدون أي مقارنة. السؤال: هل السرعة بتستاهل المساحة؟

الجواب يعتمد على :

الكثافة = عدد الحالات ÷ (المدى + 1)

تخيّل الجدول كمصفوفة بطول المدى+1. كل خانة بتاخد مساحة . لو الكثافة عالية، معظم الخانات مليانة بحالات حقيقية ما في هدر. لو الكثافة واطية، معظم الخانات مليانة بعنوان الـ default إنت تدفع مساحة لخانات فاضية.

شوف بالأرقام الحقيقية من مثالنا:

  • مثال الـ dispatch: 8 حالات، مدى 0..7. الكثافة = 8 ÷ 8 = 100%. كل خانة في الجدول مفيدة → الكومبايلر بنى الجدول.

  • مثال الـ sparse: 3 حالات، مدى 49999. الكثافة = 0.006%. لو بنى جدول، كانراحيكون 50000 خانة، 49997 منها فاضية → رفض، وراح للمقارنات.

تقريبا المعاير اللي بتشتغل فيها الكومبايلرات (بما فيها MSVC): إذا الكثافة فوق رقم معين (مثل حوالي 50% أو أكثر كنقطة انطلاق)، الجدول بيستاهل. تحت هيك، بيبدأ يفكّر بحلول ثانية. بس انتبه هاي مش معادلة Fixed، هي heuristic، والعتبة الفعلية بتتأثر بالعدد وبمستوى التحسين كمان.

3.3 عدد الحالات (Case Count) في MSVC :

العدد لحاله إله دور، لأنه في تكلفة ثابتة لبناء الجدول (تحميل العنوان، الـ scaling، القفزة غير المباشرة) — ما بتستاهل لو الحالات قليلة جداً.

العتبات التقريبية اللي بتلاحظها مع MSVC على /

O2 (

وأشدّد وانوهه: بناء على الخبرة ومعايير الكومبايلرز الحديثة لانه c2.dll مغلق المصدر كما ذكرنا (تحتاج عمل له هندسة عكسية ان كان يهمك الامر كثيرا)— حالتين أو ثلاثة: شبه دايماً سلسلة cmp/je، حتى لو كثيفة — ما بتستاهل عبء الجدول.

Image
  • حوالي 5 حالات وفوق + كثافة عالية: هون MSVC بيبدأ يميل للـ jump table. هاي الأكثر منطقية.

  • عدد كبير + متفرق: بيلجأ لـ binary search tree (cmp + jg/jl لتقسيم المدى)، وأحياناً hybrid: شجرة فوق تنتهي بـ jump tables صغيرة عند (clusters من الحالات الكثيفة).

عمليا: مش العدد لحاله، ولا الكثافة لحالها — هما الاثنين مع بعض .

3.4 المدى (Range) خطر الـ jump table :

هون لازم نوضّح ليش المدى خطر بحد ذاته، منفصل عن العدد والكثافة.

حجم الـ jump table بيتناسب طرديًا مع المدى، مش مع العدد. يعني:

C
switch (x) {
    case 1:      ...
    case 2:      ...
    case 1000000: ...   // ثلاث حالات بس
}

ثلاث حالات فقط، بس المدى مليون. لو الكومبايلر (غبي) بنى jump table، كان راح يحجز مصفوفة فيها مليون خانة × 4 بايت = 4 ميغابايت بالذاكرة، عشان 3 حالات مشكلة وعشان هيك الكومبايلر بيرفض الجدول هون رغم إن لاحظ ممكن يكون عدد الحالات "كافي".

عمليا: لما تشوف switch تحوّل لمقارنات رغم إن عدد الحالات كبير، شك بالمدى فوراً — على الأغلب القيم متباعدة. والعكس: jump table صغير بيخبّرك إن الحالات الأصلية كانت متقاربة ومتلاصقة.

في حالة وسطه: لو المدى كبير بس في عناقيد كثيفة داخله (مثلاً قيم 1-5، وبعدها 1000-1005)، الكومبايلر بيعمل hybrid: شجرة مقارنات بتفصل العناقيد، وكل عنقود بيصير jump table صغير لحاله. لما تشوف cmp كبير يفصل، وبعدها جدولين صغيرين بفرعين — هذا النمط.

C
switch (state)
 {
 case 1:
     x = 11;
     break;
 case 2:
     x = 22;
     break;
 case 3:
     x = 33;
     break;
 case 4:
     x = 44;
     break;
 case 5:
     x = 55;
     break;
    
 case 1000 : 
     x = 1111;
     break;
 case 2000 :
     x = 2222;
     break;
 case 3000 : 
     x = 3333;
     break;
 case 4000 : 
     x = 4444;
     break;
 case 5000 : 
     x = 5555;
     break;
    
 }
Image

هل تلاحظ كيف انشطرت ؟

Image
Image

3.5 تأثير /Os (الحجم) مقابل /O2 (السرعة) على القرار

نفس الـ switch، نفس الكثافة، نفس العدد — بس القرار بينقلب لما تغيّر هدف التحسين. ليش؟ لأن المقايضة نفسها (مساحة مقابل سرعة) بيتغيّر وزنها:

  • مع /O2 (السرعة اهم بألف مره): الكومبايلر بيحب الجداول. السرعة أهم، فمستعد يدفع مساحة. الكثافة المطلوبة لبناء جدول بتصير افضل — يعني بيبني جداول حتى لو الكثافة مش مثالية، لأن قفزة O(1) أسرع من سلسلة مقارنات لكن بنفس الوقت ممكن لا لانه O2 ايضا يهتم بالمساحة.

  • مع /Os (الحجم اهم بألف مره): العكس تماماً. المساحة هي العدو، فالكومبايلر بيكره الجداول الكبيرة. الكثافة بتصير اسوء— وبيفضّل سلسلة مقارنات (اللي بتاخد بايتات أقل) حتى لو أبطأ شوي. ممكن نفس الـ switch اللي طلع jump table على /O2 يطلع سلسلة cmp/je على /Os.

3.6 قرار المترجم (Decision Tree) حول الـ Switch-table :

Image

4. النمط الأول: سلسلة if-else (Comparison Chain)

4.1 متى يختارها الكومبايلر

من الباب 3، عرفنا إن الكومبايلر بيلجأ للمقارنات المتتالية في حالتين:

  1. حالات قليلة جداً (2-3) حتى لو كثيفة — ما بتستاهل عبء الجدول.

  2. حالات متفرقة (sparse) — المدى كبير فالجدول بيصير مهدر.

C
   switch (state)
    {
    case 0 :
        x = 11;
        break;
    case 1 : 
        x = 22;
        break;
    case 2 :
        x = 33;
        break;
    case 3 : 
        x = 44;
        break;
    }

/Od Flag

المثال الأول (case 0, 1, 2, 3) كثيف 100% — مدى 0..3، أربع حالات، كثافة كاملة. لكنه طلع مقارنات على /Od ليش؟

لأن /Od بيطفّي التحسين كلياً — الكومبايلر ما بيفكّر الخوارزمية الذكية أصلاً، بيترجم حرفياً قارن، لو بيساوي اقفز. يعني على /Od راح تشوف مقارنات بغض النظر عن الكثافة. هاي نقطة مهمة: المثال الأول مش اختار المقارنات لأنه sparse — هو ما اختار شيء، الـ /Od فرضها عليه.

المثال الثاني (case 0, 1, 10000, 25000) متفرق فعلاً — مدى 0..25000، كثافة = 4/25001 = 0.00016%. هون على /O2 راح يختار مقارنات (وهذا اللي شفناه)، لأن الجدول كان راح يكون 25001 خانة لأربع حالات.

الفرق اللي بدي تركز عليه: مقارنات /Od ≠ مقارنات /O2. الأولى مفروضة (ما في تحسين)، الثانية مختارة بناء على خاوارزمية هدفها الاولى والأخير التحسين (المُحسّن حسب وقرّر إن الجدول ما بيستاهل). وشكلهم بالـ ASM مختلف تماماً — وهذا بالضبط اللي راح تشوفه.

Assembly
        movsx   eax, [rsp+18h+state]    ; جيب الـ selector
        mov     [rsp+18h+var_10], eax   ; احد الاشياء الغير مهمة المضافة للتتبع)
        cmp     [rsp+18h+var_10], 0     ; state == 0 ?
        jz      short loc_14000103B     ;   -> case 0
        cmp     [rsp+18h+var_10], 1     ; state == 1 ?
        jz      short loc_140001046     ;   -> case 1
        cmp     [rsp+18h+var_10], 2     ; state == 2 ?
        jz      short loc_140001051     ;   -> case 2
        cmp     [rsp+18h+var_10], 3     ; state == 3 ?
        jz      short loc_14000105C     ;   -> case 3
        jmp     short loc_140001065     ; ما طابق ولا واحد -> نهاية (default ضمني)

النمط واضح وقابل للقراءة :

  • ـcmp ثابت + jz متكرّر، كل زوج بيمثّل case واحدة.

  • كل المقارنات على نفس المتغير (var_10) ومع ثوابت — هاي بصمة الـ switch (مش if-else ).

  • آخر jmp short بدون مقارنة الخروج لما ما يطابق ولا حالة (الـ default، حتى لو ما كتبته بالمصدر هو في النهاية بحاجة للخروج).

  • كل block بينتهي بـ jmp short loc_140001065 (= الـ break)، وكلهم بيتجمّعوا عند نقطة الخروج المشتركة.

الآن قارن مع المثال الثاني (/O2) — نفس فكرة المقارنات، بس مكتوبة مع Optimizer:

Assembly
        movsx   ecx, ax          ; selector في register (مش ستاك)
        test    ecx, ecx        
        jz      short loc_140001055   ; -> case 0
        sub     ecx, 1           ; ecx -= 1
        jz      short loc_14000104A   
;كان == 1 ?  -> case 1 
     

   sub     ecx, 270Fh       
; ecx -= 9999   (الآن ecx = state - 10000)

        jz      short loc_14000103F   
; كان == 10000 ? -> case 10000
        cmp     ecx, 3A98h      
; ecx == 15000 ? ( state == 25000)
        jnz     short loc_140001034   ; لأ -> default
        ...case 25000...

شوف الذكاء اللي ما راح تشوفه أبداً في /Od:

اولا — test ecx, ecx بدل cmp ecx, 0: عشان يقارن مع صفر، استخدم test (بيعمل AND ويظبط الـ flags) لأنه أقصر وأسرع من cmp ecx, 0. لما تشوف

test reg, reg + jz — هاي مقارنة مع صفر، يعني case 0.

ثانيا — الطرح التراكمي (sub متتالي بدل cmp مستقل): بدل ما يقارن ecx مع كل قيمة مستقلة، المُحسّن بيطرح تراكمياً ويتفحّص الصفر:

  • sub ecx, 1 + jz → كان ecx == 1؟ (= case 1)

  • بعدها ecx صار state-1.

الآن

sub ecx, 270Fh (9999) → ecx

صار

state-1-9999 = state-10000.

لو jz

state == 10000 (= case 10000).

  • بعدها ECX :

cmp ecx, 3A98h (15000)

حالياً state-10000، اذا ساوى 15000 معناه

state == 25000 (= case 25000).

هاي الحيلة (تحويل سلسلة المقارنات لسلسلة طرح تراكمي) شائعة جداً في /O2، وبتلخبط المبتدئ لأنه بيشوف 270Fh (9999) ويظن إن الحالة هي 9999 — بينما الحالة الحقيقية 10000 (لأن ecx كان أصلاً ناقص 1 من الخطوة اللي قبل).

القاعدة للـ /O2 comparison chain:

القيم مش مستقلة — هي تراكمية. عشان تطلع قيمة الـ case الحقيقية، اجمع كل الثوابت اللي اتطرحت لحد هاللحظة:

  • case 0: من test (صفر).

  • case 1: 0 + 1 = 1.

  • case 10000: 1 + 9999 = 10000.

  • case 25000: 10000 + 15000 = 25000.

ثالثا — cwde بدل mov ax:

بتشوف mov eax, 15BEh بعدها cwde. الـ cwde بيمدّ الإشارة من ax لـ eax (sign-extend) — لأن x معرّف كـ short (word)، والكومبايلر بيرجّعه ممدوداً. وكمان لاحظ: كل block بـ /O2 بينتهي بـ

add rsp, 28h + retn

4.2 كيف تميّزها بصرياً في الـ Graph View

📊 الـ Graph View بيكشف نمط الـ comparison chain فوراً قبل ما تقرأ سطر واحد — له شكل مميز اسمه السلّم (staircase / ladder):

هذا شكل الـ comparison chain في الـ Graph View: سلّم نازل. كل درجة فيها فحص واحد بفرعين — يمين للحالة (الـ jz نجح)، وتحت للفحص اللي بعده (الـ fall-through). الـ default بالأسفل بيمسك اللي ما طابق ولا درجة. وكل كتل الحالات على اليمين (في الواقع) بترجع تتجمّع بنقطة خروج واحدة عبر jmp الـ break.

Image
Image

نقطتان عمليتان للتمييز البصري السريع:

  • عدد الدرجات = عدد الحالات. عُدّ الـ cmp/jz المتتالية، بتطلع عدد الـ case.

  • شكل السلّم خطّي : كل ما القيمة أبعد بالترتيب، كل ما مرّ على فحوصات أكثر. عكس الـ jump table اللي بكون شكله fan out— كتلة وحدة بتتفرّع لكل الحالات دفعة وحدة. (رح نقارن الشكلين جنب بعض بباب الـ jump table).

4.3 مشكلة: متى تبدو if-else عادية بينما هي أصلاً switch?

لما تشوف سلسلة cmp/jz في IDA، كيف تعرف إذا كانت switch أصلية بالمصدر، ولا if/else if/else كتبها المبرمج يدوياً؟ الحقيقة: على /Od، الاثنين بيطلعوا متطابقين تماماً — لأن ما في تحسين يميّزهم. فمن ناحية هاذي، ما في فرق دايماً.

بس في علامات بتساعدك تستنتج الأرجح:

العلامة الأقوى — وحدة المتغير والثوابت. الـ switch بيقارن متغير واحد مع ثوابت فقط. شوف مثالنا:

Assembly
       cmp     [rsp+18h+var_10], 0      ; نفس المتغير
        cmp     [rsp+18h+var_10], 1      ; نفس المتغير، ثابت
        cmp     [rsp+18h+var_10], 2710h  ; نفس المتغير، ثابت
        cmp     [rsp+18h+var_10], 61A8h  ; نفس المتغير، ثابت

كل المقارنات على var_10 ومع ثوابت بصمة switch قوية. لو كانت if-else حقيقية، غالباً راح تشوف مقارنات على متغيرات مختلفة (cmp a, 5 بعدها cmp b, 10)، أو شروط (jg, jl مش بس jz)، أو شروط مركّبة (&&/|| بتطلع كقفزات متشابكة).

علامة ثانية— كلهم jz/je (مساواة فقط). الـ switch دايماً مساواة (==). لو شفت jg/jl/jge (أكبر/أصغر)، هذا مؤشر على if-else حقيقية فيها مقارنات range، مش switch. يوجد استثناءات نعم .

علامة ثالثة — التماثل. كتل الحالات في الـ switch بتكون متماثلة الشكل (كل وحدة: عمل + break/jmp لنقطة خروج مشتركة)، وكلها بترجع لـ نقطة خروج واحدة (loc_140001065 في مثالنا غير واضح هنا بشكل كبير لانه تم تجميعها بدون Default بشكل صريح). الـ if-else المعقّدة بكون شكلها عشوائي.

المهم ما تجزم. قول هذا على الأرجح switch لأن كل المقارنات على نفس المتغير مع ثوابت ومساواة فقط. وإذا الـ /O2 حوّله لـ jump table أو طرح تراكمي (زي ما شفنا)، هون الجزم بيصير أسهل — لأن الـ if-else العادية نادراً بتتحوّل لجدول.

4.4 لاب: تفكيك switch بـ 4 حالات متفرقة /Od

Assembly
[sub_0000000140001000 - main] /Od

الخطوة 1 — حدّد الـ selector ونقطة الدخول. أول شي بيتقرأ ويتقارن:

C
movsx   eax, [rsp+18h+var_14]    ; القيمة (state)  ممدودة من word
mov     [rsp+18h+var_10], eax    ; نسخة عمل (ضجيج /Od)

الـ selector هو var_14

(نوعه word = short)، واتنسخ لـ var_10 للمقارنة. يعني

C
switch (state)

على short.

الخطوة 2 — استخرج الحالات من سلسلة المقارنات. عدّ الـ cmp/jz:

Assembly
;Selector Phase
.text:000000014000100F                 mov     [rsp+18h+state], ax
.text:0000000140001014                 movsx   eax, [rsp+18h+state]
.text:0000000140001019                 mov     [rsp+18h+var_10], eax
; Cases Phase
.text:000000014000101D                 cmp     [rsp+18h+var_10], 0
.text:0000000140001022                 jz      short loc_14000103B
.text:0000000140001024                 cmp     [rsp+18h+var_10], 1
.text:0000000140001029                 jz      short loc_140001046
.text:000000014000102B                 cmp     [rsp+18h+var_10], 2
.text:0000000140001030                 jz      short loc_140001051
.text:0000000140001032                 cmp     [rsp+18h+var_10], 3
.text:0000000140001037                 jz      short loc_14000105C
.text:0000000140001039                 jmp     short loc_140001065

الخطوة 3 — استخرج جسم كل حالة.

كل

loc_ (Block/Scope)

Assembly
.text:000000014000103B ; ---------------------------------------------------------------------------
.text:000000014000103B
.text:000000014000103B loc_14000103B:                          ; CODE XREF: main+22↑j
.text:000000014000103B                 mov     eax, 0Bh
.text:0000000140001040                 mov     [rsp+18h+x], ax
.text:0000000140001044                 jmp     short default
.text:0000000140001046 ; ---------------------------------------------------------------------------
.text:0000000140001046
.text:0000000140001046 loc_140001046:                          ; CODE XREF: main+29↑j
.text:0000000140001046                 mov     eax, 16h
.text:000000014000104B                 mov     [rsp+18h+x], ax
.text:000000014000104F                 jmp     short default
.text:0000000140001051 ; ---------------------------------------------------------------------------
.text:0000000140001051
.text:0000000140001051 loc_140001051:                          ; CODE XREF: main+30↑j
.text:0000000140001051                 mov     eax, 21h ; '!'
.text:0000000140001056                 mov     [rsp+18h+x], ax
.text:000000014000105A                 jmp     short default
.text:000000014000105C ; ---------------------------------------------------------------------------
.text:000000014000105C
.text:000000014000105C loc_14000105C:                          ; CODE XREF: main+37↑j
.text:000000014000105C                 mov     eax, 2Ch ; ','
.text:0000000140001061                 mov     [rsp+18h+x], ax
.text:0000000140001065
.text:0000000140001065 default:                                ; CODE XREF: main+39↑j
.text:0000000140001065                                         ; main+44↑j ...
.text:0000000140001065                 movsx   eax, [rsp+18h+x]
.text:0000000140001069                 add     rsp, 18h
.text:000000014000106D                 retn
.text:000000014000106D main            endp

الخطوة 4 — أعد البناء. كل حالة بتعيّن x وبتقفز للخروج المشترك (break):

C
   switch (state)
    {
    case 0:
        x = 11;
        break;
    case 1:
        x = 22;
        break;
    case 2:
        x = 33;
        break;
    case 3:
        x = 44;
        break;
    /*default:
    __asm
    {
        movsx   eax, [rsp+18h+x]
        add     rsp, 18h
        retn
    }
    */
    default :
        goto END;
        // return x
    }





    END : 

    return x;

الباب الخامس: نمط شجرة البحث الثنائية (Binary Search Tree)

5.1 متى يلجأ لها الكومبايلر

من الباب الثالث، عرفنا التقاطع: لما الحالات متفرقة (sparse) بس عددها كبير، الكومبايلر بيواجه معضلة. الـ jump table مرفوض (المدى ضخم جدول عالفاضي)، بس سلسلة المقارنات الخطّية بتصير بطيئة جداً ( لو عندك ٨ حالات، أسوأ حالة بتعمل ٨ مقارنات).

المثال بالضبط على الحالة هاي. خلّيني أحسبلك ليش: ٨ حالات بقيم

set [5, 90, 300, 1500, 7000, 25000, 60000, 99999].

  • العدد: ٨ — كافٍ (فوق الـ ٤).

  • المدى: 99999 − 5 = 99994.

  • الكثافة: 8 ÷ 99995 = 0.008% — فرق كبير جداً.

عدد كبير + كثافة شبه معدومة = شجرة بحث ثنائية. وهذا بالضبط اللي بناه كما هو موضح بالصور بالاعلى الان ندخل اليها بشكل عملي.

5.2 منطق cmp + jg/jl لتقسيم المدى

هون الفرق عن باب المقارنات: في الباب الرابع كل المقارنات كانت مساواة فقط (jz/je). هون في مقارنات مختلفه (jg = أكبر) بتقسّم المدى. وكانها انقسمت بفاعل IF THEN خلّينا نقرأ رأس الدالة:

Assembly
        movsx   eax, [rsp+38h+var_14]    ; selector = atoi(argv) كـ short
        mov     [rsp+38h+var_10], eax
        cmp     [rsp+38h+var_10], 1B58h  ; قارن مع 7000
        jg      short loc_14000105E      
; لو state > 7000 -> الفرع الاعلى
        ; ... وإلا نكمّل بالفرع الاسفل بالقيم

شوف الـ

cmp [rsp+38h+var_10],1B58h (1B58h = 7000)

متبوع بـ jg (jump if greater) — هاي مش حالة هذا سؤال لتقسيم: "هل القيمة أكبر من 7000?". الـ 7000 هون هو الـ pivot (المحور)، اللي بيشطر الحالات لنصين تاخذ من اعظم قيمة من الشطر السفلي:

  • jg ينجح (state > 7000): اقفز للفرع الاكبر (loc_14000105E) اللي بيتعامل مع 25000, 60000, 99999.

  • jg يفشل (state ≤ 7000): كمّل بالفرع الاسفل اللي بيتعامل مع 5, 90, 300, 1500, 7000.

للتمييز: cmp const + jg/jl/jge/jle = عقدة تقسيم (pivot node)، مش حالة. أما cmp const + jz/je = اختبار حالة (leaf comparison).

الشجرة بتكون مزيج من الاثنين: عُقد تقسيم بالأعلى، اختبارات مساواة.

لاحظ نقطة دقيقة: الـ pivot نفسه (7000) هو حالة حقيقية (case 7000) الكومبايلر بيستغل قيمة موجودة كنقطة تقسيم. بعد ما jg يفشل (state ≤ 7000)، أول إشي بيتفحّص بالفرع السفلي:

Assembly
       cmp     [rsp+38h+var_10], 7000   ; هل هي == 7000 بالضبط؟
        jz      short loc_1400010AE      ; -> case 7000

وهذا لسبب لوغاريتمية الأداء لن نتكلم بهذا الموضوع لانه بديهي جدا .

5.3 شكل الشجرة في الـ Graph View وإعادة بناء الترتيب

شكل شجرة البحث في الـ Graph View مميز جداً ومختلف عن الباب الرابع: هو فعلاً شجرة متفرّعة — كل عقدة تقسيم بتولّد فرعين، مش فرع واحد نازل. خلّيني أرسملك البنية المنطقية اللي استخرجناها من الـ disassembly:

خلّيني أرسم بنية الشجرة المنطقية اللي استخرجناها — بتشوف فيها الـ pivot فوق، والفرعين تحته، (اختبارات المساواة):

5.4 إعادة بناء قائمة الحالات الأصلية كاملةً

الان نجمع كل شي. من الـ disassembly والصور، استخرجنا كل زوج (قيمة الـ case -> القيمة المُرجَعة). هذا الجدول الكامل بعد الفهم:

Assembly
;Selector
.text:0000000140001018                 mov     [rsp+38h+var_14], ax
.text:000000014000101D                 movsx   eax, [rsp+38h+var_14]
.text:0000000140001022                 mov     [rsp+38h+var_10], eax
; Cases ~ Lower
.text:0000000140001026                 cmp     [rsp+38h+var_10], 7000
.text:000000014000102E                 jg      short loc_14000105E
.text:0000000140001030                 cmp     [rsp+38h+var_10], 7000
.text:0000000140001038                 jz      short loc_1400010AE
.text:000000014000103A                 cmp     [rsp+38h+var_10], 5
.text:000000014000103F                 jz      short loc_14000107E
.text:0000000140001041                 cmp     [rsp+38h+var_10], 90
.text:0000000140001046                 jz      short loc_14000108A
.text:0000000140001048                 cmp     [rsp+38h+var_10], 300
.text:0000000140001050                 jz      short loc_140001096
.text:0000000140001052                 cmp     [rsp+38h+var_10], 1500
.text:000000014000105A                 jz      short loc_1400010A2
.text:000000014000105C                 jmp     short loc_1400010DC
Assembly
; Cases ~ Higher
.text:000000014000105E loc_14000105E:                          ; CODE XREF: sub_140001000+2E↑j
.text:000000014000105E                 cmp     [rsp+38h+var_10], 25000
.text:0000000140001066                 jz      short loc_1400010BA
.text:0000000140001068                 cmp     [rsp+38h+var_10], 60000
.text:0000000140001070                 jz      short loc_1400010C6
.text:0000000140001072                 cmp     [rsp+38h+var_10], 99999
.text:000000014000107A                 jz      short loc_1400010D2
.text:000000014000107C                 jmp     short loc_1400010DC
Image

الباب السادس: التعرّف على الأنماط في IDA Pro

6.1 كيف IDA يكتشف الـ switch تلقائياً

أIDA ما بيشوف switch — هو بيشوف idiom: نمط تعليمات بيطابق توقيعاً معروفاً. عنده مكتبة توقيعات لكل كومبايلر (MSVC، GCC، Clang...)، وكل توقيع بيوصف شكل الـ jump table المتوقّع. لما يلاقي تسلسل بيطابق (فحص حدود + قراءة من جدول + قفزة غير مباشرة)، بيشغّل محلّل الـ switch.

شوف رأس مثالنا الأول، وين IDA كتب تعليقاته لحالها:

Assembly
.text:000000014000100F                 cmp     ax, 9           ; switch 10 cases
.text:0000000140001013                 ja      short def_14000102B ; jumptable 000000014000102B default case, cases 3,6,7
.text:0000000140001015                 movsx   rax, ax
.text:0000000140001019                 lea     r8, cs:140000000h
.text:0000000140001020                 mov     eax, ds:(jpt_14000102B - 140000000h)[r8+rax*4]
.text:0000000140001028                 add     rax, r8
.text:000000014000102B                 jmp     rax             ; switch jump

يختبر الحد من 9 وما اعلى هو default بعدها التعليمات cases وبعدها الـ jumptable وبعدها القفزة الغير مباشرة jmp rax.

def_ تشير الى Default block .

(jpt_14000102B - 140000000h)[r8+rax*4] -> [r8+rax*4 + jpt_14000102B - 140000000h]

خطوات الكشف اللي بيعملها داخلياً (بشكل مبسط):

  • يلاقي jmp reg أو jmp [mem] غير مباشر.

  • يتتبّع بالعكس: من اين جاي العنوان؟ يلاقي القراءة من جدول.

  • يجد فحص الحدود (cmp + ja) اللي بيحمي الجدول.

  • يحدّد: قاعدة الجدول، حجم الـ element (*4)، عدد العناصر (من حد الـ cmp)، والـ default (وجهة الـ ja).

6.2 قراءة سطر jumptable ... default ... وماذا يعني كل حقل

لما تضغط على الـ jmp تبع الـ switch، IDA بيعرض سطر ملخّص. خلّينا نفككه. بالمثال الثاني، التعليق كان:

Assembly
ja      short def_14000102B ; jumptable 000000014000102B default case, cases 3,6,7
  • jumptable 000000014000102B — عنوان الـ jmp غير المباشر اللي بيستخدم هذا الجدول (نقطة التوزيع). هنا IDA بيربط فيها كل حالات الجدول.

  • default case — هذا الـ block هو وجهة الـ out-of-range (الـ default).

  • cases 3,6,7IDA بيخبرك إن الـ indices 3, 6, 7 بتروح للـ default (الـ holes اللي اكتشفناها). يعني IDA لحاله استنتج الفجوات وكتبها. ,وهذا بيوفّر عليك تتبّع الجدول يدوياً لمعرفة الحالات الناقصة.

Image

وعند كل block حالة، تجد:

Assembly
loc_14000102D:          ; jumptable 000000014000102B cases 0-2
loc_140001039:          ; jumptable 000000014000102B case 4
loc_140001045:          ; jumptable 000000014000102B case 5
loc_140001051:          ; jumptable 000000014000102B case 8
loc_14000105D:          ; jumptable 000000014000102B case 9
def_14000102B:          ; jumptable 000000014000102B default case, cases 3,6,7

مثلا الـ Block الأول :

cases 0-2 — الـ block هو هدف الـ indices 0، 1، 2 (الـ shared targets اللي اكتشفناها). IDA كتب 0-2 لأنه شاف ثلاث خانات بتأشّر على نفس العنوان.

6.3 الـ Graph View: شكل الـ "fan-out"

هون الفرق البصري عن البابين السابقين. السلّم (if-else) كان درجات نازلة، والشجرة كانت تفرّعات ثنائية. الـ jump table له شكل فريد: كتلة واحدة بتتفرّع لكل الحالات دفعة وحدةfan-out (مروحة):

Image

كتلة توزيع واحدة فوق لكل الحالات بمستوى واحد تجمّع بنقطة خروج واحدة. قارن مع البابين السابقين: السلّم (if-else) كل درجة بتولّد التالية عموديا، والشجرة بتتفرّع ثنائياً. الـ jump table مسطّح — كل الحالات على نفس المستوى، لأنه ما في ترتيب مقارنات بينها، الكل وصله بقفزة وحدة. أول ما تشوف الـ fan-out المسطّح،.

6.4 متى يفشل IDA في الكشف، وليش

في الواقع بتصادف حالات بيفشل فيها، وبتشوف jmp rax بدون switch jump ولا جدول معرّف. الأسباب الشائعة:

أولاً — قاعدة غير مباشرة (indirect base). لو الكومبايلر حسب قاعدة الجدول بطريقة غير نمطية (مثلاً جمعها من كذا register عبر خطوات متفرقة)، IDA ممكن يفقد التتبّع. توقيعه بيتوقّع نمط معين لحساب القاعدة لو انحرف عنه، يفشل.

ثانياً — offsets. أحياناً الجدول بيخزّن offsets بطريقة (مثلاً نسبة لشي غير الـ image base، أو scaling

IDA ما بعرف يحسب العناوين الهدف.

ثالثاً — الـ obfuscation. الـ packers والـ protectors بيكسروا التوقيع عمداً: بيحطوا تعليمات junk بين خطوات الـ idiom، أو بيحسبوا القفزة بطريقة ملتوية، أو بيشفّروا الجدول ويفكوه وقت التشغيل. الهدف بالضبط إن IDA (وإنت) ما تتعرّف على الـ switch.

رابعاً — الجدول مفصول عن الكود أو مش موجود data. لو IDA حسب إن منطقة الجدول هي "كود" بدل "بيانات"، رح يحاول يفكّكها كتعليمات (garbage)، وما يربطها كجدول.

في كل هاي الحالات النتيجة وحدة: jmp rax بدون سياق واضح لها، وكتل حالات معلّقة (orphan blocks) IDA ما عرف مين بيوصلها. وهون بيجي دورك اليدوي.

6.5 الإصلاح اليدوي: Specify switch idiom

لما IDA يفشل، عنده أداة مخصصة. وقّف الماوس على سطر الـ jmp غير المباشر، وبعدها:

Image

بيفتحلك dialog بتعبّيله المعلومات اللي IDA ما عرف يستنتجها لحاله. وهذا بيخلّي IDA يعيد بناء الـ switch ويعمل كل الـ annotations والربط زي ما لو تعرّف عليه تلقائياً.

الباب لسابع: الـ Decompiler (Hex-Rays) وكيف يعيد بناء الـ switch

7.1 كيف يحوّل Hex-Rays الـ jump table لـ switch نظيف

الـ disassembly اللي اشتغلنا عليه طول الأبواب السابقة بيعطيك الفكرة الكاملة — بس بتكلفة: لازم تقرأ كل cmp، تحسب كل offset، تتبّع كل قفزة. الـ Hex-Rays decompiler راح ياخذ كل الـ idiom وبيرجّعه لـ switch نظيف بـ C. هو فعلياً بيعمل الـ un-lowering اللي حكينا عنه بالباب الأول — بشكل آلي وفي بعض الحالات مفيد وفي بعض الحالات غير مفيد مثلا في الـ Binary-Search Tree في معظم الحالات لا يفيد.

مثلا. الـ disassembly و ناتج Hex-Rays بكون:

Image

7.2 الحالات التي يوضح انه اخطأ فيها ويعرضها كـ JUMPOUT أو goto

Hex-Rays بيبني على ناتج الـ disassembler. فاذا IDA فشل بتعريف الـ switch (الأسباب اللي ذكرناها بالباب الثامن: obfuscation، indirect base، offsets)، الـ decompiler يرث الفشل ايضا ويطلّع pseudocode بنفس المشاكل. الشائع يظهر لك:

JUMPOUT(...). هذا أشهر علامة فشل. لما Decompiler يشوف jmp reg لعنوان ما عرف يحدّده (لأن الجدول مش معرّف)، بيعجز يبني switch، فبيكتب:

```asm

JUMPOUT(0x14000102D); // قفزة لمكان الـ decompiler ما قدر يتبعه

```

أJUMPOUT معناها: في قفزة لهذا العنوان، بس غير قادر على تمثّيلها كـ control flow منظّم.

وجودها = الـ switch مكسور بالـ disassembly.

ـgoto بشكل غير مرتب و LABEL. بدل switch نظيف، بتلاقي شبكة من:

C
if (v1 > 7) goto LABEL_9;
goto *off_table[v1];   // أو ما يشبهها
LABEL_3: ...
LABEL_4: ...

7.3 إجبار الـ decompiler على تفسير صحيح (تصحيح الـ idiom أولاً)

الـ Hex-Rays بيبني فوق الـ disassembly. ما بتصلّح الـ pseudocode بالـ pseudocode — بتصلّحه بالـ disassembly، وبعدها بتعيد الـ decompilation.

الباب الثامن: التحليل المؤتمت (Automation)

كل اللي عملناه لحد الان كان يدوي: نفتح دالة، نقرأ الـ idiom، نتتبّع الجدول. هذا ممتاز للتعلم، بس مش عملي لما يكون عندك binary فيه مئات الـ switches (مثلاً VM dispatcher فيه 200 handler، أو برنامج ضخم). هون بتيجي الأتمتة: سكربت IDAPython بيمشي على كل الـ binary، يلاقي كل jump table، يستخرج حالاته، ويصدّرلك خريطة كاملة بثواني.

8.1 IDAPython: تعداد كل الـ jump tables في الـ binary

الفكرة الأساسية: نمشي على كل تعليمة في كل دالة، ونسأل IDA هل هذا عنوان فيه switch?". لو رجّع معلومات، لقينا switch.

ما نريد القيام به :

Pseudocode
الخوارزمية 1: ExtractSwitchStatements(Binary B)

Input: B (The analyzed executable binary)
Output: R (List of tuples containing switch address and metadata)

1:  R ← ∅
2:  for each Function f ∈ B do
3:      for each Instruction i ∈ f do
4:          SwitchInfo ← IDA_GetSwitchInfo(Address(i))
5:          if SwitchInfo ≠ NULL then
6:              R ← R ∪ { (Address(i), SwitchInfo) }
7:          end if
8:      end for
9:  end for
10: return R

▶ Execution & Output Phase
11: SwitchList ← ExtractSwitchStatements(TargetBinary)
12: Print "switch count = " + Length(SwitchList)
13: for each (Addr, Info) ∈ SwitchList do
14:     Print Addr, Info.CaseCount, Info.TableAddress
15: end for

هذه الخوارزمية هدفها استخراج جميع تعليمات switch-case التي اكتشفها IDA Pro داخل ملف تنفيذي (Binary)، ثم عرض معلومات عنها.

الصراحة لن اتعب نفسي في كتابة بايثون (اكره هذه اللغه) ساقوم باعطاء codex الكود الكاذب والـ apis وهو يقوم ببنائها :

Python
# big thanks for codex (:
import ida_funcs, ida_nalt
import idautils

def find_all_switches():
    """It follows every instruction and collects all the switch addresses."""
    switches = []
    for func_ea in idautils.Functions():                  # كل دالة
        f = ida_funcs.get_func(func_ea)
        if not f:
            continue
        for head in idautils.Heads(f.start_ea, f.end_ea): # كل تعليمة
            si = ida_nalt.get_switch_info(head)            # هل فيه switch؟
            if si:                                        
                switches.append((head, si))
    return switches

found = find_all_switches()
print(f"[+] Number of switches present: {len(found)}")
for ea, si in found:
    print(f"    jmp @ {ea:#x} | {si.get_jtable_size()} cases | table @ {si.jumps:#x}")
Image

8.2 الوصول لمعلومات الـ switch عبر switch_info_t

و الان نريد الوصول الى المعلومات في الـ switch من خلال switch_info_t

الخوارزمية :

Pseudocode
Algorithm 2: ExtractSwitchCases(JmpAddress, Info)
Input: JmpAddress (Address of the indirect jump instruction)
       Info (Metadata structure of the switch statement)
Output: C (Ordered list of tuples containing case metadata)

1:  C ← ∅
2:  TableAddr ← Info.Table
3:  Count ← Info.CaseCount
4:  ElemSize ← Info.ElementSize
5:  Base ← Info.ElBase          ▶ 0 if table uses absolute addressing
6:  LowestCase ← Info.LowCase
7:  DefaultAddr ← Info.DefJump  ▶ Address of the default execution block

8:  for i ← 0 to Count - 1 do
9:      EntryAddr ← TableAddr + (i × ElemSize)
10:     RawValue ← ReadMemory(EntryAddr, ElemSize)
        
11:     if Base ≠ 0 then        ▶ Relative Jump Table (e.g., x64 PIC)
12:         Target ← Base + RawValue
13:     else                    ▶ Absolute Jump Table (e.g., standard x86)
14:         Target ← RawValue
15:     end if

16:     CaseValue ← LowestCase + i
17:     Name ← GetNameAt(Target)
18:     IsHole ← (Target = DefaultAddr)

19:     C ← C ∪ { (CaseValue, Target, Name, IsHole) }
20: end for

21: return C

الكود في بايثون :

Python
# Again big thanks for codex (:
import ida_funcs, ida_nalt, ida_bytes, ida_name
import idautils

def extract_switch_cases(ea, si):
    """Returns a list (case_value, target_ea, label, is_hole) for each entry."""
    results = []
    loc     = si.jumps                       # عنوان الجدول
    n       = si.get_jtable_size()           # عدد العناصر
    elsize  = si.get_jtable_element_size()   # حجم كل entry
    elbase  = si.elbase                      # القاعدة
    lowcase = si.lowcase                     # أصغر case

    for i in range(n):
        entry_ea = loc + i * elsize
        raw = ida_bytes.get_wide_dword(entry_ea) if elsize == 4 \
              else ida_bytes.get_qword(entry_ea)

        target = (elbase + raw) & 0xFFFFFFFFFFFFFFFF if elbase else raw
        case_val = lowcase + i
        label = ida_name.get_name(target) or f"loc_{target:x}"
        is_hole = (target == si.defjump)
        results.append((case_val, target, label, is_hole))
    return results


def dump_all_switches():
    for func_ea in idautils.Functions():
        f = ida_funcs.get_func(func_ea)
        if not f:
            continue
        for head in idautils.Heads(f.start_ea, f.end_ea):
            si = ida_nalt.get_switch_info(head)
            if not si:
                continue
            fname = ida_funcs.get_func_name(func_ea)
            print(f"\n[switch] {fname} @ {head:#x}  "
                  f"({si.get_jtable_size()} cases, default={si.defjump:#x})")
            for case_val, target, label, is_hole in extract_switch_cases(head, si):
                tag = "  <- HOLE (default)" if is_hole else ""
                print(f"    case {case_val:<6} -> {target:#x}  {label}{tag}")

dump_all_switches()
Image

8.3 كشف الـ jump tables المخفية (heuristic scan)

المشكلة: السكربت السابق بيعتمد على get_switch_info، اللي بيرجع فاضي لو IDA فشل بالكشف (obfuscation، indirect base etc…). فالجداول المخفية ما تظهر.

الحل: scan بيدوّر على القفزات غير المباشرة (jmp reg / jmp [mem]) اللي IDA ما ربطها بأي switch — يكونو مرشّحين.

لكن لن نتكلم في هذا الموضوع لانه غير ذا اهمية في المقال هذا .

المصادر :

[0] Dennis YurichevReverse Engineering for Beginners (المعروف أيضاً بـ RE4B و Understanding Assembly Language

[1] Chris Eagle & Kara NanceThe IDA Pro Book: The Unofficial Guide to the World's Most Popular Disassembler, 2nd ed., No Starch Press.

[2] Eldad EilamReversing: Secrets of Reverse Engineering, Wiley. يقدّم

[3] Bruce Dang, Alexandre Gazet & Elias BachaalanyPractical Reverse Engineering: x86, x64, ARM, Windows Kernel, Reversing Tools, and Obfuscation, Wiley.

[4] Agner Fog

[5] Hex-Rays : docs.hex-rays.com & cpp.docs.hex-rays.com. Hex-Rays

[6] Intel® 64 and IA-32 Architectures Software Developer's Manual (Vol. 1 & 2).

[7] Microsoft Learn /Od /O1 /O2 /Os /Ot

للتنويه: تم استخدام نماذج الذكاء الاصطناعي (AI) لمراجعة هذا النص وتصحيح الأخطاء اللغوية فيه فقط كما ان اكواد بايثون المولدة عن طريق CodeX

#