memcpyで配列のアドレスをオーバーラップした場合、意図せぬ動作を引き起こすことがある。それは非常に厄介な問題を孕んでおり、通常では見つけることも困難になることも予想される。
memcpyでアドレスオーバーラップを起こしているコードの例
例えば、以下のようなコードがある。
#include <stdio.h>
#include <string.h>
int main() {
char str[] = "1234567890";
memcpy(&str[4], &str[0], sizeof(char) * 6);
printf("%s\n", str);
return 0;
}
この場合、「1234123456」と出力されることを期待するはずが、実際に出力されるのは「1234123412」となってしまうこともある。これは、memcpyではアドレスがオーバーラップした場合の動作が確定していないからである。
AddressSanitizerで検知して、強制終了できるようにする
なお、clangではAddressSanitizerというツールを使うことで、上記のようなコードが実行された時にエラーを発生させることができる。
例えば、上記のコードをfoo.cで保存したことを仮定して、「-fsanitize=address -fno-omit-frame-pointer」を加えた上でコンパイルして実行した場合、エラーメッセージを吐いて強制終了するようになる。
以下はその例である。
$ clang foo.c -o foo -g -fsanitize=address -fno-omit-frame-pointer
$ ./foo
=================================================================
==2447==ERROR: AddressSanitizer: memcpy-param-overlap: memory ranges [0x7fff5b4ee864,0x7fff5b4ee86a) and [0x7fff5b4ee860, 0x7fff5b4ee866) overlap
#0 0x104751af0 in __asan_memcpy (/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/clang/7.0.0/lib/darwin/libclang_rt.asan_osx_dynamic.dylib+0x39af0)
#1 0x104711d6f in main foo.c:6
#2 0x7fff8d57e5ac in start (/usr/lib/system/libdyld.dylib+0x35ac)
#3 0x0 (<unknown module>)
Address 0x7fff5b4ee864 is located in stack of thread T0 at offset 36 in frame
#0 0x104711c6f in main foo.c:4
This frame has 1 object(s):
[32, 43) 'str' <== Memory access at offset 36 is inside this variable
HINT: this may be a false positive if your program uses some custom stack unwind mechanism or swapcontext
(longjmp and C++ exceptions *are* supported)
Address 0x7fff5b4ee860 is located in stack of thread T0 at offset 32 in frame
#0 0x104711c6f in main foo.c:4
This frame has 1 object(s):
[32, 43) 'str' <== Memory access at offset 32 is inside this variable
HINT: this may be a false positive if your program uses some custom stack unwind mechanism or swapcontext
(longjmp and C++ exceptions *are* supported)
SUMMARY: AddressSanitizer: memcpy-param-overlap ??:0 __asan_memcpy
==2447==ABORTING
Abort trap: 6
これによって、memcpyのアドレスオーバーラップが発生していることがわかる。
この場合は、memmoveを使う
上記のことがわかったことで、代替措置はないのか?と思う人はいるだろう。当然のことながら、代替措置はある。それが、memcpyの代わりに、memmoveを使うことである。memmoveではmemcpyとは違い、オーバーラップしたとしても(別の問題を起こしていない限りは)正常に動作するようになっている。
例えば、先ほどのコードをmemmoveに置き換えたコードを以下に記載する。
#include <stdio.h>
#include <string.h>
int main() {
char str[] = "1234567890";
memmove(&str[4], &str[0], sizeof(char) * 6);
printf("%s\n", str);
return 0;
}
上記のコードを先ほどと同じようにコンパイルして実行してみる。
$ clang bar.c -o bar -g -fsanitize=address -fno-omit-frame-pointer
$ ./bar
1234123456
この場合は、AddressSanitizerを有効にしても正常に動作した。
これは、memcpyとは違い、memmoveではアドレスがオーバーラップしたとしても正常に動作するようになっているからである。アドレスがオーバーラップする処理を行う場合は、memcpyではなく、memmoveを使うのが正しいということになる。
最後に
今回はmemcpyのアドレスオーバーラップを例に挙げたが、C言語ではこう言ったダークサイドが多数あるため、意図した動作になるのか、その命令を使うのが適切かどうかなどちゃんと確認した上で使っていきたい。
なお、今回はmemcpyのアドレスオーバーラップ問題対策としてAddressSanitizerを使ったが、AddressSanitizerはそれ以外にもメモリーの使い方でバッファーオーバーランを起こしていないかどうかなどの確認に使えるので、必要に応じて使っていきたいところである。特に再現性の低いバグの対処には有効に働くだろう。
ウェブマスター。本ブログでITを中心にいろいろな情報や意見などを提供しています。主にスマートフォン向けアプリやウェブアプリの開発を携わっています。ご用の方はコメントかコンタクトフォームにて。