Loading... `__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` 结构体来维护的,其定义为 ```cpp struct _IO_FILE_plus { FILE file; const struct _IO_jump_t *vtable; }; ``` 其中的 `vtable` 虚表就是本文主要要分析的目标。由于虚表中的函数依赖于 `file` 成员变量,所以这里也放一下 FILE 结构体的定义(`typedef struct _IO_FILE FILE;`)。 ```cpp 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);` 这一个函数指针。说这个是指针略有些怪异,我们把它展开 宏的定义如下 ```cpp #define JUMP_FIELD(TYPE, NAME) TYPE NAME ``` 展开就是 ```cpp _IO_overflow_t __overflow; ``` 借用面向对象的思维,也可以称之为一个成员函数。 ## 各个函数的实现 ### __finish 函数指针的定义为 ```cpp typedef void (*_IO_finish_t) (FILE *, int); ``` 相关的宏 ```cpp #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` 类的对象的析构函数。函数的实现比较简单。 ```cpp 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)` 判断文件是否打开,这个宏的定义为 ```cpp #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` 实际是一个宏,其定义和解释如下: ````cpp /* _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 的小技巧,并没有特别好的例子,就勉强以[此题](https://www.cjovi.icu/WP/1203.html)为例)。 如果不处于,则进入 `else if` 分支,这里判断的是指针 `_IO_write_base` 和 `_IO_read_end` 是否指向同一地址,若非,则调用 `_IO_SYSSEEK`,该宏的定义如下 ```cpp #define _IO_SYSSEEK(FP, OFFSET, MODE) JUMP2 (__seek, FP, OFFSET, MODE) ``` 可见这里调用了另一个函数指针 __seek,所以下面先分析该函数实现 --- ### __seek 函数指针的定义为 ```cpp typedef off64_t (*_IO_seek_t) (FILE *, off64_t, int); ``` 相关的宏 ```cpp #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`,内部的实现为 ```cpp 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` ```cpp 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` 上。 接下来会进行执行如下流程: ```cpp 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); ``` 相关的宏 ```cpp #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` 上,在完成输出后,需要对结构体中的一些状态变量进行更新。 ```cpp 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` 相关指针进行更新。 ```cpp #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` 上,接下来指向 ```cpp if (!(fp->_flags & _IO_DELETE_DONT_CLOSE)) _IO_SYSCLOSE (fp); } _IO_default_finish (fp, 0); } ``` 如果该文件不是在删除时需要被关闭,则调用 `_IO_SYSCLOSE (fp);`,实际调用函数指针 `__close` --- ### __close ```cpp 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) ``` ```cpp 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` 的调用已经分析完了,可见他主要做了将缓冲区刷新、释放缓冲区和将文件解链的操作。 最后修改:2021 年 04 月 10 日 © 允许规范转载 打赏 赞赏作者 支付宝微信 赞 0 如果觉得我的文章对你有用,那听听上面我喜欢的歌吧