4352 字
22 分钟
_IO_FILE原理-fopen源码分析

一切皆文件Everything is FILE#

“一切皆是文件” 是Linux/Unix系统的基本哲学之一。即Linux的所有都可以通过文件的方式访问、管理。即使不是文件,也通过文件的形式来管理。例如将硬件、进程等都抽象成一个文件,使用统一的接口管理,这就是 _IO_FILE

_IO_FILE#

_IO_FILE是glibc中用于表述一个文件流的结构体。在Linux一切皆是文件的设计哲学之中,程序的所有输入输出都是通过文件流来实现。在一般的程序中总会处理输入、输出和出现错误,这就引出了我们三个非常重要的文件流:

  • _IO_2_1_stderrfd=2 标准错误流
  • _IO_2_1_stdoutfd=1 标准输出流
  • _IO_2_1_stdinfd=0 标准输入流

这三个文件流的本质都是同一个结构体_IO_FILE如下图所示。

所有的文件流之间由结构体中的成员_chain互相连接,最终形成一个_IO_list_all的链表。每当我们在程序中打开一个新的文件时就会生成一个新的文件流,glibc会将新生成的文件按照头插法连入到该链表中。

_IO_list_all是_IO_FILE_plus类型的结构体,它指向了一串_IO_FILE_plus的结构体,这些结构体的有序序列构成了管理程序中所有文件流的单链表。

glibc2.23的_IO_FILE结构体定义如下

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;
#if 0
	int _blksize;
#else
	int _flags2;
#endif
	_IO_off_t _old_offset; /* This used to be _offset but it's too small. */
#define __HAVE_COLUMN /* temporary */
/* 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;
#ifdef _IO_USE_OLD_IO_FILE
};

在标准IO中,每个程序启动时标准输入输出流是自动打开的,因此在初始状态下,_IO_list_all 只想了一个由这些文件流构成的链表。但需要注意这三个文件流位于libc.so的数据段,而fopen后续创建的文件流是分配到堆上的。

这里介绍一个重要成员_flags,这个成员标记了所在文件流的读写属性。我们在_IO_old_init会提到

事实上,_IO_FILE被封装在_IO_FILE_plus结构体中,其中包含了一个重要的眺表指针vtable

struct _IO_FILE_plus
{
  _IO_FILE file;
  IO_jump_t *vtable
}

注意在此处有一个宏定义

#ifdef _IO_USE_OLD_IO_FILE

实际上该宏并没有被定义,所以紧邻着这个宏定义判断的代码全部失效

#ifdef _IO_USE_OLD_IO_FILE
};
struct _IO_FILE_complete
{
  struct _IO_FILE _file;
#endif

也就是说,整个_IO_FILE_complete都被合并到了_IO_FILE结构体中 那么实际上我们的_IO_FILE应该是这样的

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;
#if 0
	int _blksize;
#else
	int _flags2;
#endif
	_IO_off_t _old_offset; /* This used to be _offset but it's too small. */
#define __HAVE_COLUMN /* temporary */
/* 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;
#ifdef _IO_USE_OLD_IO_FILE
#if defined _G_IO_IO_FILE_VERSION && _G_IO_IO_FILE_VERSION == 0x20001
	_IO_off64_t _offset;
# if defined _LIBC || defined _GLIBCPP_USE_WCHAR_T
/* Wide character stream stuff. */
	struct _IO_codecvt *_codecvt;
	struct _IO_wide_data *_wide_data;
	struct _IO_FILE *_freeres_list;
	void *_freeres_buf;
# else
	void *__pad1;
	void *__pad2;
	void *__pad3;
	void *__pad4;
# endif
	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)];
#endif
};

介绍一下结构体的重要成员

_IO_read_ptr : 输入缓冲区的当前地址
_IO_read_end : 输入缓冲区的结束地址
_IO_read_base : 输入缓冲区的起始地址
_IO_write_base : 输出缓冲区的起始地址
_IO_write_end : 输出缓冲区的结束地址
_IO_write_ptr : 输出缓冲区的当前地址
_IO_buf_base : 输入输出缓冲区的起始地址
_IO_buf_end : 输入输出缓冲区的结束地址
_IO_save_base : 备份缓冲区的起始地址
_IO_backup_base : 备份缓冲区第一个有效字符的指针
_IO_save_end : 备份缓冲区的结束地址
_chain : 下一个文件流的地址
_fileno : 文件描述符
_flags2 : 标志符
_old_offset : 没有初始化之前的偏移
_cur_column : 表示文件流中的行数
_vtable_offset : 虚表指针偏移
_shortbuf : 短buf地址
_lock : 锁结构体地址
_offset : 文件描述符的偏移
_codecvt : 宽字符函数表
_wide_data : 宽字节流指针

