Chinaunix首页 | 论坛 | 博客
  • 博客访问: 818135
  • 博文数量: 756
  • 博客积分: 40000
  • 博客等级: 大将
  • 技术积分: 4980
  • 用 户 组: 普通用户
  • 注册时间: 2008-10-13 14:40
文章分类

全部博文(756)

文章存档

2011年(1)

2008年(755)

我的朋友

分类:

2008-10-13 16:14:15

估计如果我一直在Windows下写程序、用VC写程序,绝对不会因为这一个小小的函数来写一篇blog。因为就算是出了差错,也在debug中随手改过来了,一点感觉和印象都没有。但是我是在做嵌入式开发,情形就不同了,我需要在Windows上用编辑器(SourceInsight或者ultraedit)写出源码,用编译器进行交叉编译,再烧到目标板上去看效果。这样一组操作的周期大概需要20分种左右(如果都先clean再build的话需要2个小时左右)。而平时的调试不仅没有VC这种IDE式的debug工具,就是连gdb这种console的单步调试工具也没有。一般没有致命错误,我是绝对不会动用JTAG之类的核子武器的,我会用Trace宏讲要看到的运行时信息打印到串口,当然更多的情况下,是用肉眼去判断。(不然完成一个项目估计1~2年是做不完了)——在这种情况下,如果概念不清、思路不清、库函数的细节不清楚,就会挂了。我这次用strcat()这个函数犯的错误就是个很好的例子。

好了,大家先看看这个程序,你能发现其中的bug吗?
#include 
#include 

int main(int argc, char *argv[])
{
    #define MAX (100)
    #define START (10000)

    char buf[512], tmp[32];
    int a[MAX];
    int count = 0;
    int i;

    printf("%d %d %d \n",a[0],a[1],a[2]);
    //initialize an array for testing
    for ( i = START; i<START + MAX; ++i)
    {
        a[i - START] = i;
    }
  
    for ( i=0; i<MAX; ++i)
    {
        sprintf(tmp, "0x%.4X,", i);
        
        strcat(buf, tmp);
        if (count++ == 9)
        {
            //print result, output to standard io stream for samulation.
            printf("%s\n", buf);
            buf[0] = '\0';
            count = 0;
        }
        
    }
 
 return 0;
}
嘿嘿,这个bug可不简单,它会导致偶的系统驱动层的所有实体全部崩溃掉。偶是用的世界著名的A公司的OS,这个OS很多公司都在用。这段代码是偶写的代码的一个简化版。a[]这个数组是显示的缓冲区,我需要将它的内容trace到串口。因为a[]中的数据量很大,如果我极为频繁的访问串口的话,会导致串口阻塞,trace数据丢失,看不到我要看的数据。所以我把数据分了组,用strcat()来合并数据,一组一组的往串口发。

好了,背景介绍完了。看出bug了没有?如果没有看出来,我就要公布答案了! :)
#include 
#include 

int main(int argc, char *argv[])
{
    #define MAX (100)
    #define START (10000)

    char buf[512], tmp[32];
    int a[MAX];
    int count = 0;
    int i;

    printf("%d %d %d \n",a[0],a[1],a[2]);
    //initialize an array for testing
    for ( i = START; i<START + MAX; ++i)
    {
        a[i - START] = i;
    }

    buf[0] = '\0'; ////////////////////////////////////////// here !
   
    for ( i=0; i<MAX; ++i)
    {
        sprintf(tmp, "0x%.4X,", i);
        
        strcat(buf, tmp);
        if (count++ == 9)
        {
            //print result, output to standard io stream for samulation.
            printf("%s\n", buf);
            buf[0] = '\0';
            count = 0;
        }
        
    }
 
 return 0;
}
问题的关键在于传递给strcat(dst,src)的两个参数都要是以'\0'结尾的。而我第一次传递给strcat()的第一个参数buf[]没有初始化,导致了buf的内容都非0,从而引起缓冲区溢出的错误。

