文字列操作は「C言語最大の欠点」か? 209
適材適所 部門より
あるAnonymous Coward 曰く、
ITProのコラム「記者の目」は複数の日経BPの雑誌記者が記事にはしにくい個人的な意見などを書いている人気コラムだが、最近「C言語最大の欠点」というタイトルのコラムが掲載された。
どうやらタレこみ子と同世代らしい記者は、C言語最大の欠点を文字列処理であると断じ、特にバッファーオーバーフローの回避のためのコードを書くのが大変なところと論じている。そして、C++でSTLのstringクラスを使うべし、と主張している。
タレこみ子の乏しい経験からいうと、バッファーオーバーフローの問題は確かに大きな問題だが、それは「文字列」処理で起きるというよりは、ネットワークから受信する「データ列」の処理で起きるケースのほうが多いように思う。STLについては詳しくないのだが、stringクラスは'\0'も含みうるデータ列を正しく処理できるのだろうか。
また、バッファーオーバーフローは問題として理解しやすく、コード中で対処しなければいけない場所も比較的容易に特定できるのに対し、整数オーバーフローの問題は、問題として理解が難しく、対処しなくてはいけない場所も格段に多いという点でより大きな欠点ではないかと感じている。
バッファーオーバーフローや整数オーバーフロー以外にもC言語には様々な欠点があると思うが、/.jp諸兄は何が「最大の欠点」であるとお考えであろうか。ご意見をお聞かせ願いたい。
また、あるAnonymous Coward 曰く、
日経ITproに「C言語の最大の欠点」なる記事が掲載されている。記事では、gets()を用いて入力文字列を受け取る例を挙げ、「C言語の最大の欠点は文字列の扱いが非常に面倒なこと」と主張している。そして、解決策として「C++」を使えとと述べ、C++のストリームとSTLのstringクラスを用い、同様の処理を簡単に記述できると例示している。
そのほか、「動的な配列として使えるvector」などをC++のメリットとして述べているのだが、これらはC++のメリットではなく、ライブラリのメリットのような気がするのだが……。
STLやストリームといった有用な標準ライブラリが用意されている、という点は確かにC++のメリットではあるが、Cでも同様のライブラリがないわけではない。これを持って「C言語には欠点がある」と述べてしまうと大きなフレームの元になってしまう気がしてならない。
Cは抽象度の低い言語 組み込み系の立場から (スコア:5, すばらしい洞察)
組み込みに代表されるようなハードウェアに根ざした用途(リアルタイムとかね)には向きません。
裏でガベージコレクションが動くとか、問題外。
そんなことやっている間にロケット落っこちまうよ。
オシロスコープで実行時間計りながら開発してるんですから。
用意されているライブラリが気に入らなければ使わなければ良いのです。
たとえば、実際、最適化されたライブラリのコードでは、ブロック転送中などに割り込みが入らくなって
しまう(あるプロセッサの例ですが、ライブラリ自体の実効効率は良い)のを回避する目的などで
ライブラリ関数に相当する機能を自作するなど茶飯事です。
ライブラリの機能制限してコンパクト化するとかね。(だから大抵の組み込み用コンパイラには
ライブラリソースが付いてくる。逆についてこないと怖くて使えない)
Cは、システム全体をコントロールできるという意味で余計な保護など要らないのです。
(保護が必要なら、その部分までプログラマの責任で作る)
nullポインタチェックなんかは「デバグの時には便利」ですが。
ここからは、当該の相手に対して---
OSや処理系(こっそり仕込まれる膨大なランタイムライブラリのサポートかあるいはJIT等の実行環境)
におまかせらくちん!なプログラマが最近増えてますよね。
だから、おまかせらくちん!なプログラマが、無意味にCに文句を言うのって、すごく
筋違いのような気がします。
黙ってインタープリタを使ってればいいのに。
#もちろん、お任せらくちんでもすごいアプリを作ることはできるし、
#要は適材適所。
最大の欠点 (スコア:4, 興味深い)
この記事の筆者は、20年以上Cでプログラムをしている様に書いているが、そのキャリアでも、この程度の理解しかしていない人間が生まれること(そして、その上、こんな文章をかいて恥を晒してしまうこと)が、最大の欠点では無いだろうか。
まあ、「アセンブラレベルの記述ができる」ということは「アセンブラレベルのことを理解できる人間でしか使いこなせない」ということで、それは、そう簡単な事ではないのは自明の話ですな。
Re:最大の欠点 (スコア:1)
元記事を流し読みしましたが,この記事の筆者は
Borland C++の大箱を買って帰った。
から20年近く経過した,とは書いていますが,「(その後継続して)Cでプログラムを作っていた」とは書いていません。
Re:最大の欠点 (スコア:2)
いえいえ、「Cの『最大の欠点』」を銭をとった文章として世に晒すのらら、コンパイラが何をしているのかは知らないといけないでしょう。
だからC言語はマシン語だと… (スコア:3, すばらしい洞察)
Cはちょっと抽象度の高いマシン語だと教わったし、
その理解は間違いではないと思う。
欠点つーか、まさにそれが利点なんじゃねーの?
#だから日本では、「使いこなせないのは修行が足りないからでは?」と考える。
Re:だからC言語はマシン語だと… (スコア:2, 興味深い)
私もCは構造化アセンブラだと割り切って使うので問題なし。
で、C++なら、より安全な文字列も使えるし、やろうと思えば、ポインタを使ったトリックも使えるので、自由度が大きいです。
#PHPで文字列操作をしようとしたときに悶絶したのは、修行が足りないからでしょうか?
本音と建て前を分離しようとがんばってるところ (スコア:1)
もう、おおむねよく使われるコンピュータのアーキテクチャも固まってきたんだし、各アーキテクチャ向けに、アーキテクチャ依存しまくりの「ぶっちゃけこうなってますC」を策定しちゃえば良いと思います。x86-Cとかamd64-Cとか。
例えば、ヌルポインターってなんなのか? [kouno.jp]の説明に過剰な慎重さが求められたりとかするのは、かなり無駄じゃないかと。もう「内部表現が『0x00000000』ポインタ」と決めてしまえば説明がすごく短くなるのに。
他にも、「i=i++」はコンパイルエラー、とか「'\0'は0x00だ」とか、「実装はそうなってるけど、それはCの仕様ではなく、実装依存でたまたまそうなっているだけで・・・」という回りくどい説明を切って捨てられるように。
Re:本音と建て前を分離しようとがんばってるところ (スコア:1, 参考になる)
とりあえずC99では除算演算子による負数の除算の動作が規定されて [seclan.dll.jp]、一歩前進。その代わり標準ライブラリにdiv関数が何のために存在するのかよくわからなくなりましたが。
Re:本音と建て前を分離しようとがんばってるところ (スコア:1)
>これ、コンパイル時に検出できないパターンがあるんですよ。どうしますか。
・・・なるほど。じゃあ、何が起こるのかを固定すべく、演算が実行される順番を厳密に定める方向で。
次はメモリ管理へ (スコア:3, すばらしい洞察)
malloc したら free しなきゃいけないのが C 言語最大(!?)の欠点だ!
という評論が載りそうな気がします。
#バッファーオーバーフローが気になるなら gets じゃなくて fgets を使えばいいような?
Re:次はメモリ管理へ (スコア:2)
Re:次はメモリ管理へ (スコア:3, すばらしい洞察)
> assert(a = malloc(...)); みたいな使い方はそこそこ便利だ。
assertの用途を考えると不適切に思えるのだが、
私がおかしいのか?
コレで行こう (スコア:3, 興味深い)
STR07-C. TR 24731 を使用し、文字列操作を行う既存のコードの脅威を緩和する [jpcert.or.jp]
Re:コレで行こう (スコア:1)
Re:コレで行こう (スコア:1)
おお、詳しく教えていただけませんか?
Re:コレで行こう (スコア:3, 興味深い)
これですか?
うーん、そこまで徹底するとなると、文字列にメモリをアロケートするライブラリを使うか、そうなると大掛かりになって来るので、本当にCが必要なのか?を問う場面ですね。
そこまで含めての言語仕様なら (スコア:3, すばらしい洞察)
STL は C++ の言語仕様に含まれているライブラリーですから、そこまで含めての "C++ のメリット" というのは何ら問題ないように思います。
Boost が使えるよ、であっても C++ のメリットとして挙げてもいいと思います。そこから「だから Python も使えるよ」とか言われると微妙な気分になれますが。
個人的には「動的配列として使える vector」はメモリイメージ的に C と互換になる必要があるため、伸ばそうとした際に直後のメモリー領域が利用されている場合、再配置 + コピーが発生するなどの点を理解していないと残念な結果が待っていそうで怖いのですが。
その辺りを意識せずに使うとパフォーマンス的にも色々残念な事になりやすいし (サイズ予約なしに追加しまくり→自動拡張呼ばれまくりのコスト)、うっかり vector にクラスのインスタンスとかをそのまま突っ込んでいて、コピー時にデストラクタが呼ばれてリソースの解放が行われて事故るとか。
その意味でも、ある程度利用する予定の領域を先に確保しておくなどの点は重要になるのですが、なんというかそこまでの説明はなさそうかなぁ……日経だし。
BOF云々のあたりの主張の是非はさておき (スコア:3, すばらしい洞察)
多くの場面においてC++のstringを使うべきだという点に関してはC++とくにSTL信者としては同意できる。
BOFの件はCの文字列操作の貧弱性の例としてあまり適切ではないが、実際問題上、STLの使える環境下で、
速度をはじめとするC++に対する数々の偏見を根拠に、C的な手法のみで文字列操作を行なおうとして、
結果として車輪の再開発的になってしまったり、また各種バグの火種を作っている例は少なくないと思う。
また、速度を考えなければ、iostreamのほうがコードのきれいさの観点から言っても良い場面は多いだろう。
それとあと、STLやBoostは一様にC++の表現力に大きく下支えされた物で、ただ単に「ライブラリのせいだ、Cだって違やしない」という主張はどうかと思う。
STLやBoostには、広く普及している事に伴う資源だってあるし容易にCで置換できる物ではない。
ただ、元のBPの記事を書いた人間がそこまで考えて書いたとは思えないが。
しかし、元の記者ほど酷い技術力じゃなくても、中・上級ぐらいの人で偏見のせいでC++が嫌いな人は多いし、
このスラドの記事と付随するコメント含めて随所にその形跡が見てとれるように思う。
#関係ないが、C/C++と書く奴は一様に信用できない。CとC++はモダンなPerlと古典的Perlの差以上に違う物だ。
#一緒くたにしている連中が偉そうにのたまう事は必ずCの事と相場が決まっている。
C言語の前提を忘れているのでは? (スコア:3, すばらしい洞察)
従って、
・コンパイラはプログラマの記述をチェックすべきではない。
・コンパイラは単純かつ小さく保つべし。(余計な事をしてコンパイルを遅くするな)
また、アプリケーションも、
・小さく単純に作れ。
・動作速度を優先せよ。移植性は犠牲にしてよい。(速さは七難隠す)
この思想についてこれない人はC言語を使うべきではないと思います。
# わたくしには厳しすぎるのでlintなどのツールを使いまくりですけど……
notice : I ignore an anonymous contribution.
欠点 (スコア:2, おもしろおかしい)
人前で話題にするのが恥ずかしい。
#「はじめてのC」とか…
Re:欠点 (スコア:1, 参考になる)
Cの生い立ち (スコア:2, すばらしい洞察)
そもそもそういうのを一々自分でやらなきゃいけない=全部自分でできる、というのがCの売りじゃないの?
Re:Cの生い立ち (スコア:3, 興味深い)
# ところでprintfってCの標準関数の中で不自然に多機能でオーパーツ感があるけど、どういう歴史があるんだろう。
Re:Cの生い立ち (スコア:2, 参考になる)
>だとしたらprintfは高級すぎる気がします。
あれはfotranやcobolからの乗り換えを考えて(or対抗して?)、
書式付き出力として付けられたものではないかな?
fortranの場合、書式付き入力がちょっと弱そうなのだけど、
高級すぎると思うのは、scanfの方ではないかと思う。
Re:Cの生い立ち (スコア:2, 参考になる)
私のcppファイルの先頭数行テンプレ
個人的に使う簡単なフィルタだったり
週末に数値計算やってみた、的なものがほとんどです。
----テンプレここから
#include <stdio.h> // printfの為だけ。しかし絶対必要
#include <string.h> // memcpyとかmemset使うかもしれないし
#include <string> // 文字列操作はもちろんstring。最後に伝家の宝刀c_strでprintfに渡す!
#include <vector> // stack,mapなんかも使いますね
#include <algorithm> // 使わなくても書いておかないと日本人には覚えられないスペル
#include <iostream> // coutなんて使いませんけど
#include <fstream> // getlineは使いますよ、と
using namespace std;
----ここまで
cppファイルなのにclassなんて滅多に書かないです。
printfは便利すぎてC++方式に移行できないです。
「%08X」をC++流に書こうとしてキレましたw
自分で組んで自分で使うプログラムだったら
こんな風にCでSTLのみ利用するのが便利で楽ちんで気に入ってます。
もちろんネット関連やチーム作業ではまずいかと思いますが。
#倉庫番ソルバーを最終目標JavaScriptで、って組んでみたら
#Cでもかなり厳しかったでござる、とほほぉ
それは使い方による。 (スコア:2, 参考になる)
それは使い方による。
結果:
検索語として不適切 (スコア:2, すばらしい洞察)
Internet 普及前の言語だから仕方ないとはいえ、検索語として "C" は不適切なのが悩みの種です。
"C++" までは時代的に仕方なかったと思いますが、これだけ Internet が普及してから登場した
"C#" とか "GO" なんかは命名した奴を小一時間問いつめたいところです。
ここまでで (スコア:2)
標準入力からgetsしている時点で素人 (スコア:2)
これはC++に限った話ではない。PHPやRubyにしても、OSから1文字ずつ受け取るか、固定長バッファに一度転送してから入力値チェックするものでしょ。
後方互換性 (スコア:1, 参考になる)
C言語やバッファオーバーフロー時や整数オーバーフロー時の動作を未定義にせざるを得ないのは後方互換性(とスピード)のため。
ちなみに未定義ということは当然エラーチェックしても何らC言語の仕様には違反しないので、Fail-Safe C [aist.go.jp]のような実装も可能。
あれ! if ( n = 1 ) { じゃないの? (スコア:1)
気が付かないで代入する件だとばっかり思っていました。
Re:あれ! if ( n = 1 ) { じゃないの? (スコア:1, 参考になる)
悪いことは言わないから、コンパイラの警告は最大に、警告内容はきちんと理解することを勧める。
Re:あれ! if ( n = 1 ) { じゃないの? (スコア:1)
ライティングソリッドコード [amazon.co.jp](カタカナだと何がなんやら。必殺技か?)に書いてあったと記憶してますが、
if (1==n) {} で解決。
両辺が変数の場合だけ気をつければ良いので、習得すると if のストレスが減る。
シュキーボード (スコア:2, おもしろおかしい)
= キーに連射装置をつけるのが正解
Re:あれ! if ( n = 1 ) { じゃないの? (スコア:1, 参考になる)
Appendix C 解答から引用
ただ、古い(役立たない)コンパイラの場合はLintで検出できるなら、#1845662さんの言うようにLintを使うに一票。
分岐とデータのスタックを分離していないこと (スコア:1, 興味深い)
auto変数の配列を(リターンアドレス等に使う)メインのスタックではなく、別のスタックに置けば
かなりの改善を得られると思うんです。
別のスタックに対する操作はコンパイラが生成したコードでやるわけですから透過的に行えるし、
そのオーバーヘッドは、いちいちヒープを使うよりも遥かに少なくそしてメモリリークのバグをやらかしにくい。
まぁリターンアドレスを細工されることは防げても、他のデータにはみ出して上書きして特権的なフラグを操作する・・・なんてことには対処できませんが。
Re:分岐とデータのスタックを分離していないこと (スコア:2, すばらしい洞察)
「スタックは上のアドレスから使われる」のが諸悪の根源。
下から使ってりゃオーバーフローしても被害はその関数の自動変数で収まる。
Re:分岐とデータのスタックを分離していないこと (スコア:3, 参考になる)
スタックがどちらに成長するかは処理系(とアーキテクチャ)依存です。とあるフリーのOSのライブラリを読んでるときに「MIPSでは下から上に成長するよ!」と書いてありました。上から下に向って成長しなければならない必然性はもはやありません。
でも、いざ切り替えるとなると、いろいろうまく動かなくなるところがでてきそうですね。
でも、切り替えたところでどのくらいメリットがあるでしょうか? overflowの攻撃を防ぐだけなら実質address space layout randomizationだけで100%防げているような感覚はあります。昨今バッファオーバーフローで攻撃が成立するのは明示的にaddress space layout randomizationをオフにしたプロセスだけではないでしょうか。
Re:分岐とデータのスタックを分離していないこと (スコア:2)
アイデアとしては面白いですが、その場合retaddrとデータ部はどこに置くことになりますか?
現状、retaddrはpushすることにより、autoの変数はスタックポインタを減ずることで実現しているわけですが、現状のCPUのSPが1つである以上、retaddrとauto変数を別の場所に置くための仕掛けが必要となります。
この場合の実装として考えられるのは、1. 2つのSPコンテキストを維持してリターン直前にretaddr用のaddrをSPにロードする 2. auto 変数をヒープを用いるよう変更してしまう という方法あたりになるかと思いますが、1ならコンテキスト(退避したレジスタの内容)をどこに配置するのか(結局固定した場所に配置してたらセキュリティ上の優位性は無いのではないか?)とか、2なら決定的に速度が低下しそうという懸念があります。
親スレッドでは「いちいちヒープを使うよりも」と書いてあるので、1ということになりますかね。
今思い付いたアイデアですが、いっそ、SPを使わないコード(全部inline展開)を生成するようにすればいいのでは? そうして出来上がったものがCと言えるかどうかはすごく微妙ですが。(苦笑)
どんなものを想定されているのか、是非ここでお話いただければ刺激になります。
二重解放 (スコア:1)
最近は安全な文字列操作関数ライブラリ使ったり、コンパイル時に書式文字列と引数の不整合を指摘してくれるコンパイラ使うとか手段があるので、タレコミ後半の通りCそのものの問題ということでもないだろうけど。
CVEの脆弱性リスト眺めてると、二重解放の脆弱性が頻度も多い割りに中々気づけない気がする。何か良い対処方法はありますか?
Re:二重解放 (スコア:1)
そう拡張すると、#1845563さんの指摘のように、解放済みポインタの不正な参照という問題が出てきますね。
文字列操作じゃない (スコア:1)
最大の欠点かどうかはさておき、問題にしている部分は正確にはC言語の配列操作、
あるいはポインタ操作じゃないの?
ポインタが何でもできるから危険なのはたしかだけどね、それを無くしたらCじゃない。
Re:記事ではない (スコア:4, すばらしい洞察)
でもこの記事を読んだらこの本は読みたくなくなるという。
Re:論理ローテートがない (スコア:1)
signed→算術
unsigned→論理
だが、確かにローテートがないのはとても困る時があるしCらしくもない。
Re:バッドノウハウ多すぎ (スコア:2)
"非スレッドセーフな関数も使いどころは殆どない"というのは同意できない。
理由は、メジャーなUNIX用のソフトウェアは、移植のためスレッドを使わないという選択ができるようになってるため。
例えば、Apacheもスレッドも使えるけど、オプション指定すれば、pselect()なりfork()なりだけで処理することもできる。
そもそもシステム自体にスレッドライブラリは組込まない場合も多い。
Re:バッドノウハウ多すぎ (スコア:1, 興味深い)
未だに、そんな幻想をお持ちの方が、いるのですね。
勤務先と取引がある大手IT企業には、RHELの新版が出るたびに、libcのソースを解析して、マルチスレッドセーフや、シグナルセーフな関数の状態に、変更がないか確認している担当者がいたりします。下手にセキュリティ修正されると、マルチスレッドセーフだったものが、そうでなくなったりする例も過去にありましたので、仕方ないでしょう。
はっきりいって、ソースが公開されているから、癖や落とし穴がわかるというだけで、Linuxのライブラリの品質は、決してよくないです。
Re:Text-C使い集まれ~! (スコア:2)
txInsertf(text,"%s %d", s, 30);
# WZ4使ってます……
notice : I ignore an anonymous contribution.
Re:C++の利用はセキュリティ上重大な懸念を残すことになる (スコア:2)
あなた、CとC++のこと分ってないでしょ?
C++に詳しくない私がちょっと調べた限りでも、stringクラスで処理可能な最大文字列長はstring::max_size()で返されるサイズに制限されることくらいわかる。つまりは、Cで適当な固定長のバッファを確保して処理するのと同じということ。
ちなみに、昨今の標準的なOSでは、malloc()でどれだけ大きいサイズのメモリを確保しても、実際にメモリを活用する段階にくるまでは、実際の物理メモリは消費されない。
そんなに制限されるのが嫌なら、常に4GBのバッファを確保して文字列処理するようにしてみてはどうだろう?
もっとも、この場合、Memory over commit するようにシステムを構成する必要があるが。
限界を意識せず、プログラムをC++で組んだところで、いざ過大な入力があったときはExceptionが飛んでプログラムが不正に終了するにすぎないのですよ、バッファオーバーフローになるよりいいかもしれないけど。;-)
Re:C++の利用はセキュリティ上重大な懸念を残すことになる (スコア:2)
だよね。
固定長の処理になっているかもしれないし、そうではないかもしれない。
1024バイトぐらいなら固定長にしてくれたほうが都合がいいけど、実際の実装では、常に大容量(例えば512MB)ものメモリを確保するというのはできない。
となると、一定量の入力があった場合、リンクリストでつないでいくか、新たにもっと広い領域を作ってコピーするか、(こういう操作ができるOSはないだろうけど)PTEを操作して仮想メモリをつなぎあわせて1つの領域にしちゃうようなAPIを使うかということになるのだけど、C++のライブラリの中で実際にどんな操作が行なわれるのか仕様が定まっていない以上、これらの処理量が予測できないというのがプログラマからみると問題。
C++でなにも考えなくてもいい入力量のオーダーなら、Cである程度固定長の領域を取って処理したほうがいいし、それを越えるようならば、C++の汎用ライブラリで処理することはできず、結局自分で再設計することになるのでしょう。
だから、僕の感覚の中では、C++はやっぱり、いらない感じがするのですよね。
Re:全メモリ空間を自プロセスが持つ配列であるかのようにさわれること (スコア:2)
そういう思想自体はもう何年も前からあって、80年代にもLISP専用実行機(LISPを高速に実行できるCPUアーキテクチャを搭載している)でGCをハードウェア実装しようという話はあった。90年代にはJavaOSやJVM, JITといった技術開発が進む中で、GCをハードウェアに取り込めるチャンスは何度もあったのだ。しかし、現実に、ハードウェアによるGC実装が普及したことなど一度も無い。
結局、最先端テクノロジの下では、最高速をたたき出すことが求められる。そのためには、Cのようなハードウェアの性能を最大限に(細かに)活用できるアーキテクチャと言語を使わなくてはならないのであり、non-CとGC実装のハードウェアというのは、最先端の技術開発を阻害する要因でしかない。
当分の間その傾向がかわることはないのではないか。