4
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

printfを自作する、%dの実装が意外と難しい Part4

Last updated at Posted at 2025-12-12

シリーズ一覧

Part タイトル 内容
Part1 令和にCでOS書く狂人の記録 VGA出力
Part2 アセンブリとCの橋渡し リンカスクリプト
Part3 メモリ管理とmalloc 動的メモリ
Part4 printfを自作する 本記事
Part5 Rustで書き直したくなった 移行検討

はじめに

デバッグの基本といえば printf デバッグ。でも自作OSには標準ライブラリがない。printf も自分で作るしかない。

%d ぐらい簡単でしょ」

...って思ってた時期が私にもありました。

可変長引数の仕組み

printf は引数の数が可変:

printf("Hello");                      // 引数1個
printf("x = %d", x);                  // 引数2個  
printf("(%d, %d)", x, y);             // 引数3個

C言語では <stdarg.h>va_list を使う。でも自作OSには <stdarg.h> がない!

...と思いきや、GCCには組み込みで用意されてる:

typedef __builtin_va_list va_list;
#define va_start(v,l)   __builtin_va_start(v,l)
#define va_end(v)       __builtin_va_end(v)
#define va_arg(v,l)     __builtin_va_arg(v,l)

これで可変長引数が使える!

基本構造

src/printf.c
#include "printf.h"

// 1文字出力(Part1で作ったやつ)
extern void putchar(char c);

int kprintf(const char* format, ...) {
    va_list args;
    va_start(args, format);
    
    int written = 0;
    
    while (*format) {
        if (*format == '%') {
            format++;
            switch (*format) {
                case 'd': /* 整数 */ break;
                case 'x': /* 16進 */ break;
                case 's': /* 文字列 */ break;
                case 'c': /* 文字 */ break;
                case '%': putchar('%'); written++; break;
                default: break;
            }
        } else {
            putchar(*format);
            written++;
        }
        format++;
    }
    
    va_end(args);
    return written;
}

%d の実装(意外と難しい)

整数を文字列に変換する。簡単そうでハマりポイントが多い。

素朴な実装(バグあり)

void print_int(int n) {
    if (n < 0) {
        putchar('-');
        n = -n;  // ← ここにバグ!
    }
    
    if (n >= 10) {
        print_int(n / 10);
    }
    putchar('0' + (n % 10));
}

バグ1: INT_MINの処理

int x = -2147483648;  // INT_MIN
-x = ???              // オーバーフロー!

32ビット符号付き整数の範囲は -2147483648 〜 2147483647。-(-2147483648) は 2147483648 になるはずだけど、表現できない!

対策: unsigned にキャストしてから処理

void print_int(int n) {
    if (n < 0) {
        putchar('-');
        print_uint((unsigned int)(-(n + 1)) + 1);  // 安全に反転
    } else {
        print_uint((unsigned int)n);
    }
}

void print_uint(unsigned int n) {
    if (n >= 10) {
        print_uint(n / 10);
    }
    putchar('0' + (n % 10));
}

バグ2: 桁数の計算

幅指定付き(%5d とか)を実装するには桁数が必要:

int count_digits(unsigned int n) {
    int count = 0;
    do {
        count++;
        n /= 10;
    } while (n > 0);
    return count;
}

do-while じゃないとダメ。n = 0 のとき while だと0桁になっちゃう。

完全版 %d

void format_int(int n, int width, char pad) {
    unsigned int abs_n;
    int is_negative = 0;
    
    if (n < 0) {
        is_negative = 1;
        abs_n = (unsigned int)(-(n + 1)) + 1;
    } else {
        abs_n = (unsigned int)n;
    }
    
    // 桁数計算
    int digits = count_digits(abs_n);
    int total_width = digits + (is_negative ? 1 : 0);
    
    // パディング
    for (int i = total_width; i < width; i++) {
        putchar(pad);
    }
    
    // 符号
    if (is_negative) {
        putchar('-');
    }
    
    // 数値本体(再帰で出力)
    print_uint(abs_n);
}

%x の実装(16進数)

void print_hex(unsigned int n, int uppercase) {
    const char* digits = uppercase ? 
        "0123456789ABCDEF" : "0123456789abcdef";
    
    if (n >= 16) {
        print_hex(n / 16, uppercase);
    }
    putchar(digits[n % 16]);
}

%x%X で大文字小文字が変わる。

%s の実装(文字列)

void print_string(const char* s, int width) {
    if (s == NULL) {
        s = "(null)";
    }
    
    int len = 0;
    const char* p = s;
    while (*p++) len++;
    
    // パディング
    for (int i = len; i < width; i++) {
        putchar(' ');
    }
    
    // 文字列本体
    while (*s) {
        putchar(*s++);
    }
}

注意: NULLポインタの処理を忘れずに!

完全な kprintf

src/printf.c
typedef __builtin_va_list va_list;
#define va_start(v,l)   __builtin_va_start(v,l)
#define va_end(v)       __builtin_va_end(v)
#define va_arg(v,l)     __builtin_va_arg(v,l)

