__IO_FILE 结构分析
参考资料:https://blog.csdn.net/yjh_fnu_ltn/article/details/141143289?spm=1001.2101.3001.10752
https://xz.aliyun.com/news/15283
https://p0ach1l.github.io/2024/12/07/IO%E7%B3%BB%E5%88%97%E4%B9%8B%E8%AE%A4%E8%AF%86IO_FILE/
__IO_FILE 结构体:
__IO_FILE 结构体:通过 chain 域链接 stderr、stdout、stdin 的链表,链表的表头是 __IO_list_all,stderr,stdout,stdin,是程序启动时打开的文件流。__IO_FILE 结构体定义在 libio.h 文件当中,gdb 查看结构体可以使用命令:p *__IO_list_all
FILE 在 Linux 系统的 IO 库中是用于描述文件的结构,称为文件流。FILE 结构在程序执行 fopen 等函数是会进行创建,并分配在堆中。我们常定义一个指向 FILE 结构的指针来接收这个返回值。FILE 结构定义在 libio.h,如下图所示:
1 | struct _IO_FILE { |
1 | stderr stdout stdin |
stdin、stdout和stderr是C语言中标准输入、标准输出和标准错误流的文件指针。它们是通过_IO_FILE结构体实现的,并在程序启动时由系统自动初始化,并与对应的_IO_FILE结构体实例相关联,提供了标准化的输入输出接口。

他们之间的连接用的就是上面结构题中的chain字段,而链表的头部是依靠全局变量io_list_all来串起来的。
我们还可以通过 ptype _IO_list_all.file查找 chain 的偏移:
1 | pwndbg> ptype _IO_list_all.file |
上面说过了,这三个部分在程序启动的时候就会自动初始化 ,所以我们只要运行程序,就可以找到这三个部分,要注意的是,他们位于libc,也就是泄露libc,就可以找到他们,当然,其实不泄露也可以找到,这三个部分会在bss上面有数据
以上是 LINUX 系统 C 语言标准库(glibc)中 FILE*的底层实现,我们常用的 fopen/fread/fwrite 都是基于它封装的。管理文件缓冲区、文件状态、读写指针、线程安全、文件描述符 等所有文件流相关信息。
__IO_FILE_plus 结构体:
定义在 libioP.h 中,包含 vtable 虚函数表:
1 | // 扩展结构体 _IO_FILE_plus(包含 vtable) |


vtable 本质是指向 _IO_jump_t 结构体的指针,而 _IO_jump_t 里的每一个成员都是**函数指针**,glibc 内部的标准 IO 函数(如 fflush、fwrite、fclose 等)最终都会通过这些函数指针执行具体逻辑。
vtable 是一个函数指针数组,存储量一个类的虚函数的地址, __IO_jump_t 是一个结构体,它定义类一组函数指针,用于实现 __IO_FILE_plus 结构体中的虚函数表。这些函数指针对应了__IO_FILE_plus 就够提的各种操作。通过虚函数表可以实现对 _IO_FILE_plus 中的函数进行动态绑定,使得在运行时可以根据具体对象的类型来调用相应的函数。
gdb查看结构体内容时可以使用命令:p *_IO_list_all->vtable
__IO_ jump_t 结构体
1 | struct _IO_jump_t |
__IO_wide_data 结构体:结构体中实现了虚表:
1 | struct _IO_wide_data |
st结构体:struct stat64是一个结构体,用于存储文件的状态信息。在_IO_file_doallocate函数中,通过调用_IO_SYSSTAT宏来获取文件的状态信息,并将其存储在st结构体中。然后根据文件的类型和块大小来确定缓冲区的大小,并使用malloc函数分配相应大小的内存。最后,使用_IO_setb函数将分配的内存设置为文件的缓冲区
1 | struct stat64 { |
简单总结一下吧,首先最外层是我们的 __IO_file_plus结构体,在 __IO_file_plus结构体之内,包括两个部分,一个是 __IO_file,另一个是 __IO_jump_t,__IO_file结构体里面有我们要找的chain字段,连接着stdin,stdout和stderr三个结构体,而 __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 中:
1 | _IO_size_t |
在_ IO_sgetn 函数中会调用 _IO_XSGETN,而 _ IO_XSGETN 是_IO_FILE_plus.vtable 中的函数指针,在调用这个函数时会首先取出 vtable 中的指针然后再进行调用:
1 | _IO_size_t |
在默认情况下函数指针是指向_IO_file_xsgetn 函数的:
1 | if (fp->_IO_buf_base |
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
fopen
fopen 在标准 IO 库中用于打开文件,函数原型如下:
1 | FILE *fopen(char *filename, *type); |
- filename: 目标文件的路径
- type: 打开方式的类型
- 返回值:返回一个文件指针
在 fopen 内部会创建 FILE 结构并进行一些初始化操作,下面来看一看这个过程:
1. 使用 malloc 分配 FILE 结构 2. 设置 FILE 结构的 vtable 3. 初始化分配的 FILE 结构 4. 将初始化的 FILE 结构链入 FILE 结构链表中 5. 调用系统调用打开文件
fclose
fclose 在标准 IO 库中用于关闭已经打开的文件,其函数原型如下:
1 | int fclose(FILE *stream) |
关闭一个文件流,使用 fclose 就可以把缓冲区最后剩余的数据输出到磁盘文件中,并释放文件指针和有关的缓冲区。
fclose 会首先调用 _IO_unlink_it 将指定的 FILE 从 _chain 链表中脱链:
1 | if (fp->_IO_file_flags & _IO_IS_FILEBUF) |
之后会调用 _IO_file_close_it 函数,该函数会调用系统接口 close 关闭文件:
1 | if (fp->_IO_file_flags & _IO_IS_FILEBUF) |
最后调用 vtable 中的 _IO_FINISH,其对应的是 _IO_file_finish 函数,其中会调用 free 函数释放之前分配的 FILE 结构:
1 | _IO_FINISH (fp); |
printf/puts
printf 和 puts 是常用的输出函数,在 printf 的参数是以’\n’结束的纯字符串时,printf 会被优化为 puts 函数并去除换行符。
puts 在源码中实现的函数是_ IO_puts,这个函数的操作与 fwrite 的流程大致相同,函数内部同样会调用 vtable 中的_ IO_sputn,结果会执行_IO_new_file_xsputn,最后会调用到系统接口 write 函数。
更新: 2026-04-01 20:00:55
原文: https://www.yuque.com/idcm/wnemg9/gky3p0het3vp91pq