Practice2 Explanation - GawinGowin/libasm GitHub Wiki
目標: x86-64アセンブリ言語を使って階乗計算を行う再帰関数を実装する
要件:
- NASMアセンブラを使用してx86-64アセンブリで実装
- 再帰を使用して階乗計算を行う関数
factorial
を作成 - System V AMD64 ABI に準拠した関数呼び出し規約を使用
- 引数:
rdi
(計算する数値n)、戻り値:rax
(n!の結果) - ベースケース:
n ≤ 1
の場合は1
を返す
ファイル名: practice2.s
関数仕様:
- 関数名:
factorial
- 引数:
rdi
(uint64_t n) - 戻り値:
rax
(uint64_t n!) - 計算式:
n! = n × (n-1) × (n-2) × ... × 1
テスト例:
factorial(0) = 1
factorial(1) = 1
factorial(3) = 6
factorial(5) = 120
コンパイル・実行手順:
nasm -f elf64 practice2.s -o practice2.o
gcc -no-pie practice2.o practice2.c -o test_factorial
./test_factorial
注意: リンカー警告を回避するため、アセンブリファイルの最後に以下を追加してください:
; セキュリティ対策: 実行可能スタックを無効化
section .note.GNU-stack noalloc noexec nowrite progbits
テスト用Cファイル (test_factorial.c
):
#include <stdio.h>
#include <stdint.h>
// アセンブリ関数の宣言
extern uint64_t factorial(uint64_t n);
int main() {
// テストケース
uint64_t test_cases[] = {0, 1, 3, 5, 10};
int num_tests = sizeof(test_cases) / sizeof(test_cases[0]);
printf("階乗計算テスト:\n");
printf("================\n");
for (int i = 0; i < num_tests; i++) {
uint64_t n = test_cases[i];
uint64_t result = factorial(n);
printf("factorial(%lu) = %lu\n", n, result);
}
return 0;
}
-
再帰関数の実装理解
- スタックの動作メカニズム
- 関数の呼び出しと戻り
- ベースケースと再帰ケースの概念
-
基本算術命令の習得
- 乗算命令(MUL)の使用方法
- 比較命令(CMP)の理解
- 条件分岐命令(JLE)の実践
-
レジスタ管理の基礎
- 値の保存と復元(PUSH/POP)
- レジスタの役割分担
- データの受け渡し方法
; practice2.s - 階乗計算
section .text
global factorial
factorial:
; 引数: rdi (n)
; 戻り値: rax
cmp rdi, 1
jle .base_case
push rdi
dec rdi
call factorial
pop rdi
mul rdi
ret
.base_case:
mov rax, 1
ret
; セキュリティ対策: 実行可能スタックを無効化
section .note.GNU-stack noalloc noexec nowrite progbits
factorial:
; 引数: rdi (n)
; 戻り値: rax
System V AMD64 ABI準拠:
- 第1引数:
rdi
レジスタ - 戻り値:
rax
レジスタ
cmp rdi, 1 ; rdi と 1 を比較
jle .base_case ; rdi <= 1 なら .base_case へジャンプ
条件フラグの動作:
-
CMP
: 減算を行い、結果でフラグを設定(値は変更しない) -
JLE
: Less or Equal (≤)でジャンプ - 階乗の数学的定義:
0! = 1
,1! = 1
cmp rdi, 1
の実行により、以下のフラグレジスタが更新されます:
ZF (Zero Flag) - ゼロフラグ:
-
rdi == 1
の場合: ZF = 1 -
rdi != 1
の場合: ZF = 0 - 用途: 等価性判定(
je
,jne
命令で使用)
SF (Sign Flag) - 符号フラグ:
-
rdi < 1
の場合: SF = 1 (結果が負) -
rdi >= 1
の場合: SF = 0 (結果が正またはゼロ) - 用途: 符号付き比較の基礎
CF (Carry Flag) - キャリーフラグ:
-
rdi < 1
(符号なし比較) の場合: CF = 1 -
rdi >= 1
(符号なし比較) の場合: CF = 0 - 用途: 符号なし比較の基礎
; 例1: rdi = 1 の場合
cmp rdi, 1
; → ZF=1, SF=0, CF=0 (1-1=0, 等しい)
; 例2: rdi = 5 の場合
cmp rdi, 1
; → ZF=0, SF=0, CF=0 (5-1=4, 大きい)
; 例3: rdi = 0 の場合
cmp rdi, 1
; → ZF=0, SF=1, CF=1 (0-1=-1, 小さい)
jle .base_case
は以下の条件でジャンプします:
-
条件:
(SF ≠ OF) || (ZF = 1)
-
意味:
rdi <= 1
(符号付き比較)
具体的な判定:
-
rdi = 0
: SF=1, OF=0, ZF=0 → SF≠OF なのでジャンプ -
rdi = 1
: SF=0, OF=0, ZF=1 → ZF=1 なのでジャンプ -
rdi = 2
: SF=0, OF=0, ZF=0 → 条件不成立でジャンプしない
push rdi ; 現在の n をスタックに保存
dec rdi ; n を 1 減らす (n-1)
call factorial ; factorial(n-1) を再帰呼び出し
pop rdi ; 元の n をスタックから復元
重要なポイント:
-
PUSH/POP
: スタックでの値保存・復元 -
DEC
: レジスタの値を1減少 -
CALL
: 関数呼び出し(戻りアドレスを自動的にスタックに保存)
mul rdi ; rax = rax * rdi (factorial(n-1) * n)
ret ; 呼び出し元に戻る
MUL命令の特徴:
-
MUL reg64
:rax = rax * reg64
- 結果は常に
rax
に格納 - オーバーフロー時は
rdx:rax
に128bit結果
1. factorial(3) 呼び出し
- rdi = 3
- 3 > 1 なので再帰へ
2. push 3, call factorial(2)
- スタック: [3]
- rdi = 2
3. factorial(2) 内で push 2, call factorial(1)
- スタック: [3, 2]
- rdi = 1
4. factorial(1) 実行
- 1 <= 1 なので base_case
- rax = 1, return
5. factorial(2) 復帰
- pop 2 → rdi = 2
- mul 2 → rax = 1 * 2 = 2
- return
6. factorial(3) 復帰
- pop 3 → rdi = 3
- mul 3 → rax = 2 * 3 = 6
- return
結果: 6
-
再帰アルゴリズムの実装
- 数学的定義をコードに変換
- 終了条件の適切な設定
- スタックオーバーフローの理解
-
スタック操作の理解
- LIFO(Last In, First Out)の概念
- レジスタ値の一時保存
- 関数呼び出し時のスタック変化
-
算術命令の活用
- 乗算命令の正しい使用
- レジスタ間でのデータ操作
- 効率的な計算処理
; 必須: PUSH と POP の対応
push rdi ; 保存
; ... 処理 ...
pop rdi ; 復元(必須!)
; ベースケースの適切な条件設定
cmp rdi, 1 ; 0 や 1 で停止
jle .base_case ; <= 条件で安全に停止
この理解を基に以下を実装できるようになります:
-
他の数学関数
- フィボナッチ数列
- べき乗計算
- 最大公約数(GCD)
-
最適化技法
- 末尾再帰最適化
- メモ化による高速化
- 反復実装への変換
; 間違い: POPし忘れ
push rdi
call factorial
; pop rdi が抜けている
mul rdi ; 間違った値を使用
; 間違い: 終了条件なし
factorial:
dec rdi
call factorial ; 無限再帰!
; 間違い: 引数レジスタを直接変更
dec rdi ; rdi を直接変更
call factorial
mul rdi ; 変更後の値を使用(間違い)
この練習により、再帰処理とスタック管理の基礎が身につき、より複雑なアルゴリズム実装への準備が整います。