printf 问题的思考

2020-03-01

前些时间,朋友问了一些问题:

  • printf 是线程安全的吗?
  • printf%s 格式输出,如果参数是其它类型的数据强制转换为 char* 的,结果会怎么样?

Linux 是开源的,何不查看源码理解程序工作原理呢?我们也可以通过 gdb 调试手段去深入理解源码的实现(gdb 调试 Linux 内核网络源码)。


1. 问题

1.1. 是否线程安全

我们写个 demo,gdb 一下看看,printf 是否线程安全的。

1
2
3
4
5
6
7
/* gcc -g -O0 test_printf.cpp -o tp */
#include <stdio.h>

int main(int argc, char** argv) {
    printf("hello %d.\n", 1);
    return 0;
}
  • gdb 调试 demo,发现 printf 的内部实现有锁操作,所以它是线程安全的。gdb 要支持调试进 glibc,系统需要安装一些插件才行。例如 Centos 的,可以参考视频:(Centos) Debugging in glibc with gdb
1
2
3
4
5
6
7
8
9
10
11
12
13
/* vfprintf.c. */
int vfprintf (FILE *s, const CHAR_T *format, va_list ap) {
  ...
  /* Lock stream.  */
  _IO_cleanup_region_start ((void (*) (void *)) &_IO_funlockfile, s);
  _IO_flockfile (s);
  ...
  all_done:
  ...
  _IO_funlockfile (s);
  _IO_cleanup_region_end (0);
  return done;
}
  • 查看 Linux 系统的 glibc 版本,然后到 glibc 下载地址 去下载对应版本的 glibc 源码去看看。
1
2
3
[wenfh2020] ldd --version
ldd (GNU libc) 2.17
Copyright (C) 2012 Free Software Foundation, Inc.
  • glibc 源码。
  • 其实 Linux 内核源码,也有各种版本的 printf 的实现,大家有兴趣也可以去 github 找来看看。

1.2. 强制转换问题

  • 64 位机器,int 大小是 4 bytes,而指针大小是 8 bytes。如果一个 int 类型的数据,被当作 char* 指针读取,那么将会多读 4 个字节数据,结果不可预料。
  • 从源码上看 strnlen 是通过查找内存的 ‘\0’ 字符串结束符的。如果强制转换的内存数据,没有 ‘\0’ 结束符,那 strnlen 就会出现问题。
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
/* https://github.com/torvalds/linux/blob/master/arch/x86/boot/printf.c */

int printf(const char *fmt, ...) {
    char printf_buf[1024];
    va_list args;
    int printed;

    va_start(args, fmt);
    printed = vsprintf(printf_buf, fmt, args);
    va_end(args);

    puts(printf_buf);

    return printed;
}

int vsprintf(char *buf, const char *fmt, va_list args) {
    ...
    switch (*fmt) {
        ...
        case 's':
            s = va_arg(args, char *);
            len = strnlen(s, precision);

            if (!(flags & LEFT))
                while (len < field_width--)
                    *str++ = ' ';
            for (i = 0; i < len; ++i)
                *str++ = *s++;
            while (len < field_width--)
                *str++ = ' ';
            continue;
    }
    ...
}

/* https://github.com/torvalds/linux/blob/master/arch/x86/boot/string.c */

size_t strnlen(const char *s, size_t maxlen) {
    const char *es = s;
    while (*es && maxlen) {
        es++;
        maxlen--;
    }

    return (es - s);
}

2. 其它的开源

例如字符串拷贝,也可以从 Linux 内核源码去找。

1
2
3
4
5
6
7
8
9
/* https://github.com/torvalds/linux/blob/master/lib/string.c */

char *strcpy(char *dest, const char *src) {
    char *tmp = dest;

    while ((*dest++ = *src++) != '\0')
        /* nothing */;
    return tmp;
}

3. 小结

  • 深入看内核源码是一个好习惯。
  • 要熟悉调试和看源码解决问题的方法和思路。