ゾンビでもわかるC言語プログラミング

C言語入門者の応援をします

Linux システムコールプログラミング 入門

Index

1. はじめに

システムコールとは、カーネルの機能を使用するための機能であり、入出力やパケットの送信など、カーネルの機能を利用するプログラムは全てシステムコールを使用している。
システムコールは、関数にラップされているので、普通意識することはないが、アセンブリを書く場合などには、意識する必要がある。

今回実施した環境は以下である。

% uname -a
Linux ubuntu 4.10.0-37-generic #41~16.04.1-Ubuntu SMP Fri Oct 6 22:42:59 UTC 2017 x86_64 x86_64 x86_64 GNU/Linux

% lsb_release -a
No LSB modules are available.
Distributor ID: Ubuntu
Description:    Ubuntu 16.04.3 LTS
Release:    16.04
Codename:   xenial

2. システムコール概要

例えば、以下のように printf() 関数を実行するとしよう。

// test.c
#include <stdio.h>

int main() {

  printf("Hello\n");

  return 0;
}
% gcc test.c
% ./a.out
Hello

この時、strace コマンドで、実行されたシステムコールをみることができる。

% strace ./a.out
execve("./a.out", ["./a.out"], [/* 60 vars */]) = 0
brk(NULL)                               = 0x1f31000
access("/etc/ld.so.nohwcap", F_OK)      = -1 ENOENT (No such file or directory)
mmap(NULL, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f133df75000

...

brk(0x1f52000)                          = 0x1f52000
write(1, "Hello\n", 6Hello
)                  = 6
exit_group(0)                           = ?
+++ exited with 0 +++

上記より、write システムコールが実行されていることがわかる。
このように、printf() 関数は、write システムコールをラップしている。
write システムコールは、書き込みに使用されるシステムコールで、標準出力以外でも、ファイルやネットワークソケットへの書き込みにも使用される。

3. システムコールを書いてみる

まずは、printf() 関数がラップしている write システムコールを例にプログラムを書いてみる。
write システムコールの引数は、manページをみるとわかる。

% man 2 write
WRITE(2)                         Linux Programmer's Manual                        WRITE(2)

NAME
       write - write to a file descriptor

SYNOPSIS
       #include <unistd.h>

       ssize_t write(int fd, const void *buf, size_t count);
...

manページより、必要な引数が以下であることがわかる。 第一引数: ファイルディスクリプタ 第二引数: 文字列のアドレス 第三引数: 書き込むバイト数

これをもとに書いたプログラムが以下になる。

#include <unistd.h>

int main() {

  write(1, "Hello\n", 6);

  return 0;
}
% gcc test.c && ./a.out
Hello

"Hello" と出力された。

また、ここで使用した write() は、write システムコールをラップした関数であり、システムコール自体ではない。
しかし、システムコールを直で呼び出すには、アレンブリを書く必要があるので、一般的にシステムコールプログラミングは、システムコール関数を使用する。

write() が関数であることは、ltrace というコマンドで確認できる。
ltrace は、プログラムが使用した関数を表示するコマンドである。

% ltrace ./a.out
__libc_start_main(0x400526, 1, 0x7ffc85642e98, 0x400550 <unfinished ...>
write(1, "Hello\n", 6Hello
)                                   = 6
+++ exited (status 0) +++

4. (おまけ) インラインアセンブリでシステムコールを書いてみる

第3章では、システムコール関数を使用してシステムコールを呼び出したが、ここではアレンブリを書いて直接システムコールを呼び出す。
また、今回はC言語の中にアセンブリを埋め込むインラインアセンブリを使用する。

アセンブリについては、以下の記事を参考にしたい。 x64 アセンブリ 入門

各システムコールには、番号があり、(今回の環境では)以下のファイルにシステムコールと番号の対応が記述されている。

/usr/include/x86_64-linux-gnu/asm/unistd_64.h

これから、write システムコールの番号は1であることがわかる。

int main() {
    asm("mov rax, 1");
    asm("mov rdi, 1");
    asm("mov rbx, 0x0a434241");
    asm("push rbx");
    asm("mov rsi, rsp");
    asm("mov rdx, 4");
    asm("syscall");

    return 0;
}

これをコンパイルし、実行する。

% gcc -masm=intel test.c
% ./a.out
ABC

% ltrace ./a.out
__libc_start_main(0x4004d6, 1, 0x7ffcc8fbb068, 0x400510ABC
 <no return ...>
+++ exited (status 0) +++

% strace ./a.out
execve("./a.out", ["./a.out"], [/* 60 vars */]) = 0
...
write(1, "ABC\n", 4ABC
)                    = 4
exit_group(0)                           = ?

"ABC" という値が出力された。
また、ltrace と strace コマンドから、write システムコールを直接呼び出していることがわかる。