Pwn glibc FILE 结构及相关函数

本来想直接写利用的, 后来做了个题, 发现我理解的有一些偏差. 趁着最近状态好, 一鼓作气还是把前置知识写了吧.

总所周知, CPU 在从硬盘读取数据的时候, 需要经过一系列过程. 这个过程由操作系统控制. 读取一次对时间的开销比较大, 所以计算机科学家们就想了个办法, 一次读取一堆数据到内存里. 然后下次需要读取某些数据, 如果在内存里, 就可以直接从内存里读了. 这就是所谓的 缓冲区. 同样的, 向硬盘里写数据的时候, 也是先写到缓冲区中, 待缓冲区满后, 再一次性写入硬盘. 由于这是操作系统封装的, 所以这块缓冲区在内核区中.

/pwn-glibc-file-struct-and-related-functions/img/kernel-buf.png
内核缓冲区 (图源自 angelboy 课件)

为什么能够这样设计呢? 首先源于读取和写入都必须是 逐字节 的, 因为磁头在扇区上转的时候就是连续转的. 这样连续读取, 一定是效率最高的. 这种物理上的限制就决定了这种数据结构最好是顺序的. 这种形式被形象地称为 Stream.

题外话
英文里 Stream 就有这种一个个通过的性质, 形容溪流的时候可以用 stream, 形容大雁一排飞过的时候也可以用 stream. 就学了这么多年的中文来看, 暂时还没有「一流大雁」的说法.

操作系统向外提供的接口, 就是 read, write 等 IO 相关的系统调用. 站在这个角度来看, read 和 write 等系统调用, 能够充分利用内核中 IO 缓冲区的优势. 但是, 如果站在高级程序设计的角度来看, 多次使用 read, write 等系统调用, 会发生 类似内核直接读写硬盘 一样的问题: 准备的开销较大. 原因在于系统调用是通过中断来进行的, 中断需要保存上下文环境, 切换到内核态, 等操作系统处理完之后再恢复回来. 这一部分所花费的时间可能是比较大的. 解决这个问题的办法也很简单, 再来一个 缓冲区 就是了.

/pwn-glibc-file-struct-and-related-functions/img/user-buf.png
用户态缓冲区 (图源 angelboy 课件)

这个缓冲区处于用户态. 设想这样一个情况: 当程序需要读取时, 先通过一次 read 系统调用, 读取较多数据, 保存在用户内存的某个地方, 称为缓冲区. 接下来程序读取数据时, 就可以直接重缓冲区读取了. 这样就减少了系统调用的次数, 从而提高程序的效率.

系统调用之上, 就是 glibc 封装的库函数一层, 对外的接口是 fread, fwrite 等. 要使用用户缓冲区, 那么只有一个内核提供的文件描述符是完全不够的, 需要把这个用来描述文件的东西拓展一下. 这就就是 glibc 实现的 FILE 结构. 系统调用具有 open, close 等来打开, 关闭文件, 同样 glibc 也提供了 fopen, fclose 来打开, 关闭文件, 只不过操作的对象不再是文件描述符了, 而是 FILE 结构.

追根溯源一下, 我们来看看源码里 FILE 结构长什么样. 本文分析的 glibc 版本为 2.23.

首先去 stdio.h 里, 找到 FILE 的定义:

1
typedef struct _IO_FILE FILE;

_IO_FILElibio.h 里定义. 下面的代码去掉了一些不影响学习的宏.

 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
struct _IO_FILE {
  int _flags;       /* High-order word is _IO_MAGIC; rest is flags. */
#define _IO_file_flags _flags
  /* The following pointers correspond to the C++ streambuf protocol. */
  /* Note:  Tk uses the _IO_read_ptr and _IO_read_end fields directly. */
  char* _IO_read_ptr;   /* Current read pointer */
  char* _IO_read_end;   /* End of get area. */
  char* _IO_read_base;  /* Start of putback+get area. */
  char* _IO_write_base; /* Start of put area. */
  char* _IO_write_ptr;  /* Current put pointer. */
  char* _IO_write_end;  /* End of put area. */
  char* _IO_buf_base;   /* Start of reserve area. */
  char* _IO_buf_end;    /* End of reserve area. */
  /* The following fields are used to support backing up and undo. */
  char *_IO_save_base; /* Pointer to start of non-current get area. */
  char *_IO_backup_base;  /* Pointer to first valid character of backup area */
  char *_IO_save_end; /* Pointer to end of non-current get area. */

  struct _IO_marker *_markers;

  struct _IO_FILE *_chain;

  int _fileno;

  int _flags2;

  _IO_off_t _old_offset; /* This used to be _offset but it's too small.  */

  /* 1+column number of pbase(); 0 is unknown. */
  unsigned short _cur_column;
  signed char _vtable_offset;
  char _shortbuf[1];

  /*  char* _save_gptr;  char* _save_egptr; */

  _IO_lock_t *_lock;

  _IO_off64_t _offset;

  /* Wide character stream stuff.  */
  struct _IO_codecvt *_codecvt;
  struct _IO_wide_data *_wide_data;
  struct _IO_FILE *_freeres_list;
  void *_freeres_buf;

  size_t __pad5;
  int _mode;
  /* Make sure we don't get into trouble again.  */
  char _unused2[15 * sizeof (int) - 4 * sizeof (void *) - sizeof (size_t)];
};

解释一下一些重要的变量都是啥:

  • _flags: 文件的标志, 高部分是 _IO_MAGIC, 第部分是权限标志, 如最常见的 rwx. _IO_MAGIC 是固定值, 为 0xFBAD0000, 在 libio.h 里可以找到.
  • buf 的两个指针: buffer 起点和终点. 只有一块, read 或 write 使用这一块 (不是使用两块)
  • 接下来可以看到 read 和 write 的三组指针. 这就是指向对应缓冲区的指针. ptr 是当前流的位置, baseend 分别是缓冲区的起点和终点位置.
  • _chain: 所有打开文件都被链成了一张链表. 这里的 _chain 就是链表中下一个节点的指针. 链表表头为全局变量 _IO_list_all. 在初始化 stdin, stdout, stderr 的时候, 会把 _IO_list_all 指向 _IO_2_1_stderr_. 初始形成的链表为 _IO_2_1_stderr_ $\rightarrow$ _IO_2_1_stdout_ $\rightarrow$ _IO_2_1_stdin_.
  • _fileno: 文件描述符
  • _lock: 锁指针, 同步用.
  • _offset: 文件指针偏移 (系统调用中的).

总所周知, stdin, stdout, stderr 就是这样的一个 FILE 指针. 在 stdio.h 中定义:

1
2
3
4
/* Standard streams.  */
extern struct _IO_FILE *stdin;    /* Standard input stream.  */
extern struct _IO_FILE *stdout;   /* Standard output stream.  */
extern struct _IO_FILE *stderr;   /* Standard error output stream.  */

