LLVM Project Blog: What Every C Programmer Should Know About Undefined Behavior #3/3
Saturday, May 21, 2011
What Every C Programmer Should Know About Undefined Behavior #3/3
すべての C プログラマーが未定義動作について知っておくべきこと (3/3)
In Part 1 of the series, took a look at undefined behavior in C and showed some of the
cases that it allows C to be more performance than "safe" languages. In Part 2,
we looked at the surprising bugs this causes and some widely held misconceptions
that many programmers have about C. In this article, we look at the challenges that
compilers face in providing warnings about these gotchas, and talk about some of the
features and tools that LLVM and Clang provide to help get the performance wins while
taking away some of the surprise.
本シリーズの第一話では C の未定義動作とそれが C を“安全な”言語よりも性能で上回らせている
要因となっていることを幾つかの例を挙げて見てきました。第二話では、未定義動作が引き起こす
surprising bugs と多くのプログラマーが広範囲に C に対して抱いている誤解 (misconceptions) の
いくつかに注目しました。今回は警告を発するためにコンパイラーが直面しているそういった
gotchas に対する challenges に注目し、さらに some of the surpise を taking away しながら
performace wins を勝ち取るのを help するために LLVM と Clang が提供している幾つかの機能と
ツールについて述べます。
Why can't you warn when optimizing based on undefined behavior?
なぜ未定義動作に基づき最適化をするときに警告できないのか?
People often ask why the compiler doesn't produce warnings when it is taking advantage
of undefined behavior to do an optimization, since any such case might actually be a
bug in the user code. The challenges with this approach are that it is 1) likely to
generate far too many warnings to be useful - because these optimizations kick in all
the time when there is no bug, 2) it is really tricky to generate these warnings only
when people want them, and 3) we have no good way to express (to the user) how a
series of optimizations combined to expose the opportunity being optimized. Lets take
each of these in turn:
ある最適化を行うために未定義動作のアドバンテージを使うとき、それは実際にはユーザーコードに
存在しているバグである可能性があるのだから、なぜコンピューターは警告をしないのだと訊ねる人
がたくさんいます。このアプローチを使った challenges は以下のようなものです
1) 警告を数多く発するのは useful からは程遠い
- なぜならこれらの最適化はバグが存在していないときに kick in するからであり、また、
2) ユーザーが必要とするときにだけ警告を生成するのはとてもトリッキーである。そして
3) expose the opportunity being optimized のために一連の最適化がどのように combine されるのかを
(ユーザーに対して) 説明する良い手段がない。
Lets take each of these in turn:
It is "really hard" to make it actually useful
これを実際に useful にするのは "really hard" です。
Lets look at an example: even though invalid type casting bugs are frequently exposed
by type based alias analysis, it would not be useful to produce a warning that
"the optimizer is assuming that P and P[i] don't alias" when optimizing
"zero_array" (from Part #1 of our series).
例を挙げて見ていきましょう: 型ベースの別名解析 (type based alias analysis) による型キ
ャストバグが頻繁に発生していたとしても、(第一回にあった)「オプティマイザーが P と P[i]
が別名付けされていないと仮定している」"zero_array" を最適化するときにその
仮定に対する警告を行うのは有用ではないでしょう。
float *P;
void zero_array() {
int i;
for (i = 0; i < 10000; ++i)
P[i] = 0.0f;
}
Beyond this "false positive" problem, a logistical problem is that the
optimizer doesn't have enough information to generate a reasonable warning. First of
all, it is working on an already-abstract representation of the code (LLVM IR) which
is quite different from C, and second, the compiler is highly layered to the point
where the optimization trying to "hoist a load from P out of the loop"
doesn't know that TBAA was the analysis that resolved the pointer alias query. Yes,
this is "the compiler guy whining" part of the article :), but it really is
a hard problem.
この “偽陽性”問題以外の logistical 問題はオプティマイザーが reasonable な警告を発す
るのに十分な情報を持っていないというものですが、これは第一には、C とは大きく異なる
(LLVM IR の) コードとしてすでに抽象化された表現に対して処理を行うためです。
第二に、コンパイラーが “P からのロードをループの外に追い出す”最適化を試みるには、
pointer alias query を解決する解析だったことがわからないような
highly layered な場所にTBAAがあるからです。
確かにこれは "the compiler guy whining" part of the article :) ですが、
実際には困難な問題です。
# XXX
It is hard to generate these warnings only when people want them
ユーザーが必要とするときにだけこれらの警告を行うのは難しい
Clang implements numerous warnings for simple and obvious cases of undefined behavior,
such as out of range shifts like"x << 421". You might think that this
is a simple and obvious thing, but it turns out that this is hard, because people
don't want to get warnings about undefined behavior in dead code (see also the duplicates).
Clang は "x << 421" といった範囲外シフト (out of range shifts) のような
単純かつ明白なケースでの未定義動作に対する警告を数多く実装しています。あなたはこれを単
純かつ明白なことであると考えたかもしれませんが、それは難しいことが判っています。なぜな
ら、ユーザーは dead code に存在する未定義動作についての警告など受け取りたくはないから
です (see also the duplicates)。
This dead code can take several forms: a macro that expands out in a funny way when
when passed a constant, we've even had complaints that we warn in cases that would
require control flow analysis of switch statements to prove that cases are not
reachable. This is not helped by the fact that switch statements in C are not
necessarily properly structured.
この dead code は several forms を取る可能性があります: 定数が渡されたときに funny way
で expands out されるマクロで、到達不能なケースを prove するための switch 文の制御フロ
ー解析を要求するケースでわたしたちが警告したことについて complaints を受けたことすらあ
ります。C の switch 文は properly structured である必要がないということはこれに対する
助けにはなりません。
The solution to this in Clang is a growing infrastructure for handling "runtime
behavior" warnings, along with code to prune these out so that they are not
reported if we later find out that the block is unexecutable. This is something of an
arms race with programmers though, because there are always idioms that we don't
anticipate, and doing this sort of thing in the frontend means that it doesn't catch
every case people would want it to catch.
Clang におけるこの問題に対しての solution は、"runtime behavior" warnings を
ハンドリングするための infrastructure の growing です。それは prune out するためのコ
ードと関係しているので、そのブロックが実行されないもの (unexecutable)であることが後
になってわかった場合でも警告が行われません。わたしたちが anticipate していないイディ
オムは常にあるので、これは something of arms race with programmers though です。
そしてフロントエンドでこの種のことを行うことは、ユーザーが捕捉して欲しいと希望するで
あろうすべてのケースを捕捉はしないことを意味しています。
Explaining a series of optimizations that exposed an opportunity
opportunity を expose している最適化の series を explaining する
If the frontend has challenges producing good warnings, perhaps we can generate them
from the optimizer instead! The biggest problem with producing a useful warning here
is one of data tracking. A compiler optimizer includes dozens of optimization passes
that each change the code as it comes through to canonicalize it or (hopefully) make
it run faster. If the inliner decides to inline a function, this may expose other
opportunities for optimizing away an"X*2/2", for example.
フロントエンドが good warnings の producing に challenges していたら、たぶんその代わりに
オプティマイザーから warnings を generate できたでしょう! ここで useful warning を生成す
るに際しての最大の問題は data tracking のひとつです。あるコンパイラーのオプティマイザ
ーは一ダースを超える最適化パスを含んでいます。そして最適化パスのそれぞれが canonicalize
や(希望としては)高速に実行させるようにすることを行ってコードを変更します。もしある関数
をインライン化すると inliner が決めたならば、"X*2/2" を最適化するために他の機会
(other opprtunities) を expose するかもしれません。例をあげましょう:
While I've given relatively simple and self-contained examples to demonstrate these
optimizations, most of the cases where they kick in are in code coming from macro
instantiation, inlining, and other abstraction-elimination activities the compiler
performs. The reality is that humans don't commonly write such silly things directly.
For warnings, this means that in order to relay back the issue to the users code, the
warning would have to reconstruct exactly how the compiler got the intermediate code
it is working on. We'd need the ability to say something like:
これらの最適化を demonstarate するために相対的に単純かつ self-contained な例を出しましたが、
they kick in are in code な most of the cases はマクロの instantiation やインライン展開、
あるいはその他コンパイラーが行った abstraction-elimination activities から来るものです。
現実には人間はそういった silly things を直接書くことはないのが一般的です。
For warnings 、
これは、ユーザーのコードに relay back the issue するためには
その警告はコンパイラーがどのように working on な intermediate code を got したか
exactly に再構築しなければならないであろうことを意味しています。
# XXX
わたしたちには次のようなことを主張する能力が必要でしょう:
warning: after 3 levels of inlining (potentially across files with Link Time Optimization),
some common subexpression elimination, after hoisting this thing out of a loop and proving
that these 13 pointers don't alias, we found a case where you're doing something undefined.
This could either be because there is a bug in your code, or because you have macros and
inlining and the invalid code is dynamically unreachable but we can't prove that it is dead.
警告: 三段階のインライン化 (リンク時最適化によりファイルをまたがる可能性があります) 、
一部の共通部分式の削除 (common subexpression elimination) の後、これら 13個のポインターがエイ
リアシングされていないことの検出 と this thing の out of a loop への hoisting の後で、
わたしたちはあなたが未定義のなにかを行っている個所を見つけました。
これは、あなたのコードにバグがあったか、あるいはマクロかインライン展開されるものがあって
それが動的に到達不能な不当コード (invalid code) となったけれどもそれが dead code であること
をわたしたちが検出できなかったかのいずれかの可能性があります。
Unfortunately, we simply don't have the internal tracking infrastructure to produce
this, and even if we did, the compiler doesn't have a user interface good enough to
express this to the programmer.
残念ながらこれを生成するための内部的な tracking infrastrucure をわたしたちは持っていません。
仮にそれを持っていたとしても、コンパイラーがそれをプログラマーに伝えるための good enough な
ユーザーインターフェースを持っていません。
Ultimately, undefined behavior is valuable to the optimizer because it is saying
"this operation is invalid - you can assume it never happens". In a case
like "*P" this gives the optimizer the ability to reason that P cannot be
NULL. In a case like "*NULL" (say, after some constant propagation and
inlining), this allows the optimizer to know that the code must not be reachable. The
important wrinkle here is that, because it cannot solve the halting problem, the
compiler cannot know whether code is actually dead (as the C standard says it must be)
or whether it is a bug that was exposed after a (potentially long) series of
optimizations. Because there isn't a generally good way to distinguish the two, almost
all of the warnings produced would be false positives (noise).
結局のところ、未定義動作はオプティマイザーにとって価値あるもの (valuable) です。なぜな
らそれ (未定義動作) は "this operation is invalid - you can assume it never happens"
(この操作は不正であり、決して発生しないものと仮定してよい) であるとされているからです。
"*P" のようなケースでは、これは P が NULLにはなりえないと判断する ability をオプティ
マイザーに与えます。(一部のsome 定数伝播 (constant propagation) と インライン展開
(inlining) の後に生じるであろう)"*NULL" のようなケースでは、これはオプティマイ
ザーがそのコードが到達可能にはなりえないことを知るのを可能にします。ここでの重要な
wrinkle は (C standard says it must be 的に)コードが本当に死んでいるかどうかであるとか、
あるいは、(長い可能性もある) series of optimizations の後ではそれがバクかどうかをコン
パイラーが知ることはできないということです。なぜならこれら二つを区別する generally
good way は存在していないので、生成された警告のほとんどすべては false positives (ノイ
ズ) になってしまうだろうからです。
Clang's Approach to Handling Undefined Behavior
未定義動作の handling に対する Clang のアプローチ
Given the sorry state that we're in when it comes to undefined behavior, you might be
wondering what Clang and LLVM are doing to try to improve the situation. I mentioned a
couple of them already: the Clang Static Analyzer, Klee project, and the
-fcatch-undefined-behavior flag are useful tools for tracking down some classes of
these bugs. The problem is that these aren't as widely used as the compiler is, so
anything we can do directly in the compiler offers even higher goodness than doing it
in these other tools. Keep in mind though that the compiler is limited by not having
dynamic information and by being limited to what it can without burning lots of
compile time.
未定義動作へ come to しているときに the sorry state を与えて Clang と LLVM がその
状況を改善しようとしていることをあなたは不思議に思うかもしれません。そのうちの
いくつかには既に触れました: Clang Static Analyzer, Klee project, -fcatch-undefined-behavior
フラグといったものはこういったバグのクラスの一部を追跡するのに有用なツールです。
問題はこれらのツールがコンパイラーと同程度に広く使われているものではないので、
こういったほかのツールで行うこと以上のなにかを
わたしたちはコンパイラーで直接 offer できることです。
コンパイラーは動的情報を持たないように、また、
コンパイル時間を爆発的に増大させることなくできることに
制限されているということを心に留めておいてください。
Clang's first step to improve the world's code is to turn on a whole lot more warnings
by default than other compilers do. While some developers are disciplined and build
with "-Wall -Wextra" (for example), many people don't know about these flags
or don't bother to pass them. Turning more warnings on by default catches more bugs
more of the time. An aspect of this that is specific to uninitialized variables is
that Clang supports -Wuninitialized even when building at -O0. You may not be aware,
but it is only possible to enable this warning with GCC when building at -O1 or higher.
world's code を改良するための Clang の第一ステップは、他のコンパイラーが行っているより
もさらに多くの警告をデフォルトで有効にすることです。一部の developer たちは (たとえば)
"-Wall -Wextra" でビルドしていますが、多くの人はこれらのフラグを知りませんし、
don't bother to pass them です。デフォルトでもっと多くの警告を行うようにすることでより
多くのバグを捕捉できるようになります。
aspect of this のひとつである未初期化変数の特定は
-O0 でビルドするときにさえ -Wuninitialized を Clangがサポートすることです。
あなたは気にしていなかったかもしれませんが、この警告は GCC では -O1 以上の指定をしてビルド
をしたときにだけ有効にできるものです。
The second step is that Clang generates warnings for many classes of undefinedbehavior
(including dereference of null, oversized shifts, etc) that are obvious in the code to
catch some common mistakes. Some of the caveats are mentioned above, but these seem to
work well in practice.
第二ステップは (null の dereference や oversized shifts 等を含む) コードの中で明らかに捕捉
可能な some common mistakes な未定義動作の many classes に対して Clang が warnings を生
成するというものです。caveats のいくつかはすでに言及しましたが、それらは実践においてはう
まく動作するように見えるものなのです。
The third step is that the LLVM optimizer generally takes much less liberty with
undefined behavior than it could. Though the standard says that any instance of
undefined behavior has completely unbound effects on the program, this is not a
particularly useful or developer friendly behavior to take advantage of. Instead, the
LLVM optimizer handles these optimizations in a few different ways (the links describe
rules of LLVM IR, not C, sorry!):
第三ステップは、LLVM オプティマイザーが未定義動作に対しては本来可能なものよりも一般的
には低い自由度しか take しないというものです。標準では未定義動作のすべての instance は
プログラム上では完全に unbound な effects を持っているように書かれていますが、これは特
に有用なものではないし、take advantage of するような developer friendly な動作でもあり
ません。代わりに LLVM オプティマイザーは a few different ways でこれらの最適化を取り扱
います (the links describe rules of LLVM IR, not C, sorry!):
1. Some cases of undefined behavior are silently transformed into implicitly trapping
operations if there is a good way to do that. For example, with Clang, this C++ function:
未定義動作の一部のケースは、もし良い手段が存在しているのであれば silently に
implicitly trapping operations へと transform されます。
たとえば Clang を使っているとき、
int *foo(long x) {
return new int[x];
}
compiles into this X86-64 machine code:
この C++ 関数は次のような X86-64 機械語コードにコンパイルされます。
__Z3fool:
movl $4, %ecx
movq %rdi, %rax
mulq %rcx
movq $-1, %rdi # Set the size to -1 on overflow オーバーフローしたら -1 をセット
cmovnoq %rax, %rdi # Which causes 'new' to throw std::bad_alloc 'new' が std::bad_alloc を throw する
jmp __Znam
instead of the code GCC produces:
GCC では次のようなコードを生成します:
__Z3fool:
salq $2, %rdi
jmp __Znam # Security bug on overflow! オーバーフロー時にセキュリティバグ!
The difference here is that we've decided to invest a few cycles in preventing a
potentially serious integer overflow bug that can lead to buffer overflows and
exploits (operator new is typically fairly expensive, so the overhead is almost never
noticable). The GCC folks have been aware of this since at least 2005 but haven't
fixed this at the time of this writing.
ここでの違いは、バッファオーバーフローや exploit に繋がりかねない深刻な整数オーバー
フローバグの可能性を preventing するためにわたしたちが数サイクルを浪費 (invest) する
ことを決断した点にあります (new 演算子は typically fairly expensive なのでそのオーバー
ヘッドはほとんど noticable にはなりません)。GCC forks は少なくとも 2005年からこれを
認識していますが、この記事を書いている時点でもまだ修正されていません。
2. Arithmetic that operates on undefined values is considered to produce a undefined value
instead of producing undefined behavior. The distinction is that undefined values can't
format your hard drive or produce other undesirable effects. A useful refinement happens
in cases where the arithmetic would produce the same output bits given any possible
instance of the undefined value. For example, the optimizer assumes that the result of
"undef & 1" has zeros for its top bits, treating only the low bit as
undefined. This means that ((undef & 1) >> 1) is defined to be 0 in LLVM, not
undefined.
未定義値に対する算術的な操作は、未定義値に対する操作ではなく未定義値を生成する操作と見な
されます。その相違点は、未定義値はあなたのハードディスクをフォーマットすることはできない
し、他の undesiable な効果を作り出したりもしないということです。
任意に与えた未定義値のインスタンスに対して算術演算が同じ出力ビットを生成する場合には
useful refinement が起きます。
例えばオプティマイザーは "undef & 1" の結果が zeros for its top bits
であると仮定して、low bit のみを undefined として扱います。
これはつまり、LLVM では ((undef & 1) >> 1) は undefined ではなく 0 と
define されるということです。
3. Arithmetic that dynamically executes an undefined operation (such as a signed integer
overflow) generates a logical trap value which poisons any computation based on it, but
that does not destroy your entire program. This means that logic downstream from the
undefined operation may be affected, but that your entire program isn't destroyed. This
is why the optimizer ends up deleting code that operates on uninitialized variables, for
example.
(符号付き整数オーバーフローのような) 未定義操作を動的に実行する算術演算は
それに基づいた計算のすべてを poison する logical trap value を生成しますが、
プログラム全体が破壊されることはありません。
これはつまり、未定義動作からの logic downstream は影響を受ける可能性があるけれども、
プログラム全体が破壊されることはない。ということです。これが、例えば
最終的に未初期化変数に対する操作を行っているコードをオプティマイザーが削除してしまう
理由です。
4. Stores to null and calls through null pointers are turned into a __builtin_trap() call
(which turns into a trapping instruction like "ud2" on x86). These happen all
of the time in optimized code (as the result of other transformations like inlining and
constant propagation) and we used to just delete the blocks that contained them because
they were "obviously unreachable".
null への格納や null ポインターを通じた呼び出しは __builtin_trap() の呼び出しへと変換さ
れます (この呼び出しは x86 上の場合 "ud2" のようなトラップ命令となります)。
これは (インライン化や定数伝播のような他の変換の結果として) 最適化されたコードで起きますが、
わたしたちはそういったものを含むブロックを単に削除します。
なぜなら“unreachable なのが明白”であるからです。
While (from a pedantic language lawyer standpoint) this is strictly true, we quickly
learned that people do occasionally dereference null pointers, and having the code
execution just fall into the top of the next function makes it very difficult to
understand the problem. From the performance angle, the most important aspect of
exposing these is to squash downstream code. Because of this, clang turns these into a
runtime trap: if one of these is actually dynamically reached, the program stops
immediately and can be debugged. The drawback of doing this is that we slightly bloat
code by having these operations and having the conditions that control their predicates.
これは (pedantic な言語弁護士の視点からすると) strictly に真ですが、ときにユーザーが
null ポインターの dereference をすることや、次の関数の先頭に fall into して問題の理解を
非常に難しくしてしまうようなコードを実行してしまうことがあるのを我々は即座に学びました。
性能の観点からは、最も重要な aspect of exposing these は downstream code を squash
することです。このために clang はそれらを runtime trap へ turn into しました:
もし実際にこれらの一つに動的に到達したならば、プログラムは即座に停止してデバッグ可能と
なります。これを行うための drawkback は、
these operations とその predicates を制御する conditions とを持つことでコードを
大幅に bloat させてしまうことです。
5. The optimizer does go to some effort to "do the right thing" when it is
obvious what the programmer meant (such as code that does "*(int*)P" when P
is a pointer to float). This helps in many common cases, but you really don't want to
rely on this, and there are lots of examples that you might think are "obvious"
that aren't after a long series of transformations have been applied to your code.
オプティマイザーは "do the right thing" (正しく最適化を行う) ために
(such as code that does "*(int*)P" when P is a pointer to float)
プログラマーの意図が明確であるときに多少の努力をします。
これは多くの common case を救済しますが、実際はこれに頼ろうとは望まないでしょうし
a long series of transformations があなたのコードに適用されたあとで “明白である”と
あなたが考えるかもしれないたくさんの例があります
6. Optimizations that don't fall into any of these categories, such the zero_array and
set/call examples in Part #1 are optimized as described, silently, without any
indication to the user. We do this because we don't have anything useful to say, and
it is very uncommon for (buggy) real-world code to be broken by these optimizations.
上記のどのカテゴリにも当てはまらない、 Part #1 での例にあった zero_array や set/call
のような最適化は、すでに説明したように silently に、ユーザーに対する indication は
一切なしに行われます。言及すべき anything useful を持っていないし、これらの最適化で
壊れる (バギーな) 現実世界のコードにとってとても一般的なので、わたしたちはこの暗黙の
最適化を行います。
One major area of improvement we can make is with respect to trap insertion. I think
it would be interesting to add an (off-by-default) warning flag that would cause the
optimizer to warn whenever it generates a trap instruction. This would be extremely
noisy for some codebases, but could be useful for others. The first limiting factor
here is the infrastructure work to make the optimizer produce warnings: it doesn't
have useful source code location information unless debugging information is turned on
(but this could be fixed).
わたしたちが改良できた one major area は respect to trap insertion を伴っています。trap 命
令を生成したときはいつでもオプティマイザーに警告させる warning flag (デフォルトではオ
フ) を追加するのは intersting であろうとわたしは考えます。これは一部のコードベースに対
しては extermely noisy になるでしょうが、そのほかのものには有用かもしれません。最初の
limiting factor はオプティマイザーに警告を生成させるための infrastructure work です:
オプティマイザーは、デバッグ情報が有効にされない限り有用なソースコードの位置情報を持っ
ていません (ただしこれは修正できるでしょう)。
The other, more significant, limiting factor is that the warning wouldn't have any of
the "tracking"information to be able to explain that an operation is the
result of unrolling a loop three times and inlining it through four levels of function
calls. At best we'll be able to point out the file/line/column of the original
operation, which will be useful in the most trivial cases, but is likely to be
extremely confusing in other cases. In any event, this hasn't been a high priority for
us to implement because a) it isn't likely to give a good experience b) we won't be
able to turn it on by default, and c) is a lot of work to implement.
別のより significant な limiting facor は、ある操作がループを三回アンローリングしさら
に4レベルの関数呼び出しのインライン展開を行った結果であることを説明することが可能な“
tracking”情報を warning が 一切持っていないことです。at best で original operation の
ファイル/行/カラム の point out は可能でしょうが、それは大部分の trivial cases で
useful であろうけれども別のケースでは extremely に混乱するものとなるでしょう。
In any event,
これはわたしたちが実装する対象として high priority になることはありませんでした。
それは
a) good experience をもたらさないであろう
b) デフォルトで有効にはしたくない
c) 実装にとても手間が掛かる
といった理由からです。
Using a Safer Dialect of C (and other options)
安全な C の方言 (やその他のオプション) を使う
A final option you have if you don't care about "ultimate performance", is
to use various compiler flags to enable dialects of C that eliminate these undefined
behaviors. For example, using the -fwrapv flag eliminates undefined behavior that
results from signed integer overflow (however, note that it does not eliminate
possible integer overflow security vulnerabilities). The -fno-strict-aliasing flag
disables Type Based Alias Analysis, so you are free to ignore these type rules. If
there was demand, we could add a flag to Clang that implicitly zeros all local
variables, one that inserts an "and" operation before each shift with a
variable shift count, etc. Unfortunately, there is no tractable way to completely
eliminate undefined behavior from C without breaking the ABI and completely destroying
its performance. The other problem with this is that you're not writing C anymore,
you're writing a similar, but non-portable dialect of C.
"ultimate performance" (最終的な性能) について don't care であるときにあなたの
手にある最後の選択肢は、これらの未定義動作を除去する dialects of C を有効にするための様々
なコンパイラーフラグを使うことです。たとえば符号付き整数オーバーフローの結果による未定義
動作を削除するフラグである -fwrapv を使う (ただし整数オーバーフローによる security
vulnerabilities の可能性は削除しないことに注意すること) といったものがあります。
-fno-strict-aliasing フラグ は Type Based Alias Analysis を無効にしてしまうので、これ
らの型規則を自由に無視できます。仮に要求があったならば、Clang にすべてのローカル変数を
implicitly にゼロにしたりシフトカウントを変数で指定している各シフトの前に“and”演算を
挿入させるフラグを追加できたでしょう。残念ながら、ABI を壊したりパフォーマンスを完全に
破壊することなくC から未定義動作を完全に eliminate する tractable way はありません。そ
れに伴うもうひとつの問題は、あなたが書くそれはもはや C ではなく、C と似てはいても移植
性のない C の方言にすぎないということです。
If writing code in a non-portable dialect of C isn't your thing, then the -ftrapv and
-fcatch-undefined-behavior flags (along with the other tools mentioned before) can be
useful weapons in your arsenal to track down these sorts of bugs. Enabling them in
your debug builds can be a great way to find related bugs early. These flags can also
be useful in production code if you are building security critical applications. While
they provide no guarantee that they will find all bugs, they do find a useful subset
of bugs.
移植性のない C の方言でコードを書くのが your thing でないのなら、(すでに言及した他のツ
ールの他にも) -ftrapv や -fcatch-undefined-behavior といったフラグはこの種のバグを
track down するためのあなたの武器庫 (your arsenal)に収められた useful weapons になるか
もしれません。デバッグビルドでこれらのフラグを有効にすることは関連するバグを早期に発見
する great way になるでしょう。あなたが serurity critical なアプリケーションを構築して
いるのならproduction コードにおいてもこれらのフラグは useful かもしれません。また(そう
いったフラグは)、すべてのバグを見つけ出す保証はしませんが、バグの useful subset を見つ
け出してくれるでしょう。
Ultimately, the real problem here is that C just isn't a "safe" language and
that (despite its success and popularity) many people do not really understand how the
language works. In its decades of evolution prior to standardization in 1989, C
migrated from being a "low level systems programming language that was a tiny
layer above PDP assembly" to being a "low level systems programming language,
trying to provide decent performance by breaking many people's expectations". On
the one hand, these C "cheats" almost always work and code is generally more
performant because of it (and in some cases, much more performant). On the other hand,
the places where C cheats are often some of the most surprising to people and
typically strike at the worst possible time.
結論としては、ここでの本当の問題は C が単に“安全な”言語ではないということと、(その成
功と popularity にも関わらず) 多くの人がC という言語がどのように動作するのかを本当には
理解していないということです。1989 年の標準化に先立つ長年にわたる進化 (decades of
evolution) の過程で C は、「PDP のアセンブリ上にある tiny layer にすぎない低水準のプロ
グラミング言語」("low level systems programming language that was a tiny layer
above PDP assembly") から 「多くの人たちの expectaions を breaking することによっ
てまずまずの性能 (decent performance) を提供することを試みる低水準のシステムプログラミ
ング言語」("low level systems programming language, trying to provide decent
performance by breaking many people's expectations") へと migrated しました。
一面ではこういった C の“チート”はほぼ常にうまくいき、コードはチートによって一般的に
はより性能の良いものになります(一部のケースではかなり向上します)。その反面、
the places where C cheats は often some of the most surprising to people
であり、
typically strike at the worst possible time
でした。
C is much more than a portable assembler, sometimes in very surprising ways. I hope
this discussion helps explain some of the issues behind undefined behavior in C, at
least from a compiler implementer's viewpoint.
C は portable assembler をはるかに超え、とても驚くようなやり方で使われることがあります。
わたしはこの discussion が、少なくともコンパイラーの実装者視点からの
C の未定義動作の背後に隠れている issues のいくつかを説明するのを助けることを希望します。
-Chris Lattner
Posted by Chris Lattner at 12:48 AM
About The LLVM Blog
This blog is intended to be a news feed for information about the LLVM Compiler
Infrastructure and related subprojects.
Note that comments are disabled here, we prefer you to respond on mailing lists like
llvmdev or cfe-dev.
We welcome new contributors. If you'd like to write a post, please get in touch with Chris.