_IO_FILE 相关源码阅读(一)——vtable 及其指向的函数:从 __finish 说起
__finish
虚函数主要处理的是对文件结构体的析构操作,同时会根据情况进行对文件进行 close
操作。通过本文的分析,应该可以基本理解 _IO_new_file_finish
、_IO_new_file_write
、__GI__IO_file_seek
、__GI__IO_file_close
等函数的实现。同时可以对面向语言中虚函数的实现有初步的了解,更重要的,可以明白面向对象是一种思维,而不是语法,使用面向过程式语言仍然可以实现面向对象的开发。
_IO_FILE_plus 的结构
我们都知道 glibc 中的每个文件都是由一个 _IO_FILE_plus
结构体来维护的,其定义为
struct _IO_FILE_plus
{
FILE file;
const struct _IO_jump_t *vtable;
};
其中的 vtable
虚表就是本文主要要分析的目标。由于虚表中的函数依赖于 file
成员变量,所以这里也放一下 FILE 结构体的定义(typedef struct _IO_FILE FILE;
)。
struct _IO_FILE
{
int _flags; /* High-order word is _IO_MAGIC; rest is flags. */
/* The following pointers correspond to the C++ streambuf protocol. */
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;
__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];
_IO_lock_t *_lock;
};
_fileno
是该文件在系统中的文件描述符的值。
注意变量 _old_offset
,注释里也有提到,这曾经是变量 _offset
,事实上,在之后的函数中也是使用的 _offset
。此变量表示文件当前的读写位置偏移(与文件头的偏移)。
结构体中的变量 _flags
标志了文件所处的状态等,这些状态的宏定义如下
#define _IO_MAGIC 0xFBAD0000 /* Magic number */
#define _IO_MAGIC_MASK 0xFFFF0000
#define _IO_USER_BUF 0x0001 /* Don't deallocate buffer on close. */
#define _IO_UNBUFFERED 0x0002
#define _IO_NO_READS 0x0004 /* Reading not allowed. */
#define _IO_NO_WRITES 0x0008 /* Writing not allowed. */
#define _IO_EOF_SEEN 0x0010
#define _IO_ERR_SEEN 0x0020
#define _IO_DELETE_DONT_CLOSE 0x0040 /* Don't call close(_fileno) on close. */
#define _IO_LINKED 0x0080 /* In the list of all open files. */
#define _IO_IN_BACKUP 0x0100
#define _IO_LINE_BUF 0x0200
#define _IO_TIED_PUT_GET 0x0400 /* Put and get pointer move in unison. */
#define _IO_CURRENTLY_PUTTING 0x0800
#define _IO_IS_APPENDING 0x1000
#define _IO_IS_FILEBUF 0x2000
/* 0x4000 No longer used, reserved for compat. */
#define _IO_USER_LOCK 0x8000
_flags2
变量中也存储了一些标志
/* Bits for the _flags2 field. */
#define _IO_FLAGS2_MMAP 1
#define _IO_FLAGS2_NOTCANCEL 2
#define _IO_FLAGS2_USER_WBUF 8
#define _IO_FLAGS2_NOCLOSE 32
#define _IO_FLAGS2_CLOEXEC 64
#define _IO_FLAGS2_NEED_LOCK 128
vtable 的定义
vtable
是一个类型为 struct _IO_jump_t
的结构体,这个结构体是这样定义的。这个结构体中存储了许多函数指针。其实这里是带有面向对象的思维的,vtable
实际上是一张虚表,通过这张虚表实现了虚函数的效果。
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);
};
函数指针的指向
这里仅讨论标准文件类型的函数实现,也就是以标准输入输出文件为代表的结构体的 vtable
中函数指针的指向,通过调试,可以获得他们的指向,整理后得到以下的对应关系
ptr | function |
---|---|
__dummy | 0 |
__dummy2 | 0 |
__finish | <_IO_new_file_finish> |
__overflow | <_IO_new_file_overflow> |
__underflow | <_IO_new_file_underflow> |
__uflow | <__GI__IO_default_uflow> |
__pbackfail | <__GI__IO_default_pbackfail> |
__xsputn | <_IO_new_file_xsputn> |
__xsgetn | <__GI__IO_file_xsgetn> |
__seekoff | <_IO_new_file_seekoff> |
__seekpos | <_IO_default_seekpos> |
__setbuf | <_IO_new_file_setbuf> |
__sync | <_IO_new_file_sync> |
__doallocate | <__GI__IO_file_doallocate> |
__read | <__GI__IO_file_read> |
__write | <_IO_new_file_write> |
__seek | <__GI__IO_file_seek> |
__close | <__GI__IO_file_close> |
__stat | <__GI__IO_file_stat> |
__showmanyc | <_IO_default_showmanyc> |
__imbue | <_IO_default_imbue> |
一些宏的定义
一个比较典型的对 vtable
中的函数指针的调用是这样写的 _IO_OVERFLOW (fp, EOF)
,把宏展开来就是
((*(struct _IO_jump_t **) ((void *) &(*(__typeof__ (((struct _IO_FILE_plus){}).vtable) *)(((char *) ((fp))) + ((size_t)&(((struct _IO_FILE_plus*)0)->vtable)))) + (fp)->_vtable_offset))->__overflow) (fp, (-1))
这样就以 fp
和 EOF
为参数调用了 fp
中的 JUMP_FIELD(_IO_overflow_t, __overflow);
这一个函数指针。说这个是指针略有些怪异,我们把它展开
宏的定义如下
#define JUMP_FIELD(TYPE, NAME) TYPE NAME
展开就是
_IO_overflow_t __overflow;
借用面向对象的思维,也可以称之为一个成员函数。
各个函数的实现
__finish
函数指针的定义为
typedef void (*_IO_finish_t) (FILE *, int);
相关的宏
#define _IO_FINISH(FP) JUMP1 (__finish, FP, 0)
#define _IO_WFINISH(FP) WJUMP1 (__finish, FP, 0)
glibc 对这个函数的解释为
/* The ‘finish’ function does any final cleaning up of an _IO_FILE object. It does not delete (free) it, but does everything else to finalize it. It matches the streambuf::~streambuf virtual destructor. */
也就是说这相当于一个 _IO_FILE
类的对象的析构函数。函数的实现比较简单。
void
_IO_new_file_finish (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)
首先注意函数的第一个参数是 FILE *
类型的,也就是 _IO_FILE
类型,这是因为成员函数不需要对虚表进行访问等操作。
进入函数后,首先通过宏 _IO_file_is_open (fp)
判断文件是否打开,这个宏的定义为
#define _IO_file_is_open(__fp) ((__fp)->_fileno != -1)
也就是判断了结构体中的 _fileno
变量是否为 -1。若为 -1 则表明该文件已被关闭。
如果文件是打开的,首先通过 _IO_do_flush (fp)
来刷新该文件的缓冲区,宏的定义为
#define _IO_do_flush(_f) \
((_f)->_mode <= 0 \
? _IO_do_write(_f, (_f)->_IO_write_base, \
(_f)->_IO_write_ptr-(_f)->_IO_write_base) \
: _IO_wdo_write(_f, (_f)->_wide_data->_IO_write_base, \
((_f)->_wide_data->_IO_write_ptr \
- (_f)->_wide_data->_IO_write_base)))
这里进行的应该是检测是否需要输出宽字符。如果是输出跨字符则调用 _IO_wdo_write
,实际调用的是 __GI__IO_wdo_write
,定义在 glibc/libio/wfileops.c
中,实现比较复杂,这里按下不表。 否则调用 _IO_do_write
进行输出,实际调用的是定义在 glibc/libio/fileops.c
中的 _IO_new_do_write
函数,其实现如下
int
_IO_new_do_write (FILE *fp, const char *data, size_t to_do)
{
return (to_do == 0
|| (size_t) new_do_write (fp, data, to_do) == to_do) ? 0 : EOF;
}
libc_hidden_ver (_IO_new_do_write, _IO_do_write)
传入的参数分别为文件对象自身(FILE *fp
),输出起始地址(_IO_FILE
结构体成员指针变量 _IO_write_base),输出长度(_IO_write_ptr - _IO_write_base
,这里也可以看出这两个指针分别代表了输出的结束和起始点)。
可见这里利用了短路求值原理,如果需要输出的字符数量为零,直接返回,否则调用 ||
后面的 (size_t) new_do_write (fp, data, to_do)
,传入的参数是一样的。也就是说这个函数只是做了对是否输出做了一个简单的判断。
new_do_write
函数的实现如下
static size_t
new_do_write (FILE *fp, const char *data, size_t to_do)
{
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)
{
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;
}
count 变量记录输出的字符个数。
进入流程后,首先检测 _flags
的 _IO_IS_APPENDING
标志位,如果处于 APPENDING
模式,则执行fp->_offset = _IO_pos_BAD;
。_IO_pos_BAD
实际是一个宏,其定义和解释如下:
/* _IO_pos_BAD is an off64_t value indicating error, unknown, or EOF. */
#define _IO_pos_BAD ((off64_t) -1)
他表示出现了错误,或者未知情况,或读到了文件尾。在这种模式下,不会进行之后的 lseek
,而是直接输出 [_IO_write_base,_IO_write_ptr]
中所有的数据。我们常常会在对 _IO_2_1_stdout_
攻击的时候利用到这一点(由于这是一个 leak 的小技巧,并没有特别好的例子,就勉强以此题为例)。
如果不处于,则进入 else if
分支,这里判断的是指针 _IO_write_base
和 _IO_read_end
是否指向同一地址,若非,则调用 _IO_SYSSEEK
,该宏的定义如下
#define _IO_SYSSEEK(FP, OFFSET, MODE) JUMP2 (__seek, FP, OFFSET, MODE)
可见这里调用了另一个函数指针 __seek,所以下面先分析该函数实现
__seek
函数指针的定义为
typedef off64_t (*_IO_seek_t) (FILE *, off64_t, int);
相关的宏
#define _IO_SYSSEEK(FP, OFFSET, MODE) JUMP2 (__seek, FP, OFFSET, MODE)
#define _IO_WSYSSEEK(FP, OFFSET, MODE) WJUMP2 (__seek, FP, OFFSET, MODE)
glibc 的解释为
/* The ‘sysseek’ hook is used to re-position an external file. It generalizes the Unix lseek(2) function. It matches the streambuf::sys_seek virtual function, which is specific to this implementation. */
指向的函数为 __GI__IO_file_seek
,内部的实现为
off64_t
_IO_file_seek (FILE *fp, off64_t offset, int dir)
{
return __lseek64 (fp->_fileno, offset, dir);
}
libc_hidden_def (_IO_file_seek)
该函数定义在 glibc/libio/fileops.c
中,传入的参数分别为文件结构体指针,移动的偏移。这里调用了定义在 sysdeps/unix/sysv/linux/lseek64.c
中的 __lseek64
off64_t
__lseek64 (int fd, off64_t offset, int whence)
{
loff_t res;
int rc = INLINE_SYSCALL_CALL (_llseek, fd,
(long) (((uint64_t) (offset)) >> 32),
(long) offset, &res, whence);
return rc ?: res;
}
在这个函数中执行了系统调用,通过 _llseek
移动了文件的读取位置。从这里可以看出 _IO_FILE
结构体中的变量_fileno
储存的是文件描述符的值。
总结:__seek
函数指针直接返回内核函数 __lseek64
,而后者是对系统调用 _llseek
的简单封装。通过调用 __seek
函数,可以实现改变文件的读写位置。
让我们回到 new_do_write
。进过上面对 __seek
的分析,我们已经知道,_IO_SYSSEEK
将当前文件的读取位置转到了 _IO_write_base
处。这里有趣的一点是 glibc 假设了在输出时,原来的文件读写位置就是在 _IO_read_end
上。
接下来会进行执行如下流程:
if (new_pos == _IO_pos_BAD)
return 0;
fp->_offset = new_pos;
}
count = _IO_SYSWRITE (fp, data, to_do);
_IO_SYSSEEK
可能会返回 _IO_pos_BAD
,代表写出的文件遇到异常,这里应该是到文件尾了,自然不需要再输出,直接返回 0,结束流程。
否则继续流程,首先设置 fp->_offset
为当前的文件读写位置,然后准备执行系统调用
#define _IO_SYSWRITE(FP, DATA, LEN) JUMP2 (__write, FP, DATA, LEN)
这里又调用了一个函数指针 __write
,所以先分析该指针指向的函数
__write
函数指针的定义为
typedef ssize_t (*_IO_write_t) (FILE *, const void *, ssize_t);
相关的宏
#define _IO_SYSSTAT(FP, BUF) JUMP1 (__stat, FP, BUF)
#define _IO_WSYSSTAT(FP, BUF) WJUMP1 (__stat, FP, BUF)
glibc 的解释为
/* The ‘syswrite’ hook is used to write data from an existing buffer to an external file. It generalizes the Unix write(2) function. It matches the streambuf::sys_write virtual function, which is specific to this implementation. */
也就是说该函数将缓冲区中已存在的数据写到了外部文件中,该函数一般化了 Unix 的内核函数 write
,也就是说这是一个对 write
的封装。
其内部实现如下。先解释一下参数,f
是文件结构体指针,data
指向(缓冲区中)要输出的数据,n
为要输出的长度。
ssize_t
_IO_new_file_write (FILE *f, const void *data, ssize_t n)
{
ssize_t to_do = n;
while (to_do > 0)
{
ssize_t count = (__builtin_expect (f->_flags2
& _IO_FLAGS2_NOTCANCEL, 0)
? __write_nocancel (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;
}
解释一下 __builtin_expect
:由gcc引入,其作用为允许程序员将最有可能执行的分支告诉编译器。指令的写法为:__builtin_expect(EXP, N)
,表示 EXP==N
的概率很大。有利于编译器的分支预测优化
进入流程后,开始尝试通过系统调用输出,通过 __builtin_expect (f->_flags2 & _IO_FLAGS2_NOTCANCEL, 0)
进行判断是否需要考虑 CANCEL
,这是为多线程处理进行的特判,调用的两个函数的定义如下
ssize_t
__write_nocancel (int fd, const void *buf, size_t nbytes)
{
return INLINE_SYSCALL_CALL (write, fd, buf, nbytes);
}
hidden_def (__write_nocancel)
/* Write NBYTES of BUF to FD. Return the number written, or -1. */
ssize_t
__libc_write (int fd, const void *buf, size_t nbytes)
{
return SYSCALL_CANCEL (write, fd, buf, nbytes);
}
libc_hidden_def (__libc_write)
weak_alias (__libc_write, __write)
注意到这里的 __write
是 __libc_write
的别名。两个函数对内核函数的简单封装,他们被定义在 glibc/sysdeps/unix/sysdep.h
中。
在这个 while
循环中一直使用系统调用进行输出,并用 count
变量维护结果,当 count < 0
时,代表出现了输出错误,此时进行置位 f->_flags |= _IO_ERR_SEEN;
,并直接返回结束流程。
当 to_do
小于等于 0 时,结束循环,将 _offset
加上实际输出的长度(实际输出的长度是可能小于指定的长度 nbytes
的),返回输出的长度,结束流程。
_IO_new_file_write
的实现比较简单,是对系统函数的封装,同时对异常等情况进行了处理。
返回到 new_do_write
上,在完成输出后,需要对结构体中的一些状态变量进行更新。
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;
}
首先通过函数 _IO_adjust_column
进行列调整,实现如下
unsigned
_IO_adjust_column (unsigned start, const char *line, int count)
{
const char *ptr = line + count;
while (ptr > line)
if (*--ptr == '\n')
return line + count - ptr - 1;
return start + count;
}
libc_hidden_def (_IO_adjust_column)
函数会对被输出的数据进行遍历,找到第一个换行符为止,返回换行符之后的字符总数;当然也可能没有输出换行符,那么就直接返回输出的字符数和原先所处在的列的和。从这样的实现可以看出,fp->_cur_column /* 1+column number of pbase(); 0 is unknown. */
维护的是当前所处的列数加一,通过调用这个函数实现了对该变量的更新。
然后调用 _IO_setg
来对 read
相关指针进行更新。
#define _IO_setg(fp, eb, g, eg) ((fp)->_IO_read_base = (eb),\
(fp)->_IO_read_ptr = (g), (fp)->_IO_read_end = (eg))
也就是把三个 read
指针的缓冲区指针都赋为相关的 buf
指针。
之后还会对三个 write
指针进行更新。这些操作是刷新缓冲区,使读入和输出的开始地址都指向缓冲区的头部,根据不同的缓冲方式也会对 _IO_write_end
置不同的值,还是比较好理解的。
更新完后返回输出的字符数,结束流程。
总结 new_do_write
做的事:将缓冲区中需要输出的数据通过系统调用输出,然后更新列状态和 read
write
相关的共计 6 个指针的指向。
之后一路返回,结束 _IO_do_flush (fp);
。这个函数做的就是将缓冲区中的残留字符全部输出,同时更新相关状态。
又回到了 _IO_new_file_finish
上,接下来指向
if (!(fp->_flags & _IO_DELETE_DONT_CLOSE))
_IO_SYSCLOSE (fp);
}
_IO_default_finish (fp, 0);
}
如果该文件不是在删除时需要被关闭,则调用 _IO_SYSCLOSE (fp);
,实际调用函数指针 __close
__close
int
_IO_file_close (FILE *fp)
{
/* Cancelling close should be avoided if possible since it leaves an
unrecoverable state behind. */
return __close_nocancel (fp->_fileno);
}
libc_hidden_def (_IO_file_close)
int
__close_nocancel (int fd)
{
return INLINE_SYSCALL_CALL (close, fd);
}
libc_hidden_def (__close_nocancel)
实现比较简单,就是调用系统调用来关闭当前文件。
关闭文件后,调用 _IO_default_finish
做最后的去除操作
void
_IO_default_finish (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);
#ifdef _IO_MTSAFE_IO
if (fp->_lock != NULL)
_IO_lock_fini (*fp->_lock);
#endif
}
libc_hidden_def (_IO_default_finish)
这里主要做了 free
缓冲区和将该文件从文件链表上解链的操作。
至此 __finish
的调用已经分析完了,可见他主要做了将缓冲区刷新、释放缓冲区和将文件解链的操作。