



السمعة:
بسم الله
من هاد المقال رح نبلش ندخل على Assembly x86-64. ليكون عنا فهم low-level ... ان شاء الله الشرح رح يكون بسيط وواضح.
قبل ما نبدا بشرح الأكواد بـ assembly لازم نتعلم أهم الأوامر وناخد نبذة بسيطة عنها وان شاء الله عالممارسة بتوضح أكتر.
Assembly: هي low level programming language واختصارا 'asm'.
سميت low-level لانها أقرب للـCPU من غيرها من اللغات التانية متل C/C++ , Java. وهي أول طبقة بيكون الانسان قادر على فهمها والتعامل معها.
وهي صورة لتوضح الامور:
كل أمر بلغة asm بيكون اسمو 'mnemonic' بيتم ترجمتو إلى ما يسمى Opcode (عبارة عن machine code ممثل بصيفة الـHex). وبيتم تحويلها من hex لـbinary لتتنفذ بواسطة الـ CPU.
خلينا نبدا نتعلم أهم الـmnemonics إلي بتمثل عادة 90% من كود الـasm :
ملاحظة: الفاصلة المنقوطة بـasm ( ; ) للـcomment
1.نقل البيانات
2.العمليات الحسابية:
3.المقارنة والتحكم:
4.إدارة المكدس:
مع الممارسة ان شاء الله بتكون هي الـmnemonics واضحة ومفهومة أكتر وباقي الأوامر اكيد رح يمروا معنا بالمقالات القادمة.
ملاحظة هامة:
قبل ما نبدا بتعلم الـassembly لازم يكون عندك معرفة بقصص الـmemory والـregister شرحناهن بمقالات الـpointers, stack vs heap.
Passing by value & Passing by reference
يتم تمرير المعاملات بلغة C إلى الدوال بطريقتين رئيسيتين:
Pass by value (التمرير بالقيمة).
Pass by reference (التمرير بالمرجع) باستخدام المؤشرات.
• Pass by value: عندما نمرر معامل (argument) بالقيمة (by value) .. الدالة بتنشىء نسخة من المتغير الأصلي (يعني أي تغيير بصير عالمعامل داخل الدالة ما بيأثر على قيمة المتغير الاصلية).
مثال:
تحليل الذاكرة:
1.num في main() بيكون الو عنوان افتراضي 0x1000 فرضاً وقيمته 10.
2.عند استدعاء modify(num) تنشأ نسخة جديدة من x فرضاً بالعنوان 0x2000 وقيمته 10.
3.التعديل x=20 بيصير بس بـالعنوان 0x2000
4.المتغير الأصلي num بيضل متل ما هو بالعنوان 0x1000
كود الاسمبلي:
قبل كيف ممكن نحول كود C الى ASM؟ في اكتر من طريقة بس لازم نتعلم نحولو يدويا بـ gcc.
الباراميترات لتسهيل قراءة الكود.
هيك بيصير الكود:
طبعا نحنا رح نركز بكل مقال عموضوع المقال وشوي شوي بيكون عنا فهم شامل ان شاء الله ..
وهلأ رح نحكي عن الـ Passing by value داخل دالة Main&Modify:
هلأ هون منروح عالدالة modify:
بعد الـmnemonic (ret) الي باخر دالة modify بترجع للدالة main:
شو هو الـ DWORD PTR إلي شفناه بأكتر من سطر؟ مؤشر حجم البيانات (منستخدمو لتحديد حجم البيانات الي بتتعامل معها الـmnemonics).
الـDOWRD (double word):
Byte = 8bits
WORD = 2Bytes = 16bits
DWORD = 4Bytes = 32bits
QWORD = 8Bytes = 64bits
الـPTR = Pointer:
يشير إلى أننا نتعامل مع عنوان بالذاكرة.
ليش منستخدم DWORD PTR؟
لأن المعالج ما بيعرف تلقائياً حجم البيانات إلي بدنا نقراها/نكتبا من الذاكرة. لهيك منحدد الحجم يدوياً.
• Pass by reference: لغة C ما فيها 'reference variables' متل لغة C++ .. فلهيك منستخدم المؤشرات وقت التمرير بالمرجع. منمرر عنوان لمتغير في الدالة.
هون التعديلات داخل الدالة بتأثر مباشرة المتغير الاصلي.
منستخدم العامل & لاستخراج عنوان الذاكرة ،و العامل *للوصول الى القيمة.
مثال:
تحليل الذاكرة:
1.num في main() قيمتو 10 عنوانو 0x1000
2.عند استدعاء modify(&num) يمرر العنوان 0x1000 إلى المؤشر x في الدالة
3.التعديل *x=20 يغير القيمة في العنوان 0x1000 مباشرة
4.المتغير الأصلي num بيصير 20
assembly code:
Passing by reference in ASM:
دالة modify:
بعد ret بيرجع للدالة main:
هون عنا mnemonic جديد إلي هو 'lea' :
وظيفتو يحسب العنوان الفعلي (effective address) لتعريف الذاكرة ويخزنه في الوجهة (dest) دون الوصول إلى الذاكرة. (يعني الأمر lea لا يصل إلى الذاكرة بس بيتعامل مع الـaddresses).
عنا حالة شاذه:
Passing by arrays: تمرير المصفوفات يتم بشكل تلقائي بالمرجع لأن المصفوفة بتشير إلى عنوان أول عنصر فيها
مثال:
ملاحظة هامة:
عند تمرير مؤشر إلى دالة يتم تمرير نسخة من العنوان (يعني فينك تعدل القيمة إلي بيشير الها العنوان (ptr=value*) بس إذا غيرت المؤشر متل (ptr=&new_ptr) ما بيأثر على المؤشر الأصلي خارج الدالة). اذا بدك تغير المؤشر الأصلي بتستخدم Pointer to Pointer مؤشر إلى مؤشر (int **ptr).
مثال:
اسئلة مهمة:
* كيف منعرف انو التمرير هو تمرير بالمرجع (pass by reference) ؟
بيكون فيه الأمر lea -> لأن بيحسب العناوين فقط بدون ما يتدخل بالذاكرة.
* ليش مع الـpass by reference استخدمنا rax ومع الـpass by value استخدمنا eax وشو الفرق بيناتن؟
eax هو نفسه rax بس rax لمعمارية 64-bit
السبب هو تعاملنا بالـ pass by reference مع العناوين وعند التعامل مع العنواين أو الـpointers لازم نستخدم سجلات الـ64-bit. لأن العنوان هو عبارة عن (8 بايت) ،اذا استخدمنا 32-bit بصير تقسيم بالعنوان لان مساحتو بس (4 بايت).
* ليش بالـ by ref كان [rbp - 12] وبالـ by val [rbp - 4]؟
بالـ by ref عنا حماية اسمها (stack canary) وهي عبارة عن 8 بايت بتحمي الـreturn address بالستاك من هجمات الـbuffer overflow والـ 4 بايت للمتغير وهيك بيكون تمثيل الستاك:
pass by value:
pass by reference:
سبب ادخال assembly بشرح لغة C: لتقدر توصل للامور الadvanced بالـreverse engineering لازم حرفياً تكون فاهم الكود سطر سطر بالـlow-level وانا صراحة بشوف انها طريقة فعالة تبدا تعلم الـasm مع C.
لان التعلم عملي مع فهم كيف بيمشي كود اسمبلي مع كود C. والسلام عليكم ورحمة الله وبركاته
من هاد المقال رح نبلش ندخل على Assembly x86-64. ليكون عنا فهم low-level ... ان شاء الله الشرح رح يكون بسيط وواضح.
قبل ما نبدا بشرح الأكواد بـ assembly لازم نتعلم أهم الأوامر وناخد نبذة بسيطة عنها وان شاء الله عالممارسة بتوضح أكتر.
Assembly: هي low level programming language واختصارا 'asm'.
سميت low-level لانها أقرب للـCPU من غيرها من اللغات التانية متل C/C++ , Java. وهي أول طبقة بيكون الانسان قادر على فهمها والتعامل معها.
وهي صورة لتوضح الامور:
كل أمر بلغة asm بيكون اسمو 'mnemonic' بيتم ترجمتو إلى ما يسمى Opcode (عبارة عن machine code ممثل بصيفة الـHex). وبيتم تحويلها من hex لـbinary لتتنفذ بواسطة الـ CPU.
خلينا نبدا نتعلم أهم الـmnemonics إلي بتمثل عادة 90% من كود الـasm :
ملاحظة: الفاصلة المنقوطة بـasm ( ; ) للـcomment
1.نقل البيانات
كود:
mov destination(الوجهة), source(المصدر) ; هاد الأمر هو المسؤول عن نقل البيانات من المصدر إلى الوجهة يعني متل إسناد القيم (x=5) على سبيل المثال.
2.العمليات الحسابية:
كود:
add dest, src ; أمر الجمع dest = dest + src
sub dest, src ; أمر الطرح dest = dest - src
inc operand ; عداد زيادة operand ++
dec operand ; عداد نقصان operand --
3.المقارنة والتحكم:
كود:
cmp op1, op2 ; يقارن op1 و op2
jmp label ; يقفز إلى الـlabel بدون أي شروط
4.إدارة المكدس:
كود:
push value ; يضع قيمة بالمكدس
pop value ; يسترجع قيمة من المكدس
call func ; يستدعي دالة (بيحط عنوان للعودة)
ret ; يعود من الدالة (بيرجع لعنوان العودة تبع الـcall)
مع الممارسة ان شاء الله بتكون هي الـmnemonics واضحة ومفهومة أكتر وباقي الأوامر اكيد رح يمروا معنا بالمقالات القادمة.
ملاحظة هامة:
قبل ما نبدا بتعلم الـassembly لازم يكون عندك معرفة بقصص الـmemory والـregister شرحناهن بمقالات الـpointers, stack vs heap.
Passing by value & Passing by reference
يتم تمرير المعاملات بلغة C إلى الدوال بطريقتين رئيسيتين:
Pass by value (التمرير بالقيمة).
Pass by reference (التمرير بالمرجع) باستخدام المؤشرات.
• Pass by value: عندما نمرر معامل (argument) بالقيمة (by value) .. الدالة بتنشىء نسخة من المتغير الأصلي (يعني أي تغيير بصير عالمعامل داخل الدالة ما بيأثر على قيمة المتغير الاصلية).
مثال:
C:
#include <stdio.h>
void modify(int x) {
x = 20; // التعديل يؤثر فقط على النسخة المحلية داخل modify
printf("inside function: %d\n", x); // 20
}
int main() {
int num = 10;
modify(num); // تمرير بالقيمة
printf("outside function: %d\n", num); // 10 (ما بيتغير)
return 0;
}
تحليل الذاكرة:
1.num في main() بيكون الو عنوان افتراضي 0x1000 فرضاً وقيمته 10.
2.عند استدعاء modify(num) تنشأ نسخة جديدة من x فرضاً بالعنوان 0x2000 وقيمته 10.
3.التعديل x=20 بيصير بس بـالعنوان 0x2000
4.المتغير الأصلي num بيضل متل ما هو بالعنوان 0x1000
كود الاسمبلي:
قبل كيف ممكن نحول كود C الى ASM؟ في اكتر من طريقة بس لازم نتعلم نحولو يدويا بـ gcc.
Bash:
gcc -S -masm=intel -fno-asynchronous-unwind-tables -fno-pie -O0 fileName.c -o fileName.s
الباراميترات لتسهيل قراءة الكود.
هيك بيصير الكود:
كود:
.LC0:
.string "inside function: %d\n"
.text
.globl modify
.type modify, @function
modify:
push rbp
mov rbp, rsp
sub rsp, 16
mov DWORD PTR [rbp-4], edi
mov DWORD PTR [rbp-4], 20
mov eax, DWORD PTR [rbp-4]
mov esi, eax
mov edi, OFFSET FLAT:.LC0
mov eax, 0
call printf
nop
leave
ret
.size modify, .-modify
.section .rodata
.LC1:
.string "outside function: %d\n"
.text
.globl main
.type main, @function
main:
push rbp
mov rbp, rsp
sub rsp, 16
mov DWORD PTR [rbp-4], 10
mov eax, DWORD PTR [rbp-4]
mov edi, eax
call modify
mov eax, DWORD PTR [rbp-4]
mov esi, eax
mov edi, OFFSET FLAT:.LC1
mov eax, 0
call printf
mov eax, 0
leave
ret
طبعا نحنا رح نركز بكل مقال عموضوع المقال وشوي شوي بيكون عنا فهم شامل ان شاء الله ..
وهلأ رح نحكي عن الـ Passing by value داخل دالة Main&Modify:
كود:
main: ; هاد الجزء خاص بالدالة main()
sup rsp, 16 ; تخصيص 16 بايت على الستاك
mov DWORD PTR -4[rbp], 10 ; [rbp - 4] = المتغير num = 10
mov eax, DWORD PTR -4[rbp] ; eax = قيمة num (10)
mov edi, eax ; نسخ القيمة 10 من متغير num الى سجل الـ edi
call modify ; استدعاء الدالة modify
هلأ هون منروح عالدالة modify:
كود:
modify:
mov DWORD PTR -4[rbp], edi ; [rbp - 4] = edi = 10 هي نسخة جديدة داخل modify مختلفة عن موقع num في main
mov DWORD PTR -4[rbp], 20 ; تعديل قيمة النسخة الجديدة الى 20 المتغير الاصلي num في main يبقى بدون تغيير
بعد الـmnemonic (ret) الي باخر دالة modify بترجع للدالة main:
كود:
; بعد العودة من modify
mov eax, DOWRD PTR -4[rbp] ; eax = num (بتضل 10)
شو هو الـ DWORD PTR إلي شفناه بأكتر من سطر؟ مؤشر حجم البيانات (منستخدمو لتحديد حجم البيانات الي بتتعامل معها الـmnemonics).
الـDOWRD (double word):
Byte = 8bits
WORD = 2Bytes = 16bits
DWORD = 4Bytes = 32bits
QWORD = 8Bytes = 64bits
الـPTR = Pointer:
يشير إلى أننا نتعامل مع عنوان بالذاكرة.
ليش منستخدم DWORD PTR؟
لأن المعالج ما بيعرف تلقائياً حجم البيانات إلي بدنا نقراها/نكتبا من الذاكرة. لهيك منحدد الحجم يدوياً.
• Pass by reference: لغة C ما فيها 'reference variables' متل لغة C++ .. فلهيك منستخدم المؤشرات وقت التمرير بالمرجع. منمرر عنوان لمتغير في الدالة.
هون التعديلات داخل الدالة بتأثر مباشرة المتغير الاصلي.
منستخدم العامل & لاستخراج عنوان الذاكرة ،و العامل *للوصول الى القيمة.
مثال:
C:
#include <stdio.h>
void modify(int *x) {
*x = 20; // التعديل يؤثر على المتغير الاصلي
printf("inside function:%d\n", *x); // 20
}
int main() {
int num = 10;
modify(&num); // تمرير بالمرجع (عنوان ذاكرة)
printf("outside function:%d\n", num); // تغير الى 20
return 0;
}
تحليل الذاكرة:
1.num في main() قيمتو 10 عنوانو 0x1000
2.عند استدعاء modify(&num) يمرر العنوان 0x1000 إلى المؤشر x في الدالة
3.التعديل *x=20 يغير القيمة في العنوان 0x1000 مباشرة
4.المتغير الأصلي num بيصير 20
assembly code:
كود:
.LC0:
.string "inside function:%d\n"
.text
.globl modify
.type modify, @function
modify:
push rbp
mov rbp, rsp
sub rsp, 16
mov QWORD PTR [rbp-8], rdi
mov rax, QWORD PTR [rbp-8]
mov DWORD PTR [rax], 20
mov rax, QWORD PTR [rbp-8]
mov eax, DWORD PTR [rax]
mov esi, eax
mov edi, OFFSET FLAT:.LC0
mov eax, 0
call printf
nop
leave
ret
.size modify, .-modify
.section .rodata
.LC1:
.string "outside function:%d\n"
.text
.globl main
.type main, @function
main:
push rbp
mov rbp, rsp
sub rsp, 16
mov rax, QWORD PTR fs:40
mov QWORD PTR [rbp-8], rax
xor eax, eax
mov DWORD PTR [rbp-12], 10
lea rax, [rbp-12]
mov rdi, rax
call modify
mov eax, DWORD PTR [rbp-12]
mov esi, eax
mov edi, OFFSET FLAT:.LC1
mov eax, 0
call printf
mov eax, 0
mov rdx, QWORD PTR [rbp-8]
sub rdx, QWORD PTR fs:40
je .L4
call __stack_chk_fail
.L4:
leave
ret
Passing by reference in ASM:
كود:
main:
mov DWORD PTR -12[rbp], 10 ; [rbp - 12] = المتغير num = 10
lea rax, -12[rbp] ; rax = عنوان num (&num)
mov rdi, rax ; rdi = عنوان num (يمرر العنوان كـ معامل للدالة)
call modify ; استدعاء دالة modify
دالة modify:
كود:
modify:
mov QWORD PTR -8[rbp], rdi ; يحفظ العنوان الاصلي لـ num بمتغير محلي
mov rax, QWORD PTR -8[rbp] ; rax = عنوان num الاصلي
mov DWORD PTR[rax], 20 ; *rax = 20 (عند العنوان الاصلي مباشرة)
بعد ret بيرجع للدالة main:
كود:
; بعد العودة للدالة main
mov eax, DWORD PTR -12[rbp] ; eax = num (20) القيمة الجديدة
هون عنا mnemonic جديد إلي هو 'lea' :
وظيفتو يحسب العنوان الفعلي (effective address) لتعريف الذاكرة ويخزنه في الوجهة (dest) دون الوصول إلى الذاكرة. (يعني الأمر lea لا يصل إلى الذاكرة بس بيتعامل مع الـaddresses).
عنا حالة شاذه:
Passing by arrays: تمرير المصفوفات يتم بشكل تلقائي بالمرجع لأن المصفوفة بتشير إلى عنوان أول عنصر فيها
مثال:
C:
#include <stdio.h>
void changeArray(int arr[]) {
arr[0] = 100; // تعديل مباشر على المصفوفة الاصلية
}
int main() {
int a[3] = {1, 2, 3};
changeArray(a); // تمرير العنوان بدون استخدام &
printf("%d\n", a); // 100
return 0;
}
ملاحظة هامة:
عند تمرير مؤشر إلى دالة يتم تمرير نسخة من العنوان (يعني فينك تعدل القيمة إلي بيشير الها العنوان (ptr=value*) بس إذا غيرت المؤشر متل (ptr=&new_ptr) ما بيأثر على المؤشر الأصلي خارج الدالة). اذا بدك تغير المؤشر الأصلي بتستخدم Pointer to Pointer مؤشر إلى مؤشر (int **ptr).
مثال:
C:
#include <stdio.h>
#include <stdlib.h>
// الدالة بتاخد مؤشر لمؤشر **
void changePointer(int **ptrToPtr) {
int *new_ptr = malloc(sizeof(int)); // انشاء مؤشر
*new_ptr = 100; // تعيين قيمة للمتغير
*ptrToPtr = new_ptr; // تغيير المؤشر الاصلي ليشير الى المؤشر الجديد
}
int main() {
int x = 5;
int *org_ptr = &x; // المؤشر الاصلي يشير الى x
printf("pointer before change: %d\n", *org_ptr); // يطبع 5
// هون منمرر عنوان المؤشر الاصلي للدالة
changePointer(&org_ptr);
printf("pointer after change: %d\n", *org_ptr); // يطبع 100
free(org_ptr); // تحرير الذاكرة
return 0;
}
اسئلة مهمة:
* كيف منعرف انو التمرير هو تمرير بالمرجع (pass by reference) ؟
بيكون فيه الأمر lea -> لأن بيحسب العناوين فقط بدون ما يتدخل بالذاكرة.
* ليش مع الـpass by reference استخدمنا rax ومع الـpass by value استخدمنا eax وشو الفرق بيناتن؟
eax هو نفسه rax بس rax لمعمارية 64-bit
السبب هو تعاملنا بالـ pass by reference مع العناوين وعند التعامل مع العنواين أو الـpointers لازم نستخدم سجلات الـ64-bit. لأن العنوان هو عبارة عن (8 بايت) ،اذا استخدمنا 32-bit بصير تقسيم بالعنوان لان مساحتو بس (4 بايت).
* ليش بالـ by ref كان [rbp - 12] وبالـ by val [rbp - 4]؟
بالـ by ref عنا حماية اسمها (stack canary) وهي عبارة عن 8 بايت بتحمي الـreturn address بالستاك من هجمات الـbuffer overflow والـ 4 بايت للمتغير وهيك بيكون تمثيل الستاك:
pass by value:
pass by reference:
سبب ادخال assembly بشرح لغة C: لتقدر توصل للامور الadvanced بالـreverse engineering لازم حرفياً تكون فاهم الكود سطر سطر بالـlow-level وانا صراحة بشوف انها طريقة فعالة تبدا تعلم الـasm مع C.
لان التعلم عملي مع فهم كيف بيمشي كود اسمبلي مع كود C. والسلام عليكم ورحمة الله وبركاته