关于sprintf 和 snprintf的自拷贝

爷,独闯天下 提交于 2020-03-05 13:36:45
朋友出了这样一道题:
char buf[100];
memset(buf, 0, 100);
strcpy(buf, "hello");
sprintf(buf, "%s world\n", buf);
printf("%s\n", buf);
结果是什么?
 
在不同的编译器上,结果不同,有的显示" world",有的显示"hello world",我们发现,对于这种自拷贝的问题,编译器可以有不同的策略,有的简单的把原先的值抹去,有的会保留,为了详细说明,我们自己动手写一个试试看。

【实践】:
思想:在内部创建一个临时buffer,去代替des,然后,实际操作sprintf/snprint,最后,把buffer中的内容导入des即可。
为了突出重点,这里只是针对%s作了处理,代码如下:
 
test.cpp:
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
 
/*
 * des      : 目标buffer
 * size     : 限定目标buffer的最大尺寸
 * strategy : 当目标buffer与源buffer相同时,是否要拷贝原来的值
 *            0 不拷贝
 *            非0 拷贝
 * return   : 0 正确
 *            非0 失败   
 */
int mysnprintf(char *des, size_t size, int strategy, char* fmt, ...)
{
  char* pArg=NULL;               //......va_list
  char c;
  pArg = (char*) &fmt;          
  pArg += sizeof(fmt);         //......va_start
 
  // allocate new buffer to contain the result
  // no change to des pointer until the end
  char *tdes;
  char *ptr;
  int i=0;
  if ((tdes = (char *)malloc(size)) == NULL)
    return -1;
  memset(tdes, 0, size);
 
  do
  {
    c =*fmt;
    if (c != '%')
    {
      *(tdes+i) = c;
      i ++;
    }
    else
    {
      switch(*++fmt)
      {
        case 's':
          ptr = *((char **)pArg);
          if (  ptr == des)
          {
            printf("WARN:des equal to src\n");
          }
          if (  ptr == des && !strategy)
          {
            printf("remove the old value\n");
          }
          else {
            while(*ptr != '\0' && *ptr !='\n' && i<size)
            {
              *(tdes+i)= *ptr;
              i ++;
              ptr ++;
            }
          }
          break;
       
        default:
          *(tdes+i)='%';
          i ++;
          break;
      }
      pArg += sizeof(char *);           //......va_arg
     }
    ++fmt;
  }while (*fmt != '\0' && *fmt != '\n' && i<size);
  // copy
  memcpy(des, tdes, size);
 
  // clean 
  pArg = NULL;                           //...va_end
  free(tdes);
  return 0;
}
 
int main(int argc, char* argv[])
{
  char des_buf[1024];
  memset(des_buf, 0, 1024);
  strcpy(des_buf, "hello");
  int stragety = 0;
  if ( argc >1 )
    stragety = atoi(argv[1]);
   
  mysnprintf(des_buf, 512, stragety, "world %s haha %s\n",des_buf, "end");
  printf("result:[%s]\n", des_buf);
  return 0;
}
 
测试结果:
# ./test
WARN:des equal to src
remove the old value
result:[world  haha end]
# ./test 1
WARN:des equal to src
result:[world hello haha end]
 
【结语】:不建议在sprintf/snprintf系列函数中进行自拷贝,一方面其结果依赖于编译器的策略,不易移植,另外更重要的,这种做法不符合人的正常思维,诸如蛇吞吃自己尾巴的构思,还是让聪明人去想吧,我们只是凡人,做我们该做的事情吧。



void f(const char *p)
{
char buf[11]={0};
sprintf(buf,"%10s",p); //very dangerous
printf("%sn",buf);
}

不要让格式标记“%10s”误导你。如果 p 的长度大于10个字符,那么sprintf() 的写操作就会越过 buf 的边界,从而产生一个缓冲区溢出。

f("hello world!"); //12 characters + nul

检测这类缺陷并不容易,因为它们只在 p 的长度大于10个字符的时候才会发生。黑客通常利用这类脆弱的代码来入侵看上去安全的系统。要修正这一缺陷,可以使用函数snprintf() 代替函数sprintf(),函数snprintf() 的原型为:

int
snprintf(char* buf, size_t maxlen, const char* fmt, ...);

第二个参数定义能被写到 buf 中的字符的最大个数,不考虑格式标志以及源字符串的大小:

snprintf(buf, 10, "%10s",p); //now safe
f("hello world!"); //string is chopped to "hello worl"

相似的,使用strncpy()、strncmp()、strncat()、strnicmp() 和 strnset() 相应地代替strcmp()、strcat()、stricmp() 和 strset()。例如:

const int LINE_SIZE=81;
char buf[LINE_SIZE]={0};
// write up to 80 chars to buf:
strncpy(buf, dest, LINE_SIZE-1);
// compare no more than 80 chars:
int equal= strncmp(buf, dest, LINE_SIZE-1);

使用缓冲区大小有限制的 C 函数版本可以降低缓冲区溢出发生的可能性,也不要求对原始代码进行实质的变化。


易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!