函数原型:
#include <stdio.h>
int snprintf(char *str, size_t size, const char *format, ...);
看看musl库的实现:
src/stdio/snprintf.c
1 2 3 4 5 6 7 8 9 10 11 12 | #include <stdio.h> #include <stdarg.h> int snprintf( char *restrict s, size_t n, const char *restrict fmt, ...) { int ret; va_list ap; va_start (ap, fmt); ret = vsnprintf(s, n, fmt, ap); va_end (ap); return ret; } |
src/stdio/vsnprintf.c
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 | #include "stdio_impl.h" #include <limits.h> #include <string.h> #include <errno.h> #include <stdint.h> struct cookie { char *s; size_t n; }; #define MIN(a, b) ((a) < (b) ? (a) : (b)) static size_t sn_write( FILE *f, const unsigned char *s, size_t l) { struct cookie *c = f->cookie; size_t k = MIN(c->n, f->wpos - f->wbase); if (k) { memcpy (c->s, f->wbase, k); c->s += k; c->n -= k; } k = MIN(c->n, l); if (k) { memcpy (c->s, s, k); c->s += k; c->n -= k; } *c->s = 0; f->wpos = f->wbase = f->buf; /* pretend to succeed, even if we discarded extra data */ return l; } int vsnprintf( char *restrict s, size_t n, const char *restrict fmt, va_list ap) { unsigned char buf[1]; char dummy[1]; struct cookie c = { .s = n ? s : dummy, .n = n ? n-1 : 0 }; FILE f = { .lbf = EOF, .write = sn_write, .lock = -1, .buf = buf, .cookie = &c, }; if (n > INT_MAX) { errno = EOVERFLOW; return -1; } *c.s = 0; return vfprintf (&f, fmt, ap); } |
最终会走到 src/stdio/vfprintf.c里面的vfprintf。printf也是走的这里,所有的打印最终归一到vfprintf。打印到内存的函数之前好奇为啥是在stdio.h头文件里面,原来它是通过借助vfprintf实现的。它模拟了一个FILE结构体,write函数为sn_write。
snprintf返回-1的情况:
vsnprintf会判断传入参数n,注意size_t是一个无符号整数类型,如果n大于INT_MAX,就返回-1.
其它返回-1的情况,就是vprintf了,在printf_core处理格式化字符串的时候发生错误,也会返回-1.
errno有EINVAL和EOVERFLOW两种。在写入FILE的时候也会返回-1。不过snprintf的sn_write是纯内存操作,不会返回错误。
所以返回-1,就两种情况n非法,或者格式化字符串非法。
snprintf不检查第一个参数str的非法性,传入一个NULL,直接导致程序奔溃。
n为0,是一个合法的值。
当n传入一个负数时,由于size_t是一个无符号数,会转成一个超大的正数。在musl上,如上会判断大于INT_MAX,返回-1。
但是在fedora的glibc上,没有这个判断,直接发生了越界。
1 2 3 4 | int len; char buf[5]; len = snprintf(buf, -1, "1234567890" ); |
上述代码,在musl上返回-1。
在glibc上,len返回10,但是buf写入了11个字符(包括结尾0),发生了越界:
$ ./a.out
len 10
*** stack smashing detected ***: terminated
Aborted (core dumped)
第一个参数str合法,n大于等于0,格式化字符串合法。
最多写入n个字符,保证不会发生越界,当n大于0时,结尾0总是会写入。
返回值是当内存足够时,写入的字符数(不包括结尾0)。
len = snprintf(buf, n, "1234567890");
所以当n大于等于0时,不管n是几,返回的len都是10。
因此,当len >= n时,可以断定发生了截断。
参考:
https://en.cppreference.com/w/c/types/size_t