C语⾔的本质(15)——C语⾔的函数接⼝⼊门
C语⾔的本质(15)——C语⾔的函数接⼝
函数的调⽤者和其实现者之间存在⼀个协议,在调⽤函数之前,调⽤者要为实现者提供某些条件,在函数返回时,实现者完成调⽤者需要的功能。
函数接⼝通过函数名,参数和返回值来描述这个协议,只要函数名和参数名命名合理,参数和返回值的类型定义的准确,调⽤者仅仅通过函数接⼝就能知道函数的⽤法。当函数接⼝不能表达函数的全部语义时,⽂档就起了重要的补充作⽤,函数⽂档的写法我们可以参照Linux下的Man Page或MSDN。
下⾯通过分析C标准库函数来说明函数接⼝:
在Linux终端下敲命令manstrcpy就可以看到下⾯的Man Page。
这个Man Page描述了两个函数,strcpy和strncpy,这两个函数的作⽤是把⼀个字符串拷贝给另⼀个字符串。SYNOPSIS部分给出了这两个函数的原型,以及要使⽤这些函数需要包含哪些头⽂件。参数dest、src和n都加了下划线,有时候并不想从头到尾阅读整个Man Page,⽽是想查⼀下某个参数的含义,通过下划线和参数名就能很快到我们关⼼的部分。
dest表⽰Destination,src表⽰Source,看名字就能猜到是把src所指向的字符串拷贝到dest所指向的内存空间。这⼀点从两个参数的类型也能看出来,dest是char *型的,⽽src是const char *型的,说明src所指向的内存空间在函数中只能读不能改写,⽽dest所指向的内存空间在函数中是要改写的,显然改写的⽬的是当函数返回后调⽤者可以读取改写的结果。通过Man Page我们可以推测到strcpy函数是这样⽤的:char buf[10];
strcpy(buf,"hello");
printf(buf);
那么strncpy的参数n是⼲什么⽤的呢?单从函数接⼝⽆法推测,我们继续看下⾯的⽂档。
在⽂档中强调了strcpy在拷贝字符串时会把结尾的'\0'也拷到dest中,因此保证了dest中是以'\0'结尾的字符串。但另外⼀个要注意的问题
是,strcpy只知道src字符串的⾸地址,不知道长度,它会⼀直拷贝到'\0'为⽌,所以dest所指向的内存空间要⾜够⼤,否则有可能写越界,例如:
char buf[10];
strcpy(buf,"hello world");
如果没有保证src所指向的内存空间以'\0'结尾,也有可能读越界,例如:
char buf[10] ="abcdefghij", str[4] = "hell";
strcpy(buf,str);
因为strcpy函数的实现者通过函数接⼝⽆法得知src字符串的长度和dest内存空间的⼤⼩,所以“确保不会写越界”应该是调⽤者的责任,调⽤者提供的dest参数应该指向⾜够⼤的内存空间,“确保不会读越界”也是调⽤者的责任,调⽤者提供的src参数指向的内存应该确保以'\0'结尾。
此外,⽂档中还强调了src和dest所指向的内存空间不能有重叠。凡是有指针参数的C标准库函数基本上都有这条要求,每个指针参数所指向的内存空间互不重叠,例如这样调⽤是不允许的:
char buf[10] ="hello";
strcpy(buf,buf+1);
strncpy的参数n指定最多从src中拷贝n个字节到dest中,如果拷贝到'\0'就结束,如果拷贝到n个字节还没有碰到'\0',那么也结束,调⽤者负责提供适当的n值,以确保读写不会越界,⽐如让n的值等于dest所指向的内存空间的⼤⼩:
char buf[10];
strncpy(buf,"hello world", sizeof(buf));
那么这意味着什么呢?⽂档中特别⽤了Warning指出,这意味着dest有可能不是以'\0'结尾的。例如上⾯的调⽤,虽然把"hello world"截断到10个字符拷贝⾄buf中,但buf不是以'\0'结尾的,如果再printf(buf)就会读越界。如果你需要确保dest以'\0'结束,可以这么调⽤:
char buf[10];
字符串拷贝函数strcpy作用strncpy(buf,"hello world", sizeof(buf));
buf[sizeof(buf)-1]= '\0';
strncpy还有⼀个特性,如果src字符串全部拷完了不⾜n个字节,那么还差多少个字节就补多少个'\0',但是正如上⾯所述,这并不保证dest⼀定以'\0'结束,当src字符串的长度⼤于n时,不但不补多余的'\0',连字符串的结尾'\0'也不拷贝。下⾯⽂档⾮常友好,为了帮助理解,还给出⼀个strncpy的简单实现。
函数的Man Page都有⼀部分专门讲返回值的。这两个函数的返回值都是dest指针。可是为什么要返回dest指针呢?dest指针本来就是调⽤者传过去的,再返回⼀遍dest指针并没有提供任何有⽤的信息。之
所以这么规定是为了把函数调⽤当作⼀个指针类型的表达式使⽤,⽐如printf(strcpy(buf, "hello")),⼀举两得,如果strcpy的返回值是void就没有这么⽅便了。
CONFORMING TO部分描述了这个函数是遵照哪些标准实现的。strcpy和strncpy是C标准库函数,当然遵照C99标准。以后我们还会看到libc中有些函数属于POSIX标准但并不属于C标准,例如write(2)。
NOTES部分给出⼀些提⽰信息。这⾥指出如何确保strncpy的dest以'\0'结尾,和我们上⾯给出的代码类似,但由于n是个变量,在执⾏buf[n - 1]= '\0';之前先检查⼀下n是否⼤于0,如果n不⼤于0,buf[n - 1]就访问越界了,所以要避免。
BUGS部分说明了使⽤这些函数可能引起的Bug,这部分⼀定要仔细看。⽤strcpy⽐⽤strncpy更加不安全,如果在调⽤strcpy之前不仔细检查src字符串的长度就有可能写越界,这是⼀个很常见的错误,例如:
void foo(char*str)
{
char buf[10];
strcpy(buf, str);
......
}
str所指向的字符串有可能超过10个字符⽽导致写越界,在第4 节 “段错误”我们看到过,这种写越界可能当时不出错,⽽在函数返回时出现段错误,原因是写越界覆盖了保存在栈帧上的返回地址,函数返回时跳转到⾮法地址,因⽽出错。像buf这种由调⽤者分配并传给函数读或写的⼀段内存通常称为缓冲区(Buffer),缓冲区写越界的错误称为缓冲区溢出(Buffer Overflow)。如果只是出现段错误那还不算严重,更严重的是缓冲区溢出Bug经常被恶意⽤户利⽤,使函数返回时跳转到⼀个事先设好的地址,执⾏事先设好的指令,如果设计得巧妙甚⾄可以启动⼀个Shell,然后随⼼所欲执⾏任何命令,可想⽽知,如果⼀个⽤root权限执⾏的程序存在这样的Bug,被攻陷了,后果将会⾮常严重。

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。