◄ BACK TO COURSES
► CHALMERS · EDA482 · 7.5 HP · RISC-V ASSEMBLY
MASKINORIENTERAD PROGRAMMERING
RISC-V · ASSEMBLY · C LANGUAGE · MEMORY · REGISTERS · STACK
► اختر المحاضرة
01
INTRO TO
RISC-V ISA
02
RISC-V ISA
DEEP DIVE
03
CONTROL FLOW
& ARRAYS
04
COMING
SOON
05
COMING
SOON
06
COMING
SOON
07
COMING
SOON
08
COMING
SOON
09
COMING
SOON
10
COMING
SOON
11
COMING
SOON
12
COMING
SOON
01

محاضرة 1 — قريباً

► المحاضرة 2 — RISC-V ISA Deep Dive
🔷 ما هو RISC-V؟

أي processor يحتاج "لغة" يفهمها — هاي اللغة تُسمى Instruction Set Architecture (ISA). هي تحدد ثلاثة أشياء: الأوامر المتاحة، الـ Registers (المخازن المؤقتة)، وكيف يتعامل المعالج مع الذاكرة.

شركات مثل Intel وARM تمتلك ISAs خاصة بها وتطلب رسوم ترخيص لاستخدامها. RISC-V مختلف تماماً — هو معيار مفتوح المصدر مجاني 100%، أي شركة أو فرد يقدر يبني عليه معالجه الخاص بدون دفع أي رسوم. هذا ما يجعله مهماً بشكل متزايد في الصناعة والبحث العلمي.

مثال عملي: في هذا الكورس نستخدم لوحة MD307 التي تحتوي على microcontroller اسمه CH32V307 — وهو يعمل على RISC-V. كذلك Raspberry Pi Pico 2 يستخدم RISC-V. لأن كلاهم يستخدم نفس الـ ISA، يمكنهم تشغيل نفس الكود في بعض الحالات!

🔧 Variants والـ Extensions

RISC-V مصمم ليكون مرناً — يستخدم في أجهزة صغيرة جداً وفي مراكز البيانات الضخمة. لذلك هو مقسم إلى Base Instruction Set يحتوي 47 أمر فقط، وعدد كبير من الـ Extensions الاختيارية.

ريموت التلفزيون يحتاج RV32I فقط (32-bit base). الآلة الحاسبة تحتاج RV64I إضافة إلى F وD للـ floating point. الساعة الذكية تحتاج C (Compressed instructions) لتوفير الطاقة. أما المعالج في الـ lab (CH32V307) فيدعم RV32I + M + A + F + C + Zicsr + Zifencei — لكن في هذا الكورس نركز فقط على RV32I + M + Zicsr.

⚖️ ISA vs ABI

الـ ISA يحدد ما يقدر الـ Hardware يفعله — "المعالج لازم يدعم 32 register". هذا مثل قانون البناء الذي يحدد كيف تُبنى المباني.

الـ ABI (Application Binary Interface) يحدد كيف يتواصل الكود مع بعضه على مستوى الـ Software — "لما دالة تُرجع نتيجة، حطها في register x10". هذا مثل قواعد السير الداخلية في المبنى. كلاهم ضروريان: ISA يضمن إن المعالج يفهم الأوامر، ABI يضمن إن برامج مختلفة تتعاون صح.

💡 في هذا الكورس، كل الكود يتبع الـ ABI لضمان توافقه مع كود C والمكتبات.
📦 الـ Registers في RV32I

أي معالج يدعم RV32I لازم يدعم 33 register: 32 general-purpose register (x0-x31) وواحد للـ Program Counter (pc). كل register عبارة عن 32-bit.

أهم register هو x0 (zero) — هذا register خاص مُقيَّد بالـ hardware ليكون صفراً دائماً. حتى لو حاولت تكتب فيه يُهمَل. هذا يبسّط الـ Hardware كثيراً ويتيح تقليص عدد الأوامر.

