ตัวอย่าง: Factorial

โปรแกรม assembly คำนวณ 7! = 5040 — ครอบคลุมแนวคิด recursion, stack frame, function calling convention และ ecall

เป้าหมายของบทเรียน

วิธีโหลดโปรแกรม

  1. เปิด Ripes
  2. เลือกเมนู File → Load Example → Assembly → Factorial
  3. โปรแกรมจะปรากฏใน Editor tab และถูก assemble โดยอัตโนมัติ

โค้ดทั้งหมด

# คำนวณ factorial ของ 7 (7! = 5040)

.data
argument: .word   7
str1:     .string "Factorial value of "
str2:     .string " is "

.text
main:
        lw  a0, argument   # โหลด argument จาก static data
        jal ra, fact       # เรียก function fact

        # พิมพ์ผลลัพธ์ออกหน้าจอ
        mv  a1, a0         # ย้ายผลลัพธ์ไป a1
        lw  a0, argument   # โหลด argument อีกครั้ง (โดน fact เปลี่ยน)
        jal ra, printResult

        # ออกจากโปรแกรม
        li a7, 10
        ecall

fact:
        addi sp, sp, -16   # จองพื้นที่ stack 16 bytes
        sw   ra, 8(sp)     # เก็บ return address
        sw   a0, 0(sp)     # เก็บ argument ปัจจุบัน
        addi t0, a0, -1
        bge  t0, zero, nfact   # ถ้า a0 - 1 >= 0 ไป recurse

        # Base case: fact(0) = 1
        addi a0, zero, 1
        addi sp, sp, 16
        jr x1              # return

nfact:
        addi a0, a0, -1    # ลด argument ลง 1
        jal  ra, fact      # recurse
        addi t1, a0, 0     # เก็บผล recurse ไว้ใน t1
        lw   a0, 0(sp)     # โหลด argument เดิมกลับมา
        lw   ra, 8(sp)     # โหลด return address กลับมา
        addi sp, sp, 16    # คืน stack
        mul a0, a0, t1     # a0 = a0 * t1 (= n * fact(n-1))
        ret

# --- printResult ---
# a0: ค่าที่นำมาคำนวณ factorial
# a1: ผลลัพธ์ factorial
printResult:
        mv t0, a0
        mv t1, a1
        la a0, str1
        li a7, 4
        ecall              # พิมพ์ "Factorial value of "
        mv a0, t0
        li a7, 1
        ecall              # พิมพ์เลข argument
        la a0, str2
        li a7, 4
        ecall              # พิมพ์ " is "
        mv a0, t1
        li a7, 1
        ecall              # พิมพ์เลขผลลัพธ์
        ret

วิเคราะห์โครงสร้าง

1) main — เริ่มต้นและจบโปรแกรม

main ทำหน้าที่:

2) fact — ฟังก์ชัน recursive

นี่คือหัวใจของโปรแกรม ทุกครั้งที่ fact ถูกเรียก:

  1. จองพื้นที่ stack 16 bytes ด้วย addi sp, sp, -16
  2. เก็บ return address (ra) และ argument ปัจจุบัน (a0) ลง stack — สำคัญมาก เพราะการ recurse จะทำให้ทั้งคู่ถูกเขียนทับ
  3. ตรวจ base case: ถ้า a0 - 1 < 0 (คือ a0 = 0) ให้คืน 1
  4. Recurse: เรียก fact(a0 - 1)
  5. คูณผลลัพธ์: ดึง argument เดิมกลับมาแล้วคูณกับผลของ recursion
  6. คืน stack และ return
ทำไมต้องเก็บ a0 ลง stack? เพราะการเรียก jal ra, fact จะ เขียนทับ ra และเมื่อ recurse สำเร็จ a0 ก็จะถูกเขียนทับด้วยผลลัพธ์ของ fact(n-1) ถ้าไม่เก็บไว้จะคูณกลับไม่ได้

3) printResult — พิมพ์ผลลัพธ์

ใช้ ecall 3 service สลับกัน:

สังเกตว่า t0, t1 ใช้เก็บค่าชั่วคราว เพราะ a0 จะถูกเขียนทับทุกครั้งที่เรียก ecall

วิธีดูการทำงานทีละขั้น

  1. กดปุ่ม Reset ใน toolbar
  2. เปิด Memory tab เลือก Go to register → sp เพื่อดู stack
  3. กลับมา Processor tab แล้วกด Clock ทีละครั้ง
  4. สังเกต:
    • ค่า sp ลดลง 16 ทุกครั้งที่เข้า fact
    • ค่าใน Stack memory เปลี่ยนตามทุกครั้งที่ recurse
    • ค่าใน a0 ลดลง 1 ทุกครั้ง
  5. เมื่อถึง base case (a0 = 0) ให้สังเกตการ unwind: sp เพิ่มขึ้นทีละ 16 และ a0 ถูกคูณกลับขึ้นเรื่อย ๆ

ผลลัพธ์ที่คาดหวัง

ใน Output console:

Factorial value of 7 is 5040

แบบฝึกหัด

  1. เปลี่ยนค่า argument จาก 7 เป็น 10 แล้วรันใหม่ ผลลัพธ์ที่ได้คือ?
  2. ลอง argument = 13 — จะเกิด integer overflow ใน 32-bit หรือไม่? ทำไม?
  3. เขียน fact แบบ iterative (ใช้ loop แทน recursion) แล้วเปรียบเทียบจำนวน cycle และ memory access
  4. เปิด Stage Table ดูว่า fact มี pipeline stall หรือไม่ จุดไหนเกิด data hazard?