C言語のコマンドライン引数を掘り下げる

## はじめに

C言語のコマンドライン引数について質問された際, よくよく考えたら何も理解していない事に気付きました.
そこで, コマンドライン引数にまつわる内容を掘り下げてみました.

- 対象: ある程度C言語を理解している方
- 環境: linux x86_64
- gcc version 10.2.0

## コマンドライン引数とは

コマンドラインから引数として文字列をmain関数に渡せる仕組み

```c
int main(int argc, char *argv[]) {
    //   ^^^^^^^^^^^^^^^^^^^^^^
    //          ↑ これ ↑
}
```

`argc` はコマンドライン引数の数, `argv` はコマンドライン引数の文字列へのポインタ配列です.

例えば, 以下のようにプログラムを実行すると

```
$ ./a.out 123 abc
```

このようにプログラムに値が渡されます.

```c
assert( argc == 3 );
assert( !strcmp(argv[0], "./a.out") );
assert( !strcmp(argv[1], "123") );
assert( !strcmp(argv[2], "abc") );
assert( argv[argc] == NULL );
```

※ `argv[0]` にはプログラム実行時のパスが渡される
※ `argv`の最後の要素の次(`argv[argc]`)には`NULL`が入っている

## そもそも引数とは

関数に渡される情報のこと.
呼び出す側に指定する引数を**実引数**, 受け取る側の変数を**仮引数**と呼びます.

```c
void func(int a) {
    //    ^^^^^
    //    仮引数
}

int main(void) {
    func(42);
    //   ^^
    //   実引数
}
```

ということはmain関数の`argc`と`argv`は仮引数となります.
では, どのようにmain関数にコマンドライン引数を渡しているのでしょうか.

## main関数の呼び出し元

とりあえず`objdump`で逆アセンブルしてみましょう.

```test.c
// argcを返すプログラム
int main(int argc, char *argv[]) { return argc; }
```

```
$ gcc test.c
$ objdump -d -M intel a.out | less
```

```
0000000000001020 <_start>:
    1020:       f3 0f 1e fa             endbr64
    1024:       31 ed                   xor    ebp,ebp
    1026:       49 89 d1                mov    r9,rdx
    1029:       5e                      pop    rsi
    102a:       48 89 e2                mov    rdx,rsp
    102d:       48 83 e4 f0             and    rsp,0xfffffffffffffff0
    1031:       50                      push   rax
    1032:       54                      push   rsp
    1033:       4c 8d 05 66 01 00 00    lea    r8,[rip+0x166]                # 11a0 <__libc_csu_fini>
    103a:       48 8d 0d ef 00 00 00    lea    rcx,[rip+0xef]                # 1130 <__libc_csu_init>
    1041:       48 8d 3d d1 00 00 00    lea    rdi,[rip+0xd1]                # 1119 <main>
    1048:       ff 15 92 2f 00 00       call   QWORD PTR [rip+0x2f92]        # 3fe0 <__libc_start_main@GLIBC_2.2.5>
    104e:       f4                      hlt
    104f:       90                      nop

--- 省略 ---

0000000000001119 <main>:
    1119:       55                      push   rbp
    111a:       48 89 e5                mov    rbp,rsp
    111d:       89 7d fc                mov    DWORD PTR [rbp-0x4],edi
    1120:       48 89 75 f0             mov    QWORD PTR [rbp-0x10],rsi
    1124:       8b 45 fc                mov    eax,DWORD PTR [rbp-0x4]
    1127:       5d                      pop    rbp
    1128:       c3                      ret
    1129:       0f 1f 80 00 00 00 00    nop    DWORD PTR [rax+0x0]
```

`main`と検索して合致したセクションのみ抜粋しました.
この結果を見ると, `_start`で`rdi`に`main`のアドレスを入れ, `__libc_start_main`を呼び,
`hlt`(CPUの動作を停止)しているのがわかります. つまり, \_\_libc_start_mainでmain関数を呼んでいるということですね.

## \_\_libc_start_mainの中身

\_\_libc_start_mainの中では何を行っているのでしょうか.

```:_start
1048:       ff 15 92 2f 00 00       call   QWORD PTR [rip+0x2f92]        # 3fe0 <__libc_start_main@GLIBC_2.2.5>
```

先ほどの`objdump`の結果を再掲しました.
後半にご丁寧にアドレス計算した呼び出し先が記述されていますが, `@GLIBC_2.2.5`となっています.
これはGLIBCから動的に呼び出しているという意味になります.
`-static`オプションを付けて静的リンクすると`__libc_start_main`セクションの処理も見ることができます.
長いのでここでは要点のみ掲載します.

```:_start
1041:       48 8d 3d d1 00 00 00    lea    rdi,[rip+0xd1]                # 1119 <main>
1048:       ff 15 92 2f 00 00       call   QWORD PTR [rip+0x2f92]        # 3fe0 <__libc_start_main@GLIBC_2.2.5>
```

