printfであそぼう(1)

printf文って、printf("hogehoge");という形式を守ってさえすればコンパイルエラーが出ない。それがたとえ、 printf("%d %d %d %d\n"); という文でも。
こういった場合、その出力はよく「無意味な数字」だと言われる。


ほんとに?
「意味のない」数字なんて胡散臭いよね。何らかの意味はありそうだもの。
と思ったので少しだけ遊んでみた。

#include <stdio.h>

int main(void){
  int a=1,b=2,c=3,d=4;

  printf("%d %d %d %d\n");
  // 意味の分からない数字が出力

  printf("%d %d %d %d\n",a,b,c,d);
  // 1 2 3 4

  printf("%d %d %d %d\n");
  // 1 2 3 4

  a=100,b=200,c=300,d=400;
  printf("%d %d %d %d\n");
  //1 2 3 4
}

printfは独自バッファを持っているように振る舞っている。
元の値を変えてもprintfバッファ上の、以前渡されたままの値が保持されていることが分かる。


ほんとに?
怪しいよね。printfがそんな機能を実現する意味なんてないもんね。
たまたま同じ数字が出力されるとすれば、それはprintfが結果的に前と同じ記憶領域を見るような関数として作られているだからだろうと思われる。


てなわけで再実験。関数を呼び出してみる。

#include <stdio.h>

int funcall(void){
  printf("%d %d %d %d %d %d %d %d\n");
}

int main(void){
  printf("%d %d %d %d %d %d %d %d\n",1,2,3,4);
  funcall();
  printf("%d %d %d %d %d %d %d %d\n");
}

で、出力結果:

1 2 3 4 47 1628696576 0 1628438944
35 2280680 4198591 4202496 1 2 3 4
1 2 3 4 47 1628696576 0 1628438944

関数を呼び出した先からも、前のprintf領域が見えているようだ。
そして、関数が戻るとprintfのバッファ*1も元の位置を指し直しているように見える。


幾つか他の例も試してみたが、どうもこの呼び出し元のprintf領域は残るように作られているようだ。
また、引数に与えた数値が四つだから四つ分ずれたのかと思ったのだけど、printfのオプション引数を三つ与えても五つ与えても、ズレは四つ分のままだった。下は三つの場合の例。

1 2 3 1628870330 47 1628696576 0 1628438944
44 2280680 4198583 4202496 1 2 3 1628870330
1 2 3 1628870330 47 1628696576 0 1628438944

正直、動作がナゾい。


他にも、面白い事例がある。他の変数をいじっているだけなのに、printf文の結果が変わっちゃうのだ。
ソースコードを載せるのが面倒なので簡単に説明する。

  • int main(void)
    • printf("%d %d %d %d %d %d %d %d",1,2,3,4);
    • funcall(5);
    • printf("%d %d %d %d %d %d %d %d");
  • int funcall(int count)
    • countが0より大きいなら
      • printf()し、countをデクリメントしてからまたprintf()し、funcall(count)を返す
    • countがゼロ以下なら
      • 0を返す

funcallでprintfに与える引数は "%d %d %d %d %d %d %d %d\n" のみ。

1 2 3 4 2281060 2280680 1628302663 1628870330 // main()内

47 2280680 4198793 5 1 2 3 4 // 関数呼び出し funcall(5)中
47 2280680 4198793 4 1 2 3 4 // count--;

30 2280616 4198542 4 47 2280680 4198793 4 // 関数呼び出し funcall(4)中
30 2280616 4198542 3 47 2280680 4198793 4 // count--;

43 2280600 4198542 3 30 2280616 4198542 3 // 関数呼び出し funcall(3)中
43 2280600 4198542 2 30 2280616 4198542 3 // count--;

43 2280584 4198542 2 43 2280600 4198542 2 // 関数呼び出し funcall(2)中
43 2280584 4198542 1 43 2280600 4198542 2 // count--;

43 2280568 4198542 1 43 2280584 4198542 1 // 関数呼び出し funcall(1)中
43 2280568 4198542 0 43 2280584 4198542 1 // count--;

1 2 3 4 2281060 2280680 1628302663 1628870330 // main()に復帰

あれれ。countを減らしただけなのに、四つ目の数値が変わってるよ?あっれー?
ということは、引数として渡された変数とprintfとは同じバッファを利用しているってこと。ならばprintf文にうまくオプション引数を与えれば、変数を書き換えることができるはず!
printf、恐ろしい子


と思って試してみた。
だめだった。その辺の安全対策はきちんとされているようだ。
引数を与えられない場合(=値を展開・保持する必要がない場合)にはprintfのバッファは適当な位置を指しており、たまたまその近くに引数がいたってことらしい。


これ以上については、ソースコードに当たるよりなさそうだ。

メモリが読めればなあ。

実行中のプログラムのメモリ内容を取得できたらprintfについてもわかりやすくなりそうなもんなんだけどな。
次回はリフレクショーンを試してみる。

*1:もしそれが存在するなら