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执行系统调用打开文件。
 
  1. malloc分配内存空间
可以看到首先调用malloc函数分配了一个struct locked_FILE大小的结构体,这个结构体比函数刚开始的地方定义,在64位系统中为0x230,该结构体包含三个_IO_FILE_plus_IO_lock_t_IO_wide_data,其中_IO_FILE_plus为使用的IO FILE的结构体
  1. _IO_no_init 对file结构体进行null初始化
函数最主要的功能是初始化locked_FILE里面的_IO_FILE_plus结构体,基本上将所有的值都初始化为null以及默认值,同时将_wide_data字段赋值并初始化。
  1. _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字段为之前的链表的值,否则直接返回。
 
 
  1. _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整体的流程可以归纳为:
  1. malloc分配内存空间。
  1. _IO_no_init 对file结构体进行null初始化。
  1. _IO_file_init将结构体链接进_IO_list_all链表。
  1. _IO_file_fopen执行系统调用打开文件。
 

fread

源码:
  • 第一部分是fp->_IO_buf_base为空的情况,表明此时的FILE结构体中的指针未被初始化,输入缓冲区未建立,则调用_IO_doallocbuf去初始化指针,建立输入缓冲区。
  • 第二部分是输入缓冲区里有输入,即fp->_IO_read_ptr小于fp->_IO_read_end,此时将缓冲区里的数据直接拷贝至目标buff。
  • 第三部分是输入缓冲区里的数据为空或者是不能满足全部的需求,则调用__underflow调用系统调用读入数据。
  1. 初始化输入缓冲区
看到 _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函数中。
  1. 拷贝输入缓冲区数据
初始化缓冲区完成之后,代码返回到_IO_file_xsgetn函数中,程序就进入到第二部分:拷贝输入缓冲区数据,如果输入缓冲区里存在已输入的数据,则把它直接拷贝到目标缓冲区里。
这部分比较简单,需要说明下的是从这里可以看出来fp->_IO_read_ptr指向的是输入缓冲区的起始地址,fp->_IO_read_end指向的是输入缓冲区的结束地址。
fp->_IO_read_end-fp->_IO_read_ptr之间的数据通过memcpy拷贝到目标缓冲区里。
 
  1. 执行系统调用读取数据
在输入缓冲区为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函数,是最终调用系统调用的地方,在最终执行系统调用之前,仍然有一些检查,整个流程为:
  1. 检查FILE结构体的_flag标志位是否包含_IO_NO_READS,如果存在这个标志位则直接返回EOF,其中_IO_NO_READS标志位的定义是#define _IO_NO_READS 4 /* Reading not allowed */
  1. 如果fp->_IO_buf_base位null,则调用_IO_doallocbuf分配输入缓冲区。
  1. 接着初始化设置FILE结构体指针,将他们都设置成fp->_IO_buf_base
  1. 调用_IO_SYSREAD(vtable中的_IO_file_read函数),该函数最终执行系统调用read,读取文件数据,数据读入到fp->_IO_buf_base中,读入大小为输入缓冲区的大小fp->_IO_buf_end - fp->_IO_buf_base
  1. 设置输入缓冲区已有数据的size,即设置fp->_IO_read_endfp->_IO_read_end += count
_IO_SYSREAD(vtable中的_IO_file_read函数)的源码比较简单,就是执行系统调用函数read去读取文件数据,文件在libio/fileops.c,源码如下:
函数执行完后,返回到_IO_file_xsgetn函数中,由于while循环的存在,重新执行第二部分,此时将输入缓冲区拷贝至目标缓冲区,最终返回。
总结
notion image
  • _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_ptrf->_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`将数据拷贝至输出缓冲区。
notion image
 

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...
文章列表
Hi~, I ‘m moyao
reverse
pwn
pentest
iot
android
others
ctf
iOS