先看看vc6.0中strcat的实现:
char * __cdecl strcat (
        char * dst,
        const char * src
        )
{
        char * cp = dst;

        while( *cp )
                cp++;                   /* find end of dst */

        while( *cp++ = *src++ ) ;       /* Copy src to end of dst */

        return( dst );                  /* return dst */

}
在find end of dst这一步,程序会从dst的起始地址开始,去找一个'\0',并且只有找到这个'\0',代码才会结束。Okey,问题明了了。由于我没有初始化buf[],而系统随便分配一块内存给它,里面可能还不为0,它就不停的跑下去了。就算它停了下来,第二步的copy src to end of dst也会接着写下去,如果此刻超出了buf的范围,就会把src中的内容写到不该写的位置上去,这样就破坏了其他的进程的代码和数据,系统就崩溃掉了。

反过来想,当系统崩溃的时候,又有谁一下子能想到是buf没有初始化引起的呢?:)  值得一提的是,这段代码在Mingw上不初始化buf也不会出错的,就是说,由于编译器实现不同,可能MingW在分配buf的时候已经替它做了初始化。所以在MingW上编译我们第一段代码,它也跑的很好,不会出错。但是如果你来做移植工作,把MingW上的一个工程移植到其他平台,系统就挂掉了,再也跑不起来,你能查到这个由于strcat()参数没有初始化的bug么?

呵呵,所以coder的责任重大,不写代码没什么问题,如果写了错误的代码就会给维护的人员造成很大的压力了。所以,一个好的coder平时就要对自己有至少两点要求:第一,要对库函数实现有清晰的认识,最好能了解几种不同实现的代码,要知道他们的实现原理,知其然并且知其所以然;第二,平时工作就要多用几种不同的编译器,这样才好及时的发现不能跨平台的问题,及时去解决。Coder任重而道远,虽不能妄自尊大,也不要妄自菲薄呀!:)


----------------------------------------
吃完饭回来,再补充两句:不要以为strcat()不作边界检查、或者不强制初始化是C语言的不足。没有任何一个有实用价值的操作系统实现会去强制执行边界检查或者缓冲区溢出的错误的,不然它在市场上就没有竞争力。以下给出几种strcat的实现,希望有参考价值:

Linux 0.11的实现:
//////// Linux 0.11 ///////////
extern inline char * strcat(char * dest,const char * src)
{
__asm__("cld\n\t"
 "repne\n\t"
 "scasb\n\t"
 "decl %1\n"
 "1:\tlodsb\n\t"
 "stosb\n\t"
 "testb %%al,%%al\n\t"
 "jne 1b"
 ::"S" (src),"D" (dest),"a" (0),"c" (0xffffffff));
return dest;
}
Linux 2.4.26的实现:
///// LINUX 2.4.26 /////

#ifndef __HAVE_ARCH_STRCAT
/**
 * strcat - Append one %NUL-terminated string to another
 * @dest: The string to be appended to
 * @src: The string to append to it
 */
char * strcat(char * dest, const char * src)
{
 char *tmp = dest;

 while (*dest)
  dest++;
 while ((*dest++ = *src++) != '\0')
  ;

 return tmp;
}
#endif
GNU glibc的实现:
/* Copyright (C) 1991, 1997, 2003 Free Software Foundation, Inc.
   This file is part of the GNU C Library.

   The GNU C Library is free software; you can redistribute it and/or
   modify it under the terms of the GNU Lesser General Public
   License as published by the Free Software Foundation; either
   version 2.1 of the License, or (at your option) any later version.

   The GNU C Library is distributed in the hope that it will be useful,
   but WITHOUT ANY WARRANTY; without even the implied warranty of
   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
   Lesser General Public License for more details.

   You should have received a copy of the GNU Lesser General Public
   License along with the GNU C Library; if not, write to the Free
   Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA
   02111-1307 USA.  */

#include 
#include 

#undef strcat

/* Append SRC on the end of DEST.  */
char *
strcat (dest, src)
     char *dest;
     const char *src;
{
  char *s1 = dest;
  const char *s2 = src;
  reg_char c;

  /* Find the end of the string.  */
  do
    c = *s1++;
  while (c != '\0');

  /* Make S1 point before the next character, so we can increment
     it while memory is read (wins on pipelined cpus).  */
  s1 -= 2;

  do
    {
      c = *s2++;
      *++s1 = c;
    }
  while (c != '\0');

  return dest;
}
libc_hidden_builtin_def (strcat)

VC 7.1的实现:
        page    ,132
        title   strcat - concatenate (append) one string to another