fopen#

_IO_new_fopen#

fopen是一个可以被调用的外部函数,它主要用于在程序中创建文件流,现在我们来分析它的源码。

FILE *fopen(char *filename, *type)

首先fopen经过_dl_runtime_resolve_xsavec解析过后是_IO_new_fopen

_IO_FILE *
_IO_new_fopen (const char *filename, const char *mode)
{
  return __fopen_internal (filename, mode, 1);
}

该函数的两个参数都存储在.rodata上,该函数调用了__fopen_internal

_IO_new_fopen -> __fopen_internal#

_IO_FILE *
__fopen_internal (const char *filename, const char *mode, int is32)
{
	struct locked_FILE
	{
		struct _IO_FILE_plus fp;
#ifdef _IO_MTSAFE_IO
	_IO_lock_t lock;
#endif
		struct _IO_wide_data wd;
	} *new_f = (struct locked_FILE *) malloc (sizeof (struct locked_FILE));

	if (new_f == NULL)
		return NULL;
#ifdef _IO_MTSAFE_IO
	new_f->fp.file._lock = &new_f->lock;
#endif
#if defined _LIBC || defined _GLIBCPP_USE_WCHAR_T
	_IO_no_init (&new_f->fp.file, 0, 0, &new_f->wd, &_IO_wfile_jumps);
#else
	_IO_no_init (&new_f->fp.file, 1, 0, NULL, NULL);
#endif
	_IO_JUMPS (&new_f->fp) = &_IO_file_jumps;
	_IO_file_init (&new_f->fp);
#if !_IO_UNIFIED_JUMPTABLES
	new_f->fp.vtable = NULL;
#endif
	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;
}

进入到__fopen_internal立即申请了一个locked_FILE结构体,我们在这里做一下拆分

----- lock_FILE
  |
  ----- struct _IO_FILE_plus fp
  |  |
  | ----- _IO_FILE file
  |	|  |
  | |  --- ...
  |	----- const struct _IO_jump_t *vtable
  ----- _IO_lock_t lock;
  |
  ----- struct _IO_wide_data wd
    |
    ----- ...

可以看出结构体locked_FILE由三部分组成

  • 文件流_IO_FILE_plus:这是该结构体的结构核心,locked_FILE由该结构体拓展而来。如果把_IO_FILE看作是一个父类,那么locked_FILE就是它的子类
  • _IO_lock_t:代表该文件流的锁,意味着在同一个时间点只能有一个线程访问该临界区
  • _IO_wide_data:为了读取宽字节而定义的结构体,结构与_IO_FILE_plus相似
*new_f = (struct locked_FILE *) malloc (sizeof (struct locked_FILE));

随后申请一个大小为locked_FILE相同的堆去作为缓冲区,让new_f指针指向这个user_data如果失败NULL说明文件读取失败

否则继续执行接下来的操作

#ifdef _IO_MTSAFE_IO
	new_f->fp.file._lock = &new_f->lock;
#endif

获取已经展开的结构体lock_FILE的成员lock的地址并赋值给_IO_FILE_plus的成员_lock这样我们就可直接访问file来访问_IO_lock_t的内存

__fopen_internal实际上创建了一个存放在堆中的新的文件流

__fopen_internal -> _IO_no_init#

对于_IO_no_init存在两个入口

#if defined _LIBC || defined _GLIBCPP_USE_WCHAR_T
	_IO_no_init (&new_f->fp.file, 0, 0, &new_f->wd, &_IO_wfile_jumps);
#else
	_IO_no_init (&new_f->fp.file, 1, 0, NULL, NULL);
#endif

第一个入口使用宽字节,第二个入口不使用宽字节。我们重点看第一个入口

为了兼容相较于ascii更广泛的编码格式,比如UTF-8,glibc提供了宽字节流也就是_IO_wide_data结构体。结构体成员中wchar_t成员为四字节,要比char类型大得多,包含一个struct _IO_codecvt _codecvt字符编码转换函数表对非ascii进行兼容

