最近在RHEL7上使用字符串拷贝函数wcscpy(),发现出来的结果不对,好像是dest的缓冲区被破坏了:
int main()
{
wchar_tbuf[256] = L"\n \n Total memory used\n Total buffer size\n \n Totalphysical memory used\n Total memory actually used.\n";
wchar_tnlStr[5] = L"\n ";
wchar_t *tmp =buf;
int nlStrLen =wcslen(nlStr);
while (tmp =wcsstr(tmp, nlStr))
{
wcscpy(tmp + nlStrLen -1, tmp + nlStrLen);
tmp ++;
}
printf("%ls",buf);
return 0;
}
这段代码的作用是将buf中的"\n"后的空格去掉,关键是对wcscpy()的调用。wcscpy()将src字串拷贝到dest。而本例中,src与dest出现了内存上的交叠。
一般来讲,src与dest的交叠方式分两种 (如下图):
1. dest的头与src的尾相交叠。
2. src的头与dest的尾相交叠,本例尾这种情况。
虽然manpage对wcscpy()的说明中强调了,src与dest不应该交叠,但分析wcscpy()的实现逻辑,发现对于交叠情况2,也应该正确工作,因为wcscpy()总是从src的头开始向dest的头拷贝数据,所以当拷贝指针移动到src的尾部并拷贝数据写入dest的尾部,也即src的头部时,并不会损害dest。
但是现在问题来了,最终输出的结果显示buf仍然被损坏了:
Toaal memory used
Total buffer siz
Ttaal physiclmmeemory used
Toaal memory actually used.
这其实问题是一个很出名的问题:即*cpy系列的函数不能正确处理src与dest内存交叠的情况。附一篇大牛们为该问题吵架的连接:
https://sourceware.org/bugzilla/show_bug.cgi?id=12518
回答也非常简单:
当src与dest内存有交叠时,改用*move系列函数: memmove / wmemmove。
只是,我感兴趣为什么这个问题会出现?
如果把wcscpy()的实现拷贝来放到本地,名字改成wcscpy_local 并调用之,发现工作正常了,附上wcscpy_local()的实现:
char_t* wcscpy_local (wchar_t * dest, const wchar_t * src)
{
wint_t c;
wchar_t*wcp;
if(__alignof__ (wchar_t) >= sizeof (wchar_t))
{
constptrdiff_t off = dest - src - 1;
wcp = (wchar_t *) src;
do
{
c =*wcp++;
wcp[off] = c;
}
while(c != L'\0');
}
else
{
wcp =dest;
do
{
c = *src++;
*wcp++ = c;
}
while (c != L'\0');
}
return dest;
}
那么,为什么调用系统提供的wcscpy就会出问题呢?
我查到的理解是:系统定义的wcscpy原型中,参数多了一个限制符“__restrict”:
File: /usr/include/wchar.h
extern wchar_t *wcscpy (wchar_t *__restrict __dest, constwchar_t *__restrict __src) __THROW;
关于restrict
在C99中,restrict是一个关键字,而C++中却没有该关键字,但当代编译器也在C++实现了差不多的功能,用的关键字是__restrict,或__restrict__。(为了简便,以下只称restrict)
restrict 用来定义指针变量,表明该变量没有别名,意思就是:除了该变量以外,没有别的方法可以访问其指向的地址空间。
举个有别名的例子:
int *pA, *pB;
pB = pA; //那么pB就是pA的别名,通过pB照样可以访问pA指向的内容
编译器如果知道一个变量没有别名,那么,它就会放心大胆的作优化,比如这段代码:
void update(int *ptrA, int * ptrB, int * val)
{
*ptrA +=*val;
*ptrB +=*val;
}
编译出来的ASM类似于:
mov eax val;
add ptrA eax;
mov eax val;
add ptrB eax;
其中,变量val加载了两次。因为它不知道ptrA,ptrB是否与val有内存上的交叠,所以为了保险起见,只能每次用到val时都从内存里读。
如果加上__restrict:
void update_restrict(int * __restrict ptrA, int * __restrictptrB, int * __restrict val);
则汇编变成(需要打开编译优化选项,底下有讲到):
mov eax val;
add ptrA eax;
add ptrB eax;
可看到,val只加载了一次,因为编译器知道val没有别名,不会被别人改变,所以可以重复使用寄存器里的值。
猜想wcscpy的错误原因
那么,我就在想,因为wcscpy的参数加了__restrict,这就要求src与dest不能出现内存交叠,而事实上,我们传进的参数没有保证这点,导致了优化后的结果不正确。
不过我并没能重现个问题,即便对自己版本的wcscpy_local()参数加上__restrict,并打开编译优化选项,仍然没有重现问题。
抛砖引玉,希望有人解开我的疑惑~
restrict/__restrict/__restrict__的编译选项:
为了观察__restrict的汇编结果,需要打开优化选项,以下是GCC / G++的选项:
对C:
-O3 -std=c99
对C++,-std=c99已经默认开启,因此只用:
-O3
参考:
(1) WiKi: http://en.wikipedia.org/wiki/Restrict
(2) Demystifying The Restrict Keyword:
http://cellperformance.beyond3d.com/articles/2006/05/demystifying-the-restrict-keyword.html