在程序初始化的时候会把他们指向 glibc 数据段上写好的对应结构, 分别为 _IO_2_1_stdin_, _IO_2_1_stdout_, _IO_2_1_stderr_. 这些结构在 libio.h 中定义:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
extern struct _IO_FILE_plus _IO_2_1_stdin_;
extern struct _IO_FILE_plus _IO_2_1_stdout_;
extern struct _IO_FILE_plus _IO_2_1_stderr_;
#ifndef _LIBC
#define _IO_stdin ((_IO_FILE*)(&_IO_2_1_stdin_))
#define _IO_stdout ((_IO_FILE*)(&_IO_2_1_stdout_))
#define _IO_stderr ((_IO_FILE*)(&_IO_2_1_stderr_))
#else
extern _IO_FILE *_IO_stdin attribute_hidden;
extern _IO_FILE *_IO_stdout attribute_hidden;
extern _IO_FILE *_IO_stderr attribute_hidden;
#endif

可以单看到, 这里定义的并不是 _IO_FILE 类型, 而是一个叫 _IO_FILE_plus 的. 他在 libioP.h 中定义:

1
2
3
4
5
6
7
8
9
/* We always allocate an extra word following an _IO_FILE.
   This contains a pointer to the function jump table used.
   This is for compatibility with C++ streambuf; the word can
   be used to smash to a pointer to a virtual function table. */
struct _IO_FILE_plus
{
  _IO_FILE file;
  const struct _IO_jump_t *vtable;
};

就是加了一个类似于 c++ 里的虚表. 通过注释也可以看到, 这个虚表就是用来同时 兼容 c++ 的 streambuf 而写的.

_IO_FILE_plus 里的 vtable 是一个 _IO_jump_t 结构的指针, 这个结构定义在 libioP.h:

 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
#define JUMP_FIELD(TYPE, NAME) TYPE NAME
struct _IO_jump_t
{
    JUMP_FIELD(size_t, __dummy);
    JUMP_FIELD(size_t, __dummy2);
    JUMP_FIELD(_IO_finish_t, __finish);
    JUMP_FIELD(_IO_overflow_t, __overflow);
    JUMP_FIELD(_IO_underflow_t, __underflow);
    JUMP_FIELD(_IO_underflow_t, __uflow);
    JUMP_FIELD(_IO_pbackfail_t, __pbackfail);
    /* showmany */
    JUMP_FIELD(_IO_xsputn_t, __xsputn);
    JUMP_FIELD(_IO_xsgetn_t, __xsgetn);
    JUMP_FIELD(_IO_seekoff_t, __seekoff);
    JUMP_FIELD(_IO_seekpos_t, __seekpos);
    JUMP_FIELD(_IO_setbuf_t, __setbuf);
    JUMP_FIELD(_IO_sync_t, __sync);
    JUMP_FIELD(_IO_doallocate_t, __doallocate);
    JUMP_FIELD(_IO_read_t, __read);
    JUMP_FIELD(_IO_write_t, __write);
    JUMP_FIELD(_IO_seek_t, __seek);
    JUMP_FIELD(_IO_close_t, __close);
    JUMP_FIELD(_IO_stat_t, __stat);
    JUMP_FIELD(_IO_showmanyc_t, __showmanyc);
    JUMP_FIELD(_IO_imbue_t, __imbue);
};

这里的变量由 JUMP_FIELD(TYPE, NAME) 宏封装了一下, 实际上就是定义 TYPE 类型的变量 NAME. 除了两个 dummy, 剩下的打 _IO 开头的都是函数指针. 每一个函数指针都在源码中有详细的注释说明功能. 这里仅以 __overflow 来举例:

1
2
3
4
5
6
7
8
9
# define _IO_JUMPS_FUNC(THIS) \
 (*(struct _IO_jump_t **) ((void *) &_IO_JUMPS_FILE_plus (THIS) \
         + (THIS)->_vtable_offset))
#define JUMP1(FUNC, THIS, X1) (_IO_JUMPS_FUNC(THIS)->FUNC) (THIS, X1)
/* The 'overflow' hook flushes the buffer.
   The second argument is a character, or EOF.
   It matches the streambuf::overflow virtual function. */
typedef int (*_IO_overflow_t) (_IO_FILE *, int);
#define _IO_OVERFLOW(FP, CH) JUMP1 (__overflow, FP, CH)

别看他玩的这么花, 实际上就是调用函数.

注意

源码里定义了几个如 JUMPX 的宏, 其中 X 是数字, 意思就是调用对应函数, 函数除了 _IO_FILE 指针外还要传入的参数个数.

同时还有一个 _IO_WOVERFLOW. 类似地, 其他函数指针也有, 暂时不管他. 因为我不会.

之后分析库函数的时候, 会碰到这写函数指针, 具体功能之后再来看.

了解了结构以后, 来看一下 glibc 提供的函数, 是怎么操作 FILE 的吧.

stdio.h 里找到:

1
2
3
4
5
/* Open a file and create a new stream for it.
   This function is a possible cancellation point and therefore not
   marked with __THROW.  */
extern FILE *fopen (const char *__restrict __filename,
        const char *__restrict __modes) __wur;

fopen 定义在 iofopen.c 中:

1
2
3
4
5
6
7
8
_IO_FILE *
_IO_new_fopen (const char *filename, const char *mode)
{
  return __fopen_internal (filename, mode, 1);
}
strong_alias (_IO_new_fopen, __new_fopen)
versioned_symbol (libc, _IO_new_fopen, _IO_fopen, GLIBC_2_1);
versioned_symbol (libc, __new_fopen, fopen, GLIBC_2_1);

实际上调用的都是 __fopen_internal. 下面的代码去掉了一些宏.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
_IO_FILE *
__fopen_internal (const char *filename, const char *mode, int is32)
{
  struct locked_FILE
  {
    struct _IO_FILE_plus fp;
    _IO_lock_t lock;
    struct _IO_wide_data wd;
  } *new_f = (struct locked_FILE *) malloc (sizeof (struct locked_FILE));
  if (new_f == NULL)
    return NULL;
  new_f->fp.file._lock = &new_f->lock;
  _IO_no_init (&new_f->fp.file, 1, 0, NULL, NULL);
  _IO_JUMPS (&new_f->fp) = &_IO_file_jumps;
  _IO_file_init (&new_f->fp);
  if (_IO_file_fopen ((_IO_FILE *) new_f, filename, mode, is32) != NULL)
    return __fopen_maybe_mmap (&new_f->fp.file);
  _IO_un_link (&new_f->fp);
  free (new_f);
  return NULL;
}

首先可以看到, 程序使用 malloc 开辟了一块内存用以存放新的 FILE 结构 + vtable ptr (即 _IO_FILE_plus), 然后还有一个锁和一个 _IO_wide_data. 这个 wide data 是用来处理宽字符的一个类似 FILE 的结构, 暂时不管他. 从这里能够看出, 由 fopen 打开的 FILE, 以及它的 vtable ptr 和 lock, 是放在 上的.

注意

这里有一个 is32 参数, 表示是 32 位还是 64 位. 由于不同字长的机器在打开文件方面稍有不同, 所以需要不同处理. 这里暂时忽略.

在源码中能够找到 fopen64 以及相关的定义.

接下来调用 _IO_no_init. 去简单初始化 FILE, 同样也去除一些宏.

 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