函数原型

void

_IO_no_init (_IO_FILE *fp, int flags, int orientation,
	struct _IO_wide_data *wd, const struct _IO_jump_t *jmp)

我们重点看传入的_IO_jump_t的结构体指针jmp

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);
#if 0
	get_column;
	set_column;
#endif
};

_IO_FILE_plus中的vtable相同的跳表指针。而vtable正是C++使用的虚函数的虚表指针,而_IO_jump_t则是与其对应的虚函数表,即虚表。

我们来看一下JUMP FIELD的宏定义

#define JUMP_FIELD(TYPE, NAME) TYPE NAME

会对每一个成员进行定义,之后在_IO_wfile_jumps对其赋值。

const struct _IO_jump_t _IO_wfile_jumps =
{
	JUMP_INIT_DUMMY,
	JUMP_INIT(finish, _IO_new_file_finish),
	JUMP_INIT(overflow, (_IO_overflow_t) _IO_wfile_overflow),
	JUMP_INIT(underflow, (_IO_underflow_t) _IO_wfile_underflow),
	JUMP_INIT(uflow, (_IO_underflow_t) _IO_wdefault_uflow),
	JUMP_INIT(pbackfail, (_IO_pbackfail_t) _IO_wdefault_pbackfail),
	JUMP_INIT(xsputn, _IO_wfile_xsputn),
	JUMP_INIT(xsgetn, _IO_file_xsgetn),
	JUMP_INIT(seekoff, _IO_wfile_seekoff),
	JUMP_INIT(seekpos, _IO_default_seekpos),
	JUMP_INIT(setbuf, _IO_new_file_setbuf),
	JUMP_INIT(sync, (_IO_sync_t) _IO_wfile_sync),
	JUMP_INIT(doallocate, _IO_wfile_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)
};

JUMP_INIT

#define JUMP_INIT(NAME, VALUE) VALUE

注意到它会返回VALUE,也就是对应函数在内存中的起始位置。

我们继续追踪_IO_no_init

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;
#if defined _LIBC || defined _GLIBCPP_USE_WCHAR_T
	if (orientation >= 0)
		{
			fp->_wide_data = wd;
			fp->_wide_data->_IO_buf_base = NULL;
			fp->_wide_data->_IO_buf_end = NULL;
			fp->_wide_data->_IO_read_base = NULL;
			fp->_wide_data->_IO_read_ptr = NULL;
			fp->_wide_data->_IO_read_end = NULL;
			fp->_wide_data->_IO_write_base = NULL;
			fp->_wide_data->_IO_write_ptr = NULL;
			fp->_wide_data->_IO_write_end = NULL;
			fp->_wide_data->_IO_save_base = NULL;
			fp->_wide_data->_IO_backup_base = NULL;
			fp->_wide_data->_IO_save_end = NULL;

			fp->_wide_data->_wide_vtable = jmp;
		}
	else
/* Cause predictable crash when a wide function is called on a byte
stream. */
		fp->_wide_data = (struct _IO_wide_data *) -1L;
#endif
fp->_freeres_list = NULL;
}

如果启用了宽字节的情况下,会初始化一系列的_wide_data中的成员和_wide_vtable宽字节虚表的指针。在此之前会进入_IO_lod_init

_IO_no_init -> _IO_old_init#

_IO_no_init的名称可以直译为“当前的文件流没有初始化”,所以这个函数会执行初始化流程。那么_IO_old_init可以译为“先前的初始化”。

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;
#if _IO_JUMPS_OFFSET
	fp->_vtable_offset = 0;
#endif
#ifdef _IO_MTSAFE_IO
	if (fp->_lock != NULL)
		_IO_lock_init (*fp->_lock);
#endif
}

可以看到这个函数主要是对_IO_FILE进行初始化,也就是我们文件流的核心。而先前的函数是对宽字节进行初始化。如果没有宽字节,那么先前的函数相当于对这个函数的一次封装。

_IO_old_init会将_IO_FILE fp中的所有成员全部置空。最重要的是对_flags成员进行赋值,它由与魔术头_IO_MAGIC=0xFBADflags进行与运算得到。而flags是在_IO_no_init传入的,考查是否启用了宽字节。

