C言語のsystem
は、プログラムからOSのコマンドを簡単に実行できる便利な関数です。
ただし、便利さの裏に多くの落とし穴があります。
本記事では、初心者の方でも安全に扱えるように、基本の使い方から戻り値の解釈、Windows/Linuxの違い、注意点、代替APIまでを丁寧に説明します。
特にセキュリティ面の理解は必須です。
stdlib.h の systemとは
コマンド実行の仕組みと役割
system
は、文字列で与えたコマンドをOSのコマンドプロセッサ(シェル)に渡し、そのシェルに実行してもらう関数です。
Linux/Unix系では/bin/sh -c "…"
が実行され、Windowsではcmd.exe /c "…"
が動作します。
処理はブロッキングで、コマンドが終了するまで呼び出し元のプログラムは待機します。
標準入出力や環境変数は基本的に親プロセスを継承します。
この構造により、ワンライナーで外部コマンド呼び出しやリダイレクト、パイプなどのシェル機能を使える反面、シェル解釈によるセキュリティリスクや、戻り値/エラーの扱いが実装依存になりやすいといった短所があります。
関数宣言とインクルード
system
の宣言は#include <stdlib.h>
で提供されます。
- 宣言:
int system(const char *command);
- 特別な呼び出し:
system(NULL)
は「コマンドプロセッサが利用可能か」を判定し、利用可能なら非0、不可なら0を返します。
Linux/Unix系で戻り値を詳細に解釈する場合は#include <sys/wait.h>
を併用し、WIFEXITED
やWEXITSTATUS
といったマクロで終了ステータスを分解します。
Windowsではこれらのマクロは使えないため、扱いが異なります。
Windows/Linuxの違い
次のような相違が実務上のポイントになります。
- Linux/Unix:
/bin/sh -c "…"
を経由。終了ステータスはwaitpid
の形式で返り、WIFEXITED
/WEXITSTATUS
等で解釈します。 - Windows:
cmd.exe /c "…"
を経由。戻り値は「コマンドの終了コード」になることが多いですが、-1などの特別値が返る場合もあります。シグナル概念は基本的にありません。
以下に差分を簡潔にまとめます。
項目 | Linux/Unix | Windows |
---|---|---|
実行主体 | /bin/sh -c “…” | cmd.exe /c “…” |
PATH解決 | PATH環境変数に従う | PATHとPATHEXTに従う(.EXE .BAT等) |
戻り値の形式 | wait系のステータス(要sys/wait.hで分解) | コマンドの終了コード(実装依存あり) |
未発見時の慣習 | 127(見つからない), 126(実行不可) | 9009(未発見, cmd規約)が多いが一定ではない |
文字コード | ロケール依存(UTF-8推奨) | マルチバイト版はACP、_wsystem はUTF-16 |
シグナル | あり | 基本なし |
「同じ文字列を渡しても結果がOSで変わる」点は必ず念頭に置いてください。
systemの使い方
基本構文とサンプル
最小限の使い方はコマンド文字列をそのまま渡す形です。
OSごとにコマンドが異なるため、#ifdef
で分けるのが現実的です。
// 基本サンプル: OSごとに簡単なコマンドを実行します
// コンパイル例(Linux): gcc basic_system.c -o basic
// コンパイル例(Windows, MSVC): cl basic_system.c
#include <stdio.h>
#include <stdlib.h>
int main(void) {
#if defined(_WIN32)
// Windowsでは "dir" はcmdの組み込みコマンド
const char *cmd = "echo Hello from Windows && dir";
#else
// Linux/Unix系では "ls" を利用
const char *cmd = "echo Hello from Linux && ls -1";
#endif
printf("Executing: %s\n", cmd);
int status = system(cmd); // コマンド実行。終了まで待機します。
printf("system returned: %d\n", status);
return 0;
}
Executing: echo Hello from Linux && ls -1
Hello from Linux
a.out
basic_system.c
system returned: 0
status
は「素の終了コード」ではない場合があります。
Linux/Unixではビットに情報が詰め込まれており、分解が必要です(後述)。
PATHとカレントディレクトリ
シェルはカレントディレクトリとPATH環境変数を使ってコマンドを見つけます。
意図しないバイナリが実行されるリスクを避けるため、極力フルパスで指定するのが安全です。
- Linux例:
/bin/ls /usr/bin
- Windows例:
C:\Windows\System32\ipconfig.exe /all
// フルパスで安全に実行する例
#include <stdio.h>
#include <stdlib.h>
int main(void) {
#if defined(_WIN32)
const char *cmd = "C:\\Windows\\System32\\ipconfig.exe /all";
#else
const char *cmd = "/bin/ls -l /usr/bin";
#endif
int status = system(cmd);
printf("status: %d\n", status);
return 0;
}
PATH書き換えや攻撃者が配置した同名コマンドを拾う危険を抑える観点からも、フルパス指定は重要です。
system(NULL)で利用可否を確認
system(NULL)
はコマンドプロセッサの利用可否を返します。
実行前チェックに使えます。
// system(NULL) でコマンドプロセッサの有無を確認
#include <stdio.h>
#include <stdlib.h>
int main(void) {
int available = system(NULL);
if (available != 0) {
printf("Command processor is available.\n");
} else {
printf("Command processor is NOT available.\n");
}
return 0;
}
Command processor is available.
出力の扱い
system
は子プロセスの標準出力/標準エラーを親プロセスに継承します。
出力をプログラム側で取得したい場合は、シェルのリダイレクトを使うか、POSIXならpopen
、Windowsなら_popen
の利用を検討します。
// 出力を受け取るなら popen/_popen が便利です
// 注意: popenはPOSIX, _popenはWindows固有です
#include <stdio.h>
#include <stdlib.h>
int main(void) {
#if defined(_WIN32)
const char *cmd = "cmd /C ver"; // Windowsのバージョン表示
FILE *fp = _popen(cmd, "r");
#else
const char *cmd = "uname -srm"; // カーネル情報
FILE *fp = popen(cmd, "r");
#endif
if (!fp) {
perror("popen/_popen failed");
return 1;
}
char buf[256];
while (fgets(buf, sizeof(buf), fp)) {
fputs(buf, stdout); // 取得した出力をそのまま表示
}
#if defined(_WIN32)
int rc = _pclose(fp);
#else
int rc = pclose(fp);
#endif
fprintf(stderr, "pclose/_pclose rc=%d\n", rc);
return 0;
}
Linux 6.8.0-xx x86_64
pclose/_pclose rc=0
出力を解析したいならsystem
ではなくpopen
を使うのが基本です。
戻り値とエラー処理
返り値の意味
system
は実装依存の整数を返します。
system(NULL)
: 0なら不可、非0なら可。- 一般の呼び出し: 実行自体ができなければ-1等、できた場合は「終了ステータス」を返します。
- Linux/Unix: 値は
waitpid
の戻りと同様に、ビットに様々な情報(終了/シグナル等)が詰め込まれています。 - Windows: 多くの実装で「コマンドの終了コード」が返されますが、-1が返ることもあります。
単にstatus == 0
かだけを見て成功判定しないで、OSごとに正しく解釈しましょう。
終了コードの見方
Linux/Unixでの代表的な分解例です。
// Linux/Unix向け: system の戻り値を分解して表示
#include <stdio.h>
#include <stdlib.h>
#include <sys/wait.h> // WIFEXITED, WEXITSTATUS など
static void interpret_status(int status) {
if (status == -1) {
printf("system failed to execute command processor.\n");
return;
}
if (WIFEXITED(status)) {
printf("Exited normally with code=%d\n", WEXITSTATUS(status));
} else if (WIFSIGNALED(status)) {
printf("Terminated by signal=%d\n", WTERMSIG(status));
#ifdef WCOREDUMP
if (WCOREDUMP(status)) {
printf("Core dumped.\n");
}
#endif
} else if (WIFSTOPPED(status)) {
printf("Stopped by signal=%d\n", WSTOPSIG(status));
} else {
printf("Unknown status=0x%x\n", status);
}
}
int main(void) {
int st1 = system("true"); // 戻りコード0
int st2 = system("false"); // 戻りコード1
printf("true -> raw=%d\n", st1);
interpret_status(st1);
printf("false -> raw=%d\n", st2);
interpret_status(st2);
return 0;
}
true -> raw=0
Exited normally with code=0
false -> raw=256
Exited normally with code=1
Windowsではこれらのマクロは使えません。
多くの場合、status
にコマンドの終了コードが入る想定で扱いますが、-1は「cmd.exeが起動できず実行不可」を示すことがあります。
実装のドキュメントを確認してください。
コマンド未発見の見分け方
- Linux/Unix: 多くの
sh
系シェルは、コマンド未発見で127、見つかったが実行不可で126を返す慣習があります。従ってWIFEXITED(status)
かつWEXITSTATUS(status) == 127
で「未発見の可能性が高い」と判断できます。
// Unix: "nosuchcmd" を実行して 127 を検知する例
#include <stdio.h>
#include <stdlib.h>
#include <sys/wait.h>
int main(void) {
int st = system("nosuchcmd"); // 存在しないコマンド
if (st != -1 && WIFEXITED(st)) {
int code = WEXITSTATUS(st);
if (code == 127) {
printf("command not found (likely from shell)\n");
} else if (code == 126) {
printf("found but not executable (permission or format)\n");
} else {
printf("exit code=%d\n", code);
}
} else {
printf("failed to run shell or abnormal status.\n");
}
return 0;
}
Windows: cmd.exeでは「未発見」に対してERRORLEVEL 9009が返るのが一般的です。
ただし、これはcmd
の慣習であり、全ての状況で保証されません。
確実に判定したい場合は事前にSearchPath
や_access
で実体の有無を探すか、フルパス指定にします。
注意点と安全な使い方
セキュリティ
最大の注意点はシェルインジェクションです。
system
は文字列をシェルに渡して解釈させるため、例えばユーザー入力に;
や&&
、リダイレクト、パイプ、置換などが含まれると、意図しないコマンドが実行されます。
権限のあるプロセスで実行すると深刻な被害につながります。
原則として外部から受け取った文字列をsystem
に渡してはいけません。
ユーザー入力を渡さない
次のようなコードは非常に危険です。
// 悪い例: ユーザー入力をそのままコマンドへ
#include <stdio.h>
#include <stdlib.h>
int main(void) {
char arg[256];
// 例: ユーザーからファイル名を入力させる
if (!fgets(arg, sizeof(arg), stdin)) return 1;
// 改行除去を忘れるとさらに危険・不具合の原因
// 悪意のある入力: "hello.txt; rm -rf /"
char cmd[512];
snprintf(cmd, sizeof(cmd), "cat %s", arg);
system(cmd); // 危険: シェルにそのまま渡る
return 0;
}
ユーザー入力でコマンドを組み立てる必要があるなら、system
ではなくexecvp
/posix_spawn
/CreateProcess
等で「引数配列」として安全に渡すのが基本です。
安全側の例(Unix: execvp)
// Unixでの安全側: fork + execvp で引数を配列として渡す
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h> // fork, execvp
#include <sys/wait.h> // waitpid
int main(void) {
const char *prog = "/bin/ls";
char *const argv[] = { "ls", "-l", "/usr", NULL };
pid_t pid = fork();
if (pid == -1) { perror("fork"); return 1; }
if (pid == 0) {
execvp(prog, argv); // 置換。シェルを介さないためインジェクション耐性が高い
perror("execvp"); // ここに来たら失敗
_exit(127);
}
int st;
if (waitpid(pid, &st, 0) == -1) { perror("waitpid"); return 1; }
if (WIFEXITED(st)) printf("child exit=%d\n", WEXITSTATUS(st));
return 0;
}
安全側の例(Windows: CreateProcess)
// Windowsでの安全側: CreateProcess で直接起動し、必要なら引数を適切に引用
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#include <stdio.h>
int main(void) {
// 実行ファイルはフルパスで
const char *exe = "C:\\Windows\\System32\\ipconfig.exe";
// CreateProcessはコマンドライン文字列の構築規約が難しいため、最小限の例に留める
char cmdline[] = "ipconfig.exe /all";
STARTUPINFOA si = {0};
PROCESS_INFORMATION pi = {0};
si.cb = sizeof(si);
if (!CreateProcessA(exe, cmdline, NULL, NULL, FALSE, 0, NULL, NULL, &si, &pi)) {
fprintf(stderr, "CreateProcess failed. err=%lu\n", GetLastError());
return 1;
}
WaitForSingleObject(pi.hProcess, INFINITE);
DWORD exitCode = 0;
GetExitCodeProcess(pi.hProcess, &exitCode);
printf("child exit=%lu\n", exitCode);
CloseHandle(pi.hThread);
CloseHandle(pi.hProcess);
return 0;
}
引数のエスケープは難しい
シェル文字列の適切な引用やエスケープは非常に複雑です。
POSIXとWindowsの規約は大きく異なり、スペース、クォート、バックスラッシュ、Unicode、環境変数展開、ワイルドカード展開など、考慮点は多岐にわたります。
安全と移植性を求める場合は「シェルに渡さない」設計が最善です。
環境依存と移植性
systemは環境依存性が高いAPIです。
同じ文字列でもシェルの違い、PATH/PATHEXT、ロケールとファイル名の文字コード、ビルトインコマンドの存在などで結果が変わります。
CIや本番サーバの環境差が不具合の原因になりやすいため、実行環境を固定するか、フルパスと固定引数でぶれを抑えるとよいです。
フルパスと固定コマンドを使う
攻撃面を狭めるため、許可されたコマンドのみをフルパスで固定的に呼ぶ方針が有効です。
// 固定された安全なコマンドのみ許可する設計(例)
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int run_safe_command(const char *name) {
#if defined(_WIN32)
if (strcmp(name, "ipconfig") == 0) {
return system("C:\\Windows\\System32\\ipconfig.exe /all");
}
#else
if (strcmp(name, "lsusr") == 0) {
return system("/bin/ls -l /usr");
}
#endif
fprintf(stderr, "command not allowed\n");
return -1;
}
int main(void) {
int st = run_safe_command("lsusr");
printf("status=%d\n", st);
return 0;
}
文字コード
Linux/Unix: 多くの環境でUTF-8が一般的ですが、system
に渡すchar*
はロケール依存で解釈されます。UTF-8ロケール(例: LANG=ja_JP.UTF-8
)を使うのが安全です。
Windows: system
は実行時のANSIコードページ(ACP)に依存します。Unicodeに強いのは_wsystem
(UTF-16)です。
// WindowsでUnicodeパスを扱うなら _wsystem を利用
#include <wchar.h>
#include <stdlib.h>
int wmain(void) {
// 例: 日本語パスを含むディレクトリを列挙
const wchar_t *cmd = L"dir C:\\データ\\テスト";
int st = _wsystem(cmd);
wprintf(L"_wsystem returned: %d\n", st);
return 0;
}
マルチバイト版system
で日本語パスが化ける/失敗する場合、_wsystemやCreateProcessWなどのワイド版APIへ移行してください。
タイムアウト/非同期が必要なとき
system
は同期実行でタイムアウト機能がありません。
ハング対策や時間制限が必要なら、OS固有APIでプロセスを直接起動し、待機にタイムアウトを設定します。
Unix系: fork + exec + waitpid(WNOHANG)
// タイムアウト付きで実行する簡易例(Unix)
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
#include <signal.h>
#include <time.h>
int run_with_timeout(const char *path, char *const argv[], int timeout_sec) {
pid_t pid = fork();
if (pid == -1) { perror("fork"); return -1; }
if (pid == 0) {
execvp(path, argv);
perror("execvp");
_exit(127);
}
time_t start = time(NULL);
for (;;) {
int st;
pid_t r = waitpid(pid, &st, WNOHANG);
if (r == pid) return st; // 終了
if (r == -1) { perror("waitpid"); return -1; }
if ((int)(time(NULL) - start) >= timeout_sec) {
kill(pid, SIGKILL);
waitpid(pid, NULL, 0);
return 0xFFFF; // タイムアウト指標
}
usleep(100 * 1000);
}
}
int main(void) {
char *const argv[] = { "sleep", "5", NULL };
int st = run_with_timeout("sleep", argv, 2);
printf("status=%d\n", st);
return 0;
}
Windows: CreateProcess + WaitForSingleObject
// Windows: タイムアウト待機
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#include <stdio.h>
int main(void) {
STARTUPINFOA si = {0};
PROCESS_INFORMATION pi = {0};
si.cb = sizeof(si);
if (!CreateProcessA("C:\\Windows\\System32\\ping.exe",
"ping.exe 127.0.0.1 -n 5",
NULL, NULL, FALSE, 0, NULL, NULL, &si, &pi)) {
fprintf(stderr, "CreateProcess failed: %lu\n", GetLastError());
return 1;
}
DWORD wr = WaitForSingleObject(pi.hProcess, 2000); // 2秒待機
if (wr == WAIT_TIMEOUT) {
TerminateProcess(pi.hProcess, 1);
fprintf(stderr, "timeout, terminated.\n");
}
DWORD code = 0;
GetExitCodeProcess(pi.hProcess, &code);
printf("exit=%lu\n", code);
CloseHandle(pi.hThread);
CloseHandle(pi.hProcess);
return 0;
}
タイムアウトや非同期が要件ならsystem
を避け、直接プロセス制御する方が堅牢です。
代替API
用途に応じて、より安全・柔軟なAPIを選べます。
用途 | Linux/Unix | Windows | 特徴 |
---|---|---|---|
引数配列で安全に実行 | execve/execvp, fork+exec, posix_spawn | CreateProcess(Ex/W), _spawn*, ShellExecuteEx | シェルを介さずインジェクション耐性が高い |
出力を取得 | popen/pclose | _popen/_pclose | パイプで標準出力を受け取れる |
タイムアウト | fork+exec + waitpid(WNOHANG)/kill | CreateProcess + WaitForSingleObject | 制御がしやすい |
簡易にシェル機能を使う | system | system | 手軽だがリスク高め |
「簡単さが必要」ならsystem
、「制御と安全性が必要」ならプロセス生成APIという選択が基本です。
まとめ
systemは「シェルに文字列を渡す」関数であり、簡単な自動化には便利ですが、セキュリティ/移植性/制御性の面で多くの注意点があります。
特に以下を覚えておくと安全です。
- ユーザー入力を渡さない。必要なら
execvp
/posix_spawn
/CreateProcess
などへ移行する。 - フルパスと固定引数で実行し、PATHやシェル依存を減らす。
- 戻り値の解釈はOSごとに異なる。Unixは
WIFEXITED
等で分解、Windowsは実装の規約に従う。 - 出力取得は
popen/_popen
、タイムアウトや非同期は直接プロセス制御APIで行う。 - 文字コードはロケール/コードページの影響を受ける。Windowsでは
_wsystem
やW関数を検討。
初心者の方は、まずは本記事のサンプルで動作を確かめ、「どこからが危ないのか」を体感することをおすすめします。
慣れてきたら、要件に合わせてより安全な代替APIへの置き換えを進めてください。