الـ ABI يعطي كل register اسماً يصف وظيفته: t0-t6 للبيانات المؤقتة، a0-a7 للـ function arguments والنتائج، s0-s11 للقيم المحفوظة، sp للـ Stack Pointer، ra لعنوان العودة. يمكنك استخدام الاسم الرقمي (x5) أو اسم الـ ABI (t0) — كلاهم يشير لنفس الـ register.

RegisterABI Nameالاستخدام
x0zeroدايماً = 0 (hardwired!)
x1raReturn Address
x2spStack Pointer
x5-x7t0-t2Temporary — مش محفوظ بعد الدالة
x10-x17a0-a7Arguments / Return Value ⭐
x18-x27s2-s11Saved Registers — محفوظة بعد الدالة
pcpcProgram Counter — عنوان الأمر الجاي
⚡ فلسفة RISC وأوامر Assembly

RISC تعني "Reduced Instruction Set Computer". الفلسفة بسيطة: أوامر قليلة وبسيطة أفضل من أوامر كثيرة ومعقدة. كل أمر بسيط يمكن تنفيذه في clock cycle واحد، مما يسهّل الـ Pipelining ويجعل الـ Hardware أبسط وأسرع وأرخص.

Assembly Language هي أقرب لغة للمبرمج من الـ Machine Code. تحتوي على كل الأوامر الحقيقية، وأيضاً Pseudo Instructions وهي اختصارات يترجمها الـ Assembler تلقائياً لأوامر حقيقية. مثلاً mv x1, x2 تتحول لـ add x1, x2, x0 لأن RISC-V ما عنده أمر mv حقيقي — لكن جمع أي قيمة مع الصفر يعطي نفس النتيجة.

💡 ما تحتاج تحفظ كيف تتترجم الـ Pseudo Instructions — فقط تعرف كيف تستخدمها. الـ Assembler يتكفل بالباقي.
💾 Load/Store Architecture

في RISC-V، الأوامر الحسابية تعمل على Registers فقط — لا تستطيع أن تجمع رقماً من الذاكرة مع register مباشرة. إذا أردت الحساب على بيانات في الذاكرة، لازم تمر بثلاث خطوات: أولاً Load (جيب البيانات من الذاكرة للـ register)، ثانياً احسب، ثالثاً Store (احفظ النتيجة من الـ register للذاكرة).

هذا يبدو أطول من Intel x86 الذي يسمح بـ add r0, M(r1) مباشرة، لكن البساطة تجعل الـ Hardware أسرع وأقل أخطاءً وأسهل في الـ Pipeline.

الأمرالحجمالنوعالوصف
lb rd, offset(rs1)8-bitsignedsign extend → 32-bit
lh rd, offset(rs1)16-bitsignedsign extend → 32-bit
lw rd, offset(rs1)32-bitكامل
lbu rd, offset(rs1)8-bitunsignedzero extend → 32-bit
lhu rd, offset(rs1)16-bitunsignedzero extend → 32-bit
sb rs2, offset(rs1)8-bitيكتب أدنى byte
sh rs2, offset(rs1)16-bitيكتب أدنى halfword
sw rs2, offset(rs1)32-bitيكتب الكلمة كاملة
⚠️ في أوامر Store، الـ operand الأول هو المصدر (rs2) مش الوجهة — عكس بقية الأوامر!
📐 Sign Extension — ليش مهم؟

لنفترض عندنا متغيرين: unsigned char a = 251 و signed char b = -5. في الذاكرة، كلاهم يُخزَّن كـ 0xFB — نفس الـ bits تماماً! إذاً كيف يعرف المعالج الفرق؟

