質問をすることでしか得られない、回答やアドバイスがある。

15分調べてもわからないことは、質問しよう!

新規登録して質問してみよう
ただいま回答率
85.50%
C

C言語は、1972年にAT&Tベル研究所の、デニス・リッチーが主体となって作成したプログラミング言語です。 B言語の後継言語として開発されたことからC言語と命名。そのため、表記法などはB言語やALGOLに近いとされています。 Cの拡張版であるC++言語とともに、現在世界中でもっとも普及されているプログラミング言語です。

Q&A

解決済

5回答

2722閲覧

バッファオーバーフローによるリターンアドレスの上書き

kelt22

総合スコア46

C

C言語は、1972年にAT&Tベル研究所の、デニス・リッチーが主体となって作成したプログラミング言語です。 B言語の後継言語として開発されたことからC言語と命名。そのため、表記法などはB言語やALGOLに近いとされています。 Cの拡張版であるC++言語とともに、現在世界中でもっとも普及されているプログラミング言語です。

0グッド

0クリップ

投稿2020/05/08 03:10

質問

下記のプログラムのmain関数内の
int buf[1000];
buf[999] = 10;
の所がなにをしているのかわかりません。int buf[1000];buf[999] = 10;を削除してプログラムを実行した結果hello関数の呼び出した数が変わりました。hello関数の呼び出しを制限するためにあるのでは?と考えましたが、考えが正しいのか、どのような役割を果たしているのか解説していただけると嬉しいです。

該当のソースコード

#include <stdio.h> void hello(void) { fprintf(stderr, "hello!\n"); } void func(void) { void *buf[10]; static int i; for (i = 0; i < 100; i++) { buf[i] = hello; } } int main(void) { int buf[1000]; buf[999] = 10; func(); return 0; }

補足情報(FW/ツールのバージョンなど)

ここにより詳細な情報を記載してください。

気になる質問をクリップする

クリップした質問は、後からいつでもMYページで確認できます。

またクリップした質問に回答があった際、通知やメールを受け取ることができます。

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

guest

回答5

0

目的はわからないという結論のようですが、私には想像がつきます。
buf[999] = 10; が手掛かりです。ここに、int buf[1000] 配列を間違いなく確保したいという気持ちが表れています。
作者はこういう事を考えたのです(もちろん私の想像)。

  • (タイトル通り)バッファオーバーランでリターンアドレスを上書きする実験をしよう。

  • func()関数は、スタック上の配列 char *buff[10]; に対して、

hello() 関数のエントリアドレスを(100個も)書いていけば、どこかはともかく、どこかでリターンアドレスを上書きするだろう(不正は百も承知)。その状態で func() がリターンすれば、本来の戻り先である main() の中ではなく、hello() 関数へ「リターン」していくだろう(実際、質問者の手元でも、私の手元でもそうなった)。

  • (何もしないまま、とはbuf[1000]を配置せずに)要素数10の配列に対して範囲を超えて100個も書けば予想外の不具合を起こすかもしれない。それで肝心の実験が失敗するようでは困る。

  • 安全に(笑)バッファオーバーランしたいので、main() のスタック領域に

要素数1000の配列をとっておこう。こうすれば、上書きされるのは配列の先頭部分(インデックスの若い領域)とリターンアドレス付近だけで済むのではないか。

  • まてよ、int buf[1000]; だけだと、実際にはこの配列を使わないと見做され、コンパイラが最適化してしまい配列が割り当てられないかもしれない

  • そうだ!buf[999] をアクセスすれば buf[0] ~ buf[999] を使うという意思をコンパイラに伝えられて配列を確実にスタックに配置できる・・・


最後の「buf[999] をアクセスすれば…」はC言語ではない、別の言語の経験が反映しているという感じもします。

プログラム中に現れる10, 100, 1000 という値は、この値でなければならないという根拠のある数字ではありません。だいたいこういう値にしておけば実験できるだろうという値です。

int buf[1000];buf[999] = 10;を削除してプログラムを実行した結果hello関数の呼び出した数が変わりました

もとより不正で未定義な動作を意図的に実行しているのですから、動作結果が違うことに不思議は無いのですが、
私が2つのCコンパイラで試したhello()の実行回数を次に示します。

int buf[1000]有int buf[1000]無
コンパイラA89回89回
コンパイラB89回789回

89回(100-10=90に近い回数)hello()を実行する理由は比較的容易に理解できますが、789回も実行する理由は私にもわかりません。これは上記の「予想外の不具合を起こすかも」に相当すると思います。大雑把な言い方ですが、コンパイラBの場合はbuf[1000]が無いとメモリをハデに壊してしまうのでしょう。元の作者も似たような現象に遭遇したのではないでしょうか。

