مضى على الشبكة و يوم من العطاء.

[ شرح ] لغة C&ASM | التمرير بالمرجع والتمرير بالقيمة Passing by reference vs Passing by value & intro to assembly 0x13

N0Tb1t

./عضو
>:: v1p ::<

firefox
linux

السمعة:

بسم الله

من هاد المقال رح نبلش ندخل على Assembly x86-64
. ليكون عنا فهم low-level ... ان شاء الله الشرح رح يكون بسيط وواضح.

قبل ما نبدا بشرح الأكواد بـ assembly لازم نتعلم أهم الأوامر وناخد نبذة بسيطة عنها وان شاء الله عالممارسة بتوضح أكتر.

Assembly:
هي low level programming language واختصارا 'asm'.
سميت low-level لانها أقرب للـCPU من غيرها من اللغات التانية متل C/C++ , Java. وهي أول طبقة بيكون الانسان قادر على فهمها والتعامل معها.

وهي صورة لتوضح الامور:

WhatsApp Image 2025-06-26 at 11.05.45 PM.webp


كل أمر بلغة 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:

2025-06-27 12_05_11-Word‪ - مستند1.webp


pass by reference:

2025-06-27 12_09_22-Word‪ - مستند1.webp



سبب ادخال assembly بشرح لغة C: لتقدر توصل للامور الadvanced بالـreverse engineering لازم حرفياً تكون فاهم الكود سطر سطر بالـlow-level وانا صراحة بشوف انها طريقة فعالة تبدا تعلم الـasm مع C.

لان التعلم عملي مع فهم كيف بيمشي كود اسمبلي مع كود C.
والسلام عليكم ورحمة الله وبركاته
 

آخر المشاركات

عودة
أعلى