事の始まり
研究でプログラムを書いていたときの話。
ローカル(Mac OS)では問題なく動作していたプログラムが、サーバ(Linux)に移した途端なぜか Segmentation Fault でクラッシュするということがあった。
セグフォは許可されていないメモリ領域にアクセスした時に起こるエラーで、経験上 配列へのアクセスでミスをしているパターンが多い。
どうせ今回もそのパターンだろうと思ったけど、何度確認してもおかしいところは見つからない。
特にそのプログラムではきちんとテストを書いていたので、配列へのアクセスミス程度であればすぐに気づくはずだった。
埒が明かなかったので、今度は標準ライブラリの関数の使い方にミスがないかをチェックしていくことにした。
(そのプログラムでは memcpy
を多用していたので、もしかすると memcpy
の第1引数と第2引数の順番を間違えたのかも…と思ってのことだった)
愛用の Dash でドキュメントを読みまくっていると、memcpy
の説明の一文が目に留まった。
If the objects overlap, the behavior is undefined.
まさにこれが原因だった。
つまり、メモリのコピー元とコピー先の領域が重なるようなケースがあり、この undefined
な動作によってメモリの中身が滅茶苦茶になっていたのだった。
(具体的には、ポインタの配列が壊れたせいで変な領域にアクセスしてしまい、セグフォが起こっていた)
コピー元とコピー先の領域が重なっていると何が問題なのかは、だいたい想像がつく。
例えば、memcpy
がコピー元の先頭から順に1バイトずつコピーしているのだとしたら、dest > src
のときにコピー元の重複部分が上書きされてしまう。
じゃあ一体どうすればいいのかというと、実は memmove
という関数が用意されているので、そっちを使う。
memmove
ではまず src
から一時的な領域にバイトがコピーされ、さらにそこから dest
にバイトのコピーが行われたかのようにコピーが行われるので、src
と dest
の領域が重なっていても問題にならない。(参考)
そんなわけで、無事にプログラムのバグは解決したのだけど、memcpy
の中で一体何が起こっていたのかが気になったので、実装を覗いてみることにした。
Mac OS の memcpy
http://www.opensource.apple.com/source/xnu/xnu-2050.18.24/libsyscall/wrappers/memcpy.c
予想通り、memcpy
の中で dest < src
かどうかで条件分岐をしていた。
dest < src
なら前から順にコピー、そうでないなら後ろから順にコピーしているのだと思う。
if ((unsigned long)dst < (unsigned long)src) { /* * Copy forward. */ // 省略 } else { /* * Copy backwards. Otherwise essentially the same. * Alignment works as before, except that it takes * (t&wmask) bytes to align, not wsize-(t&wmask). */ // 省略 }
さらに、memmove
が引数そのままで memcpy
を呼び出していることも分かった。
今回のプログラムが Mac OS で問題なく動作していたのはこのためだった。
__private_extern__ void * memmove(void *s1, const void *s2, size_t n) { return memcpy(s1, s2, n); }
Linux の memcpy
https://github.com/torvalds/linux/blob/master/arch/alpha/lib/memcpy.c
void * memcpy(void * dest, const void *src, size_t n) { if (!(((unsigned long) dest ^ (unsigned long) src) & 7)) { __memcpy_aligned_up ((unsigned long) dest, (unsigned long) src, n); return dest; } __memcpy_unaligned_up ((unsigned long) dest, (unsigned long) src, n); return dest; }
dest
と src
の下位3ビットを見てアラインメントをチェックし、処理を分けているが、それ以上のことはしていないように見える。
ここで使われている __memcpy_aligned_up
、 __memcpy_unaligned_up
とは別に __memcpy_aligned_dn
、__memcpy_unaligned_dn
というのも定義されてるけど、使われていない。
up
down
は forward
backward
という意味ではないのかな?よくわからない。
まとめ
memcpy
でコピー元とコピー先の領域が重なるような場合はmemmove
を使う- Mac OS では
memcpy
でも問題ない (けどもちろんmemmove
を使うほうが安全)
これまでいくつも C のプログラムを書いてきたし、memcpy
も何度も使ってきたけど、今回の undefined
な動作については全く知らなかった。
さらに memmove
なんて関数があるのも今回はじめて知った。
後日このことを先生に話したら、先生はこの仕様のことを知っているようだった。
経験の差なのだろうか。
ただ、先生も memmove
の存在は知らなかったらしい。
今回の件で一番感じたのは、やはりドキュメントはしっかり読むべきだということ。
特に『わかったつもり』になっている C の標準ライブラリの使い方とか、そういう基本的なところが実は怪しかったりする。
あと Dash 最高。
以上、memcpy
の間違った使い方をしてハマったのと、それを解決する中でいろいろ勉強になった、という話でした。
2015-01-13 追記
ブコメで以下のようなご指摘をいただきました。
id:mnogu さん、ありがとうございます!
引用されているalphaマシン用のLinux kernelコードは関係ないのでは?
ただ、glibcのmemcpy.cには「Overlap is NOT handled correctly.」とあるので、結論は同じ。
https://sourceware.org/git/?p=glibc.git;a=blob;f=string/memcpy.c;hb=HEAD
memcpy
の実装についてどこを見るべきなのかよく調べなかったため、見るべきソースコードを間違えていました。
申し訳ありません。
間違いを教えてもらうことでまたひとつ勉強になりました。
ブログに書いてよかった…