;***
;strcat.asm - contains strcat() and strcpy() routines
;
;       Copyright (c) Microsoft Corporation. All rights reserved.
;
;Purpose:
;       STRCAT concatenates (appends) a copy of the source string to the
;       end of the destination string, returning the destination string.
;
;*******************************************************************************

        .xlist
        include cruntime.inc
        .list


page
;***
;char *strcat(dst, src) - concatenate (append) one string to another
;
;Purpose:
;       Concatenates src onto the end of dest.  Assumes enough
;       space in dest.
;
;       Algorithm:
;       char * strcat (char * dst, char * src)
;       {
;           char * cp = dst;
;
;           while( *cp )
;                   ++cp;           /* Find end of dst */
;           while( *cp++ = *src++ )
;                   ;               /* Copy src to end of dst */
;           return( dst );
;       }
;
;Entry:
;       char *dst - string to which "src" is to be appended
;       const char *src - string to be appended to the end of "dst"
;
;Exit:
;       The address of "dst" in EAX
;
;Uses:
;       EAX, ECX
;
;Exceptions:
;
;*******************************************************************************

page
;***
;char *strcpy(dst, src) - copy one string over another
;
;Purpose:
;       Copies the string src into the spot specified by
;       dest; assumes enough room.
;
;       Algorithm:
;       char * strcpy (char * dst, char * src)
;       {
;           char * cp = dst;
;
;           while( *cp++ = *src++ )
;                   ;               /* Copy src over dst */
;           return( dst );
;       }
;
;Entry:
;       char * dst - string over which "src" is to be copied
;       const char * src - string to be copied over "dst"
;
;Exit:
;       The address of "dst" in EAX
;
;Uses:
;       EAX, ECX
;
;Exceptions:
;*******************************************************************************


        CODESEG

%       public  strcat, strcpy      ; make both functions available
strcpy  proc
        push    edi                 ; preserve edi
        mov     edi,[esp+8]         ; edi points to dest string
        jmp     short copy_start

strcpy  endp

        align   16

strcat  proc

        .FPO    ( 0, 2, 0, 0, 0, 0 )

        mov     ecx,[esp+4]         ; ecx -> dest string
        push    edi                 ; preserve edi
        test    ecx,3               ; test if string is aligned on 32 bits
        je      short find_end_of_dest_string_loop

dest_misaligned:                    ; simple byte loop until string is aligned
        mov     al,byte ptr [ecx]
        add     ecx,1
        test    al,al
        je      short start_byte_3
        test    ecx,3
        jne     short dest_misaligned

        align   4

find_end_of_dest_string_loop:
        mov     eax,dword ptr [ecx] ; read 4 bytes
        mov     edx,7efefeffh
        add     edx,eax
        xor     eax,-1
        xor     eax,edx
        add     ecx,4
        test    eax,81010100h
        je      short find_end_of_dest_string_loop
        ; found zero byte in the loop
        mov     eax,[ecx - 4]
        test    al,al               ; is it byte 0
        je      short start_byte_0
        test    ah,ah               ; is it byte 1
        je      short start_byte_1
        test    eax,00ff0000h       ; is it byte 2
        je      short start_byte_2
        test    eax,0ff000000h      ; is it byte 3
        je      short start_byte_3
        jmp     short find_end_of_dest_string_loop
                                    ; taken if bits 24-30 are clear and bit
                                    ; 31 is set
start_byte_3:
        lea     edi,[ecx - 1]
        jmp     short copy_start
start_byte_2:
        lea     edi,[ecx - 2]
        jmp     short copy_start
start_byte_1:
        lea     edi,[ecx - 3]
        jmp     short copy_start
start_byte_0:
        lea     edi,[ecx - 4]
;       jmp     short copy_start

;       edi points to the end of dest string.
copy_start::
        mov     ecx,[esp+0ch]       ; ecx -> sorc string
        test    ecx,3               ; test if string is aligned on 32 bits
        je      short main_loop_entrance

src_misaligned:                     ; simple byte loop until string is aligned
        mov     dl,byte ptr [ecx]
        add     ecx,1
        test    dl,dl
        je      short byte_0
        mov     [edi],dl
        add     edi,1
        test    ecx,3
        jne     short src_misaligned
        jmp     short main_loop_entrance