الجواب هو في أمر الـ Load نفسه. عندما تستخدم lbu (Load Byte Unsigned)، المعالج يفهم الرقم كـ unsigned ويحشي الـ 24 bit المتبقية بأصفار → نتيجة: 0x000000FB = 251. أما lb (Load Byte Signed)، يشوف إن الـ MSB = 1 (يعني سالب في Two's Complement) ويحشي الـ 24 bit بـ 1 → نتيجة: 0xFFFFFFFB = -5.

القاعدة: الـ MSB يحدد الإشارة في Two's Complement. Sign Extension يكرر الـ MSB في كل الـ bits الأعلى عشان يحافظ على نفس القيمة عند التكبير من 8-bit لـ 32-bit. هذا فهم وليس حفظ — لو شفت MSB=1 في رقم signed تعرف فوراً إنه سالب.

lbu t0, 0(t0) # zero extend → t0 = 0x000000FB = 251 lb t1, 0(t1) # sign extend → t1 = 0xFFFFFFFB = -5
⛓️ Instruction Cycle

عندما يعمل المعالج يمر بدورة ثابتة من 4 مراحل لكل أمر. أولاً Fetch: يجيب الأمر من العنوان الموجود في PC ثم يزيد PC بـ 4 (لأن كل أمر = 4 bytes). ثانياً Decode: يفهم الأمر — هل هو add؟ load؟ branch؟ ثالثاً Execute: ينفذ العملية الفعلية. رابعاً Writeback: يحفظ النتيجة في register أو ذاكرة.

بالنظرية = 4 cycles لكل أمر. بالواقع = ~1 cycle لكل أمر بسبب Pipelining (خارج نطاق الكورس). المهم أن تفهم إن PC يزيد بـ 4 بعد كل أمر ويشير دائماً للأمر القادم.

🔢 تحميل أرقام كبيرة — LUI + ADDI

أمر addi عنده 12-bit فقط للرقم الثابت (immediate)، يعني أكبر قيمة = 2047. لو أردنا تحميل رقم أكبر مثل 1,000,000 ماذا يحدث؟

الـ Assembler يقسمه تلقائياً لأمرين: lui (Load Upper Immediate) يضع الـ 20 bits العليا في الـ register، ثم addi يضيف الـ 12 bits السفلى. 1,000,000 بالـ hex = 0xF4240، فيصير: lui t0, 0xF4 (يضع 0x000F4000) ثم addi t0, t0, 0x240 (يضيف 576) = 0x000F4240 = 1,000,000.

💡 ما تحتاج تعمل هذا يدوياً — فقط اكتب li t0, 1000000 والـ Assembler يولد الأمرين تلقائياً.
📝 Variables في Assembly

بدل استخدام عناوين مطلقة، نستخدم labels لتعريف المتغيرات. في قسم .data نعرّف المتغيرات، وفي قسم .text نكتب الكود. للوصول لمتغير نستخدم la (Load Address) لنجيب عنوانه، ثم lb/lw لنقرأ قيمته.

la x1, var_a # x1 = عنوان var_a lb x2, 0(x1) # x2 = قيمة var_a = 10 la x1, var_b lb x3, 0(x1) # x3 = قيمة var_b = 20 add x1, x2, x3 # x1 = 30 var_a: .byte 10 var_b: .byte 20
⚠️ Memory Alignment — القاعدة الذهبية

لأن الذاكرة تعمل بكلمات (words) من 32-bit، هناك قيود على عناوين الوصول. إذا أردت قراءة word كاملة (4 bytes) من عنوان لا يقبل القسمة على 4، المعالج سيحتاج قراءتين منفصلتين مما يعقّد الـ Hardware — لذلك معظم المعماريات ومنها RISC-V تمنع ذلك وتعطي Exception.

القاعدة: lw/sw يحتاج عنوان يقبل القسمة على 4، lh/sh يحتاج قسمة على 2، lb/sb بدون قيود. مشكلة شائعة: تعرّف byte ثم word مباشرة بعدها — الـ byte يأخذ عنواناً واحداً، فالـ word ستبدأ من عنوان لا يقبل القسمة على 4 → كراش!

الحل: استخدم .align 2 قبل أي word. هذا يجبر الـ Assembler على إدراج bytes فاضية عشان العنوان التالي يكون قابلاً للقسمة على 2² = 4.

# مشكلة: var_a: .byte 10 # عنوان 0x100 ✅ var_b: .word 20 # عنوان 0x101 ❌ كراش! # الحل: var_a: .byte 10 .align 2 # العنوان التالي يقسم على 4 var_b: .word 20 # عنوان 0x104 ✅
💥 تكسر قاعدة الـ Alignment في الـ Hardware الحقيقي = كراش فوري! Venus (المحاكي) يتجاهل هذه القاعدة — لذلك اعتد على استخدام .align من الآن.
► تمارين المحاضرة 2 (10 تمارين)
🌐 استخدم المحاكي: venus.kvakil.me
◈ تمارين المحاضرة 2 — RISC-V Assembly
تمرين 01 — سهل
مجموع من 1 إلى 5
اكتب برنامج يحسب 1+2+3+4+5 ويخزن النتيجة في t0. النتيجة المتوقعة = 15.
استخدم: li, add, addi, blt, label
تمرين 02 — سهل
الضرب بدون mul
احسب 7 × 3 = 21 بدون استخدام أمر mul. استخدم loop + add فقط. تلميح: 7×3 = 7+7+7
counter يبدأ من 0، result يبدأ من 0، كرر إضافة var_a لـ var_b مرات
تمرين 03 — سهل
Variables في الذاكرة
عرّف var_a=15 و var_b=8 في .data section. احسب الفرق (var_a - var_b) واخزنه في s0. النتيجة = 7.
استخدم: .data, .word, la, lw, sub
تمرين 04 — متوسط
Sign Extension — lb vs lbu
عرّف byte يساوي 0xFF في الذاكرة. احمله مرتين: مرة بـ lb في t0 ومرة بـ lbu في t1. ما الفرق؟
0xFF: lb → 0xFFFFFFFF = -1، lbu → 0x000000FF = 255
تمرين 05 — متوسط
Array في الذاكرة
احفظ الأرقام 10,20,30,40,50 كـ words. اقرأهم كلهم بـ loop وأضفهم. النتيجة = 150.
استخدم base address + offset (0,4,8,12,16) — كل word = 4 bytes
تمرين 06 — متوسط
Memory Alignment
اكتب: var_a: .byte 5 ثم var_b: .word 10 مباشرة. شوف إذا يشتغل في Venus. بعدين أضف .align 2 بينهم. ما الفرق؟
Venus يتجاهل الـ Alignment — لكن الـ Hardware الحقيقي سيكرش!
تمرين 07 — متوسط
العد التنازلي + حفظ في الذاكرة
اكتب loop يبدأ من 10 وينقص 1 حتى الصفر. في كل iteration احفظ القيمة في array. النتيجة: array = [10,9,8,...,1].
احتاج base address + counter يزيد بـ 4 كل iteration
تمرين 08 — صعب
إيجاد الـ Maximum
عندك array: [3,7,2,9,1]. اكتب loop يجد أكبر رقم ويخزنه في s0. النتيجة = 9.
max = أول عنصر. loop: إذا العنصر الحالي > max حدّث max
تمرين 09 — صعب
Byte vs Halfword vs Word — Little Endian
احفظ 0x12345678 كـ word. اقرأه بـ lb (byte)، lh (halfword)، lw (word). ما النتائج؟ لماذا؟
Little Endian: البايت الأصغر في العنوان الأصغر → lb:0x78, lh:0x5678, lw:0x12345678
تمرين 10 — تحدي 🏆
Bubble Sort
اكتب Bubble Sort يرتب [5,3,8,1,4] تصاعدياً → [1,3,4,5,8]. تحتاج nested loops + swap.
للـ swap: register مؤقت. lw عنصرين → sw معكوسين
► المحاضرة 3 — Control Flow, Functions & Arrays
🦘 Jump Instructions — jal و jalr

في RISC-V، عندنا أمرين فقط للقفز غير الشرطي: jal و jalr. كلمة "Link" في اسمهم تعني إنهم يحفظون عنوان العودة — يعني العنوان الجاي بعد الأمر الحالي — عشان نقدر نرجع بعد ما نخلص من القفز. هاد مهم جداً لما نستدعي دوال.

الفرق بين jal و jalr هو إن jal تقفز بناءً على offset محسوب من مكانك الحالي (PC)، بينما jalr تقفز لعنوان موجود في register. jal تقدر تقفز +/- 1MB فقط (لأن الـ offset 20-bit)، لكن jalr تقدر تقفز لأي عنوان في الذاكرة — مفيد لو الكود في مكان بعيد مثل FLASH memory.

الأمرالمعنىالنتيجة
jal rd, offsetJump and Linkrd = PC+4 ثم PC += offset
jalr rd, offset(rs1)Jump and Link Registerrd = PC+4 ثم PC = rs1+offset

المبرمج نادراً ما يستخدم jal و jalr مباشرة — بدلاً من ذلك نستخدم Pseudo Instructions أوضح وأسهل للقراءة. كلها تتحول تلقائياً لـ jal أو jalr:

j label (Jump) يتحول لـ jal zero, offset. لاحظ إن العنوان يُكتب في zero — يعني يُهمَل — لأننا ما نحتاج نرجع. نستخدمه للـ loops الأبدية أو القفزات البسيطة.

call label يتحول لـ jal ra, offset. هنا العنوان يُحفظ في ra (register x1) عشان نقدر نرجع بـ ret.

jr rs (Jump Register) يتحول لـ jalr zero, 0(rs). مثل j بس العنوان في register — نستخدمه لو العنوان بعيد أكثر من 1MB.

ret (Return) يتحول لـ jalr zero, 0(ra). يقفز للعنوان المحفوظ في ra — هكذا نرجع من الدالة.

💡 j و call و jr و ret = كلهم Pseudo Instructions. الـ Assembler يترجمهم لـ jal أو jalr تلقائياً. الفرق الوحيد: أي register يحفظون فيه عنوان العودة.
⚠️ jal يقفز +/- 1MB فقط. لو العنوان أبعد (مثل FLASH memory) استخدم: li t0, 0x00000100 ثم jr t0
🔀 Branching — القفز الشرطي

Branching يعني القفز بناءً على شرط معين. على عكس بعض المعماريات القديمة التي تستخدم "flag register" منفصل، RISC-V يجعل الأمر أبسط: كل أمر branch يقارن مباشرة بين register-ين ويقرر القفز أم لا — هاد يبسّط الـ Hardware ويقلل مشاكل الـ Pipeline.

عندنا 6 أوامر حقيقية فقط: beq, bne, blt, bge وإصداراتهم unsigned (bltu, bgeu). ربما تتساءل: ليش ما في bgt أو ble؟ الجواب بسيط — لأن (a > b) = (b < a). بمجرد عكس ترتيب الـ operands نحصل على نفس النتيجة. لهذا bgt هو Pseudo Instruction يتحول لـ blt مع عكس الترتيب.

الأمر الحقيقيالشرط
beq rs1, rs2, labelrs1 = rs2
bne rs1, rs2, labelrs1 ≠ rs2
blt rs1, rs2, labelrs1 < rs2 (signed)
bge rs1, rs2, labelrs1 ≥ rs2 (signed)

هناك أيضاً Pseudo Instructions للمقارنة مع الصفر مثل beqz, bnez, bgtz, bltz — كلها تتحول لأوامر حقيقية باستخدام zero register.

تنبيه مهم: ما في Immediate versions للـ branch! لا تكتب bgt t1, 5, end — ما يشتغل. لازم تحط الرقم في register أولاً: li t2, 5 ثم bgt t1, t2, end.

► مثال — Factorial (5! = 120)
li t0, 1 # y = 1 li t1, 1 # i = 1 forloop: li t2, 5 bgt t1, t2, end # إذا i > 5 اخرج ← يتحول لـ blt t2, t1, end mul t0, t0, t1 # y = y × i addi t1, t1, 1 # i++ j forloop # ارجع للبداية end: # t0 = 120 ✅
📚 الـ Stack

الـ registers عددها 32 فقط. أحياناً نحتاج نحفظ قيماً مؤقتاً لما تنتهي الـ registers المتاحة. الـ Stack هو منطقة ذاكرة مخصصة لهذا الغرض. sp (register x2) يشير دايماً لرأس الـ Stack.

قاعدة مهمة: الـ Stack ينمو للأسفل — يعني لما نضيف قيمة (PUSH) ننقص sp، ولما نأخذ قيمة (POP) نزيد sp. في RISC-V ما في أوامر PUSH وPOP مدمجة — نبنيها بأنفسنا من sw/lw وaddi.

لماذا لازم ننقص sp أولاً ثم نحفظ؟ لأن sp يشير دايماً لآخر قيمة محفوظة. لو حفظنا أولاً كتبنا فوق قيمة موجودة! إذاً لازم نحجز مكان جديد أولاً بتنقيص sp، ثم نكتب في المكان الجديد.

# PUSH t0 للـ Stack: addi sp, sp, -4 # حجز مكان جديد أولاً sw t0, 0(sp) # احفظ t0 في المكان الجديد # POP t0 من الـ Stack: lw t0, 0(sp) # اقرأ القيمة أولاً addi sp, sp, 4 # ثم حرر المكان # حفظ 3 registers معاً: addi sp, sp, -12 # مساحة لـ 3 words sw t0, 8(sp) sw t1, 4(sp) sw t2, 0(sp) # استرجاعهم (عكسي): lw t2, 0(sp) lw t1, 4(sp) lw t0, 8(sp) addi sp, sp, 12
🔧 Function Calls — ABI Conventions

Assembly ما عندها functions مدمجة — نبنيها بأنفسنا. بس لو كل مبرمج عمل طريقته الخاصة، الكود ما يتوافق مع بعضه ولا مع مكتبات C. لهذا ABI يحدد قواعد موحدة يتبعها الجميع.

القاعدة بسيطة: الـ parameters تُحط في a0-a7 بالترتيب قبل استدعاء الدالة. النتيجة ترجع في a0. عنوان العودة يُحفظ تلقائياً في ra بأمر call. والدالة ترجع بأمر ret.

لو نسينا ret ماذا يحدث؟ البرنامج لا يرجع للمكان الصح — يكمل ينفذ الكود الجاي في الذاكرة بشكل عشوائي مما يؤدي لنتائج غير متوقعة أو كراش.

# مثال: min(a, b) ترجع الأصغر li a0, 20 # parameter a = 20 li a1, 10 # parameter b = 10 call min # ra = عنوان العودة # a0 = 10 بعد العودة ✅ min: blt a0, a1, min_end # إذا a < b ارجع مباشرة mv a0, a1 # b أصغر → a0 = b min_end: ret # = jalr zero, 0(ra)
💾 Register Saving — من يحفظ ماذا؟

المشكلة: لما نستدعي دالة، ما نعرف أي registers تغيّرها. لو وضعنا قيمة مهمة في t2 ثم استدعينا دالة تستخدم t2، القيمة ضاعت!

الـ ABI يحل هاي المشكلة بتقسيم الـ registers لنوعين. الأول هو Caller-Saved (t0-t6, a0-a7, ra) — هاي registers الدالة المستدعاة تقدر تغيّرها بحرية. إذا كنت تحتاجها بعد الـ call، أنت مسؤول عن حفظها. الثاني هو Callee-Saved (s0-s11) — هاي registers الدالة ملزمة بحفظها قبل استخدامها وإرجاعها قبل ret. مضمون ما تتغير بعد الـ call.

هاد الضمان مش تقني — هو اتفاقية (convention). أي دالة مكتوبة صح تلتزم بهاد القانون. لو دالة ما تتبع ABI = هي المخطئة وليس كودك.

الحل 1 — Stack: احفظ t2 قبل الـ call في Stack وأعده بعدها.
الحل 2 — s registers (أنظف): انقل القيمة لـ s0 قبل الـ call. الدالة ملزمة بعدم تغيير s0.

# الحل بالـ Stack: addi sp, sp, -4 sw t2, 0(sp) # احفظ t2 call max lw t2, 0(sp) # أعد t2 addi sp, sp, 4 # الحل بـ s register (أنظف): mv s0, t2 # s0 مضمون ما يتغير بعد call call max mv a1, s0 # القيمة لا تزال موجودة ✅
📋 Prologue & Epilogue — دوال متداخلة

لما دالة تستدعي دالة ثانية، call سيكتب فوق ra! هذا يعني إنك لو رجعت بـ ret ستقفز للمكان الغلط. الحل: احفظ ra في الـ Stack في بداية الدالة (Prologue) وأعده قبل ret (Epilogue).

الأسلوب الأنظف لكتابة الدوال: استخدم t registers للحسابات المؤقتة. لو تحتاج قيمة تبقى بعد call استخدم s register. ولو استخدمت s register احفظه في Prologue وأعده في Epilogue.

max_of_three: # PROLOGUE: addi sp, sp, -8 sw s0, 4(sp) # احفظ s0 (Callee-Saved) sw ra, 0(sp) # احفظ ra (call سيكتب فوقه!) mv s0, a2 # s0 = c (مضمون لا يتغير) call max # a0 = max(a, b) mv a1, s0 # a1 = c call max # a0 = max(max(a,b), c) ✅ # EPILOGUE: lw s0, 4(sp) lw ra, 0(sp) addi sp, sp, 8 ret
✅ قاعدة ذهبية: لو دالتك تستدعي دالة ثانية = احفظ ra في الـ Prologue دايماً!
📋 Arrays في Assembly

الـ Array هو قائمة من متغيرات نفس النوع مخزنة بشكل متتالي في الذاكرة. للوصول لعنصر معين نحتاج عنوان البداية وال offset (الفرق بالـ bytes) من ذلك العنوان.

لو الـ array من bytes، الـ offset = رقم العنصر مباشرة. لو من words (4 bytes لكل عنصر)، الـ offset = رقم العنصر × 4. هاد ضروري لأن الذاكرة عبارة عن سلسلة bytes وكل word تأخذ 4 منها.

# تعريف arrays: numbers: .byte 10, 20, 30 # array من bytes numbers: .space 5 # 5 bytes فاضية .align 2 numbers: .word 2, 4, 6, 8 # array من words .align 2 numbers: .space 20 # 5 words فاضية (5×4) # الوصول للعناصر: la t0, numbers lw t1, 0(t0) # numbers[0] lw t1, 4(t0) # numbers[1] (1×4) lw t1, 16(t0) # numbers[4] (4×4)

للتنقل بين عناصر array في loop، نحسب الـ offset بضرب index × حجم العنصر. بدل mul نستخدم slli (Shift Left) لأن الضرب بـ 4 = shift left بـ 2. هاد أسرع لأن shift أبسط للـ Hardware.

# loop لحساب مجموع 10 أرقام: li t0, 0 # i = 0 li t1, 0 # sum = 0 la t2, numbers loop: slli t3, t0, 2 # t3 = i × 4 (بدل mul) add t3, t2, t3 # عنوان numbers[i] lw t3, 0(t3) # numbers[i] add t1, t1, t3 # sum += numbers[i] addi t0, t0, 1 # i++ li t4, 10 blt t0, t4, loop .align 2 numbers: .word 2,4,6,8,0,1,2,3,4,5

لما نرسل array لدالة، ما نرسل العناصر كلها — بدل ذلك نرسل العنوان والحجم. الدالة تستخدم الـ addi على الـ base address للتنقل بين العناصر.

main: la a0, numbers # a0 = عنوان أول عنصر li a1, 10 # a1 = حجم الـ array call sum sum: li t0, 0 sum_loop: lw t1, 0(a0) add t0, t0, t1 addi a0, a0, 4 # انتقل للعنصر الجاي addi a1, a1, -1 bnez a1, sum_loop mv a0, t0 ret
► تمارين المحاضرة 3 (10 تمارين)
🌐 استخدم المحاكي: venus.kvakil.me
◈ تمارين المحاضرة 3 — Control Flow, Functions & Arrays
تمرين 01 — سهل
Factorial بالـ Loop
اكتب برنامج يحسب 5! = 120 باستخدام loop. خزّن النتيجة في t0.
y=1, i=1, loop: y*=i, i++, إذا i ≤ 5 كرر. استخدم mul t0, t0, t1
تمرين 02 — سهل
دالة بسيطة — add(a, b)
اكتب دالة add تأخذ parametersين في a0 و a1 وترجع مجموعهم في a0. استدعِها بـ a=15, b=25. النتيجة = 40.
call add ← في الدالة: add a0, a0, a1 ثم ret
تمرين 03 — سهل
Infinite Loop
اكتب برنامج يزيد عداد بـ 1 لانهاية باستخدام j. شاهد كيف تسير الـ registers بالـ Step.
li t0, 0 → loop: addi t0, t0, 1 → j loop
تمرين 04 — متوسط
دالة max(a, b)
اكتب دالة max ترجع الأكبر في a0. استدعِها مرتين: max(3,7)=7 ثم max(10,2)=10.
blt a0, a1, end → mv a0, a1 → end: ret
تمرين 05 — متوسط
Stack — حفظ واسترجاع
احفظ t0=10, t1=20, t2=30 في الـ Stack. غيّر قيمهم (t0=99, t1=88, t2=77). أعدهم من Stack وتحقق إن القيم رجعت.
addi sp, sp, -12 → sw t0,8(sp) → sw t1,4(sp) → sw t2,0(sp) ← ثم عكسي
تمرين 06 — متوسط
مجموع Array
عندك array: [10, 20, 30, 40, 50]. اكتب loop يحسب مجموعهم باستخدام slli للـ offset. النتيجة = 150.
slli t3, t0, 2 = i×4 → add t3, t2, t3 → lw t3, 0(t3)
تمرين 07 — متوسط
دالة مع Prologue/Epilogue
اكتب دالة sum_three(a,b,c) تجمع ثلاثة أرقام باستدعاء دالة add(a,b) مرتين. لا تنس حفظ ra وs registers!
Prologue: sw ra + sw s0. mv s0, a2. call add. mv a1, s0. call add. Epilogue: lw ra + lw s0. ret
تمرين 08 — صعب
دالة sum_array
اكتب دالة sum_array(عنوان, حجم) ترجع مجموع العناصر. استدعِها مع [1,2,3,4,5]. النتيجة = 15.
la a0, numbers → li a1, 5 → call sum_array. في الدالة: lw + addi a0,a0,4 + addi a1,a1,-1 + bnez
تمرين 09 — صعب
إيجاد الـ Maximum في Array
اكتب دالة max_array(عنوان, حجم) تجد أكبر عنصر. جرّبها مع [3, 9, 1, 7, 2]. النتيجة = 9.
max = أول عنصر. loop: إذا العنصر الحالي > max حدّث max. addi a0, a0, 4 للانتقال.
تمرين 10 — تحدي 🏆
اكتشف الـ Bug
الكود التالي فيه bug — اكتشفه وصلحه:
li t0,5 | li t1,3 | li t2,7 | mv a0,t0 | mv a1,t1 | call max | mv a1,t2 | call max
max ممكن تغيّر t2! الحل: احفظ t2 في Stack قبل أول call أو انقله لـ s0
04

محاضرة 4 — قريباً

05

محاضرة 5 — قريباً

06

محاضرة 6 — قريباً

07

محاضرة 7 — قريباً

08

محاضرة 8 — قريباً

09

محاضرة 9 — قريباً

10

محاضرة 10 — قريباً

11

محاضرة 11 — قريباً

12

محاضرة 12 — قريباً