void
_IO_old_init (_IO_FILE *fp, int flags)
{
  fp->_flags = _IO_MAGIC|flags;
  fp->_flags2 = 0;
  fp->_IO_buf_base = NULL;
  fp->_IO_buf_end = NULL;
  fp->_IO_read_base = NULL;
  fp->_IO_read_ptr = NULL;
  fp->_IO_read_end = NULL;
  fp->_IO_write_base = NULL;
  fp->_IO_write_ptr = NULL;
  fp->_IO_write_end = NULL;
  fp->_chain = NULL; /* Not necessary. */
  fp->_IO_save_base = NULL;
  fp->_IO_backup_base = NULL;
  fp->_IO_save_end = NULL;
  fp->_markers = NULL;
  fp->_cur_column = 0;
  fp->_vtable_offset = 0;
  if (fp->_lock != NULL)
    _IO_lock_init (*fp->_lock);
}
void
_IO_no_init (_IO_FILE *fp, int flags, int orientation,
       struct _IO_wide_data *wd, const struct _IO_jump_t *jmp)
{
  _IO_old_init (fp, flags);
  fp->_mode = orientation;
  fp->_freeres_list = NULL;
}

可以看到, 首先进入 _IO_old_init, 设置一下 flag 为传入的 1 (加上魔数), 即权限 r. FILE 里其他东西一律清空 (锁清空对应地址里的值, 跟进 _IO_lock_init 可以看到是清空为 0).

然后设置 vtable 地址为 &_IO_file_jumps. _IO_file_jumps 是默认的虚表, 为 glibc 的一个全局变量, 定义在 fileops.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
#define JUMP_INIT(NAME, VALUE) VALUE
#define JUMP_INIT_DUMMY JUMP_INIT(dummy, 0), JUMP_INIT (dummy2, 0)
const struct _IO_jump_t _IO_file_jumps =
{
  JUMP_INIT_DUMMY,
  JUMP_INIT(finish, _IO_file_finish),
  JUMP_INIT(overflow, _IO_file_overflow),
  JUMP_INIT(underflow, _IO_file_underflow),
  JUMP_INIT(uflow, _IO_default_uflow),
  JUMP_INIT(pbackfail, _IO_default_pbackfail),
  JUMP_INIT(xsputn, _IO_file_xsputn),
  JUMP_INIT(xsgetn, _IO_file_xsgetn),
  JUMP_INIT(seekoff, _IO_new_file_seekoff),
  JUMP_INIT(seekpos, _IO_default_seekpos),
  JUMP_INIT(setbuf, _IO_new_file_setbuf),
  JUMP_INIT(sync, _IO_new_file_sync),
  JUMP_INIT(doallocate, _IO_file_doallocate),
  JUMP_INIT(read, _IO_file_read),
  JUMP_INIT(write, _IO_new_file_write),
  JUMP_INIT(seek, _IO_file_seek),
  JUMP_INIT(close, _IO_file_close),
  JUMP_INIT(stat, _IO_file_stat),
  JUMP_INIT(showmanyc, _IO_default_showmanyc),
  JUMP_INIT(imbue, _IO_default_imbue)
};
libc_hidden_data_def (_IO_file_jumps)

也就是说, 这个虚表是默认虚表. 其中的函数指针已经写好, 之后我们会从这里寻找需要的函数继续往下研究.

接着调用 _IO_file_init:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
#define _IO_pos_BAD ((_IO_off64_t)(-1))
#define CLOSED_FILEBUF_FLAGS \
  (_IO_IS_FILEBUF+_IO_NO_READS+_IO_NO_WRITES+_IO_TIED_PUT_GET)
versioned_symbol (libc, _IO_new_file_init, _IO_file_init, GLIBC_2_1);
void
_IO_new_file_init (struct _IO_FILE_plus *fp)
{
  /* POSIX.1 allows another file handle to be used to change the position
     of our file descriptor.  Hence we actually don't know the actual
     position before we do the first fseek (and until a following fflush). */
  fp->file._offset = _IO_pos_BAD;
  fp->file._IO_file_flags |= CLOSED_FILEBUF_FLAGS;
  _IO_link_in (fp);
  fp->file._fileno = -1;
}
libc_hidden_ver (_IO_new_file_init, _IO_file_init)

这个函数对 _offset 进行了初始化为 -1. 还设置了 flag. 调用 _IO_link_in 将其链接到 _IO_list_all 上之后, 设置了文件描述符 _fileno 为 -1. 其中, _IO_link_in 定义如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
void
_IO_link_in (struct _IO_FILE_plus *fp)
{
  if ((fp->file._flags & _IO_LINKED) == 0)
    {
      fp->file._flags |= _IO_LINKED;
      ... // 省略锁
      fp->file._chain = (_IO_FILE *) _IO_list_all;
      _IO_list_all = fp;
      ++_IO_list_all_stamp;
      ... // 省略锁
    }
}
libc_hidden_def (_IO_link_in)

可以看到, 如果已经有被 link 标志, 那么什么也不做. 否则设置标志, 并链接到 _IO_list_all 的头部. 之后操作次数记录 _IO_list_all_stamp 加 1. 非常简单.

准备工作结束了, 重要可以着手打开文件了, 也就是这里:

1
2
3
4
5
6
7
8
_IO_FILE *
__fopen_internal (const char *filename, const char *mode, int is32)
{
  ...
  if (_IO_file_fopen ((_IO_FILE *) new_f, filename, mode, is32) != NULL)
    return __fopen_maybe_mmap (&new_f->fp.file);
  ...
}

由于内容较多, 还是只看重要部分:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
_IO_FILE *
_IO_new_file_fopen (_IO_FILE *fp, const char *filename, const char *mode,
        int is32not64)
{
  int oflags = 0, omode;
  int read_write;
  int oprot = 0666;
  int i;
  _IO_FILE *result;
  const char *cs;
  const char *last_recognized;
  // 打开了就不用再开了
  if (_IO_file_is_open (fp))
    return 0;
  ... // 省略计算打开模式对应的编码 omode, oflags, read_write
  result = _IO_file_open (fp, filename, omode|oflags, oprot, read_write,
        is32not64);
  ... // 剩下一堆我也看不懂
  return result;
}
libc_hidden_ver (_IO_new_file_fopen, _IO_file_fopen)

设置完 flag 后, 调用 _IO_file_open:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
#define _IO_mask_flags(fp, f, mask) \
       ((fp)->_flags = ((fp)->_flags & ~(mask)) | ((f) & (mask)))
_IO_FILE *
_IO_file_open (_IO_FILE *fp, const char *filename, int posix_mode, int prot,
         int read_write, int is32not64)
{
  int fdesc;
  ... // 省略取消打开后的继续打开
  // 调用 open 系统调用
  fdesc = open (filename, posix_mode | (is32not64 ? 0 : O_LARGEFILE), prot);
  if (fdesc < 0)
    return NULL;
  fp->_fileno = fdesc;
  // 打开成后, 设置 FILE 的 flag
  _IO_mask_flags (fp, read_write,_IO_NO_READS+_IO_NO_WRITES+_IO_IS_APPENDING);
  ... // 省略处理 "+" 追加模式 和 取消打开
  return fp;
}
libc_hidden_def (_IO_file_open)