`_start`セクションでは`rdi`レジスタにmain関数のアドレスを格納して, `__libc_start_main`を呼び出しています.

```:__libc_start_main
4022ec:       48 89 7c 24 18          mov    QWORD PTR [rsp+0x18],rdi

--- 省略 ---

402843:       48 8b 15 46 ea 0a 00    mov    rdx,QWORD PTR [rip+0xaea46]        # 4b1290 <__environ>
40284a:       8b 7c 24 0c             mov    edi,DWORD PTR [rsp+0xc]
40284e:       48 8b 74 24 10          mov    rsi,QWORD PTR [rsp+0x10]
402853:       48 8b 44 24 18          mov    rax,QWORD PTR [rsp+0x18]
402858:       ff d0                   call   rax
40285a:       89 c7                   mov    edi,eax
40285c:       e8 2f 61 00 00          call   408990 <exit>
```

そして`__libc_start_main`では`rsp+0x18`の位置に`rdi`を格納し,
なんやかんや処理をした後, `rax`にmain関数のアドレスを入れて, `call`しています.

この時, `edi`には`argc`, `rsi`には`argv` そして`rdx`には`__environ`が入っています.

最後に`edi`に`eax`を格納し, `exit`をcallしています. `eax`にはmain関数の戻り値が格納されているので, その戻り値を引数にしてexit関数を呼んでいるということです.

こんなイメージ

```
exit(main(argc, argv, envp));
```

### 余談

