FILE相关结构
FILE相关结构
CTF-wiki上对FILE的介绍如下:
1 | FILE 在 Linux 系统的标准 IO 库中是用于描述文件的结构,称为文件流。 FILE 结构在程序执行 fopen 等函数时会进行创建,并分配在堆中。我们常定义一个指向 FILE 结构的指针来接收这个返回值。 |
FILE 结构定义在 libio.h 中

1 | struct _IO_FILE { |
进程中的 FILE 结构会通过 chain域彼此连接形成一个链表,链表头部用全局变量_IO_list_all 表示,通过这个值我们可以遍历所有的 FILE 结构。
1 | extern struct _IO_FILE_plus *_IO_list_all; |
在标准 I/O 库中,每个程序启动时有三个文件流是自动打开的:stdin、stdout、stderr。因此在初始状态下,_IO_list_all 指向了一个有这些文件流构成的链表,但是需要注意的是这三个文件流位于 libc.so 的数据段。而我们使用 fopen 创建的文件流是分配在堆内存上的。
我们可以在 libc.so 中找到 stdin\stdout\stderr 等符号,这些符号是指向 FILE 结构的指针,真正结构的符号是:
1 | _IO_2_1_stderr_ |
但是事实上_IO_FILE 结构外包裹着另一种结构_IO_FILE_plus,其中包含了一个重要的指针 vtable 指向了一系列函数指针。
在 libc2.23 版本下,32 位的 vtable 偏移为 0x94,64 位偏移为 0xd8

vtable 是 IO_jump_t 类型的指针,IO_jump_t 中保存了一些函数指针,在后面我们会看到在一系列标准 IO 函数中会调用这些函数指针
fread
fread 是标准 IO 库函数,作用是从文件流中读数据,函数原型如下
1 | size_t fread ( void *buffer, size_t size, size_t count, FILE *stream) ; |
- buffer 存放读取数据的缓冲区。
- size:指定每个记录的长度。
- count: 指定记录的个数。
- stream:目标文件流。
- 返回值:返回读取到数据缓冲区中的记录个数
fread 的代码位于 / libio/iofread.c 中,函数名为_IO_fread,但真正的功能实现在子函数_IO_sgetn 中。

_IO_sgetn在/ libio/genops.c中

在_IO_sgetn 函数中会调用_IO_XSGETN,而_IO_XSGETN 是_IO_FILE_plus.vtable 中的函数指针,在调用这个函数时会首先取出 vtable 中的指针然后再进行调用。在默认情况下函数指针是指向_IO_file_xsgetn 函数的,
fwrite
fwrite 同样是标准 IO 库函数,作用是向文件流写入数据,函数原型如下
1 | size_t fwrite(const void* buffer, size_t size, size_t count, FILE* stream); |
- buffer: 是一个指针,对 fwrite 来说,是要写入数据的地址;
- size: 要写入内容的单字节数;
- count: 要进行写入 size 字节的数据项的个数;
- stream: 目标文件指针;
- 返回值:实际写入的数据项个数 count。
fwrite 的代码位于 / libio/iofwrite.c 中,函数名为_IO_fwrite。 在_IO_fwrite 中主要是调用_IO_XSPUTN 来实现写入的功能。
_IO_XSPUTN 位于_IO_FILE_plus 的 vtable 中,调用这个函数需要首先取出 vtable 中的指针,再跳过去进行调用
1 | written = _IO_sputn (fp, (const char *) buf, request); |
在_IO_XSPUTN 对应的默认函数_IO_new_file_xsputn 中会调用同样位于 vtable 中的_IO_OVERFLOW
_IO_OVERFLOW 默认对应的函数是_IO_new_file_overflow
1 | _IO_new_file_overflow (_IO_FILE *f, int ch) |
在_IO_new_file_overflow 内部最终会调用系统接口 write 函数
fopen
fopen 在标准 IO 库中用于打开文件,函数原型如下
1 | FILE *fopen(char *filename, *type); |
- filename: 目标文件的路径
- type: 打开方式的类型
- 返回值: 返回一个文件指针
在 fopen 内部会创建 FILE 结构并进行一些初始化操作
首先在 fopen 对应的函数__fopen_internal 内部会调用 malloc 函数,分配 FILE 结构的空间。因此我们可以获知 FILE 结构是存储在堆上的,之后会为创建的 FILE 初始化 vtable,并调用_IO_file_init 进一步初始化操作
1 | struct _IO_wide_data wd; |
在_IO_file_init 函数的初始化操作中,会调用_IO_link_in 把新分配的 FILE 链入_IO_list_all 为起始的 FILE 链表中
总结一下 fopen 的操作是
- 使用 malloc 分配 FILE 结构
- 设置 FILE 结构的 vtable
- 初始化分配的 FILE 结构
- 将初始化的 FILE 结构链入 FILE 结构链表中
- 调用系统调用打开文件
fclose
fclose 是标准 IO 库中用于关闭已打开文件的函数,其作用与 fopen 相反
功能:关闭一个文件流,使用 fclose 就可以把缓冲区内最后剩余的数据输出到磁盘文件中,并释放文件指针和有关的缓冲区
fclose 首先会调用_IO_unlink_it 将指定的 FILE 从_chain 链表中脱链
1 | if (fp->_IO_file_flags & _IO_IS_FILEBUF) |
之后会调用_IO_file_close_it 函数,_IO_file_close_it 会调用系统接口 close 关闭文件
1 | if (fp->_IO_file_flags & _IO_IS_FILEBUF) |
最后调用 vtable 中的_IO_FINISH,其对应的是_IO_file_finish 函数,其中会调用 free 函数释放之前分配的 FILE 结构

printf/puts
printf 和 puts 是常用的输出函数,在 printf 的参数是以’\n’结束的纯字符串时,printf 会被优化为 puts 函数并去除换行符。
puts 在源码中实现的函数是_IO_puts,这个函数的操作与 fwrite 的流程大致相同,函数内部同样会调用 vtable 中的_IO_sputn,结果会执行_IO_new_file_xsputn,最后会调用到系统接口 write 函数。
printf 的调用栈回溯如下,同样是通过_IO_file_xsputn 实现
1 | vfprintf+11 |