此外_IO_old_init调用了_IO_lock_init,在此不多赘述。

阶段性总结#

总结一下当前的调用链

fopen -> _IO_new_fopen -> __fopen_internal -> _IO_no_init -> _IO_old_init

自此完成部分初始化流程:宽字节+_IO_FILE

__fopen_internal <- _IO_no_init#

返回到__fopen_internal进行跟踪。

_IO_JUMPS (&new_f->fp) = &_IO_file_jumps;

这里初始化眺表指针vtable

_IO_file_init (&new_f->fp);

然后调用了_IO_file_init传入了文件流指针

__fopen_internal -> _IO_file_init(_IO_new_file_init)#

注意

# define _IO_new_file_init _IO_file_init

进入_IO_new_file_init

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;
}

_offset_flags重新赋值,跟进_IO_pos_BAD

#define _IO_pos_BAD ((_IO_off64_t)(-1))
#define _IO_off64_t __off64_t

_IO_pos_BAD是一个值为_IO_off64_t -1的宏定义(long int)。它表示操作文件遇到了错误、未知或异常。设置后_offset == -1

跟进CLOSED_FILEBUF_FLAGS

#define CLOSED_FILEBUF_FLAGS \
	(_IO_IS_FILEBUF+_IO_NO_READS+_IO_NO_WRITES+_IO_TIED_PUT_GET)

#define _IO_IS_FILEBUF 0x2000
#define _IO_NO_READS 4 /* Reading not allowed */
#define _IO_NO_WRITES 8 /* Writing not allowd */
#define _IO_TIED_PUT_GET 0x400 /* Set if put and get pointer logicly tied. */
  • _IO_NO_READS:不允许对当前文件流进行读取
  • _IO_NO_WRITES:不允许对当前文件流进行写入
  • _IO_TIED_PUT_GET:表示与puts和gets函数进行了逻辑绑定
  • _IO_IS_FILEBUF:文件流标识符

最后的该宏表示当前新建的文件流缓冲区已经关闭,不允许在当前执行流进行读取和写入。设置后的_flags == 0xFBAD240C 然后进入_IO_link_in

这个函数主要将新生成的文件流链入_IO_list_all链表,我们来看实现

void
_IO_link_in (struct _IO_FILE_plus *fp)
{
	if ((fp->file._flags & _IO_LINKED) == 0)
		{
			fp->file._flags |= _IO_LINKED;
#ifdef _IO_MTSAFE_IO
			_IO_cleanup_region_start_noarg (flush_cleanup);
			_IO_lock_lock (list_all_lock);
			run_fp = (_IO_FILE *) fp;
			_IO_flockfile ((_IO_FILE *) fp);
#endif
			fp->file._chain = (_IO_FILE *) _IO_list_all;
			_IO_list_all = fp;
			++_IO_list_all_stamp;
#ifdef _IO_MTSAFE_IO
			_IO_funlockfile ((_IO_FILE *) fp);
			run_fp = NULL;
			_IO_lock_unlock (list_all_lock);
			_IO_cleanup_region_end (0);
#endif
		}
}

第一步判断_flags是否设置了_IO_LINKED标志位,判断该文件流是否已经链入链表。

#define _IO_LINKED 0x80

进入判断,对_flags做处理,_flags == 0xfbad248c

为了防止条件竞争,对文件流加锁

#ifdef _IO_MTSAFE_IO
			_IO_cleanup_region_start_noarg (flush_cleanup);
			_IO_lock_lock (list_all_lock);
			run_fp = (_IO_FILE *) fp;
			_IO_flockfile ((_IO_FILE *) fp);
#endif

注意此时_IO_list_all已经初始化,图示如下

_IO_list_all是_IO_FILE_plus类型的结构体,它指向了一串_IO_FILE_plus的结构体,这些结构体的有序序列构成了管理程序中所有文件流的单链表。

阶段性总结#

总结一下当前的调用链

fopen -> _IO_new_fopen -> __fopen_internal -> _IO_file_init <=> _IO_new_file_init -> _IO_link_in

该调用链初始化了虚表,将该文件流链入了_IO_list_all链表

__fopen_internal <- _IO_file_init#

回退到__fopen_internal

#if !_IO_UNIFIED_JUMPTABLES
	new_f->fp.vtable = NULL;