调用系统调用 open 打开文件, 并设置 _fileno 为文件描述符, 向 flag 中加入打开模式.

打开完后, 调用 __fopen_maybe_mmap 进行返回. 这里封装的目的是测试是否需要 mmap 来得到 FILE 的空间, 我们可以不管它.

1
2
3
4
5
6
_IO_FILE *
__fopen_maybe_mmap (_IO_FILE *fp)
{
  ... // 省略可能的 mmap 相关
  return fp;
}

如果打开失败, 则 unlink, free 之前申请的空间, 并返回 NULL:

1
2
3
4
5
6
7
8
_IO_FILE *
__fopen_internal (const char *filename, const char *mode, int is32)
{
  ...
  _IO_un_link (&new_f->fp);
  free (new_f);
  return NULL;
}

函数流程大致如下:

  1. malloc 分配 FILE + vtable + lock 空间
  2. 初始化 FILE, 设置 vtable 到 glibc 写好的各个函数上
  3. 将 FILE 链入 _IO_list_all
  4. 系统调用 open 打开文件, 并设置文件描述符号

stdio.h 里找到:

1
2
3
4
5
/* Read chunks of generic data from STREAM.
   This function is a possible cancellation point and therefore not
   marked with __THROW.  */
extern size_t fread (void *__restrict __ptr, size_t __size,
         size_t __n, FILE *__restrict __stream) __wur;

找到 iofread.c 中的定义:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
_IO_size_t
_IO_fread (void *buf, _IO_size_t size, _IO_size_t count, _IO_FILE *fp)
{
  _IO_size_t bytes_requested = size * count;  // 计算需要读取的字节
  _IO_size_t bytes_read;
  CHECK_FILE (fp, 0);
  if (bytes_requested == 0)
    return 0;
  _IO_acquire_lock (fp);
  bytes_read = _IO_sgetn (fp, (char *) buf, bytes_requested);
  _IO_release_lock (fp);
  return bytes_requested == bytes_read ? count : bytes_read / size;
}
libc_hidden_def (_IO_fread)
#ifdef weak_alias
weak_alias (_IO_fread, fread)

CHECK_FILE 现在什么也不做, 以前会检查魔数 (就是上面说过的 _IO_MAGIC). _IO_acquire_lock_IO_release_lock 分别是获得锁和释放锁. 稍微有一点点复杂, 这里只简单介绍一下. IO 锁和其他操均被定义在 stdio-lock.h 中:

1
typedef struct { int lock; int cnt; void *owner; } _IO_lock_t;

简单来说, 就是设置这个结构的三个变量, 锁类型 lock, 一个类似于信号量的东西 cnt, 和所拥有该锁的线程 owner. 锁没有被任何一个线程拥有的话, 三个变量的值都是 0 (NULL). 可以参考源代码. (nmd 为什么写宏写得这么复杂, 看都看不懂)

接下来进入 _IO_sgetn 函数中, 该函数定义在 genops.c 中:

1
2
3
4
5
6
7
_IO_size_t
_IO_sgetn (_IO_FILE *fp, void *data, _IO_size_t n)
{
  /* FIXME handle putback buffer here! */
  return _IO_XSGETN (fp, data, n);
}
libc_hidden_def (_IO_sgetn)

最后调用了 _IO_XSGETN. 这玩意就是调用对应 vtable 中的 __xsgetn 函数指针. 从名字也可以看出来, 就是读取 n 个字节. 当 vtable 初始化后, __xsgetn 指向的函数为 _IO_file_xsgetn. 定义在 fileops.c 中. 下面的代码省略了 backup 部分.

 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
56
57
58
59
60
61
_IO_size_t
_IO_file_xsgetn (_IO_FILE *fp, void *data, _IO_size_t n)
{
  _IO_size_t want, have;
  _IO_ssize_t count;
  char *s = data;
  want = n;
  if (fp->_IO_buf_base == NULL) {
    ... // 省略 backup
    _IO_doallocbuf (fp);
  }
  while (want > 0) {
    have = fp->_IO_read_end - fp->_IO_read_ptr;
    if (want <= have) {
      memcpy (s, fp->_IO_read_ptr, want);
      fp->_IO_read_ptr += want;
      want = 0;
    }
    else {
      if (have > 0) {
        s = __mempcpy (s, fp->_IO_read_ptr, have);
        want -= have;
        fp->_IO_read_ptr += have;
      }
      ... // 省略 bakcup
      /* If we now want less than a buffer, underflow and repeat
         the copy.  Otherwise, _IO_SYSREAD directly to
         the user buffer. */
      if (fp->_IO_buf_base && want < (size_t) (fp->_IO_buf_end - fp->_IO_buf_base)) {
          if (__underflow (fp) == EOF)
            break;
          continue;
      }
      /* These must be set before the sysread as we might longjmp out
         waiting for input. */
      _IO_setg (fp, fp->_IO_buf_base, fp->_IO_buf_base, fp->_IO_buf_base);
      _IO_setp (fp, fp->_IO_buf_base, fp->_IO_buf_base);
      /* Try to maintain alignment: read a whole number of blocks.  */
      count = want;
      if (fp->_IO_buf_base) {
        _IO_size_t block_size = fp->_IO_buf_end - fp->_IO_buf_base;
        if (block_size >= 128)
          count -= want % block_size;
      }
      count = _IO_SYSREAD (fp, s, count);
      if (count <= 0) {
        if (count == 0)
          fp->_flags |= _IO_EOF_SEEN;
        else
          fp->_flags |= _IO_ERR_SEEN;
        break;
      }
      s += count;
      want -= count;
      if (fp->_offset != _IO_pos_BAD)
        _IO_pos_adjust (fp->_offset, count);
    }
  }
  return n - want;
}
libc_hidden_def (_IO_file_xsgetn)

一部分一部分来看.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
_IO_size_t
_IO_file_xsgetn (_IO_FILE *fp, void *data, _IO_size_t n)
{
  ...
  if (fp->_IO_buf_base == NULL) {
    ... // 省略 backup
    _IO_doallocbuf (fp);
  }
  ...
}

这一块是看文件有没有设置过缓冲区, 如果没有 (比如刚 fopen), 则调用 _IO_doallocbuf 函数去设置缓冲区. 改函数定义在 genops.c 中:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
void
_IO_doallocbuf (_IO_FILE *fp)
{
  if (fp->_IO_buf_base)
    return;
  if (!(fp->_flags & _IO_UNBUFFERED) || fp->_mode > 0)
    if (_IO_DOALLOCATE (fp) != EOF)
      return;
  _IO_setb (fp, fp->_shortbuf, fp->_shortbuf+1, 0);
}
libc_hidden_def (_IO_doallocbuf)

进行一些检查后, 进入 _IO_DOALLOCATE. 这个函数是 vtable 中的函数指针, 初始化后指向 _IO_file_doallocate:

 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
