C言語によるインラインアセンブリとシステムコールの低水準実装

標準ライブラリ(libc)を一切使用せず、Linuxカーネルが提供するシステムコールをC言語のインラインアセンブリ経由で直接呼び出す手法について解説します。通常のC言語の抽象化レイヤーを介さずにプログラムのエントリーポイントから記述することで、OSとのやり取りやスタック構造を深く理解することができます。

実装コード例

以下に、`write`、`nanosleep`、`exit` の各システムコールを直接利用して、引数で指定された秒数だけスリープするプログラムの実装を示します。標準ライブラリに依存しないため、文字列操作や数値解析も自前で行っています。

#include <stddef.h>

/* システムコール番号の定義 */
enum SyscallNumber {
    SYS_WRITE = 1,
    SYS_NANOSLEEP = 35,
    SYS_EXIT = 60
};

/* nanosleep用の時間構造体 */
struct TimeSpec {
    long tv_sec;   /* 秒 */
    long tv_nsec;  /* ナノ秒 */
};

/* システムコールを発行する共通ラッパー */
static long invoke_syscall(long num, long arg1, long arg2, long arg3) {
    long ret_val;
    
    __asm__ __volatile__(
        "syscall"
        : "=a" (ret_val)          /* 出力: RAXに戻り値が格納される */
        : "a" (num),              /* 入力: RAXにシステムコール番号 */
          "D" (arg1),             /* 入力: RDIに第1引数 */
          "S" (arg2),             /* 入力: RSIに第2引数 */
          "d" (arg3)              /* 入力: RDXに第3引数 */
        : "rcx", "r11", "memory"  /* クラッシュするレジスタ */
    );

    return ret_val;
}

/* 文字列長の計算 */
static size_t str_length(const char *s) {
    size_t len = 0;
    while (s[len] != '\0') {
        len++;
    }
    return len;
}

/* 文字列を整数に変換 */
static long parse_long(const char *str) {
    long val = 0;
    while (*str >= '0' && *str <= '9') {
        val = val * 10 + (*str - '0');
        str++;
    }
    return val;
}

/* 標準出力への書き込み */
static void print_output(const char *msg) {
    invoke_syscall(SYS_WRITE, 1, (long)msg, str_length(msg));
}

/* 指定秒数のスリープ */
static void sleep_custom(long seconds) {
    struct TimeSpec req = { .tv_sec = seconds, .tv_nsec = 0 };
    invoke_syscall(SYS_NANOSLEEP, (long)&req, 0, 0);
}

/* プロセスの終了 */
static void terminate_process(int code) {
    invoke_syscall(SYS_EXIT, code, 0, 0);
    /* ここには戻らない */
    __builtin_unreachable();
}

/* メインロジック */
int program_main(long argc, char **argv) {
    if (argc != 2) {
        print_output("Usage: ./custom_sleep <seconds>\n");
        return 1;
    }

    char *input = argv[1];
    long seconds = parse_long(input);

    print_output("Sleeping for ");
    print_output(input);
    print_output(" seconds...\n");
    
    sleep_custom(seconds);

    return 0;
}

/* プログラムのエントリーポイント */
__attribute__((naked)) void _start() {
    __asm__ __volatile__(
        "xor %rbp, %rbp\n\t"        /* RBPを0クリア */
        "mov (%rsp), %rdi\n\t"      /* スタックトップからargcを取得してRDIへ */
        "lea 8(%rsp), %rsi\n\t"     /* argvのアドレスを計算してRSIへ */
        "and $-16, %rsp\n\t"        /* スタックを16バイト境界にアライメント */
        "call program_main\n\t"     /* メインロジックを呼び出し */
        "mov %eax, %edi\n\t"        /* 戻り値をEDI(終了コード)へ */
        "call terminate_process\n\t" /* 終了処理を呼び出し */
    );
}

_start関数とスタック操作の仕組み

__attribute__((naked)) の役割

通常、C言語の関数はコンパイラによって「プロローグ(`push rbp; mov rbp, rsp`など)」と「エピローグ」が自動的に挿入されます。しかし、プログラムの最初に実行される _start では、カーネルが設定した直後のスタックレイアウトをそのまま利用する必要があります。コンパイラが勝手にスタックを操作(ポインタの移動やプッシュ)すると、引数である argcargv の位置がずれてしまいます。__attribute__((naked)) を指定することで、この余計なコード生成を抑制し、完全にアセンブリで制御を行います。

スタックポインタ(RSP)からの引数取得

C言語の標準構文だけでは現在のスタックポインタ(レジスタ %rsp)の値を直接変数に代入することはできません。そのため、インラインアセンブリを使用します。

mov (%rsp), %rdi

この命令は、スタックトップ(%rsp が指し示すメモリ番地)にある値を %rdi レジスタ(関数の第1引数)にコピーします。Linux x86-64環境では、プログラム起動時にスタックは以下のように構成されています。

  • %rspargc(引数の個数)
  • %rsp + 8argv[0](プログラム名へのポインタ)
  • %rsp + 16argv[1](1つ目の引数へのポインタ)
  • ...

lea命令による効率的なアドレス計算

コード中の lea 8(%rsp), %rsi は、メモリの値を「読み込む」のではなく「アドレスを計算する」命令です。mov 命令を使って mov 8(%rsp), %rsi と記述した場合、メモリの値(ここでは argv[0] のポインタ値)がコピーされますが、lea を使うと「%rsp のアドレス + 8」という計算結果そのものが %rsi に格納されます。これにより、argv 配列の先頭アドレスを効率的に main 関数の第2引数として渡すことができます。

型キャストによるポインタ操作

_start から渡された argvchar ** 型として扱われます。スタック上の数値を単なる整数ではなく、「文字列を指すポインタの配列」として解釈するために、適切な型キャストが行われています。これにより、argv[0] から順にコマンドライン引数の文字列へアクセスすることが可能になります。

タグ: C InlineAssembly linux SystemCall x86-64

5月24日 15:33 投稿