extern void putchar(char c);

static int count_digits(unsigned int n) {
    int count = 0;
    do { count++; n /= 10; } while (n > 0);
    return count;
}

static void print_uint(unsigned int n) {
    if (n >= 10) print_uint(n / 10);
    putchar('0' + (n % 10));
}

static void print_int(int n) {
    if (n < 0) {
        putchar('-');
        print_uint((unsigned int)(-(n + 1)) + 1);
    } else {
        print_uint((unsigned int)n);
    }
}

static void print_hex(unsigned int n, int upper) {
    const char* d = upper ? "0123456789ABCDEF" : "0123456789abcdef";
    if (n >= 16) print_hex(n / 16, upper);
    putchar(d[n % 16]);
}

int kprintf(const char* format, ...) {
    va_list args;
    va_start(args, format);
    
    int written = 0;
    
    while (*format) {
        if (*format != '%') {
            putchar(*format++);
            written++;
            continue;
        }
        
        format++;  // '%' をスキップ
        
        // フラグ解析
        int zero_pad = 0;
        if (*format == '0') {
            zero_pad = 1;
            format++;
        }
        
        // 幅解析
        int width = 0;
        while (*format >= '0' && *format <= '9') {
            width = width * 10 + (*format - '0');
            format++;
        }
        
        // 変換指定子
        switch (*format) {
            case 'd':
            case 'i': {
                int val = va_arg(args, int);
                // 簡易版(幅指定は省略)
                print_int(val);
                break;
            }
            case 'u': {
                unsigned int val = va_arg(args, unsigned int);
                print_uint(val);
                break;
            }
            case 'x': {
                unsigned int val = va_arg(args, unsigned int);
                print_hex(val, 0);
                break;
            }
            case 'X': {
                unsigned int val = va_arg(args, unsigned int);
                print_hex(val, 1);
                break;
            }
            case 'p': {
                void* ptr = va_arg(args, void*);
                putchar('0');
                putchar('x');
                print_hex((unsigned int)ptr, 0);
                break;
            }
            case 's': {
                const char* s = va_arg(args, const char*);
                if (!s) s = "(null)";
                while (*s) { putchar(*s++); written++; }
                break;
            }
            case 'c': {
                char c = (char)va_arg(args, int);
                putchar(c);
                written++;
                break;
            }
            case '%': {
                putchar('%');
                written++;
                break;
            }
            default:
                putchar('%');
                putchar(*format);
                written += 2;
                break;
        }
        format++;
    }
    
    va_end(args);
    return written;
}

テスト

void test_printf(void) {
    kprintf("=== printf Test ===\n");
    
    // 基本
    kprintf("Hello, %s!\n", "World");
    
    // 整数
    kprintf("Decimal: %d\n", 12345);
    kprintf("Negative: %d\n", -42);
    kprintf("Zero: %d\n", 0);
    kprintf("INT_MIN: %d\n", -2147483648);
    
    // 16進数
    kprintf("Hex lower: %x\n", 0xDEADBEEF);
    kprintf("Hex upper: %X\n", 0xCAFEBABE);
    
    // ポインタ
    int x = 42;
    kprintf("Pointer: %p\n", &x);
    
    // 文字
    kprintf("Char: %c\n", 'A');
    
    // エスケープ
    kprintf("Percent: %%\n");
    
    // 複合
    kprintf("x=%d, y=%d, name=%s\n", 10, 20, "test");
}

出力:

=== printf Test ===
Hello, World!
Decimal: 12345
Negative: -42
Zero: 0
INT_MIN: -2147483648
Hex lower: deadbeef
Hex upper: CAFEBABE
Pointer: 0x00103abc
Char: A
Percent: %
x=10, y=20, name=test

浮動小数点数(%f)

...は実装してない。理由:

  1. FPUの初期化が必要
  2. IEEE 754 の処理が複雑
  3. カーネルでは普通使わない

必要なら固定小数点数で代用するか、ソフトウェア実装するかだけど、自作OSレベルでは不要なことが多い。

sprintf も作る

バッファに出力する版:

static char* sprintf_buf;

static void sprintf_putchar(char c) {
    *sprintf_buf++ = c;
}

int ksprintf(char* buf, const char* format, ...) {
    sprintf_buf = buf;
    // ... (kprintfと同様、ただしputcharの代わりにsprintf_putcharを使う)
    *sprintf_buf = '\0';
    return sprintf_buf - buf;
}

まとめ

printf を自作した:

  • 可変長引数: GCCビルトインを使用
  • %d: INT_MINのオーバーフローに注意
  • %x: 大文字小文字両対応
  • %s: NULLチェック忘れずに
  • %p: ポインタは16進で

意外とハマりポイントが多かった:

  • 負数の処理
  • ゼロの桁数(1桁)
  • NULLポインタ

次回Part5(最終回)では、ここまで作ったOSを振り返って、「Rustで書き直したい」衝動と戦う。


次回: Cで書いたOS、Rustで書き直したくなった Part5

4
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?