Practice5 Explanation - GawinGowin/libasm GitHub Wiki

Practice5: strlen実装プログラムの詳細解説

なぜこの練習を行うのか

学習目的

  1. メモリアドレッシングの習得

    • ポインタ操作の実践
    • メモリアクセスパターンの理解
    • 効率的なメモリ読み取り技術
  2. ループ構造の実装

    • 条件付きループの設計
    • インデックス管理
    • 終了条件の適切な設定
  3. 標準ライブラリ関数の理解

    • C標準ライブラリの内部動作
    • 文字列処理の基礎
    • 実用的な関数設計パターン

コード詳細解析

; practice5.s - strlen 実装
section .text
    global my_strlen

my_strlen:
    ; 引数: rdi (文字列ポインタ)
    ; 戻り値: rax (長さ)
    mov rax, 0
    
.loop:
    cmp byte [rdi + rax], 0
    je .done
    inc rax
    jmp .loop
    
.done:
    ret

strlen関数の仕様理解

C標準ライブラリ仕様

size_t strlen(const char *s);

機能:

  • NULL終端文字列の長さを計算
  • NULL文字('\0')は長さに含まない
  • 戻り値: 文字数(size_t型 = unsigned long)

メモリレイアウト例

文字列 "Hello"のメモリ配置:
アドレス: 0x1000  0x1001  0x1002  0x1003  0x1004  0x1005
内容:      'H'     'e'     'l'     'l'     'o'     '\0'
インデックス: 0       1       2       3       4       5

実装解析

初期化処理

mov rax, 0              ; カウンタを0で初期化

設計判断:

  • rax をカウンタとして使用
  • 戻り値レジスタと兼用で効率化
  • 初期値0から開始

メインループ

.loop:
    cmp byte [rdi + rax], 0    ; [文字列先頭 + オフセット] と 0 を比較
    je .done                   ; NULL文字なら終了
    inc rax                    ; カウンタをインクリメント  
    jmp .loop                  ; ループ継続

重要なポイント:

アドレッシングモード
[rdi + rax]    ; ベースレジスタ + インデックスレジスタ
  • rdi: 文字列の開始アドレス(ベース)
  • rax: 現在の文字位置(インデックス)
  • 組み合わせで現在文字のアドレスを計算
バイト単位アクセス
cmp byte [rdi + rax], 0
  • byte: 1バイト(8bit)単位でアクセス
  • 文字は通常1バイトで格納
  • 64bit全体ではなく必要な部分のみアクセス

byteの詳細解説

byteはメモリオペランドサイズ指定子です。

byte [rdi + rax]の意味
cmp byte [rdi + rax], 0

このbyteは:

  • メモリから1バイト(8bit)を読み取ることを指定
  • [rdi + rax]のメモリアドレスから1バイト分のデータを取得
  • 文字列の1文字(char型)を読み取る
他のサイズ指定子との比較
cmp byte [rdi + rax], 0    ; 1バイト読み取り
cmp word [rdi + rax], 0    ; 2バイト読み取り  
cmp dword [rdi + rax], 0   ; 4バイト読み取り
cmp qword [rdi + rax], 0   ; 8バイト読み取り
なぜbyteが必要?

文字列の場合:

  • C言語のcharは1バイト
  • 文字列は1バイトずつ格納されている
  • NULL終端文字\0も1バイト

もしbyteを省略すると:

cmp [rdi + rax], 0    ; サイズ不明でアセンブラエラー

正しい指定:

cmp byte [rdi + rax], 0    ; 1バイト比較 = 1文字ずつチェック
実際の動作例
文字列 "Hello" のメモリ:
アドレス: 0x1000  0x1001  0x1002  0x1003  0x1004  0x1005
内容:      'H'     'e'     'l'     'l'     'o'     '\0'
           ↑
byte [rdi + rax] = 'H' (1バイト = 0x48)

重要なポイント:

  • byteは変数型ではなく、メモリアクセスのサイズを指定する修飾子
  • アセンブリでは明示的にサイズを指定する必要がある
  • 文字列処理ではbyte指定が必須

実行フローの追跡

"ABC" の strlen 実行例

初期状態:
- rdi = 0x1000 (文字列先頭アドレス)
- rax = 0
- メモリ: [0x1000]='A', [0x1001]='B', [0x1002]='C', [0x1003]='\0'

1回目ループ:
- [rdi + rax] = [0x1000 + 0] = 'A' (0x41)
- cmp byte [0x1000], 0 → 'A' ≠ 0
- inc rax → rax = 1

