函数原型:
#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