ILD

你真的了解snprintf吗
作者:Yuan Jianpeng 邮箱:yuanjianpeng@xiaomi.com
发布时间:2022-10-10 站点:Inside Linux Development

函数原型:

#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


Copyright © linuxdev.cc 2017-2024. Some Rights Reserved.