#define _G_BUFSIZ 8192
#define _IO_BUFSIZ _G_BUFSIZ
int
_IO_file_doallocate (_IO_FILE *fp)
{
  _IO_size_t size;
  char *p;
  struct stat64 st;
  ... // 省略, 后面有这部分的东西
  size = _IO_BUFSIZ;
  // 检查, 获得文件状态 stat.
  if (fp->_fileno >= 0 && __builtin_expect (_IO_SYSSTAT (fp, &st), 0) >= 0) {
    // 如果是字符型设备
    if (S_ISCHR (st.st_mode)) {
    /* Possibly a tty.  */
      if (
#ifdef DEV_TTY_P
          DEV_TTY_P (&st) ||
#endif
          local_isatty (fp->_fileno))
        fp->_flags |= _IO_LINE_BUF;
    }
    // 将一块的大小设置为 文件中设置的 块大小.
    if (st.st_blksize > 0)
      size = st.st_blksize;
  }
  // 调用 malloc 分配缓冲区空间
  p = malloc (size);
  if (__glibc_unlikely (p == NULL))
    return EOF;
  // 设置指针
  _IO_setb (fp, p, p + size, 1);
  return 1;
}
libc_hidden_def (_IO_file_doallocate)

函数的整个逻辑很简单, 就是调用 _IO_SYSSTAT 获取文件属性中一块的大小, 没有的话就使用默认的 8KB, 然后 malloc 这么多, 最后调用 _IO_setb 去设置指针.

_IO_SYSSTAT 也是 vtable 中的一项. 初始化后默认指向 _IO_file_stat:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
int
_IO_file_stat (_IO_FILE *fp, void *st)
{
  return __fxstat64 (_STAT_VER, fp->_fileno, (struct stat64 *) st);
}
libc_hidden_def (_IO_file_stat)
int
__fxstat (int vers, int fd, struct stat *buf)
{
  if (vers == _STAT_VER_KERNEL || vers == _STAT_VER_LINUX)
    return INLINE_SYSCALL (fstat, 2, fd, buf);
  __set_errno (EINVAL);
  return -1;
}
strong_alias (__fxstat, __fxstat64);

就是一个 fstat 系统调用.

_IO_setbgenops.c 中定义:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
void
_IO_setb (_IO_FILE *f, char *b, char *eb, int a)
{
  // 如果使用用户的 buffer, 则考虑把自己的 buffer free 掉
  if (f->_IO_buf_base && !(f->_flags & _IO_USER_BUF))
    free (f->_IO_buf_base);
  f->_IO_buf_base = b;
  f->_IO_buf_end = eb;
  // 设置是否使用用户 buffer
  if (a)
    f->_flags &= ~_IO_USER_BUF;
  else
    f->_flags |= _IO_USER_BUF;
}
libc_hidden_def (_IO_setb)

也就是设置了 _IO_buf_base_IO_buf_end 指针, 同时更新了一下 flag.

问题

_IO_file_doallocate 省略的地方是一个指针, 类似于 hook, 或许是一种利用方式?

1
2
3
4
5
6
7
8
#ifndef _LIBC
  /* If _IO_cleanup_registration_needed is non-zero, we should call the
     function it points to.  This is to make sure _IO_cleanup gets called
     on exit.  We call it from _IO_file_doallocate, since that is likely
     to get called by any program that does buffered I/O. */
  if (__glibc_unlikely (_IO_cleanup_registration_needed != NULL))
    (*_IO_cleanup_registration_needed) ();
#endif

_IO_cleanup_registration_needed 在 libc 数据段 (是吗?), 和 _IO_list_all 相邻. 定义在 libioP.h 中:

1
2
3
extern struct _IO_FILE_plus *_IO_list_all;
libc_hidden_proto (_IO_list_all)
extern void (*_IO_cleanup_registration_needed) (void);

如果 malloc 失败, 会尝试使用 _IO_setb (fp, fp->_shortbuf, fp->_shortbuf+1, 0); 设置一个较小的缓冲区.

分配完空间以后, 就可以开始读取了. while 循环比较长, 同样是一点点看.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
_IO_size_t
_IO_file_xsgetn (_IO_FILE *fp, void *data, _IO_size_t n)
{
  while (want > 0) {
    have = fp->_IO_read_end - fp->_IO_read_ptr;
    if (want <= have) {
      memcpy (s, fp->_IO_read_ptr, want);
      fp->_IO_read_ptr += want;
      want = 0;
    }
    if (have > 0) {
      s = __mempcpy (s, fp->_IO_read_ptr, have);
      want -= have;
      fp->_IO_read_ptr += have;
    }
    ...
  }
  return n - want;
}

如果 read buf 有可以读的内容, 并且需要的可以满足, 那么就把这么多读到传入的地址 (data) 对应部分 (由指针 s 控制). 不够的话, 先把有的这部分读了. 当然第一次读取的时候, 由于 read 相关的三个指针都是 NULL, 所以 have 为 0.

到这里, 还需要读取, 可是当前缓冲区已经没得读了. 那么接下来分成两个部分:

  1. 如果一整块缓冲区能够满足 want, 那么就读取到缓冲区上
  2. 如果一整块都不能满足, 那么直接使用系统调用读取, 没必要再 copy 到缓冲区上了
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
_IO_size_t
_IO_file_xsgetn (_IO_FILE *fp, void *data, _IO_size_t n)
{
  while (want > 0) {
    ...
    else {
      ...
      /* If we now want less than a buffer, underflow and repeat
         the copy.  Otherwise, _IO_SYSREAD directly to
         the user buffer. */
      if (fp->_IO_buf_base && want < (size_t) (fp->_IO_buf_end - fp->_IO_buf_base)) {
          if (__underflow (fp) == EOF)
            break;
          continue;
      }
      ...
    }
    ...
  }
  return n - want;
}

这个 if 就是在判断是刷新缓冲区还是直接系统调用 read. 如果刷新缓冲区的话, 刷新完了就 continue 去读取.

刷新缓冲区就一个函数 __underflow. 叫 underflow 的原因是, 读取的缓冲区是 ptr 到 end 这部分有效, 在 base 到 end 的上部分, 而读取之后, 缓冲区的下部分 ptr 溢出了. 对应 overflow 是因为向缓冲区写数据的时候, 有数据的部分是 base 到 ptr, 是整个 base 到 end 的下部分. 写满了, 就向上溢出了.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
int
__underflow (_IO_FILE *fp)
{
  ... // 省略 wide
  if (_IO_in_put_mode (fp))
    if (_IO_switch_to_get_mode (fp) == EOF)
      return EOF;
  if (fp->_IO_read_ptr < fp->_IO_read_end)
    return *(unsigned char *) fp->_IO_read_ptr;
  ... // 省略 backup
  return _IO_UNDERFLOW (fp);
}
libc_hidden_def (__underflow)

如果这个文件之前是写入模式, 那么需要切换到读取模式 (因为缓冲区只有一个, 要读就不能同时写), 调用 _IO_switch_to_get_mode, 如果写缓冲区有东西, 那么调用 overflow 将写的缓冲区中数据写入文件, 然后设置三个 write 的指针相等, 接着找到写入的位置, 然后检查是否真的下溢出了. 如果是, 则调用 vtable 中的函数 _IO_UNDERFLOW:

 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