つまり、int buf[1000]があることで上記「安全に(笑)バッファオーバーランしたい」を実現できる場合があると言えます。

投稿2020/05/09 08:52

rubato6809

総合スコア1380

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

kelt22

2020/05/11 04:49

ありがとうございます。自分の知識がたりないのですべては理解できませんでしたが、このコードを作った人の意図が少し理解できました。
kmaebashi

2020/05/11 14:25

このソースコードは「C言語 ポインタ完全制覇」改訂版のp.118、List2-7ですね。 エゴサしてこの質問を見つけました。著者です。 int buf[1000]; buf[999] = 10; の意図は、rubato6809さんのおっしゃるとおりです。お見事な推測でした。 このプログラムは20年前の「C言語 ポインタ完全制覇」初版本から掲載されていますが、その時は、「要素数10の配列に、範囲を超えて100まで代入すれば、その時点で落ちてしまってその先の実験ができない可能性があるから、main()でも領域を確保しておこう」という程度の意図だったかと思います。ただ当時、FreeBSDとWindows98で試して、どちらも落ちなかったような気がするのですが、書籍にする以上、未知の環境でもできるだけ落ちないようにしようと考えました。 buf[999] = 10; を付けたのは2017年12月の改訂版からです。これの意図もrubato6809さんの推測通りコンパイラによる最適化で消えることを防ぐためです(こんなのあっても消えるときは消えるかもしれませんが)。
rubato6809

2020/05/12 09:28

ご本人にお墨付きをいただけるとは思っていませんでした(驚)。 わざわざお越しいただき、ありがとうございます。 > 改訂版のp.118、List2-7 勤務地にあったので、早速そのページを確認しました。すみません、ちゃんと読んでなかったので、この本だとは気づきませんでした(苦笑)。
guest

0

なにをしているのかわかりません。

誰にもわかりません。バッファオーバーランは未定義の動作ですので、「buf[999] = 10;と書いたからこのような動作が起きる」ということは、C言語のソースコードレベルでは何も言えません。

実際に生成した機械語と突き合わせて、「結果的にどういう意味を持つのか」は分析できるかもしれません。

投稿2020/05/08 03:16

maisumakun

総合スコア145121

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

0

ベストアンサー

これって、この段階で(配列の外部に書き込みをしているため)未定義動作です。

c

1 for (i = 0; i < 100; i++) { 2 buf[i] = hello; 3 }

また、clang 10.0で最適化をかけるとmain()は、func()が未定義動作のため、
何もしないで0を返すだけの関数に成ります。

asm

1main: # @main 2 .cfi_startproc 3# %bb.0: 4 xorl %eax, %eax 5 retq 6.Lfunc_end2: 7 .size main, .Lfunc_end2-main 8 .cfi_endproc

投稿2020/05/08 05:45

編集2020/05/08 06:03
cateye

総合スコア6851

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

cateye

2020/05/08 05:53

そうなんですよ、何もしない;;・・・・hello()は、printf()があるんで展開はされるんですが何処からも呼ばれないw
cateye

2020/05/08 06:06

回答修正しました“何もしない”→“未定義動作”
kelt22

2020/05/08 06:22

結果的に実行したときに影響があっただけで、意図してコードを書いてないということですか?
cateye

2020/05/08 06:29

ではなくて、未定義動作が含まれるコードなので“何が起こっても(例えば、OSが吹っ飛んでも)文句は言えない”ということです。・・・最近は。OSも頭がいいからまず無いけど・・・
kaina

2020/05/08 07:04

C言語には未定義動作というものがあり、予測不能な動作を引き起こす可能性があるので、 未定義の動作に依存するプログラムを書かないようにしましょうね。 参考ページ載せときます、未定義動作こんなにあるのね。。。 http://www.c-lang.org/detail/undefined_behavior.html
kelt22

2020/05/08 07:28

解説ありがとうございます。しかし、結局質問に戻ってしまうんですが、未定義動作を意図的に行っているプログラムでint buf[1000];buf[999] = 10;をfunc関数の前で行うのはなぜなんでしょう?int buf[1000];buf[999] = 10;の部分がなくても目的は達成しているのでは?と自分は思います。
maisumakun

2020/05/08 07:33 編集

> 未定義動作を意図的に行っているプログラムでint buf[1000];buf[999] = 10;をfunc関数の前で行うのはなぜなんでしょう? 「なんとなく入れてみた」だけかもしれませんし、「特定の環境で特定の挙動を起こすために」入れてみたのかもしれません。さらなる説明がなければ判断が付きません。
cateye

2020/05/08 07:34

意図は図りかねますが、 配列の最後に目印(フラグ?)を付けているだけかと?
kelt22

2020/05/08 08:20

