va_listってなんだ


こんにちは、[42tokyo Advent Calendar 2021](https://qiita.com/advent-calendar/2021/42tokyo) の15日目を担当する、在校生のnori26です。

## はじめに
この記事はC言語で可変長引数を実現するための`va_list`やその他マクロについて、42で学んだことをまとめたものです
処理系は`x86-64 Linux`を想定して書いています
話を簡単にするため、浮動小数点レジスタの話は省きます
もっとちゃんとした[記事1](https://qiita.com/hotpepsi/items/bd1f496411a2df74b704)や[記事2](https://qiita.com/qnighy/items/be04cfe57f8874121e76)があったのでより詳しく知りたい方はこっち見た方がいいと思います

## 結論
* `va_list`は32bitと64bitでデータ構造が違う
* 多分引数の渡し方が違うからだと思う
* `va~`系マクロは処理系ごとの違いを隠蔽してくれてる

#### `va_list`のデータ構造
32bit
[i386 stdarg.h](http://fxr.watson.org/fxr/source/i386/include/stdarg.h?v=FREEBSD22;im=excerpts)

```c
typedef char *va_list;
```

64bit
[System V AMD64 ABI](http://refspecs.linux-foundation.org/elf/x86_64-abi-0.99.pdf#page=54)

```c
typedef struct {
	unsigned int gp_offset;      //汎用レジスタ分のオフセット
	unsigned int fp_offset;      //浮動小数点レジスタ分のオフセット、この記事では無視する
	void	 *overflow_arg_area; //スタック渡し分のアドレス
	void	 *reg_save_area;     //レジスタ渡し分の先頭アドレス
} va_list[1];
```
## 前提知識
### 呼び出し規約
32bitと64bitでは関数を呼び出すときのルールが違う
32bitでは引数はスタックで渡される
64bitでは6個までレジスタで渡され、それ以降はスタックで渡される（今回は浮動小数点は無視する）
その他ルールはここでは割愛
詳しくは [すごい方の記事](https://qiita.com/hotpepsi/items/bd1f496411a2df74b704#%E5%91%BC%E3%81%B3%E5%87%BA%E3%81%97%E8%A6%8F%E7%B4%84%E3%81%AB%E3%81%A4%E3%81%84%E3%81%A6), [すごい方の記事2](https://qiita.com/qnighy/items/be04cfe57f8874121e76#abi%E3%81%A8%E3%81%AF), [System V AMD64 ABI](http://refspecs.linux-foundation.org/elf/x86_64-abi-0.99.pdf)

### スタック
メインメモリの領域の一つで、普通に変数を宣言したときに割り付けられる領域、いつも使ってるやつ
メインメモリは低速だが、容量が大きい
スタックには変数の他に関数の始まりのアドレスや、関数からreturnしたあとコードのどこに戻ればいいかなど、C言語のコード上からは見えていない色々な情報が格納されている
メモリレイアウトの詳しい説明は割愛
詳しくは [ここ](http://www.coins.tsukuba.ac.jp/~yas/coins/os2-2010/2011-01-25/) がいいかも

### レジスタ
CPUに付いてる記憶装置
非常に高速だが、容量が小さい
演算装置とメモリ間のデータのやり取りを中継したりとか色々やる
メモリとは別の装置なのでメモリアドレスは振られていない
C言語では基本的に扱えない（レジスタを意識しなくても書けるようにしてくれてる）
詳しくは [ここ](https://ufcpp.net/study/computer/CPU.html) がいいかも
## `va~`系ってなんだ
### 実行環境

```
$ LC_ALL=C lscpu | grep -E '^(Architecture|Model name)' 
Architecture:        x86_64
Model name:          AMD Ryzen 5 5600X 6-Core Processor

$ uname -srmo
Linux 5.4.0-90-generic x86_64 GNU/Linux

$ lsb_release -d
Description:    Ubuntu 18.04.6 LTS

$ clang --version | head -n 3
clang version 9.0.0-2~ubuntu18.04.2 (tags/RELEASE_900/final)
Target: x86_64-pc-linux-gnu
Thread model: posix

$ /lib/x86_64-linux-gnu/libc.so.6 | grep version
GNU C Library (Ubuntu GLIBC 2.27-3ubuntu1.4) stable release version 2.27.
Compiled by GNU CC version 7.5.0.
```



### `va_arg`はスタックを掘り返す
`printf`を指定子だけで呼び出してみる ※未定義動作

```c
printf("%p%p%p%p%p\n");
```
謎の出力が出ると思う
コード上で既にスタックに配置した変数や、C言語が暗黙的にスタックに格納した情報などを`va_arg`が順に取り出して出力させてる気がする

確認してみる ※未定義動作

```c
#include <stdio.h>
#include <stdint.h>

int	main(void)
{
	uint64_t ary[10] = {1,2,3,4,5,6,7,8,9,0xdeadbeef};

	printf("%p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p\n");
}
```
**出力**

```
0x400690 0xdeadbeef 0x5 0x6 0x7 0x9 0x7f91baa51660 0x1 0x2 0x3 0x4 0x5 0x6 0x7 0x8 0x9 0xdeadbeef 0x4005b0
```
スタックに置いた配列 `{1..9, 0xdeadbeef}` がいる！
でも`0xdeadbeef 0x5, 0x6 0x7 0x9`は何故か最初にも出てる
なんでだ


### `va_start`はレジスタの中身をスタックに配置する
配列`ary`に値を代入するとき、CPUは値を一度レジスタにセットし、レジスタから`ary`のメモリ領域に格納する
そのまま`printf`が呼び出され、`va_start`がレジスタの中身をスタックに保存している
レジスタは容量が小さく、不要な値はすぐに別の用途で上書きされてしまうため、配列の残骸が中途半端にメモリに複写されたようだ

でもこれだけだと`va_arg`はどこからスタックを掘り始めればいいのかわからないので、開始アドレスが必要かも
`va_start`はどこかにアドレスをセットしてるはずだ！

### `va_list`のデータ構造
**32bit**
ここで一旦レジスタのことは忘れて32bitのことを考えてみる
先述のとおり、32bitでは引数は関数呼び出し直前にスタックに連続して全て配置される
そうすると話は単純で、`va_start`は最終仮引数の次の領域を先頭のアドレスとして`va_list`にセットすればいい
`va_list`がポインタ型変数を一つ持っていれば、`va_arg`は呼びだされる度にそいつを進めていくだけで次の引数を取り出せる

```c
typedef char *va_list;
```
これでおっけー！

**64bit**
一方64bitではレジスタ渡し分(1~6個目)とスタック渡し分(7個目~)で少し扱いが変わる
まずレジスタ分とスタック分それぞれの先頭アドレスと、レジスタ分を何回取り出したかを知るためのオフセット変数が必要になるらしい
アドレスが二つ必要なのは、32bitとは違って6個目と7個目の引数が連続したアドレスに配置されないからだと思う
7個目と6個目の間に関数呼び出しが入ることで、スタックに色々追加されて隔たりができる
（1~6個目は関数呼び出し後`va_start`によってスタックに配置されるのに対して、それ以降の引数は32bitと同様関数呼び出し前に配置されるから）[^1]
でアドレス連続してないから今何個目の引数かを把握するためのオフセット変数が必要になる

```c
typedef struct {
	unsigned int gp_offset;      //オフセット変数
	unsigned int fp_offset;      //これは今回は無視
	void	 *overflow_arg_area; //スタック渡し分のアドレス
	void	 *reg_save_area;     //レジスタ渡し分の先頭アドレス
} va_list[1];
```
あとポインタ型と同じように扱いたいから構造体の配列で定義してるのかな多分

## おわり
`va~`系マクロは処理系ごとの違いを隠蔽してくれてる抽象化レイヤーなんだなーと思いました
明日は、@harinez2さんが最強のMakefileについて書いてくれる予定ですので、そちらの記事もお楽しみに！

## 参考URL
[自作 OS で可変長引数実装 (for x86-64)](https://blog.tiqwab.com/2020/11/07/vararg-x86-64.html)
[x86-64とARM64の可変長引数関数の呼び出し規約](https://qiita.com/hotpepsi/items/bd1f496411a2df74b704)
[Cの可変長引数とABIの奇妙な関係](https://qiita.com/qnighy/items/be04cfe57f8874121e76)



[^1]: あと本当は間に浮動小数点レジスタ分の領域も入る