int
_IO_new_file_underflow (_IO_FILE *fp)
{
  _IO_ssize_t count;
  // 检查可读权限
  if (fp->_flags & _IO_NO_READS) {
      fp->_flags |= _IO_ERR_SEEN;
      __set_errno (EBADF);
      return EOF;
  }
  // 检查是否真的需要刷新缓冲区
  if (fp->_IO_read_ptr < fp->_IO_read_end)
    return *(unsigned char *) fp->_IO_read_ptr;
  // 检查是否还没有缓冲区, 或者使用 backup, 省略 backup
  if (fp->_IO_buf_base == NULL) {
    ...
    _IO_doallocbuf (fp);
  }
  ... // 省略如果是行设备 (stdout?), 则 IO_OVERFLOW 将数据写到 stdout, 不重要, 不管他
  // 又来一次...
  _IO_switch_to_get_mode (fp);
  /* This is very tricky. We have to adjust those
     pointers before we call _IO_SYSREAD () since
     we may longjump () out while waiting for
     input. Those pointers may be screwed up. H.J. */
  // 设置指针先指向 base
  fp->_IO_read_base = fp->_IO_read_ptr = fp->_IO_buf_base;
  fp->_IO_read_end = fp->_IO_buf_base;
  fp->_IO_write_base = fp->_IO_write_ptr = fp->_IO_write_end = fp->_IO_buf_base;
  // 调用 vtable 中的 _IO_SYSREAD 读取数据
  count = _IO_SYSREAD (fp, fp->_IO_buf_base,
           fp->_IO_buf_end - fp->_IO_buf_base);
  // 读取到 EOF 或者读取失败, 设置 flag
  if (count <= 0) {
      if (count == 0)
        fp->_flags |= _IO_EOF_SEEN;
      else
        fp->_flags |= _IO_ERR_SEEN, count = 0;
  }
  fp->_IO_read_end += count;
  // 处理读取到 EOF
  // 设置 offset, 加上读取了的数据
  if (count == 0) {
      /* If a stream is read to EOF, the calling application may switch active
   handles.  As a result, our offset cache would no longer be valid, so
   unset it.  */
    fp->_offset = _IO_pos_BAD;
    return EOF;
  }
  if (fp->_offset != _IO_pos_BAD)
    fp->_offset += count;
  return *(unsigned char *) fp->_IO_read_ptr;
}
libc_hidden_ver (_IO_new_file_underflow, _IO_file_underflow)

就是先检查, 然后再进行必要的设置, 调用 _IO_SYSREAD 后, 进行后续设置, 并返回 read ptr.

_IO_SYSREAD 是 vtable 的一项, 初始化后默认是 _IO_file_read:

1
2
3
4
5
6
7
8
_IO_ssize_t
_IO_file_read (_IO_FILE *fp, void *buf, _IO_ssize_t size)
{
  return (__builtin_expect (fp->_flags2 & _IO_FLAGS2_NOTCANCEL, 0)
    ? read_not_cancel (fp->_fileno, buf, size)
    : read (fp->_fileno, buf, size));
}
libc_hidden_def (_IO_file_read)

这里就是封装了一下 read 系统调用, not cacel 看不懂 先不管.

这样, 缓冲区就刷新完了, read base 到 read ptr 上就又是数据了.

 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
_IO_size_t
_IO_file_xsgetn (_IO_FILE *fp, void *data, _IO_size_t n)
{
  while (want > 0) {
    ...
    else {
      ...
      /* These must be set before the sysread as we might longjmp out
         waiting for input. */
      // 这两个是宏, 就是设置 read 和 write 的三个指针都为 _IO_buf_base
      _IO_setg (fp, fp->_IO_buf_base, fp->_IO_buf_base, fp->_IO_buf_base);
      _IO_setp (fp, fp->_IO_buf_base, fp->_IO_buf_base);
      /* Try to maintain alignment: read a whole number of blocks.  */
      count = want;
      if (fp->_IO_buf_base) {
        _IO_size_t block_size = fp->_IO_buf_end - fp->_IO_buf_base;
        if (block_size >= 128)
          count -= want % block_size;
      }
      // 系统调用 read 读取
      count = _IO_SYSREAD (fp, s, count);
      if (count <= 0) {
        // 读完了
        if (count == 0)
          fp->_flags |= _IO_EOF_SEEN;
        // 读取失败
        else
          fp->_flags |= _IO_ERR_SEEN;
        break;
      }
      s += count;
      want -= count;
      if (fp->_offset != _IO_pos_BAD)
        fp->_offset += count;
    }
  }
  return n - want;
}

函数大致流程如下:

  1. 调用 vtable 中的 _IO_XSGETN (_IO_file_xsgetn)
  2. 如果没有缓冲区, 则调用 vtable 中的 _IO_DOALLOCATE (_IO_file_doallocate) 分配.
  • 调用 vtable 中的 _IO_SYSSTAT (_IO_file_stat) 查看文件信息, 设置缓冲区大小
  • malloc 一块区域作为缓冲区
  1. 读取缓冲区
  • 使用 _IO_UNDERFLOW (_IO_new_file_underflow) 刷新缓冲区
    • 如果之前在写, 则还调用到 _IO_OVERFLOW 刷新写的缓冲区

fwrite 和 fread 封装的逻辑上差不多, 这里就直接从 vtable 中的 _IO_XSPUTN (_IO_new_file_xsputn) 开始:

 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
56
57
58
59
60
61
62
63
64
65
66
67
_IO_size_t
_IO_new_file_xsputn (_IO_FILE *f, const void *data, _IO_size_t n)
{
  const char *s = (const char *) data;
  _IO_size_t to_do = n;
  int must_flush = 0;
  _IO_size_t count = 0;
  if (n <= 0)
    return 0;
  /* This is an optimized implementation.
     If the amount to be written straddles a block boundary
     (or the filebuf is unbuffered), use sys_write directly. */
  /* First figure out how much space is available in the buffer. */
  if ((f->_flags & _IO_LINE_BUF) && (f->_flags & _IO_CURRENTLY_PUTTING)) {
    count = f->_IO_buf_end - f->_IO_write_ptr;
    if (count >= n) {
      // 由于行设备 (stdout) 需要逐行显示, 那么找到最后一个回车
      const char *p;
      for (p = s + n; p > s; ) {
        if (*--p == '\n') {
          count = p - s + 1;  // 将要能够写入的字节 count 设为到这个回车这里
          must_flush = 1;     // 同时设置 must flush 为 1
          break;
        }
      }
    }
  }
  // 如果不是行设备, 那么可用空间就是 end - ptr
  else if (f->_IO_write_end > f->_IO_write_ptr)
    count = f->_IO_write_end - f->_IO_write_ptr; /* Space available. */
  /* Then fill the buffer. */
  // 写入的数据不溢出缓冲区, 则写入缓冲区
  // 超出的话, 先填满
  if (count > 0) {
    if (count > to_do)
      count = to_do;
    f->_IO_write_ptr = __mempcpy (f->_IO_write_ptr, s, count);
    s += count;
    to_do -= count;
  }
  if (to_do + must_flush > 0) {
    _IO_size_t block_size, do_write;
    /* Next flush the (full) buffer. */
    if (_IO_OVERFLOW (f, EOF) == EOF)
      /* If nothing else has to be written we must not signal the
         caller that everything has been written.  */
      return to_do == 0 ? EOF : n - to_do;
    /* Try to maintain alignment: write a whole number of blocks.  */
    block_size = f->_IO_buf_end - f->_IO_buf_base;
    // do_write 是整数块所有的大小
    do_write = to_do - (block_size >= 128 ? to_do % block_size : 0);
    if (do_write) {
      count = new_do_write (f, s, do_write);
      to_do -= count;
      if (count < do_write)
        return n - to_do;
    }
    /* Now write out the remainder.  Normally, this will fit in the
       buffer, but it's somewhat messier for line-buffered files,
       so we let _IO_default_xsputn handle the general case. */
    // 剩下比一块小的部分
    if (to_do)
      to_do -= _IO_default_xsputn (f, s+do_write, to_do);
  }
  return n - to_do;
}
libc_hidden_ver (_IO_new_file_xsputn, _IO_file_xsputn)

