io入门
type
status
date
slug
tags
summary
category
icon
password
io 结构
struct
以及:_IO_FILE_plus
可以知道:
进程中的 FILE 结构会通过_chain 域彼此连接形成一个链表,链表头部用全局变量_IO_list_all 表示,通过这个值我们可以遍历所有的 FILE 结构。
在标准 I/O 库中,每个程序启动时有三个文件流是自动打开的:stdin、stdout、stderr。因此在初始状态下,_IO_list_all 指向了一个有这些文件流构成的链表,但是需要注意的是这三个文件流位于 libc.so 的数据段。而使用 fopen 创建的文件流是分配在堆内存上的。
vtable 是 IO_jump_t 类型的指针,IO_jump_t 中保存了一些函数指针,在后面我们会看到在一系列标准 IO 函数中会调用这些函数指针
gdb调试观察:
这个系列的很详细:
重点函数
fopen
整个
__fopen_internal
函数包含四个部分:malloc
分配内存空间。
_IO_no_init
对file结构体进行null
初始化。
_IO_file_init
将结构体链接进_IO_list_all
链表。
_IO_file_fopen
执行系统调用打开文件。
- malloc分配内存空间
可以看到首先调用
malloc
函数分配了一个struct locked_FILE
大小的结构体,这个结构体比函数刚开始的地方定义,在64位系统中为0x230,该结构体包含三个_IO_FILE_plus
、_IO_lock_t
、_IO_wide_data
,其中_IO_FILE_plus
为使用的IO FILE
的结构体
- _IO_no_init 对file结构体进行null初始化
函数最主要的功能是初始化
locked_FILE
里面的_IO_FILE_plus
结构体,基本上将所有的值都初始化为null以及默认值,同时将_wide_data
字段赋值并初始化。
- _IO_file_init将结构体链接进_IO_list_all
在执行完
_IO_no_init
函数后,回到__fopen_internal
函数,函数将_IO_FILE_plus
结构体的vtable设置成了_IO_file_jumps
,然后调用_IO_new_file_init_internal 将_IO_FILE_plus
结构体链接进入_IO_list_all
链表,跟进去函数,函数在/libio/fileops.c
中:FILE结构体是通过
_IO_list_all
的单链表进行管理的,这里_IO_link_in
函数的功能是检查FILE结构体是否包含_IO_LINKED
标志,如果不包含则表示这个结构体没有链接进入_IO_list_all
,则再后面把它链接进入_IO_list_all
链表,同时设置FILE结构体的_chain
字段为之前的链表的值,否则直接返回。- _IO_file_fopen打开文件句柄
将FILE结构体链接到
_IO_list_all
链表后,程序返回到__fopen_internal
中,接下来就调用_IO_new_file_fopen
函数,跟进去该函数,函数在libio/fileops.c
文件中:函数先检查文件描述符是否打开,然后设置文件打开的模式,最后调用
_IO_file_open
函数,跟进去_IO_file_open
函数,该函数在/libio/fileops.c
里面:函数的主要功能就是执行系统调用
open
打开文件,并将文件描述符赋值给FILE结构体的_fileno
字段,最后再次调用_IO_link_in
函数,确保该结构体被链接进入_IO_list_all
链表。该函数执行完后,程序返回FILE结构体指针
小结
可以将fopen整体的流程可以归纳为:
malloc
分配内存空间。
_IO_no_init
对file结构体进行null
初始化。
_IO_file_init
将结构体链接进_IO_list_all
链表。
_IO_file_fopen
执行系统调用打开文件。
fread
源码:
- 第一部分是
fp->_IO_buf_base
为空的情况,表明此时的FILE结构体中的指针未被初始化,输入缓冲区未建立,则调用_IO_doallocbuf
去初始化指针,建立输入缓冲区。
- 第二部分是输入缓冲区里有输入,即
fp->_IO_read_ptr
小于fp->_IO_read_end
,此时将缓冲区里的数据直接拷贝至目标buff。
- 第三部分是输入缓冲区里的数据为空或者是不能满足全部的需求,则调用
__underflow
调用系统调用读入数据。
- 初始化输入缓冲区
看到 _IO_doallocbuf:
函数先检查
fp->_IO_buf_base
是否为空,如果不为空的话表明该输入缓冲区已被初始化,直接返回。如果为空,则检查fp->_flags
看它是不是_IO_UNBUFFERED
或者fp->_mode
大于0,如果满足条件调用FILE的vtable中的_IO_file_doallocate在
/libio/filedoalloc.c
中:可以看到
_IO_file_doallocate
函数是分配输入缓冲区的实现函数,首先调用_IO_SYSSTAT
去获取文件信息,_IO_SYSSTAT
函数是vtable中的__stat
函数,获取文件信息,修改相应需要申请的size。
空间申请出来后,调用_IO_setb
:设置了
_IO_buf_base
和_IO_buf_end
到此,初始化缓冲区就完成了,函数返回_IO_file_doallocate
后,接着_IO_file_doallocate
也返回,回到_IO_file_xsgetn
函数中。
- 拷贝输入缓冲区数据
初始化缓冲区完成之后,代码返回到
_IO_file_xsgetn
函数中,程序就进入到第二部分:拷贝输入缓冲区数据,如果输入缓冲区里存在已输入的数据,则把它直接拷贝到目标缓冲区里。这部分比较简单,需要说明下的是从这里可以看出来
fp->_IO_read_ptr
指向的是输入缓冲区的起始地址,fp->_IO_read_end
指向的是输入缓冲区的结束地址。将
fp->_IO_read_end-fp->_IO_read_ptr
之间的数据通过memcpy
拷贝到目标缓冲区里。- 执行系统调用读取数据
在输入缓冲区为0或者是不能满足需求的时候则会执行最后一步
__underflow
去执行系统调用read
读取数据,并放入到输入缓冲区里。因为demo里第一次读取数据,此时的
fp->_IO_read_end
以及fp->_IO_read_ptr
都是0,因此会进入到__underflow
,跟进去细看,文件在/libio/genops.c
中:如果
fp->_IO_read_ptr
小于fp->_IO_read_end
则表明输入缓冲区里存在数据,可直接返回,否则则表示需要继续读入数据。检查都通过的话就会调用
_IO_UNDERFLOW
函数,该函数是FILE结构体vtable里的_IO_new_file_underflow
,跟进去看,文件在/libio/fileops.c
里:这个
_IO_new_file_underflow
函数,是最终调用系统调用的地方,在最终执行系统调用之前,仍然有一些检查,整个流程为:- 检查FILE结构体的
_flag
标志位是否包含_IO_NO_READS
,如果存在这个标志位则直接返回EOF
,其中_IO_NO_READS
标志位的定义是#define _IO_NO_READS 4 /* Reading not allowed */
。
- 如果
fp->_IO_buf_base
位null,则调用_IO_doallocbuf
分配输入缓冲区。
- 接着初始化设置FILE结构体指针,将他们都设置成
fp->_IO_buf_base
- 调用
_IO_SYSREAD
(vtable中的_IO_file_read
函数),该函数最终执行系统调用read,读取文件数据,数据读入到fp->_IO_buf_base
中,读入大小为输入缓冲区的大小fp->_IO_buf_end - fp->_IO_buf_base
。
- 设置输入缓冲区已有数据的size,即设置
fp->_IO_read_end
为fp->_IO_read_end += count
。
_IO_SYSREAD
(vtable中的_IO_file_read
函数)的源码比较简单,就是执行系统调用函数read去读取文件数据,文件在libio/fileops.c
,源码如下:函数执行完后,返回到
_IO_file_xsgetn
函数中,由于while
循环的存在,重新执行第二部分,此时将输入缓冲区拷贝至目标缓冲区,最终返回。
总结
_IO_sgetn
函数调用了vtable的_IO_file_xsgetn
。
_IO_doallocbuf
函数调用了vtable的_IO_file_doallocate
以初始化输入缓冲区。
- vtable中的
_IO_file_doallocate
调用了vtable中的__GI__IO_file_stat
以获取文件信息。
__underflow
函数调用了vtable中的_IO_new_file_underflow
实现文件数据读取。
- vtable中的
_IO_new_file_underflow
调用了vtable__GI__IO_file_read
最终去执行系统调用read。
先提一下,后续如果想通过IO FILE实现任意读的话,最关键的函数应是
_IO_new_file_underflow
,它里面有个标志位的判断,是后面构造利用需要注意的一个比较重要条件:fwrite
fwrite
函数中涉及的几个 IO FILE 结构体里的指针:指针 | 描述 |
_IO_buf_base | 输入输出缓冲区基地址 |
_IO_buf_end | 输入输出缓冲区结束地址 |
_IO_write_base | 输出缓冲区基地址 |
_IO_write_ptr | 输出缓冲区已使用的地址 |
_IO_write_end | 输出缓冲区结束地址 |
其中
_IO_buf_base
和_IO_buf_end
是缓冲区建立函数_IO_doallocbuf
会在里面建立输入输出缓冲区,并把基地址保存在_IO_buf_base
中,结束地址保存在_IO_buf_end
中。在建立里输入输出缓冲区后,如果缓冲区作为输出缓冲区使用,会将基址址给_IO_write_base
,结束地址给_IO_write_end
,同时_IO_write_ptr
表示为已经使用的地址。即_IO_write_base
到_IO_write_ptr
之间的空间是已经使用的缓冲区,_IO_write_ptr
到_IO_write_end
之间为剩余的输出缓冲区。
调用了
_IO_sputn
函数,该函数是vtable
中的__xsputn
分段看:
主要功能就是判断输出缓冲区还有多少空间
如果还有目标输出数据,表明输出缓冲区未建立或输出缓冲区已经满了,此时调用
_IO_OVERFLOW
函数,该函数功能主要是实现刷新输出缓冲区或建立缓冲区的功能,该函数是 vtable 函数中的__overflow
(_IO_new_file_overflow
),文件在/libio/fileops.c
中:
首先检测 IO FILE 的
_flags
是否包含_IO_NO_WRITES
标志位,如果包含的话则直接返回。接着判断
f->_IO_write_base
是否为空,如果为空的话表明输出缓冲区尚未建立,就调用_IO_doallocbuf
函数去分配输出缓冲区,它的功能是分配输入输出缓冲区并将指针_IO_buf_base
和_IO_buf_end
赋值。在执行完_IO_doallocbuf
分配空间后调用_IO_setg
宏,该宏的定义为如下,它将输入相关的缓冲区指针赋值为_IO_buf_base
指针:#define _IO_setg(fp, eb, g, eg) ((fp)->_IO_read_base = (eb),\
(fp)->_IO_read_ptr = (g), (fp)->_IO_read_end = (eg))
接着就执行
_IO_do_write
来调用系统调用write
输出输出缓冲区,输出的内容为f->_IO_write_ptr
到f->_IO_write_base
之间的内容到了调用
_IO_SYSWRITE
的地方,进行一个判断,判断fp->_IO_read_end
是否等于fp->_IO_write_base
,如果不等的话,调用_IO_SYSSEEK
去调整文件偏移
接着就调用
_IO_SYSWRITE
函数,该函数是 vtable 中的__write
(_IO_new_file_write
)函数,也是最终执行系统调用的地方,跟进去看,文件在/libio/fileops.c
中:执行完
_IO_SYSWRITE
函数后,回到new_do_write
函数,刷新设置缓冲区指针并返回。经历了缓冲区建立以及刷新缓冲区,程序返回到
_IO_new_file_xsputn
函数中,进入到如下代码功能块:运行到此处,此时已经经过了
_IO_OVERFLOW
函数(对输出缓冲区进行了初始化或者刷新),也就是说此时的 IO FILE 缓冲区指针的状态是处于刷新的初始化状态,输出缓冲区中也没有数据。上面这部分代码检查剩余目标输出数据大小,如果超过输入缓冲区
f->_IO_buf_end - f->_IO_buf_base
的大小,则为了提高效率,不再使用输出缓冲区,而是以块为基本单位直接将缓冲区调用new_do_write
输出。new_do_write
函数在上面已经跟过了就是输出,并刷新指针设置。在以大块为基本单位把数据直接输出后可能还剩余小块数据,IO 采用的策略则是将剩余目标输出数据放入到输出缓冲区里面,相关源码如下:
可以看到函数最主要的作用就是将剩余的目标输出数据拷贝到输出缓冲区里。为了性能优化,当长度大于 20 时,使用 memcpy 拷贝,当长度小于 20 时,使用 for 循环赋值拷贝。如果输出缓冲区为空,则调用
_IO_OVERFLOW
进行输出。
总结
整体流程包含四个部分:
- 首先判断输出缓冲区还有多少剩余,如果有剩余则将目标输出数据拷贝至输出缓冲区。
- 如果输出缓冲区没有剩余(输出缓冲区未建立也是没有剩余)或输出缓冲区不够则调用`_IO_OVERFLOW`建立输出缓冲区或刷新输出缓冲区。
- 输出缓冲区刷新后判断剩余的目标输出数据是否超过块的 size,如果超过块的 size,则不通过输出缓冲区直接以块为单位,使用`new_do_write`输出目标数据。
- 如果按块输出数据后还剩下一点数据则调用`_IO_default_xsputn`将数据拷贝至输出缓冲区。
fclose
_IO_un_link将结构体从_IO_list_all链表中取下
第一部分,调用
_IO_un_link
函数将IO FILE结构体从_IO_list_all
链表中取下函数先检查标志位是否包含
_IO_LINKED
标志,该标志的定义是#define _IO_LINKED 0x80
,表示该结构体是否被链接到了_IO_list_all
链表中。如果没有
_IO_LINKED
标志(不在_IO_list_all
链表中)或者_IO_list_all
链表为空,则直接返回。否则的话即表示结构体为
_IO_list_all
链表中某个节点,所要做的就是将这个节点取下来,接下来就是单链表的删除节点的操作,首先判断是不是_IO_list_all
链表头,如果是的话直接将_IO_list_all
指向_IO_list_all->file._chain
就好了,如果不是链表头则遍历链表,找到该结构体,再将其取下。最后返回之前设置
file._flags
为~_IO_LINKED
表示该结构体不在_IO_list_all
链表中_IO_file_close_it关闭文件并释放缓冲区
第二部分就是调用
_IO_file_close_it
关闭文件,释放缓冲区,并清空缓冲区指针。跟进去该函数,文件在/libio/fileops.c
中:这个函数也做了很多事情,首先是调用
_IO_file_is_open
宏检查该文件是否处于打开的状态,宏的定义为#define _IO_file_is_open(__fp) ((__fp)->_fileno != -1)
,只是简单的判断_fileno
。接着判断是不是输出缓冲区,如果是的话,则调用
_IO_do_flush
刷新此时的输出缓冲区,_IO_do_flush
也是一个宏定义:可以看到它对应的是调用
_IO_do_write
函数去输出此时的输出缓冲区,_IO_do_write
函数主要的作用就是调用系统调用输出缓冲区,并刷新输出缓冲区的值。回到
_IO_new_file_close_it
函数中,可以看到在调用了_IO_do_flush
后,代码调用了_IO_SYSCLOSE
函数,该函数是vtable中的__close
函数
close_not_cancel
的定义为#define close_not_cancel(fd) \ __close (fd)
该函数直接调用了系统调用close
关闭文件描述符。在调用了
_IO_SYSCLOSE
函数关闭文件描述符后,_IO_new_file_close_it
函数开始释放输入输出缓冲区并置零输入输出缓冲区。一口气调用了_IO_setb
、_IO_setg
、_IO_setp
三个函数,这三个函数在缓冲区建立的时候都看过了,_IO_setb
是设置结构体的buf指针,_IO_setg
是设置read相关的指针,_IO_setp
是设置write相关的指针,在这里还需要重新看下_IO_setb
函数,因为在这个函数里还释放了缓冲区,函数在libio/genops.c
中:释放FILE内存以及确认文件关闭
结束
_IO_file_close_it
函数后,程序回到_IO_new_fclose
中,开始第三部分代码,调用_IO_FINISH
进行最后的确认,跟进去该函数,该函数是vtable中的__finish
函数,在/libio/fileops.c
中:可以看到代码首先检查了文件描述符是否打开,在第二步中已经将其设置为-1,所以不会进入该流程。如果文件打开的话则会调用
_IO_do_flush
和_IO_SYSCLOSE
刷新缓冲区以及关闭文件。接着调用
_IO_default_finish
确认缓冲区确实被释放,以及结构体从_IO_list_all
中取了下来,并设置指针,函数源码在libio/genops.c
中:程序回到
_IO_new_fclose
中,到此时已经将结构体从链表中删除,刷新了缓冲区,释放了缓冲区内存,只剩下结构体内存尚未释放,调用free
释放结构体内存。
总结fclose
函数实现主要是在_IO_new_fclose
函数中,大致可分为三步,基本上可以与fopen
相对应:- 调用
_IO_un_link
将文件结构体从_IO_list_all
链表中取下。
- 调用
_IO_file_close_it
关闭文件并释放缓冲区。
- 释放FILE内存以及确认文件关闭。
- 在清空缓冲区的
_IO_do_write
函数中会调用vtable中的函数。
- 关闭文件描述符
_IO_SYSCLOSE
函数为vtable中的__close
函数。
_IO_FINISH
函数为vtable中的__finish
函数。
上一篇
windows api记录
下一篇
fsop
Loading...