main_loop:                          ; edx contains first dword of sorc string
        mov     [edi],edx           ; store one more dword
        add     edi,4               ; kick dest pointer
main_loop_entrance:
        mov     edx,7efefeffh
        mov     eax,dword ptr [ecx] ; read 4 bytes

        add     edx,eax
        xor     eax,-1

        xor     eax,edx
        mov     edx,[ecx]           ; it is in cache now

        add     ecx,4               ; kick dest pointer
        test    eax,81010100h

        je      short main_loop
        ; found zero byte in the loop
; main_loop_end:
        test    dl,dl               ; is it byte 0
        je      short byte_0
        test    dh,dh               ; is it byte 1
        je      short byte_1
        test    edx,00ff0000h       ; is it byte 2
        je      short byte_2
        test    edx,0ff000000h      ; is it byte 3
        je      short byte_3
        jmp     short main_loop     ; taken if bits 24-30 are clear and bit
                                    ; 31 is set
byte_3:
        mov     [edi],edx
        mov     eax,[esp+8]         ; return in eax pointer to dest string
        pop     edi
        ret
byte_2:
        mov     [edi],dx
        mov     eax,[esp+8]         ; return in eax pointer to dest string
        mov     byte ptr [edi+2],0
        pop     edi
        ret
byte_1:
        mov     [edi],dx
        mov     eax,[esp+8]         ; return in eax pointer to dest string
        pop     edi
        ret
byte_0:
        mov     [edi],dl
        mov     eax,[esp+8]         ; return in eax pointer to dest string
        pop     edi
        ret

strcat  endp

        end



-------------
乾坤一笑 写于2005年6月18 日  转载请标明出处和原文链接


--------------------next---------------------
/////// vc6.0 source ////////////

char * __cdecl strncpy (
        char * dest,
        const char * source,
        size_t count
        )
{
        char *start = dest;

        while (count && (*dest++ = *source++))    /* copy string */
                count--;

        if (count)                              /* pad out with zeroes */
                while (--count)
                        *dest++ = '\0';

        return(start);
}

strncpy更加不需要对dest进行初始化了。虽然dest和source内含的字符长度相等的时候不会在dest的末尾加0,但这也正好证明了我的观点:要对库的实现很了解,就不会出错了。不要想当然的乱用库函数。

--------------------next---------------------

你看MSDN看的很细,可惜的是没有正确理解“NULL结尾的字符串”的含义。MSDN上所谓的Null-terminated string 其实就是着的是“以'\0'结尾的字符串”。要理解这个概念要说到三个方面:
一、NULL是什么?
     纵观VC6.0的库代码,有22处定义了NULL宏,但是所有定义都是一模一样的:

#undef  NULL
#ifndef NULL
#ifdef __cplusplus
#define NULL    0
#else  /* __cplusplus */
#define NULL    ((void *)0)
#endif  /* __cplusplus */
#endif  /* NULL */

二、'\0'是什么?
'\0'是C语言定义的用'\'加8进制ASCII码来表示字符的一种方法,'\0'就是表示一个ASCII码值为0的字符。同样的,你用'\x0'来表示也可以,这是用16进制的ASCII码来表示,虽然很不常见。

三、0值的意义何在?
0是一个整数。由于各种标准转换,0可以被用于作为任意整型、浮点类型、指针。0的类型将用上下文来确定。典型情况下0被表示为一个适当大小的全零二进制位的模式。所以,无论NULL是定义为常数0还是((void *)0)这个零指针,NULL都是指的是0值,而不是非0值。而字符的'\0'和整数的0也可以通过转型来相互转换。

综上所述,NULL就是指的是0值,Null-terminated string就是以0值结尾的string,也就是以'\0'。其实C语言中的变量只有4中char、int、float、poiner四大类。所谓的字符串只是一堆char的后面加上一个0而已。


--------------------next---------------------
晕倒!看来咱们俩之间确实误会不小。偶表达能力差,又不善解人意。:p

规矩是人定的,当然可以有其他的方法表示字符串。历史上FORTRAN一脉的强编译型语言表示字符串大体有两种方法:1>类C语言使用'\0'来终止字符数组;2>pascal之类的语言,用一个表示字符串长度的量捆绑到字符串上。两者各有千秋。但总体来说在于使用的人。