首先处理了一下行缓冲设备的情况, 然后写满 buf, 不需要刷新缓冲区的话, 就结束了.

需要刷新缓冲区的话, 就会先调用 vtable 中的 _IO_OVERFLOW (_IO_new_file_overflow) 去刷新, 将缓冲区内的数据写入文件:

 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
int _IO_new_file_overflow (_IO_FILE *f, int ch) {
  // 检查权限
  if (f->_flags & _IO_NO_WRITES) /* SET ERROR */
    {
      f->_flags |= _IO_ERR_SEEN;
      __set_errno (EBADF);
      return EOF;
    }
  /* If currently reading or no buffer allocated. */
  if ((f->_flags & _IO_CURRENTLY_PUTTING) == 0 || f->_IO_write_base == NULL) {
    /* Allocate a buffer if needed. */
    if (f->_IO_write_base == NULL) {
      _IO_doallocbuf (f);
      _IO_setg (f, f->_IO_buf_base, f->_IO_buf_base, f->_IO_buf_base);
    }
      /* Otherwise must be currently reading.
         If _IO_read_ptr (and hence also _IO_read_end) is at the buffer end,
         logically slide the buffer forwards one block (by setting the
         read pointers to all point at the beginning of the block).  This
         makes room for subsequent output.
         Otherwise, set the read pointers to _IO_read_end (leaving that
         alone, so it can continue to correspond to the external position). */
    ... // 省略 backup

    // 看不懂的设置......
    if (f->_IO_read_ptr == f->_IO_buf_end)
      f->_IO_read_end = f->_IO_read_ptr = f->_IO_buf_base;
    f->_IO_write_ptr = f->_IO_read_ptr;
    f->_IO_write_base = f->_IO_write_ptr;
    f->_IO_write_end = f->_IO_buf_end;
    f->_IO_read_base = f->_IO_read_ptr = f->_IO_read_end;
    f->_flags |= _IO_CURRENTLY_PUTTING;
    if (f->_mode <= 0 && f->_flags & (_IO_LINE_BUF | _IO_UNBUFFERED))
      f->_IO_write_end = f->_IO_write_ptr;
  }
  if (ch == EOF)
    return _IO_do_write (f, f->_IO_write_base, f->_IO_write_ptr - f->_IO_write_base);
  ... // 由于上层函数传入的 ch 是 EOF, 所以后面都不会执行到了, 就不看了吧
}
libc_hidden_ver (_IO_new_file_overflow, _IO_file_overflow)

经过一些检查和设置以后, 调用 _IO_do_write 向文件中写入缓冲区的内容.

1
2
3
4
5
6
_IO_new_do_write (_IO_FILE *fp, const char *data, _IO_size_t to_do)
{
  return (to_do == 0
    || (_IO_size_t) new_do_write (fp, data, to_do) == to_do) ? 0 : EOF;
}
libc_hidden_ver (_IO_new_do_write, _IO_do_write)

由于缓冲区是整块写的, 所以这里调用 new_do_write, 这个函数在 系统调用写 部分讲.

回到 _IO_new_file_xsputn, 如果要写的数据大于一块, 那么直接用系统调用. 计算出整数块的大小后, 调用 new_do_write:

 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
static
_IO_size_t
new_do_write (_IO_FILE *fp, const char *data, _IO_size_t to_do)
{
  _IO_size_t count;
  if (fp->_flags & _IO_IS_APPENDING)
    /* On a system without a proper O_APPEND implementation,
       you would need to sys_seek(0, SEEK_END) here, but is
       not needed nor desirable for Unix- or Posix-like systems.
       Instead, just indicate that offset (before and after) is
       unpredictable. */
    fp->_offset = _IO_pos_BAD;
  else if (fp->_IO_read_end != fp->_IO_write_base)
    {
      _IO_off64_t new_pos
  = _IO_SYSSEEK (fp, fp->_IO_write_base - fp->_IO_read_end, 1);
      if (new_pos == _IO_pos_BAD)
  return 0;
      fp->_offset = new_pos;
    }
  count = _IO_SYSWRITE (fp, data, to_do);
  if (fp->_cur_column && count)
    fp->_cur_column = _IO_adjust_column (fp->_cur_column - 1, data, count) + 1;
  _IO_setg (fp, fp->_IO_buf_base, fp->_IO_buf_base, fp->_IO_buf_base);
  fp->_IO_write_base = fp->_IO_write_ptr = fp->_IO_buf_base;
  fp->_IO_write_end = (fp->_mode <= 0
           && (fp->_flags & (_IO_LINE_BUF | _IO_UNBUFFERED))
           ? fp->_IO_buf_base : fp->_IO_buf_end);
  return count;
}

如果之前在读, 则调用 vtable 中的 _IO_SYSSEEK (_IO_file_seek) 去找到写入的位置:

1
2
3
4
5
6
_IO_off64_t
_IO_file_seek (_IO_FILE *fp, _IO_off64_t offset, int dir)
{
  return __lseek64 (fp->_fileno, offset, dir);
}
libc_hidden_def (_IO_file_seek)

接着调用 vtable 中的 _IO_SYSWRITE (_IO_new_file_write) 进行写入:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
_IO_ssize_t
_IO_new_file_write (_IO_FILE *f, const void *data, _IO_ssize_t n)
{
  _IO_ssize_t to_do = n;
  while (to_do > 0)
    {
      _IO_ssize_t count = (__builtin_expect (f->_flags2
               & _IO_FLAGS2_NOTCANCEL, 0)
         ? write_not_cancel (f->_fileno, data, to_do)
         : write (f->_fileno, data, to_do));
      if (count < 0)
  {
    f->_flags |= _IO_ERR_SEEN;
    break;
  }
      to_do -= count;
      data = (void *) ((char *) data + count);
    }
  n -= to_do;
  if (f->_offset >= 0)
    f->_offset += n;
  return n;
}

