Hello, Worldに潜むバグ 67
ストーリー by nagazou
Hello,-World 部門より
Hello,-World 部門より
route127 曰く、
プログラミングの第一歩としてお馴染みの課題であるHello, Worldであるが、これをANSI-Cに基づいてmainの戻り値をEXIT_SUCCESSマクロで記述し、出力をENOSPCエラーを返す疑似デバイスファイルである/dev/nullへリダイレクトさせるよう実行すると正常終了するというバグがあるようだ。
(https://blog.sunfishcode.online/bugs-in-hello-world/)
C言語以外でもJava、Haskell、Node.js、Ruby、およびPython 2では同様の動作であるが、Python 3、Perl、およびBashでは正しくエラーとなるとのことである。
Linuxでは/dev/fullの存在はお馴染みとなっているがBSD系では2014年にFreeBSD 11.0-CURRENTが、2018年にNetBSD 8が/dev/fullを追加しているようだ。
こうしたANSI-Cに比べれば追加されて日が浅い機能である/dev/fullがバグを顕在化させた側面もあるのだろうか?
豊富な実務経験を持つスラド諸兄から本件の「バグ」についてご意見を頂戴したいところである。
/dev/full 関係ないじゃん。 (スコア:5, すばらしい洞察)
・C言語の高水準入出力 printf(3) とか fputs(3) で出力した内容はキャッシュされるので fclose(3) とか fflush(3) とかで吐き出して戻り値を確認しないと正常に出力されたことは保証されない。
・exit(3) なり main() からの return で暗黙にフラッシュした場合はエラーチェックできないので、失敗してもわからない。
というのは基本仕様なので知っとく必要がある。
/dev/full とかだけでなく、write(2)で発生するあらゆるエラーの可能性がある。
Re:/dev/full 関係ないじゃん。 (スコア:2, 興味深い)
C言語の仕様と離れるけど補足しておくと、書き込み先が NFS のような場合にはカーネルが遅延書き込みをするので fflush(3) が成功したとしてもまだ十分ではない。
あとから遅延してエラーが帰ってくるかもしれないので fclose(3) の戻り値を確認するまでは安心できない。
別件としてカーネル内にもページキャッシュがあるので、ディスクドライブへ書き込みされたことを保証したいなら fsync() なり sync() なりを検討する必要もある。
Re: (スコア:0)
> 別件としてカーネル内にもページキャッシュがあるので、ディスクドライブへ書き込みされたことを保証したいなら fsync() なり sync() なりを検討する必要もある。
つまり書き込み保証を移植性のある方法で行うことは不可能
Re:/dev/full 関係ないじゃん。 (スコア:2)
1回目のsyncシステムコールは、syncシステムコールは非同期処理なため、
バッファのフラッシュを要求するが、書き出し完了前に返ってくる。
2回目のsyncシステムコールは、1回目の処理が終わるまでブロックされるので、
返ってきた時には、1回目の要求によるバッファのフラッシュができたことが保証できる
3回目は何の意味も無いおまじない
Re:/dev/full 関係ないじゃん。 (スコア:1)
syncなのにasyncなのかややこしい
Re:/dev/full 関係ないじゃん。 (スコア:1)
それは大昔の sync() の実行が非同期だった頃の風習。(BSD 系だと今でも現役?)
Linux だと kernel 1.3.21 (1995年8月)以降は同期で終了を待つので1回で十分。
Re:/dev/full 関係ないじゃん。 (スコア:2, 興味深い)
ちなみに、coreutilsのechoは正しくハンドルしていたのでその実装を確認してみると、
atexit()で登録した関数close_stdout()とそこから呼ばれるclose_stream()にて、
fclose()他のstdout周りで起こったエラーがないかをチェックしている。
https://github.com/coreutils/gnulib/blob/master/lib/closeout.c#L117 [github.com]
Re: (スコア:0)
それはCの話なのかUnixの話なのかオーエス全般の話なのか…
ショリケイイゾンテクソダナ
Re:/dev/full 関係ないじゃん。 (スコア:1)
バッファキャッシュの問題はC言語の実装の話。正確には libc にある C言語の標準関数の実装の話。
Re: (スコア:0)
これC言語にフォーカスしているからおかしい(のとC言語もバグ扱いにしているのがおかしい)。
JavaとかNode.jsみたいに例外処理がある処理系でも発生しているのは確かに不思議。
Javaは調べてみると、PrintStreamはIOExceptionを投げない [oracle.com]となっているから、そもそもそういう仕様なのね。
# バグを仕様と言い張るのと逆で、仕様をバグと言い張っているのか
Re: (スコア:0)
Javaでもmainメソッドの外で暗黙に閉じられたstreamが仮に例外を投げるとしてもcatchのしようがないのでは?
Re: (スコア:0)
Javaも例外が拾われずに終了する場合は1(エラー)を返すので、問題はキャッシュしたstreamがflushされるタイミングですね。
普通に考えて処理最後にはflushされるはずなので、仮にstreamが例外をthrowするならエラーで終わる気がします。
C#はエラーになっているので、そのあたりは作りしだいですが。
Re:/dev/full 関係ないじゃん。 (スコア:3)
c# (net6)は挙動とソースをみると、Console.Writen系では毎回呼び出し毎にflushしているので、呼んだ時点で例外が起きる。
CはターミナルならLine Bufferingだが、リダイレクトされているとFull Bufferingになるのでfwriteした時点では気づかないとかの話になる。まあこれもパフォーマンスの話で気になるならsetlinebufなりsetvbuf(_IONBF)なり呼べば、というのもわかる話。
API設計の時代背景(かWindows文化的にリダイレクトをバッファリングするとデメリットがでるのでやらない)だろうね。
Re: (スコア:0)
>普通に考えて処理最後にはflushされるはず
これな、Streamクラスによっては仕様で担保されてなくて、まあほとんどはSun由来のJRE使うから問題ないんだけど、IBMの組み込み向けJ2MEでflushされないのをライブラリのソース読んで確認した。
大昔の話。
Re: (スコア:0)
PCみたいな富豪環境以外は終了処理をリソース食わずに素早く終わらせることを優先するのが普通だよ。Cまでいくと仕様上は振る舞いを説明するだけでなんの保証もなく、ただホストに投げると明記される程度だし。ioはホスト/プラットフォーム依存の塊だし。
本当の問題は富豪環境でもRAIIやファイナライザのコールバックがioのcloseの際のエラーを握りつぶすしかないことだよ。きれいにやりたいなら手動でやるしかない。
何がバグなのかわからない (スコア:4, すばらしい洞察)
putsの実行結果戻り値をハンドリングせずにプログラムを常に正常終了するようなコードなんだから、putsが成功しようが失敗しようが正常終了するのが当たり前に思えるんですけど。
Re:何がバグなのかわからない (スコア:2)
全く同意
「エラー処理してるつもりが効いてない」ならわかるけどエラー処理してないんだからなぁ
エラーになるんならバグだけど
Re:何がバグなのかわからない (スコア:1)
> putsの実行結果戻り値をハンドリングせずに
知ったかぶってるけど、この場合 puts() の戻り値をチェックするだけでは不足なんだよ。
やってみればすぐわかるけど、標準的な環境でファイルへの puts() は (メモリが足りてる限り) 必ず成功する。
書き込みエラーの確認をしたい場合には上に書かれているように、その後で fflush() とか fclose() して、
その戻り値を確認する必要がある。
Re: (スコア:0)
そこは本質じゃないと思うのですが。
問題は「必ず正常終了するプログラムを実行したら必ず正常終了する。どこがバグ?」というところなので。
# ま、正確には「puts()が失敗した時にプログラムを異常終了する処理なしに」と書くべきでしたね。
Re: (スコア:0)
元コメとは別人だけど、
>> putsの実行結果戻り値をハンドリングせずに
こんな風に無駄に間違った詳細を書いてる以上、ツッコまれるのは当然で、「そこは本質じゃない」はゴマカシに見えるかな。
Re: (スコア:0)
> 正確には「puts()が失敗した時にプログラムを異常終了する処理なしに」と書くべきでしたね。
やっぱりわかってないよね?
今回の /dev/full にリダイレクトする条件では puts("Hello World") は失敗しないので、「puts() が失敗した時にプログラムが異常終了する処理」を記述したとしても違いはないよ。
Re: (スコア:0)
そもそもputsが成功しているかすらチェックしていない、すなわち必ず書き込まれることを期待していないのだから
書き込みに失敗していようが正常終了だろって意味では
Re: (スコア:0)
いや、だから、今回の例では puts() は、確実に成功するので戻り値を確認する意味なんて元々ない。
書き込まれたか確認したいなら、その後で fclose(stdout) して、その戻り値を確認するべき。(それで必要十分)。
Re: (スコア:0)
それがputs()が失敗したかチェックするという事ではないのですか?
Re: (スコア:0)
「俺の思う最強のhelloworldの仕様を満たしてないから、この実装はバグってる!」って主張してるだけだよな。
Bashはバグってないとか言ってるけど、Cの実装に寄せて
function helloworld {
echo "Hello World"
return 0
}
hwlloworld > /dev/full
ってやれば、終了ステータスは0になるだろうし。
Re: (スコア:0)
Hello Worldの仕様書とかあるのかな。
Re: (スコア:0)
K&R第2版には「Cでは,hello, worldと印字するためのプログラムは,次のようになる。」として書いてある。
ということは、書き込みが行われない場合はプログラムの目的は果たせてないのでエラーなのかな。
誤字 (スコア:3, 参考になる)
/dev/full と /dev/null を間違えては意味不明になるよ・・・
Re:誤字 (スコア:2)
一番肝心なところで間違えてるからなぁ。
/dev/null ってそんなデバイスだったっけ?
って意味が分からなかった。
リンク先を読んでようやく誤字だと悟った。
屍体メモ [windy.cx]
Re: (スコア:0)
自分もはじめて元記事見たときに/dev/nullと見間違えてた。
そして元記事で/dev/fullの存在を知った。
Re: (スコア:0)
というか、nullとfullで挙動が正反対になるのはイケてない仕様かも。
Re: (スコア:0)
ぱっと見気づかない確率恐ろしいほど高い紛らわしい名前にするセンスの無さよ…
overflowとかじゃダメなのかい?
Re: (スコア:0)
nullとfullのどっちをoverflowが適切だと思ったの?
どちらもoverflowと関係のない機能を提供してると思うんだけど。
誤字と分かる誤字だけにして。 (スコア:0)
>出力をENOSPCエラーを返す疑似デバイスファイルである/dev/nullへリダイレクトさせるよう
誤字でいいんだよね。ね?マジで確かめさせて。
「/dev/nullにリダイレクトして~」を読んで意味不明で
リアル世界でSAN値と「いつトラック君 [blogspot.com]に出会ったっけ」を疑ったわ
Re: (スコア:0)
/dev/fool なんちて
/dev/futol (スコア:2)
そんなデバイス名があるとは知らなかった
うじゃうじゃ
Re:/dev/futol (スコア:1)
devはfutottelですよね。
Re:/dev/futol (スコア:1)
健康志向なのが /dev/zero です
Re: (スコア:0)
ダイエットをするもののリバウンドも激しい/dev/randomの事もたまには思い出してあげて下さい。
Re: (スコア:0)
僕の周囲はどんどん/dev/hageへ移行してますね
Hello WorldというよりC処理系の問題では? (スコア:0)
戻り値出力のときのエラーを正しく返すのはC処理系の役目であって、それが正しく返さないんだからHello Worldプログラムであろうがなんだろうがエラーをキャッチできなければどうしようもないよね
Re: (スコア:0)
処理系がmainの戻り値を無視して勝手に終了コード変えていいの?
Re:Hello WorldというよりC処理系の問題では? (スコア:4, 参考になる)
C11 5.1.2.2.3/1
C11 7.22.4.4/5
だから、規格適合の処理系はmainの戻り値が0かEXIT_SUCCESSなら勝手に終了ステータスを失敗にしてはならない。
Re: (スコア:0)
それは規格が結果的に間違ってるんじゃないかなぁ
処理系は規格に従って正しく実装されているかもしれないけど、現実は失敗してるのが事実なわけで…
Re:Hello WorldというよりC処理系の問題では? (スコア:2, 参考になる)
何も失敗してないだろ?
puts(): バッファーに書き込め。(条件によってはフラッシュせよ)
return: (前略)、バッファーをフラッシュし、ストリームをクローズし、(中略)、フラッシュやクローズなどの際に発生した下位エラーは無視して、指定した値でプログラムを終了しろ。
というプログラマからの指示を受けたので、その通りに動作している。完全に予想どおりの動作。
それ以外の指示していない動作をしたりしたら逆に困る。
ヘボなプログラマーが自分が何を指示したかもわからずに文句言ってるだけ。
「指示した通りに動くんじゃない。俺が心で思った通りに動け!!」
とか言い出す人はプログラマには向いていない。
つまり、こういうこと? (スコア:0)
バグってんのは、お前だ、でFA?
# 不具合はバグを包含するけどバグは不具合を包含しないと思う
Re: (スコア:0)
バグってるけど不具合を出していないのがあなたの言うお前のことです?
ていうか (スコア:0)
putsで書いたHello Worldは初めて見た
そんなのHello Worldじゃないよ
Re: (スコア:0)
最初に学ぶプログラムでputsを使うのは適切じゃないよね。
勝手に'\n'を最後に出力しちゃうから。
(printlnとかWriteLineみたいに分かりやすい名前ならともかく)
# fputsは出力しないのに、なんでputsだけ改行するんだろ。
Re: (スコア:0)
今は亡きgetsが勝手に改行を取っ払うのと対応してるんですよね。
まあgetsにはそれ以前に大問題があったわけですが、それを含めてfput/fget系とput/get系は出自とかが違うんですかね。