你们没有仔细看偶的文章,自然觉得意义不大。我的这篇blog是借题发挥,寓意并非研究strcat()这么肤浅。当然,寓意是什么,我相信仔细看过这篇文章的人都知道。

开发领域不同,自然对软件工程师的要求也自然不一样:本文针对的是系统级开发的软件工程师说的,在这个领域里效率是第一位,效率和安全要兼顾。至于上层的开发人员,我也认为没必要了解库细节——专注做业务逻辑就可以了,合理的软件构架才是他们需要关心的。
--------------------next---------------------
欢迎发表读后感!本站特别欢迎发表读后感!Welcome to my blog!:p

我之所以贴出那些实现也是想说,这些实现都一样的。这些实现都没有对strcat(dst,src)中的dst参数进行检查就开始操作了。所以我们要小心。

另外,0.1ms对我来说是至关重要的,0.01ms对我来说也是不能小觑的。这段代码的原本用于trace一个LCD的驱动的,我要trace出每一个点的值来确认LCD buffer是否被其他因素干扰致使画面局部显示不正常。假设每一个点要delay 0.1ms的话,整个LCD有128*160个点,就要delay 0.1ms * 128 * 160 = 2048ms,想想看,当你的画面每刷新1次,就需要等待2048ms是一个什么样的情形?:) 所以我说开发领域不同要求也不同,是真心话。我没有看过任何一个LCD厂商的驱动参考设计中使用 x/2的方法来计算x的一半是多少,它们都是用x>>2来计算,而这点效率的差别对普通上层开发来说是无关紧要的,但是对于底层驱动开发来说体现出的就是整个产品品质的差异了。
--------------------next---------------------
okey, okey.
顺便介绍一下PASCAL的字符串用法:
var s : string;
pascal的字符串的索引是从1开始的,s[0]是什么?s[0]就是s这个字符串的长度。由于最早(dos下)字符串都是中的每个组成单元都是一个字符,也就是8位的char,所以s[0]的大小受陷于2^8=256,也就是说早起的pascal的string的长度不能超过256。不过到了windows之后就好了,object pascal把字符串前导字节增加为32位,也就是说字符串的最大长度为2^32=4294967296,所以一般来说这个字符串长度还是够用了。Null-terminated字符串的好处是字符串不受长度的限制,这对于将底层操作统一为串操作有极大的好处(比如Linux下把所有的设备都映射为文件,可以用串操作函数来处理);有长度的字符串也有很多好处,因为它得到整体字符串长度的开销很小,所以它很容易就获得了做静态检查、越界检查的能力。
--------------------next---------------------
我没看到你的文章,只看到你的评论,所以现在只能说你看起来很牛,嗯。至于争论,我觉得没有什么不对的,真理越辩越明,没有人生下来就什么都知道,什么都了解。如果人人都不辨不争不明,那怎么进步呢?至于这个bug,是的,在每个公司的编码规范中,都有变量初始化的要求,但是将这个问题了解清楚其实现不是更好吗?

我是一个低手中的低手,因为我肯定我比一笑要低的多,比很多人都低得多,但是低手也有争论的权利,低手也许在某些方面比某些方面的高手知道得多,术业也有专攻呢。而高手,我觉得也没有什么好狂的地方。只是逻辑的缜密性和了解的多少而已。

我最佩服一笑的,不是他的水平,而是他的态度,这种态度才是治学的态度,看看原文的修改就知道了。

最后,我希望能够看到“满头大汗”兄台的大作,嗯,也许你是有狂的本钱,拿出来大家看看吧。

最后,我还是希望自己有成为高手的一天。。。
--------------------next---------------------
luomingjian


这些函数原型就有些问题,我是很难明白返回一个char*有何意义, 

char* strncat (char* dst, const char* src, size_t len);  
改为:  
int strncat (char* dst, const char* src, size_t len);  

返回个char*作什么?鼓励坏习惯吗?(strcat(strcat(buf,"abc"), "def");这种坏习惯)。 