这里用 while 循环来写多个块. 同样有一个 not cancel 的判断.

写完以后, 重新设置指针.

如果还有剩余部分, 则调用 _IO_default_xsputn 写:

 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
_IO_size_t
_IO_default_xsputn (_IO_FILE *f, const void *data, _IO_size_t n)
{
  const char *s = (char *) data;
  _IO_size_t more = n;
  if (more <= 0)
    return 0;
  for (;;) {
    /* Space available. */
    if (f->_IO_write_ptr < f->_IO_write_end) {
      _IO_size_t count = f->_IO_write_end - f->_IO_write_ptr;
      if (count > more)
        count = more;
      // 大于 20 个用 memcpy
      if (count > 20) {
          f->_IO_write_ptr = __mempcpy (f->_IO_write_ptr, s, count);
          s += count;
      }
      // 小于的话直接循环写
      else if (count) {
        char *p = f->_IO_write_ptr;
        _IO_ssize_t i;
        for (i = count; --i >= 0; )
          *p++ = *s++;
        f->_IO_write_ptr = p;
      }
      more -= count;
    }
    // 还需要写的话, 说明缓冲区满了, 刷新一下
    // 但是由于上层函数满足了条件, 这里一般不会执行到
    if (more == 0 || _IO_OVERFLOW (f, (unsigned char) *s++) == EOF)
      break;
    more--;
  }
  return n - more;
}
libc_hidden_def (_IO_default_xsputn)

函数大致调用过程如下:

  1. 调用 vtable 中的 _IO_XSPUTN (_IO_new_file_xsputn)
  2. 先尝试填满缓冲区, 没填满的话就结束了
  3. 需要刷新缓冲区, 会调用 _IO_OVERFLOW (_IO_new_file_overflow) (第一次 overflow)
  • 如果没有缓冲区, 则调用 _IO_DOALLOCBUF (_IO_file_doallocate) malloc 分配
  • 在写之前如果是读, 则调用 _IO_SYSSEEK (_IO_file_seek) 中的系统调用 fseek 找到正确的写位置
  • 然后调用 _IO_SYSWRITE (_IO_new_file_write) 中的系统调用 write 写
  1. 第一次 overflow 还不够, 则计算写入数据是不是比正块大, 是的话整块 _IO_SYSWRITE (_IO_new_file_write) 系统调用 write 写
  2. 如果有剩余部分, 再调用 _IO_default_xsputn 写剩余部分到缓冲区上

fclose 定义在 iofclose.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
int
_IO_new_fclose (_IO_FILE *fp)
{
  int status;
  ...
  /* First unlink the stream.  */
  if (fp->_IO_file_flags & _IO_IS_FILEBUF)
    _IO_un_link ((struct _IO_FILE_plus *) fp);
  _IO_acquire_lock (fp);
  if (fp->_IO_file_flags & _IO_IS_FILEBUF)
    status = _IO_file_close_it (fp);
  else
    status = fp->_flags & _IO_ERR_SEEN ? -1 : 0;
  _IO_release_lock (fp);
  _IO_FINISH (fp);
  if (fp->_mode > 0)
    ...
  else {
    ...
    if (fp != _IO_stdin && fp != _IO_stdout && fp != _IO_stderr) {
      fp->_IO_file_flags = 0;
      free(fp);
    }
  }
  return status;
}
versioned_symbol (libc, _IO_new_fclose, _IO_fclose, GLIBC_2_1);
strong_alias (_IO_new_fclose, __new_fclose)
versioned_symbol (libc, __new_fclose, fclose, GLIBC_2_1);

首先会调用 _IO_un_link, 将其从 _IO_list_all 中删除:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
void
_IO_un_link (struct _IO_FILE_plus *fp)
{
  if (fp->file._flags & _IO_LINKED) {
    struct _IO_FILE **f;
    ... // 省略锁
    if (_IO_list_all == NULL) ;
    else if (fp == _IO_list_all) {
      _IO_list_all = (struct _IO_FILE_plus *) _IO_list_all->file._chain;
      ++_IO_list_all_stamp;
    }
    else
    for (f = &_IO_list_all->file._chain; *f; f = &(*f)->_chain)
      if (*f == (_IO_FILE *) fp) {
        *f = fp->file._chain;
        ++_IO_list_all_stamp;
        break;
      }
    fp->file._flags &= ~_IO_LINKED;
    ... // 省略锁
  }
}
libc_hidden_def (_IO_un_link)

然后调用 vtable 中的 _IO_FINISH (_IO_new_file_finish):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
void
_IO_new_file_finish (_IO_FILE *fp, int dummy)
{
  if (_IO_file_is_open (fp)) {
    _IO_do_flush (fp);
    if (!(fp->_flags & _IO_DELETE_DONT_CLOSE))
      _IO_SYSCLOSE (fp);
  }
  _IO_default_finish (fp, 0);
}
libc_hidden_ver (_IO_new_file_finish, _IO_file_finish)

关闭前需要刷新一下缓冲区, 保证写缓冲区的数据能够写入文件. 然后如果可以关闭, 则会调用 vtable 中的 _IO_SYSCLOSE (_IO_file_close) 关闭文件.

1
2
3
4
5
6
7
8
9
int
_IO_file_close (_IO_FILE *fp)
{
  /* Cancelling close should be avoided if possible since it leaves an
     unrecoverable state behind.  */
  return close_not_cancel (fp->_fileno);
  // 一个宏, 调用了 close 系统调用
}
libc_hidden_def (_IO_file_close)

接着调用 _IO_default_finish 去 free 缓冲区.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
void
_IO_default_finish (_IO_FILE *fp, int dummy)
{
  struct _IO_marker *mark;
  if (fp->_IO_buf_base && !(fp->_flags & _IO_USER_BUF))
    {
      free (fp->_IO_buf_base);
      fp->_IO_buf_base = fp->_IO_buf_end = NULL;
    }
  for (mark = fp->_markers; mark != NULL; mark = mark->_next)
    mark->_sbuf = NULL;
  if (fp->_IO_save_base)
    {
      free (fp->_IO_save_base);
      fp->_IO_save_base = NULL;
    }
  _IO_un_link ((struct _IO_FILE_plus *) fp);
  if (fp->_lock != NULL)
    _IO_lock_fini (*fp->_lock);
}
libc_hidden_def (_IO_default_finish)

退回到 _IO_new_fclose, 最后如果不是 stdin, stdou, stderr, 则 free 掉堆上的 FILE.

函数流程大致如下:

  1. _IO_list_all 中删除
  2. 调用 vtable 中的 _IO_FINISH (_IO_new_file_finish)
  • 刷新缓冲区 (如果有需要写入的数据则写入文件)
  • 调用 vtable 中的 _IO_SYSCLOSE (_IO_file_close), 使用系统调用 close 关闭文件
  • free 缓冲区
  1. 如果不是 stdin, stdout, stderr, 则 free 堆上的 FILE 结构

终于写完了, 好像有些地方太详细了, 完全没必要. 后面写累了, 好多就简单写了.

看个乐就好.