2回目ループ:
- [rdi + rax] = [0x1000 + 1] = 'B' (0x42) 
- cmp byte [0x1001], 0 → 'B' ≠ 0
- inc rax → rax = 2

3回目ループ:
- [rdi + rax] = [0x1000 + 2] = 'C' (0x43)
- cmp byte [0x1002], 0 → 'C' ≠ 0  
- inc rax → rax = 3

4回目ループ:
- [rdi + rax] = [0x1000 + 3] = '\0' (0x00)
- cmp byte [0x1003], 0 → '\0' = 0
- je .done → ループ終了

結果: rax = 3 (文字列長)

学習効果

この練習で身につくスキル

  1. メモリ操作の基礎

    • ポインタ演算の実装
    • インデックスアクセスパターン
    • バイト単位メモリ操作
  2. ループアルゴリズム

    • 線形探索の実装
    • 条件判定とループ制御
    • 効率的な反復処理
  3. 文字列処理の基礎

    • NULL終端文字列の理解
    • 文字エンコーディングの考慮
    • 標準ライブラリ互換性

パフォーマンス分析

時間計算量

O(n) - n は文字列長
- 各文字を1回ずつ検査
- 線形時間で完了

空間計算量

O(1) - 定数時間
- 追加メモリ使用なし
- レジスタのみ使用

命令サイクル分析

1文字あたりの処理:
- cmp byte [rdi + rax], 0  ; 2-3サイクル(メモリアクセス)
- je .done                 ; 1サイクル(予測成功時)
- inc rax                  ; 1サイクル
- jmp .loop               ; 1サイクル

1文字あたり約5-6サイクル
文字列長nに対して約5n-6nサイクル

最適化技法

1. ワードアライメント最適化

my_strlen_optimized:
    mov rax, 0
    
    ; 8バイト境界までの調整
.align_loop:
    test rdi, 7                    ; 8バイト境界チェック
    jz .word_loop                  ; 境界なら8バイト処理へ
    
    cmp byte [rdi + rax], 0       ; 1バイトずつ処理
    je .done
    inc rax
    jmp .align_loop

.word_loop:
    mov rdx, [rdi + rax]          ; 8バイト一括読み込み
    ; マジックナンバーでNULL検出
    ; (実装詳細は高度なため省略)
    
.done:
    ret

2. SIMD命令利用版

my_strlen_simd:
    ; SSE2/AVX命令を使用した高速実装
    ; 16バイト/32バイト単位での並列処理
    ; (実装詳細は発展課題として)

よくある間違いとデバッグ

1. バイト指定忘れ

; 間違い: バイト指定なし
cmp [rdi + rax], 0              ; 8バイト比較してしまう

問題点:

  • 意図しない8バイト読み込み
  • 隣接メモリの値も影響
  • 不正な結果

2. 無限ループ

; 間違い: カウンタ更新忘れ
.loop:
    cmp byte [rdi + rax], 0
    je .done
    ; inc rax が抜けている!
    jmp .loop

3. ポインタの間違った使用

; 間違い: ポインタ自体を変更
.loop:
    cmp byte [rdi], 0
    je .done
    inc rdi                     ; ポインタを変更(危険)
    inc rax
    jmp .loop

問題点:

  • 元のポインタ値を破壊
  • 呼び出し元に影響
  • デバッグ困難

発展的な実装課題

1. 他の文字列関数

; strchr - 文字検索
; strcmp - 文字列比較  
; strncmp - 長さ制限比較
; strcpy - 文字列コピー

2. Unicode対応

; UTF-8文字列の長さ計算
; マルチバイト文字の考慮
; 文字境界の正確な検出

3. エラーハンドリング

; NULLポインタチェック
; メモリ境界の検証
; セグメンテーションフォルト回避

実際の利用例

C言語との連携

// main.c
extern size_t my_strlen(const char *s);

#include <stdio.h>
#include <string.h>

int main() {
    const char *test = "Hello, Assembly!";
    
    size_t my_len = my_strlen(test);
    size_t std_len = strlen(test);
    
    printf("my_strlen: %zu\n", my_len);    // 16
    printf("strlen:    %zu\n", std_len);   // 16
    printf("Match: %s\n", my_len == std_len ? "Yes" : "No");
    
    return 0;
}

コンパイルとテスト

nasm -f elf64 practice5.s -o practice5.o
gcc -c main.c -o main.o  
gcc practice5.o main.o -o test
./test

この練習により、メモリ操作と文字列処理の基礎が身につき、実用的なライブラリ関数の実装能力が向上します。

⚠️ **GitHub.com Fallback** ⚠️