#endif
if (_IO_file_fopen ((_IO_FILE *) new_f, filename, mode, is32) != NULL)
	return __fopen_maybe_mmap (&new_f->fp.file);

这里先宏判断,如果眺表没定义就定义为NULL,然后调用fopen的核心函数_IO_file_fopen

__fopen_internal -> _IO_file_fopen(_IO_new_file_fopen)#

同样,_IO_file_fopen被包装为了_IO_new_file_fopen

这个函数比较长我们分段分析,先来看看原型

_IO_FILE *
_IO_new_file_fopen (_IO_FILE *fp, const char *filename, const char *mode,
	int is32not64)

注意这里传入的fp指针不再是_IO_FILE_plus的指针,这里发生了强制类型转换。返回值同样是一个_IO_FILE结构体指针。

开头先定义了一些变量

int oflags = 0, omode;
int read_write;
int oprot = 0666;
int i;
_IO_FILE *result;
#ifdef _LIBC
	const char *cs;
	const char *last_recognized;
#endif
  • oflag :表示文件修改的方式,新建or追加
  • omode :表示文件打开的模式,读写执行
  • oprot :表示文件的权限
  • i:index
  • result:返回的文件流指针

然后在此处调用了新的函数

if (_IO_file_is_open (fp))
	return 0;

_IO_file_fopen -> _IO_file_is_open#

这是一个宏函数,它检查结构体中的文件标识符是否为-1,即检查_IO_list_all中是否有新增加的文件流,如果没有会直接返回。

#define _IO_file_is_open(__fp) ((__fp)->_fileno != -1)

_IO_file_fopen <- _IO_file_is_open#

回退。继续跟进_IO_file_fopen

switch (*mode)
{
	case 'r':
		omode = O_RDONLY;
		read_write = _IO_NO_WRITES;
	break;
	case 'w':
		omode = O_WRONLY;
		oflags = O_CREAT|O_TRUNC;
		read_write = _IO_NO_READS;
	break;
	case 'a':
		omode = O_WRONLY;
		oflags = O_CREAT|O_APPEND;
		read_write = _IO_NO_READS|_IO_IS_APPENDING;
	break;
	default:
		__set_errno (EINVAL);
	return NULL;
}

这里对omode进行定义。我们用一个表来整理这些变量的关系

