<< セキュリティキャンプ2024 2日目のCコンパイラゼミ | Cコンパイラゼミ2024 | セキュリティキャンプ2024 4日目のCコンパイラゼミ >>

R15を挟むのをやめる

セキュリティキャンプ2024 2日目のCコンパイラゼミでR15を挟む形で実装していたが、これR15を挟む意味がないので挟まないようにした。

今日の最初の作業

複合リテラル、そんなものがあったんだ

仕様読み

ドーナツがセグフォする整数拡張算術型変換まわりをちゃんと実装しないといけなくなったので、仕様書を読むところから始める。

英語を読むのが辛いので、代わりにJIS規格を読んでいる。C99の少し古い仕様だけれど、特に問題はないでしょ。

  • 拡張整数型なるものがあるらしい。gccには存在せず、どういうものなのかは謎
  • 実引数の個数と仮引数の個数が等しくない場合,その動作は未定義とする(未定義動作)

char型

Transclude of char(C言語)

算術型変換

Transclude of 算術型変換

ポインタの変換

未定義動作の嵐

  • 任意のポインタ型は整数型に型変換できる。結果は処理系定義とする。結果が整数型で表現できなければその動作は未定義とする
  • 関数ポインタを変換するとき、異なる型の関数ポインタを無理やり動かした場合の動作は未定義とする

整数拡張

Transclude of 整数拡張

整数拡張と算術型変換の動作

char + charを計算する場合

  • 計算をする時点で整数拡張が行われ、charは勝手にintになる
  • よってint + intをする
char c1 = 30;
char c2 = (30 * 30) / 25

このようなコードの場合、結果は-4ではなく36になる

int * longをする場合

  • intとlongは異なる型であるため算術型変換が行われ、intがlongへと変換される
  • よって long * longをする

余談

intはアーキテクチャにとって自然な長さだから、それより短い型は自動的にintへ変換される。自然な型というのは計算しやすい型でもあるので、charやshortではなくintで計算する。

単項演算子でも昇格は発生する。+ c1とか! c1とか

-200 / 2が21億になってしまう

もう一人の受講生「-200が42億だと思われてそう。32ビットで計算するべき値を64ビットで計算していそう」

cqoについて

cdqe命令を足すと上手く動いた。これだと64ビット除算が動かないけれど、とりあえずこれで通すことにした

int型からvoid型への変換のエラー

今、空のreturnは42を入れる形にしていた。とりあえずint型からvoid型へは無条件に変換できるようにしてお茶を濁すことにした。

整数拡張のテストが通らない

char a = 30;
char b = (a * a) / 25; // a*aは整数拡張によってint型に変換される
assert(36, b, "(a * a) / 25");
char c = a * a;
char d = c / 25; // char型に変換されるため、切り捨てられる
assert(-4, d, "c / 25");

これの2つ目のテストが通らないというものだった。

  • char の 0xfc が、int の 0xfffffffc ではなく int の 0x000000fc になっている
  • どこかでmovsxが抜けていそうという考えに至る

昼食を食べた後の作業

  • プリンターでアセンブリを印刷して紙の上でデバッグして見つけた
  • movsxにするべき箇所がmovzxになっていた
  • (後から振り返ると、先にmovsxが抜けていそうだと判断したならば、アセンブリ中のmovzxに注目して見ることが出来たはず)
  • アセンブリを読むスピードが上がってきた感じがする

2147483647 * 2 + 2に関するテスト

longの値の範囲を調べるテストが失敗する

int printf();
int main() {
  long l1;
  long l2;
  l1 = 2147483647 * 2 + 2;
  l2 = 0;
  printf("%d\n", l1 != l2);
}

このとき、l1 != l2はfalseと判定される。これまでこれは間違った仕様だと思っていたけれど、実は正しい仕様。2147483647 * 2 + 2というのは、long型ではなくint型として計算される。すると、オーバーフローが起きた結果0となる。整数拡張の仕様をちゃんと把握していないとここで間違える。

宣言時に代入すると左辺の型に引きずられる

int printf();
int main() {
  long l1 = 2147483647 * 2 + 2;
  long l2 = 0;
  printf("%d\n", l1 != l2);
}

これを動かすと、1が出てしまう。正しい整数拡張について、代入は対処したが、宣言時の代入の方はまだ対応できてなかった。

ドーナツでセグフォ

int printf();
int main() {
  int N = -1;
  printf("%d\n", N > 0);
}

これの実行結果が1になってしまう。負数の比較で整数拡張が上手く出来てない。

eaxに0xffff_ffffを入れた場合、それに対してcmp rax, rdiするときを考える。するとraxは42億になってしまう。これに対処するために、適切に整数拡張を行うか、cmp eax, ediなどの命令を使うようにする必要がある。

よくよく考えると他のオペランドも同じような問題がある。なぜここまで露呈しなかったのかというと、整数に対して剰余を取るという演算は準同型写像になる。2022年セキュリティ・キャンプL3ゼミ(Cコンパイラ班)ログ。この手のバグは割り算と比較以外で露呈しづらい。

整数拡張・算術型変換の実装のセグフォ

ポインタの足し算のテストでセグフォする

int *a;
a = alloc_int(1, 2, 3, 4);
assert(2, *(a + 1), "*(a + 1)");

このプログラムをコンパイルしようとするとセグフォが出る。

analyze_semanticsで型を入れるようにして、大体の式で自動で型が入るようにした。

analyzeでセグフォする

これで上手く行くかと思ったらもっと悪化した。

どこかでanalyze_semanticsのtypeにnullを渡していた箇所があるらしい。nullの場合は無視する形にした所上手く動いた。

剰余算でfloating-point exception

剰余算でidivの前にcdqeを使っていたが、これはrdxを触らない。なので、idivの前に置く命令としては不適当。cdqeではなく、cdqを使うべきだった。

昨日今日あたりでアセンブリの使ってる範囲の命令に対しての知識がだいぶ増えた気がする。

これで整数拡張・算術型演算の実装は出来たので、次はドーナツを動かしてみる。

2フレーム描画したところでセグフォ

Cコンパイラはつらいよ。絶賛上映中