-------------------------- 
同问! 
------------------------- 
Joel也提到过这个问题, P5-P9。但我一直相信写库代码的人是牛,不会仅仅为了(strca(strcat(buf,"abc"), "def")而改变返回值类型,他的本意就是是什么?盼指点! 


这个的目的就只是为了串式表达式的需要。没有别的了
去看看《高质量c++编程指南》这个书,里面有提到

strcpy 能把strSrc 的内容复制到strDest,为什么还要char * 类型的返回值? 
答:为了实现链式表达式。
    例如   int length = strlen( strcpy( strDest, "hello world") );


这样是很坏的习惯莫?我不这样看。

如果需要高效的情况下,
int length = strlen( strcpy( strDest, "hello world") );

应该会比

strcpy(strDest, "hello world");
int length = strlen(strDest);

要略快一些
--------------------next---------------------
建议楼主养成局部变量数组初始化的习惯,如果是全局数组的话就没必要初始化了,默认就是以0做为初值,这也是为什么一些C++书上讲的局部变量编译器不初始化(其实这里是错的,编译一样初始化的只是以0xCC),全局变量初始化为0。
void main()
{
  int i;
  cout<}
观察下结构是否为0xCCCCCCCC
总的一局话,C++给局部变量以0xCC初始化,全局变量0初始化。
所以你如果在函数中声明数组的话,记住一定要调用memset函数,给该字符串数组进行初始化,这是一种习惯,一定要养成。
因为局部变量是在栈上分配的每个字节c++都是以0xCC来初始化的,并非0,而字符串的结尾是以0做为标志的,当然Unicode码不是这样的(Unicode码是在字符串的前四个字节编译器事先放入字符串长度,你可以随便找个VB的程序调试下)。

--------------------next---------------------
今天我也是用strcat出了错才来这里

大家好:)

char_file=(char*)malloc(sizeof(char)*n);

for(i=0;i
{

char_file[i]=*(argv0+i);

}//char_file的n个char字符空间被装满~

char_file[i]='\0';

strcat(char_file,"abc...");

程序在VC中可以编译通过,然后我拿去linux gcc编译执行

提示"段错误",后来知道了linux下strcat要求strcat(dst,src)dst大小可以容纳src,我的程序可以看出是将src加在dst(malloc产生的空间)后面,故而出错

但是请问,为什么VC中就可以执行呢?

个人猜测和weindows和linux堆管理策略有关,具体为何不知道。



还有看了楼主的帖子Linux 2.4.26 和GNU glibc实现不同

有点迷茫:

既然Linux 2.4.26 已经实现了strcat

然后我们如果安装GNU glibc的话,

我们编程的话,那编译器会使用那种实现?

(我对GNU glibc的理解是glibc是gcc编译器使用的库函数,不知对不?)



还有个问题*_*

char_file[i]='\0';

这一句如果没有,

那么char_file通常会为c:/x/x/    abc...

有的话是c:/x/x/abc...,没有那几个空格

(c:/x/x/ 代表一个路径,abc...代表字符串)

请问为何





*_*


--------------------next---------------------
今天我也是用strcat出了错才来这里

大家好:)

char_file=(char*)malloc(sizeof(char)*n);

for(i=0;i
{

char_file[i]=*(argv0+i);

}//char_file的n个char字符空间被装满~

char_file[i]='\0';

strcat(char_file,"abc...");

程序在VC中可以编译通过,然后我拿去linux gcc编译执行

提示"段错误",后来知道了linux下strcat要求strcat(dst,src)dst大小可以容纳src,我的程序可以看出是将src加在dst(malloc产生的空间)后面,故而出错

但是请问,为什么VC中就可以执行呢?

个人猜测和weindows和linux堆管理策略有关,具体为何不知道。



还有看了楼主的帖子Linux 2.4.26 和GNU glibc实现不同

有点迷茫:

既然Linux 2.4.26 已经实现了strcat

然后我们如果安装GNU glibc的话,

我们编程的话,那编译器会使用那种实现?

(我对GNU glibc的理解是glibc是gcc编译器使用的库函数,不知对不?)



还有个问题*_*

char_file[i]='\0';

这一句如果没有,

那么char_file通常会为c:/x/x/    abc...

有的话是c:/x/x/abc...,没有那几个空格

(c:/x/x/ 代表一个路径,abc...代表字符串)

请问为何





*_*


--------------------next---------------------

阅读(511) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~