ตัวอย่าง: Factorial
โปรแกรม assembly คำนวณ 7! = 5040 — ครอบคลุมแนวคิด recursion, stack frame, function calling convention และ ecall
เป้าหมายของบทเรียน
- เข้าใจการใช้ stack สำหรับเก็บ return address และ local variable
- เห็นการเรียกฟังก์ชันแบบ recursive ในระดับ assembly
- เรียนรู้ calling convention ของ RISC-V (a0-a7 สำหรับ argument, ra สำหรับ return address)
- ฝึกใช้ ecall สำหรับพิมพ์ผลลัพธ์
วิธีโหลดโปรแกรม
- เปิด Ripes
- เลือกเมนู
File → Load Example → Assembly → Factorial - โปรแกรมจะปรากฏใน 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 ทำหน้าที่:
- โหลดเลข 7 จาก
.dataเข้าa0 - เรียก
factด้วยjal ra, fact— ตอนกลับมาa0จะมีผลลัพธ์ - เรียก
printResultโดยใส่ argument เข้าa0และผลลัพธ์เข้าa1 - เรียก
ecall 10เพื่อหยุดโปรแกรม
2) fact — ฟังก์ชัน recursive
นี่คือหัวใจของโปรแกรม ทุกครั้งที่ fact ถูกเรียก:
- จองพื้นที่ stack 16 bytes ด้วย
addi sp, sp, -16 - เก็บ return address (
ra) และ argument ปัจจุบัน (a0) ลง stack — สำคัญมาก เพราะการ recurse จะทำให้ทั้งคู่ถูกเขียนทับ - ตรวจ base case: ถ้า
a0 - 1 < 0(คือa0 = 0) ให้คืน 1 - Recurse: เรียก
fact(a0 - 1) - คูณผลลัพธ์: ดึง argument เดิมกลับมาแล้วคูณกับผลของ recursion
- คืน stack และ return
ทำไมต้องเก็บ
a0 ลง stack?
เพราะการเรียก jal ra, fact จะ เขียนทับ ra และเมื่อ recurse สำเร็จ a0 ก็จะถูกเขียนทับด้วยผลลัพธ์ของ fact(n-1) ถ้าไม่เก็บไว้จะคูณกลับไม่ได้
3) printResult — พิมพ์ผลลัพธ์
ใช้ ecall 3 service สลับกัน:
ecall 4— พิมพ์ stringecall 1— พิมพ์ integer
สังเกตว่า t0, t1 ใช้เก็บค่าชั่วคราว เพราะ a0 จะถูกเขียนทับทุกครั้งที่เรียก ecall
วิธีดูการทำงานทีละขั้น
- กดปุ่ม Reset ใน toolbar
- เปิด Memory tab เลือก
Go to register → spเพื่อดู stack - กลับมา Processor tab แล้วกด Clock ทีละครั้ง
- สังเกต:
- ค่า
spลดลง 16 ทุกครั้งที่เข้าfact - ค่าใน Stack memory เปลี่ยนตามทุกครั้งที่ recurse
- ค่าใน
a0ลดลง 1 ทุกครั้ง
- ค่า
- เมื่อถึง base case (
a0 = 0) ให้สังเกตการ unwind:spเพิ่มขึ้นทีละ 16 และa0ถูกคูณกลับขึ้นเรื่อย ๆ
ผลลัพธ์ที่คาดหวัง
ใน Output console:
Factorial value of 7 is 5040
แบบฝึกหัด
- เปลี่ยนค่า
argumentจาก 7 เป็น 10 แล้วรันใหม่ ผลลัพธ์ที่ได้คือ? - ลอง
argument = 13— จะเกิด integer overflow ใน 32-bit หรือไม่? ทำไม? - เขียน fact แบบ iterative (ใช้ loop แทน recursion) แล้วเปรียบเทียบจำนวน cycle และ memory access
- เปิด Stage Table ดูว่า
factมี pipeline stall หรือไม่ จุดไหนเกิด data hazard?