- バイナリレベルのインターフェース

    どのように関数に引数を渡すのか等をプラットフォームごとにある程度決めた仕様のことをABI(Application Binary Interface)と言います.
    ABIの一部に[関数の呼び出し規約](https://docs.microsoft.com/ja-jp/cpp/build/x64-calling-convention?view=msvc-160)があります. これに従うことで, どのレジスタがどの引数に対応しているか等がわかります.

- エントリーポイント

    プログラムにはエントリーポイントから始まります. ELFファイルの場合は`readelf`でELFフォーマットを解析することができます.

    `$ readelf -h a.out`

    ```
    ELF Header:
      Magic:   7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
      Class:                             ELF64
      Data:                              2's complement, little endian
      Version:                           1 (current)
      OS/ABI:                            UNIX - System V
      ABI Version:                       0
      Type:                              DYN (Shared object file)
      Machine:                           Advanced Micro Devices X86-64
      Version:                           0x1
      Entry point address:               0x1020
      Start of program headers:          64 (bytes into file)
      Start of section headers:          14640 (bytes into file)
      Flags:                             0x0
      Size of this header:               64 (bytes)
      Size of program headers:           56 (bytes)
      Number of program headers:         11
      Size of section headers:           64 (bytes)
      Number of section headers:         27
      Section header string table index: 26
    ```

    `Entry point address`という欄に `0x1020`と書かれています.
    これは先ほどの`_start`セクションのアドレスですね.
    まとめると, 以下のような順番で処理が呼び出されているということがわかります

    ```
    _start => __libc_start_main => main
    ```

## main関数の定義

そもそもmain関数の仮引数の個数や型は決められているのでしょうか?
[Microsoft C](https://docs.microsoft.com/ja-jp/cpp/c-language/arguments-to-main?view=msvc-160)には以下の定義ができると記載されています.

```c
int main( void )
int main( int argc, char *argv[] )
int main( int argc, char *argv[], char *envp[] )
```

あまり馴染みのない`envp`という変数が出てきました. これは環境変数へのポインターの配列です. 先程しれっと登場した`__environ`がこれに該当します.

### Q. 引数1個や4個以上は無理なの?

A. いけます

「`main`」というシンボルさえ定義されていれば, 引数がどうであってもプログラムを生成することは可能です. すなわち, `main(int ,char*[])`である必要はなく, `main(int, int)`であっても処理系はちゃんとmain関数として取り扱うわけです. もちろん, `main(a,b,c,d,e,f,g){}`のようなコードでも大丈夫です.

## シェルからプログラムへ

main関数からコマンドライン引数を読み込む手順はわかったが, シェルからプログラムへはどのように渡しているのだろうか.

```:__libc_start_main
40284a:       8b 7c 24 0c             mov    edi,DWORD PTR [rsp+0xc]
40284e:       48 8b 74 24 10          mov    rsi,QWORD PTR [rsp+0x10]
```

`argc`と`argv`に対応するレジスタから遡っていく

```:__libc_start_main
4022e0:       48 89 54 24 10          mov    QWORD PTR [rsp+0x10],rdx
--- 省略 ---
4022f1:       89 74 24 0c             mov    DWORD PTR [rsp+0xc],esi
```

\_\_libc_start_mainの先頭らへんで`rsp+0x10`のメモリに`rdx`, `rsp+0xc`のメモリに`esi`を格納している.

```:_start
401c69:       5e                      pop    rsi
401c6a:       48 89 e2                mov    rdx,rsp
```

\_\_libc_start_mainに飛ぶ前に\_startで`rsi`にスタックから`pop`し, `rdx`に`rsp`を格納している. つまり\_startのスタックポインタの先頭に`argc`があり, `sp+4`の位置にargvの先頭アドレスが格納されていたことになる. このことから, シェルがプログラムを起動する際に, コマンドライン引数の個数とコマンドライン引数として渡された文字列へのポインタをスタックにpushしていると予想できます.

実際に`gdb`で`_start`にbreakポイントを設定すると, スタックに`argc`,`argv`,`envp`があるのが確認できる. また, `libc/glibc/sysdeps/x86_64/start.S`のコメントにも詳しく書かれています.

### プログラムの実行

シェルがプログラムを実行するとき, fork -> execveシステムコールの流れで行われる.
execveシステムコールが発行された後のカーネルの作業は以下のようなものがある.

- 実行ファイルを読み込み, 仮想メモリ上にマッピングする
- argc/argv[], BSSの初期化, 環境変数の引き渡し
- 実行ファイル上のエントリ・ポイントから実行を開始する

カーネルがelfファイルをロードする際にスタック上にargcを積み, その直下にargv[]を作成しています.
ポインタの配列の実体をスタック上に作成しているので, アプリケーションのスタートアップには, 配列へのポインタが渡されるわけではありません.

## 仮引数を書かずにコマンドライン引数を取得

シェルがコマンドライン引数への実体をスタックへ格納しているという事は, 仮引数を書かなくてもアクセスできるかもしれません.

### argcからargvにアクセス

```c
int main(int argc) {
    return argc;
}
```

このプログラムをコンパイルし, 逆アセンブルすると

```
0000000000401d85 <main>:
401d85:       55                      push   rbp
401d86:       48 89 e5                mov    rbp,rsp
401d89:       89 7d fc                mov    DWORD PTR [rbp-0x4],edi
401d8c:       8b 45 fc                mov    eax,DWORD PTR [rbp-0x4]
401d8f:       5d                      pop    rbp
401d90:       c3                      ret
```

スタックには`argc`しか確保されていません. しかし, `_start`と`__libc_start_main`ではきちんと`argv`が存在するように振る舞っています. つまり, \_\_libc_start_mainセクションの状態のスタックポインタ+0x10のアドレスにargvのポインタがあるはずです.
すなわち, `argc`のアドレス+0x4(mainセクションでの`argc`の`rbp`からのオフセット)+0x8(`psuh rbp`で下がったrsp分)+0x8(`call`でpushされた戻りアドレス分)+0x10(\_\_libc_start_mainセクションでの`rsp`からのオフセット)をすればアクセスできそうです.

```c:test.c
#include <stdio.h>
int main(int argc) {
    printf("%s\n", **((char***)((char*)&argc+0x4+0x8+0x8+0x10)));
    return 0;
}
```
```
$ gcc test.c -static
$ ./a.out
./a.out
```

`argc`を元にargvの要素へアクセスすることができました!

### main(void)

次に, main関数内でスタックに変数を確保していない場合のアクセス方法です.

`argv[0]`のアドレスを事前に取得し, 直接アクセスすればいけそうな気がします.

```c
#include <stdio.h>
int main(int argc, char **argv) {
    printf("%p\n", &*argv[0]);
    return 0;
}
```

```
$ ./a.out
0x7ffeb3f5d2e7
$ ./a.out
0x7fffda4682e7
```

しかし, 実行するたびにアドレスが変わります.
これにはASLRの影響が考えられます.

#### ASLR

ASLR(Address Space Layout Randomisation)とは, メモリ上のスタック等の配置を実行毎にランダムにする機能です.
スタックオーバーフロー等の脆弱性をついた攻撃を成功させずらくすることができます.

以下のコマンドで無効化します

```
$ sudo sysctl -w kernel.randomize_va_space=0
```

すると, `argv`のアドレスが固定化されます.

```c
#include <stdio.h>
int main(int argc, char **argv) {
    printf("%p\n", &*argv[0]);
    return 0;
}
```
```
$ ./a.out
0x7fffffffe2e7
$ ./a.out
0x7fffffffe2e7
```

このアドレスに直接アクセスすることでargvを参照することができました!

```c
#include <stdio.h>
int main(void) {
    printf("%s\n", 0x7fffffffe2e7);
    return 0;
}
```

```
$ ./a.out
./a.out
```

実験が終わったら必ず以下のコマンドでASLRを有効にしましょう.

```
$ sudo sysctl -w kernel.randomize_va_space=2
```

## まとめ

コマンドライン引数がどのような仕組みで用意され, アクセスされるのか掘り下げてきました.
main関数でも他の関数と同様に仮引数はABIに従った方法で受け取るが, main関数に渡す側の\_\_libc_start_mainの処理内容は変わらないことや, コマンドライン引数の実体もスタック上にあることがわかりました.

## 参考

- [main()関数の前には何があるのか](https://www.atmarkit.co.jp/ait/series/5503/)
- [ShortCoding職人達の技法](https://www.amazon.co.jp/dp/4839925232)