シリーズ一覧
| 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)
これで可変長引数が使える!
基本構造
#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
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)
...は実装してない。理由:
- FPUの初期化が必要
- IEEE 754 の処理が複雑
- カーネルでは普通使わない
必要なら固定小数点数で代用するか、ソフトウェア実装するかだけど、自作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で書き直したい」衝動と戦う。