なるほど、コードを見ただけでは目的まではわからないんですね。いろいろ質問に答えてくださりありがとうございます。
guest

0

質問のコードは、C言語レベルでは未定義の動作であり、
OS やコンパイラによって実行結果に違いがあります。

VC++ や Ubuntu の gcc では、hello! は表示されませんでした。しかし、
Windows 10 上の cygwin の gcc では、80数個の hello! が表示されました。

そこで次のようなコードに書き換えて検証してみました。

C

1#include <stdio.h> 2 3void **vp; 4 5void show(void) 6{ 7 for (int i = 0; i < 100; i++) 8 fprintf(stderr, "%p: buf[%d] = %p\n", vp + i, i, vp[i]); 9} 10 11void hello(void) 12{ 13 fprintf(stderr, "hello!\n"); 14} 15 16void func(void) 17{ 18 void *buf[10] = { 0 }; 19 static int i; 20 vp = buf; 21 show(); 22 23 for (i = 0; i < 100; i++) { 24 buf[i] = hello; 25 } 26} 27 28int main(void) 29{ 30 fprintf(stderr, "hello = %p\n", hello); 31 fprintf(stderr, "func = %p\n", func); 32 fprintf(stderr, "main = %p\n", main); 33 int buf[1000]; 34 static int i; 35 for (i = 0; i < 999; i++) buf[i] = i; 36 buf[999] = 10; 37 fprintf(stderr, "main: &buf[0] = %p\n", &buf[0]); 38 fprintf(stderr, "main: &buf[999] = %p\n", &buf[999]); 39 40 func(); 41 42 return 0; 43}

実行結果

hello = 0x1004010ff func = 0x100401131 main = 0x1004011de main: &buf[0] = 0xffffbc80 main: &buf[999] = 0xffffcc1c 0xffffbc00: buf[0] = 0x0 0xffffbc08: buf[1] = 0x0 0xffffbc10: buf[2] = 0x0 0xffffbc18: buf[3] = 0x0 0xffffbc20: buf[4] = 0x0 0xffffbc28: buf[5] = 0x0 0xffffbc30: buf[6] = 0x0 0xffffbc38: buf[7] = 0x0 0xffffbc40: buf[8] = 0x0 0xffffbc48: buf[9] = 0x0 0xffffbc50: buf[10] = 0xffffbce0 0xffffbc58: buf[11] = 0x1004012e4 0xffffbc60: buf[12] = 0x0 0xffffbc68: buf[13] = 0x0 0xffffbc70: buf[14] = 0xffffcc1c 0xffffbc78: buf[15] = 0x18015f160 0xffffbc80: buf[16] = 0x100000000 0xffffbc88: buf[17] = 0x300000002 0xffffbc90: buf[18] = 0x500000004 ... 0xffffbf10: buf[98] = 0xa5000000a4 0xffffbf18: buf[99] = 0xa7000000a6 hello! hello! hello! ... hello!

これから分かることは、
func の buf[16] が main の {buf[1],buf[0]}
func の buf[17] が main の {buf[3],buf[2]}
func の buf[99] が main の {buf[167],buf[166]}

ポインタは 8バイトで、int は 4バイトなので、このようになります。

実行ファイルを逆アセンブルすると、
buf[10] が main のフレームポインタ、
buf[11] が main で func を呼び出したときの戻り番地だと分かります。
buf[12]~buf[15] はスタックフレームのパディングだと思います。

func で 10個しかない buf に 100個の hello を書き込むと
スタック上にある戻り番地や main の buf[167] までをすべて壊してしまいます。

func の実行が終了して、main に戻ろうとするとき、戻り番地が hello に
変わっているので、そこへジャンプします。
そして、"hello!\n" を表示します。

hello の実行が終了して、呼び出し元に戻ろうとするとき、スタックにあるのは
hello のアドレスです。それを pop してプログラムカウンタに入れるので
また hello にジャンプします。
こうして、90回近い hello の実行をして、最後はどこか分からないところへ
飛んでいき実行が終了するようです。

投稿2020/05/09 05:28

kazuma-s

総合スコア8224

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

0

int buf[1000];
buf[999] = 10;
の所がなにをしているのか

配列に値を入れているだけです.

バッファオーバーフローによるリターンアドレスの上書き

という件とは関係ありません.

投稿2020/05/08 04:33

jimbe

総合スコア12545

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

あなたの回答

tips

太字

斜体

打ち消し線

見出し

引用テキストの挿入

コードの挿入

リンクの挿入

リストの挿入

番号リストの挿入

表の挿入

水平線の挿入

プレビュー

15分調べてもわからないことは
teratailで質問しよう!

ただいまの回答率
85.50%

質問をまとめることで
思考を整理して素早く解決

テンプレート機能で
簡単に質問をまとめる

質問する

関連した質問