modeomodeoflags
rO_RDONLY 只读NULL 无
wO_WRONLY 只写O_CREAT | O_TRUNC 新建或覆盖
aO_WRONLY 只写O_CREAT | O_APPEND 新建或追加
接下来继续遍历mode进行处理。
for (i = 1; i < 7; ++i)
{
	switch (*++mode)
	{
		case '\0':
		break;
		case '+':
			omode = O_RDWR;
			read_write &= _IO_IS_APPENDING;
#ifdef _LIBC
		last_recognized = mode;
#endif
		continue;
		case 'x':
			oflags |= O_EXCL;
#ifdef _LIBC
		last_recognized = mode;
#endif
		continue;
		case 'b':
#ifdef _LIBC
			last_recognized = mode;
#endif
		continue;
		case 'm':
			fp->_flags2 |= _IO_FLAGS2_MMAP;
		continue;
		case 'c':
			fp->_flags2 |= _IO_FLAGS2_NOTCANCEL;
		continue;
		case 'e':
#ifdef O_CLOEXEC
			oflags |= O_CLOEXEC;
#endif
			fp->_flags2 |= _IO_FLAGS2_CLOEXEC;
		continue;
		default:
/* Ignore. */
		continue;

整理为表格

modeomodeoflagsread_write_flags2
’\0’NoChangeNoChangeNochangeNochange
’+‘O_RDWRNoChange&=_IO_IS_APPENDING
’x’NoChange|= O_EXCLNoChangeNoChange
’b’NoChangeNoChangeNoChangeNoChange
’m’NoChangeNoChangeNoChange|= _IO_FLAGS2_MMAP
’c’NoChangeNoChangeNoChange|= _IO_FLAGS2_NOTICANCEL
’e’NoChange|= O_CLOEXECNoChange|= _IO_FLAGS2_CLOEXEC

随后调用_IO_file_open

result = _IO_file_open (fp, filename, omode|oflags, oprot, read_write,
	is32not64);

_IO_file_fopen -> _IO_file_open#

_IO_FILE *
_IO_file_open (_IO_FILE *fp, const char *filename, int posix_mode, int prot,
	int read_write, int is32not64)
{
	int fdesc;
#ifdef _LIBC
	if (__glibc_unlikely (fp->_flags2 & _IO_FLAGS2_NOTCANCEL))
		fdesc = open_not_cancel (filename,
		posix_mode | (is32not64 ? 0 : O_LARGEFILE), prot);
	else
		fdesc = open (filename, posix_mode | (is32not64 ? 0 : O_LARGEFILE), prot);
#else
	fdesc = open (filename, posix_mode, prot);
#endif
	if (fdesc < 0)
		return NULL;
	fp->_fileno = fdesc;
	_IO_mask_flags (fp, read_write,_IO_NO_READS+_IO_NO_WRITES+_IO_IS_APPENDING);
/* For append mode, send the file offset to the end of the file. Don't
update the offset cache though, since the file handle is not active. */
	if ((read_write & (_IO_IS_APPENDING | _IO_NO_READS))
		== (_IO_IS_APPENDING | _IO_NO_READS))
	{
		_IO_off64_t new_pos = _IO_SYSSEEK (fp, 0, _IO_seek_end);
		if (new_pos == _IO_pos_BAD && errno != ESPIPE)
		{
			close_not_cancel (fdesc);
			return NULL;
		}
	}
	_IO_link_in ((struct _IO_FILE_plus *) fp);
	return fp;
}

在函数的开头先判断该文件流的_flags2是否设置了_IO_FLAGS2_NOTICANCEL属性,有则执行open_not_cancel没有执行open。这里进入else分支,实际上调用了syscall open。返回后fdesc == 3

_IO_file_open -> _IO_mask_flags#

然后调用这个函数对_flags进行重新设置

	_IO_mask_flags (fp, read_write,_IO_NO_READS+_IO_NO_WRITES+_IO_IS_APPENDING);

这是一个宏函数

#define _IO_mask_flags(fp, f, mask) \
	((fp)->_flags = ((fp)->_flags & ~(mask)) | ((f) & (mask)))

会根据之前switch case得到的read_write_flags重新设置。

_IO_file_open <- _IO_mask_flags#

回退。

	if ((read_write & (_IO_IS_APPENDING | _IO_NO_READS))
		== (_IO_IS_APPENDING | _IO_NO_READS))
	{
		_IO_off64_t new_pos = _IO_SYSSEEK (fp, 0, _IO_seek_end);
		if (new_pos == _IO_pos_BAD && errno != ESPIPE)
		{
			close_not_cancel (fdesc);
			return NULL;
		}
	}
	_IO_link_in ((struct _IO_FILE_plus *) fp);
	return fp;

如果读写方式为追加,那么将文件末尾作为文件流的指针。最后还会调用一次_IO_link_in

这里调用linkin是确保该结构体已经链入_IO_list_all,至此_IO_file_open执行完毕

	if ((fp->file._flags & _IO_LINKED) == 0)

阶段性总结#

由于_IO_file_fopen剩下的代码是对宽字节进行处理,这里不多研究,直接回退到__fopen_internal 在此之前总结一下当前的调用链

fopen -> _IO_new_fopen -> _fopen_internal -> _IO_file_fopen -> _IO_file_is_open

初始化局部变量,检查文件描述符是否设置

fopen -> _IO_new_fopen -> _fopen_internal -> _IO_file_fopen -> _IO_file_open -> _IO_mask_flags -> _IO_file_open -> _IO_link_in

对文件进行进一步初始化,特别初始化了权限和打开模式,正式调用syscall打开了一个文件流。

__fopen_internal <- _IO_file_fopen#

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);//uaf!
return NULL;

如果无法进入if语句,即返回值为NULL,那么表示文件打开失败,此时会释放new_f。反之进入__fopen_maybe_mmap

主要是处理mmap的情况,堆空间不足。

总结#

到这里,glibc2.23的fopen源码就分析完毕了。我们由一张思维导图来总结一下函数的执行流。

_IO_FILE原理-fopen源码分析
https://k4per-blog.xyz/posts/io_file原理-fopen源码分析/
作者
K4per
发布于
2025-05-15
许可协议
CC BY-NC-SA 4.0