io file这一部分的知识,因为发现之前接触的太浅了
这一次源码级调试一下,看一下各种各样的io结构的原理,笔者这里照着这个师傅 调试的
本文章基本算是转载这位师傅 的,转载!
IO FILE 怎么源码级调试?这里笔者直接用的pwndbg里面的dir将glibc的源码带到了调试里。dir /glibc-2.23/libio/
,需要注意的是启动gdb的时候需要这样:gdb 文件名
,这样就可以愉快的调试啦。
需要先理解一下什么是IO FILE。进程中的FILE结构会通过_chain域链接形成一个链表。链表头部用全局变量__IO_list_all表示。一个程序启动时有三个文件流是打开的:stderr,stdout,stdin。这三个文件流位于libc.so数据段,__IO_FILE
结构外包裹着另一种结构__IO_FILE_plus
。如下
fopen 一个简单的fopen程序2.23下的libc。
1 2 3 4 5 6 7 8 #include <stdio.h> #include <stdlib.h> int main () { FILE*fp = fopen("test" ,"wb" ); char *ptr = malloc (0x20 ); return 0 ; }
gdb启动完成之后s进去就可以看到fopen实际上是_IO_new_fopen
函数。
1 2 3 4 5 94 _IO_FILE * 95 _IO_new_fopen (const char *filename, const char *mode) 96 { ► 97 return __fopen_internal (filename, mode, 1 ); 98 }
从上面的源码中可以很清楚的看到又调用了__fopen_internal
函数。看一下源码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 _IO_FILE * __fopen_internal (const char *filename, const char *mode, int is32) { struct locked_FILE { struct _IO_FILE_plus fp ; #ifdef _IO_MTSAFE_IO _IO_lock_t lock; #endif struct _IO_wide_data wd ; } *new_f = (struct locked_FILE *) malloc (sizeof (struct locked_FILE)); if (new_f == NULL ) return NULL ; #ifdef _IO_MTSAFE_IO new_f->fp.file._lock = &new_f->lock; #endif #if defined _LIBC || defined _GLIBCPP_USE_WCHAR_T _IO_no_init (&new_f->fp.file, 0 , 0 , &new_f->wd, &_IO_wfile_jumps); #else _IO_no_init (&new_f->fp.file, 1 , 0 , NULL , NULL ); #endif _IO_JUMPS (&new_f->fp) = &_IO_file_jumps; _IO_file_init (&new_f->fp); #if !_IO_UNIFIED_JUMPTABLES new_f->fp.vtable = NULL ; #endif if (_IO_file_fopen ((_IO_FILE *) new_f, filename, mode, is32) != NULL ) return __fopen_maybe_mmap (&new_f->fp.file); locked_FILE _IO_un_link (&new_f->fp); free (new_f); return NULL ; }
malloc
分配内存空间。
_IO_no_init
对file结构体进行null
初始化。
_IO_file_init
将结构体链接进_IO_list_all
链表。
_IO_file_fopen
执行系统调用打开文件
malloc分配内存空间 首先malloc了一个struct locked_FILE
大小的结构体,这个结构体内有_IO_FILE_plus
、_IO_lock_t
、_IO_wide_data
这三个结构,其中_IO_FILE_plus
为使用的IO_FILE
结构体。malloc之后会发现都为0,很明显的可以看到这个结构体的大小为0x230。
1 2 3 4 5 6 7 8 9 10 11 12 13 pwndbg> p new_f $2 = (struct locked_FILE *) 0x55555555b010 pwndbg> x/20 gx 0x55555555b010 - 0x10 0x55555555b000 : 0x0000000000000000 0x0000000000000231 0x55555555b010 : 0x0000000000000000 0x0000000000000000 0x55555555b020 : 0x0000000000000000 0x0000000000000000 0x55555555b030 : 0x0000000000000000 0x0000000000000000 0x55555555b040 : 0x0000000000000000 0x0000000000000000 0x55555555b050 : 0x0000000000000000 0x0000000000000000 0x55555555b060 : 0x0000000000000000 0x0000000000000000 0x55555555b070 : 0x0000000000000000 0x0000000000000000 0x55555555b080 : 0x0000000000000000 0x0000000000000000 0x55555555b090 : 0x0000000000000000 0x0000000000000000
_IO_no_init对file结构体进行初始化操作 继续住下走会调用_IO_no_init
这个函数对上面的结构体进行初始化操作,这个文件在libio/genops.c
。跟着源码还可以看到的是还利用了_IO_old_init
这个函数对flags这些进行初始化。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 void _IO_no_init (_IO_FILE *fp, int flags, int orientation, struct _IO_wide_data *wd, const struct _IO_jump_t *jmp) { _IO_old_init (fp, flags); fp->_mode = orientation; #if defined _LIBC || defined _GLIBCPP_USE_WCHAR_T if (orientation >= 0 ) { fp->_wide_data = wd; fp->_wide_data->_IO_buf_base = NULL ; fp->_wide_data->_IO_buf_end = NULL ; fp->_wide_data->_IO_read_base = NULL ; fp->_wide_data->_IO_read_ptr = NULL ; fp->_wide_data->_IO_read_end = NULL ; fp->_wide_data->_IO_write_base = NULL ; fp->_wide_data->_IO_write_ptr = NULL ; fp->_wide_data->_IO_write_end = NULL ; fp->_wide_data->_IO_save_base = NULL ; fp->_wide_data->_IO_backup_base = NULL ; fp->_wide_data->_IO_save_end = NULL ; fp->_wide_data->_wide_vtable = jmp; } else fp->_wide_data = (struct _IO_wide_data *) -1L ; #endif fp->_freeres_list = NULL ; } void _IO_old_init (_IO_FILE *fp, int flags) { fp->_flags = _IO_MAGIC|flags; fp->_flags2 = 0 ; fp->_IO_buf_base = NULL ; fp->_IO_buf_end = NULL ; fp->_IO_read_base = NULL ; fp->_IO_read_ptr = NULL ; fp->_IO_read_end = NULL ; fp->_IO_write_base = NULL ; fp->_IO_write_ptr = NULL ; fp->_IO_write_end = NULL ; fp->_chain = NULL ; fp->_IO_save_base = NULL ; fp->_IO_backup_base = NULL ; fp->_IO_save_end = NULL ; fp->_markers = NULL ; fp->_cur_column = 0 ; #if _IO_JUMPS_OFFSET fp->_vtable_offset = 0 ; #endif #ifdef _IO_MTSAFE_IO if (fp->_lock != NULL ) _IO_lock_init (*fp->_lock); #endif
看一下最后的file结构被初始化成什么样子了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 pwndbg> p new_f->fp $4 = { file = { _flags = -72548352 , _IO_read_ptr = 0x0 , _IO_read_end = 0x0 , _IO_read_base = 0x0 , _IO_write_base = 0x0 , _IO_write_ptr = 0x0 , _IO_write_end = 0x0 , _IO_buf_base = 0x0 , _IO_buf_end = 0x0 , _IO_save_base = 0x0 , _IO_backup_base = 0x0 , _IO_save_end = 0x0 , _markers = 0x0 , _chain = 0x0 , _fileno = 0 , _flags2 = 0 , _old_offset = 0 , _cur_column = 0 , _vtable_offset = 0 '\000' , _shortbuf = "" , _lock = 0x55555555b0f0 , _offset = 0 , _codecvt = 0x0 , _wide_data = 0x55555555b100 , _freeres_list = 0x0 , _freeres_buf = 0x0 , __pad5 = 0 , _mode = 0 , _unused2 = '\000' <repeats 19 times> }, vtable = 0x0 }
_IO_file_init
将结构体链接到_IO_list_all
结束_IO_no_init
之后我们可以看到回到了__fopen_internal
并继续执行_IO_file_init
这个函数,跟进看一下是干什么的,跟进之后是/libio/fileops.c
这个文件里的_IO_new_file_init
。
1 2 3 4 5 6 7 8 9 10 11 void _IO_new_file_init (struct _IO_FILE_plus *fp) { fp->file._offset = _IO_pos_BAD; fp->file._IO_file_flags |= CLOSED_FILEBUF_FLAGS; _IO_link_in (fp); fp->file._fileno = -1 ;
这个函数主要调用了_IO_link_in
这个函数,继续跟进这个函数,libio/genops.c
里面的_IO_link_in
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 void _IO_link_in (struct _IO_FILE_plus *fp) { if ((fp->file._flags & _IO_LINKED) == 0 ) { fp->file._flags |= _IO_LINKED; #ifdef _IO_MTSAFE_IO _IO_cleanup_region_start_noarg (flush_cleanup); _IO_lock_lock (list_all_lock); run_fp = (_IO_FILE *) fp; _IO_flockfile ((_IO_FILE *) fp); #endif fp->file._chain = (_IO_FILE *) _IO_list_all; _IO_list_all = fp; ++_IO_list_all_stamp; #ifdef _IO_MTSAFE_IO _IO_funlockfile ((_IO_FILE *) fp); run_fp = NULL ; _IO_lock_unlock (list_all_lock); _IO_cleanup_region_end (0 ); #endif } }
首先这个if是判断flag的标志位是否是_IO_LINKED
,这个有什么用呢?FILE结构体是通过_IO_list_all
的单链表进行管理的,如果这个结构体没有_IO_LINKED
就说明这个结构体没有链接进入_IO_list_all
。后面把它链接进入_IO_list_all
链表,同时设置FILE结构体的_chain
字段为之前的链表的值,否则直接返回。所以_IO_file_init
主要功能是将FILE结构体链接进入_IO_list_all
链表。
1 2 pwndbg> p _IO_list_all $6 = (struct _IO_FILE_plus *) 0x7ffff7dd2540 <_IO_2_1_stderr_>
在没有执行下面的操作之前可以看到_IO_list_all
链接的是_IO_2_1_stderr_
,执行完之后_IO_list_all
就指向的是申请出来的结构体。
1 2 pwndbg> p _IO_list_all $7 = (struct _IO_FILE_plus *) 0x55555555b010
同时此时的_chain
字段也指向了_IO_2_1_stderr_
这里。
_IO_file_fopen打开文件句柄 设置好了_IO_LINKED
这里东西之后又会回到__fopen_internal
这里,接下来会执行_IO_file_fopen
这个函数,跟进后发现位于libio/fileops.c
这里面的_IO_new_file_fopen
。这个_IO_new_file_fopen
函数有点长,这里就放一部分比较重要的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 _IO_FILE * _IO_new_file_fopen (_IO_FILE *fp, const char *filename, const char *mode, int is32not64) { ... ## 检查文件是否已打开,打开则返回 if (_IO_file_is_open (fp)) return 0 ; ## 设置文件打开模式 switch (*mode) { case 'r' : omode = O_RDONLY; read_write = _IO_NO_WRITES; break ; ... } ... ## 调用_IO_file_open函数 result = _IO_file_open (fp, filename, omode|oflags, oprot, read_write, is32not64); ... } libc_hidden_ver (_IO_new_file_fopen, _IO_file_fopen)
会先检查文件是否打开,然后设置打开模式,最后调用了_IO_file_open
这个函数跟进它。位于libio/fileops.c
这个文件中的_IO_file_open
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 _IO_FILE * _IO_file_open (_IO_FILE *fp, const char *filename, int posix_mode, int prot, int read_write, int is32not64) { int fdesc; #ifdef _LIBC if (__glibc_unlikely (fp->_flags2 & _IO_FLAGS2_NOTCANCEL)) fdesc = open_not_cancel (filename, posix_mode | (is32not64 ? 0 : O_LARGEFILE), prot); else fdesc = open (filename, posix_mode | (is32not64 ? 0 : O_LARGEFILE), prot); #else fdesc = open (filename, posix_mode, prot); #endif if (fdesc < 0 ) return NULL ; fp->_fileno = fdesc; _IO_mask_flags (fp, read_write,_IO_NO_READS+_IO_NO_WRITES+_IO_IS_APPENDING); if ((read_write & (_IO_IS_APPENDING | _IO_NO_READS)) == (_IO_IS_APPENDING | _IO_NO_READS)) { _IO_off64_t new_pos = _IO_SYSSEEK (fp, 0 , _IO_seek_end); if (new_pos == _IO_pos_BAD && errno != ESPIPE) { close_not_cancel (fdesc); return NULL ; } } _IO_link_in ((struct _IO_FILE_plus *) fp); return fp; }
这个函数就是调用open系统调用打开文件,将文件描述符赋值给FILE结构体的_fileno
字段,最后再次调用_IO_link_in
函数,确保该结构体被链接进入_IO_list_all
链表。查看new_f->fp
就可以看到_fileno
被设置为0x3。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 pwndbg> p new_f->fp $9 = { file = { _flags = -72539004 , _IO_read_ptr = 0x0 , _IO_read_end = 0x0 , _IO_read_base = 0x0 , _IO_write_base = 0x0 , _IO_write_ptr = 0x0 , _IO_write_end = 0x0 , _IO_buf_base = 0x0 , _IO_buf_end = 0x0 , _IO_save_base = 0x0 , _IO_backup_base = 0x0 , _IO_save_end = 0x0 , _markers = 0x0 , _chain = 0x7ffff7dd2540 <_IO_2_1_stderr_>, _fileno = 3 , _flags2 = 0 , _old_offset = 0 , _cur_column = 0 , _vtable_offset = 0 '\000' , _shortbuf = "" , _lock = 0x55555555b0f0 , _offset = -1 , _codecvt = 0x0 , _wide_data = 0x55555555b100 , _freeres_list = 0x0 , _freeres_buf = 0x0 , __pad5 = 0 , _mode = 0 , _unused2 = '\000' <repeats 19 times> }, vtable = 0x7ffff7dd06e0 <_IO_file_jumps> }
执行完成之后返回FILE结构体指针。至此对fopen的调试结束。
fread 和fopen一样用个程序来,这里还是用那位师傅的程序,笔者只跟着动态调试。
1 2 3 4 5 6 7 8 #include <stdio.h> int main () { char data[20 ]; FILE *fp = fopen("test" , "rb" ); fread(data, 1 , 20 , fp); return 0 ; }
需要创建一个test文件并写入一些东西进去即可。gdb启动调试,断点下在fread这里就会看到调用了_IO_fread
,看一下FILE结构体fp的内容。可以看到此时的_IO_read_ptr
和_IO_buf_base
等指针都还是空的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 pwndbg> p *_IO_list_all $1 = { file = { _flags = -72539000 , _IO_read_ptr = 0x0 , _IO_read_end = 0x0 , _IO_read_base = 0x0 , _IO_write_base = 0x0 , _IO_write_ptr = 0x0 , _IO_write_end = 0x0 , _IO_buf_base = 0x0 , _IO_buf_end = 0x0 , _IO_save_base = 0x0 , _IO_backup_base = 0x0 , _IO_save_end = 0x0 , _markers = 0x0 , _chain = 0x7ffff7f995e0 <_IO_2_1_stderr_>, _fileno = 3 , _flags2 = 0 , _old_offset = 0 , _cur_column = 0 , _vtable_offset = 0 '\000' , _shortbuf = "" , _lock = 0x555555559380 , _offset = -1 , _codecvt = 0x0 , _wide_data = 0x555555559390 , _freeres_list = 0x0 , _freeres_buf = 0x0 , __pad5 = 0 , _mode = 0 , _unused2 = '\000' <repeats 19 times> }, vtable = 0x7ffff7f9a4a0 <_IO_file_jumps> }
vtable中的指针内容如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 pwndbg> p *_IO_list_all->vtable $2 = { __dummy = 0 , __dummy2 = 0 , __finish = 0x7ffff7e4a440 <_IO_new_file_finish>, __overflow = 0x7ffff7e4aea0 <_IO_new_file_overflow>, __underflow = 0x7ffff7e4ab50 <_IO_new_file_underflow>, __uflow = 0x7ffff7e4bf10 <__GI__IO_default_uflow>, __pbackfail = 0x7ffff7e4d2d0 <__GI__IO_default_pbackfail>, __xsputn = 0x7ffff7e4a030 <_IO_new_file_xsputn>, __xsgetn = 0x7ffff7e49c10 <__GI__IO_file_xsgetn>, __seekoff = 0x7ffff7e49470 <_IO_new_file_seekoff>, __seekpos = 0x7ffff7e4c2a0 <_IO_default_seekpos>, __setbuf = 0x7ffff7e48d30 <_IO_new_file_setbuf>, __sync = 0x7ffff7e48bc0 <_IO_new_file_sync>, __doallocate = 0x7ffff7e3d8d0 <__GI__IO_file_doallocate>, __read = 0x7ffff7e4a210 <__GI__IO_file_read>, __write = 0x7ffff7e49a70 <_IO_new_file_write>, __seek = 0x7ffff7e491a0 <__GI__IO_file_seek>, __close = 0x7ffff7e48d20 <__GI__IO_file_close>, __stat = 0x7ffff7e49a60 <__GI__IO_file_stat>, __showmanyc = 0x7ffff7e4d460 <_IO_default_showmanyc>, __imbue = 0x7ffff7e4d470 <_IO_default_imbue> }
fread实际上是_IO_fread
函数,文件目录为/libio/iofread.c
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 _IO_size_t _IO_fread (void *buf, _IO_size_t size, _IO_size_t count, _IO_FILE *fp) { _IO_size_t bytes_requested = size * count; _IO_size_t bytes_read; CHECK_FILE (fp, 0 ); if (bytes_requested == 0 ) return 0 ; _IO_acquire_lock (fp); # 调用_IO_sgetn函数 bytes_read = _IO_sgetn (fp, (char *) buf, bytes_requested); _IO_release_lock (fp); return bytes_requested == bytes_read ? count : bytes_read / size; }
源码中可以看到又调用了_IO_sgetn
函数,跟进它。
1 2 3 4 5 6 463 _IO_size_t 464 _IO_sgetn (_IO_FILE *fp, void *data, _IO_size_t n) 465 { 466 ► 467 return _IO_XSGETN (fp, data, n); 468 }
又调用了_IO_XSGETN
,#define _IO_XSGETN(FP, DATA, N) JUMP2 (__xsgetn, FP, DATA, N)
,继续跟进就可以发现最终调用了_IO_file_xsgetn
实际上就是FILE结构体中vtable的__xsgetn
函数,位于libio/fileops.c
。下面贴一下比较重要的部分
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 _IO_size_t _IO_file_xsgetn (_IO_FILE *fp, void *data, _IO_size_t n) { _IO_size_t want, have; _IO_ssize_t count; char *s = data; want = n; if (fp->_IO_buf_base == NULL ) { ... # 第一部分,如果fp->_IO_buf_base为空的话则调用`_IO_doallocbuf` _IO_doallocbuf (fp); } while (want > 0 ) { have = fp->_IO_read_end - fp->_IO_read_ptr; ## 第二部分,输入缓冲区里已经有足够的字符,则直接把缓冲区里的字符给目标buff if (want <= have) { memcpy (s, fp->_IO_read_ptr, want); fp->_IO_read_ptr += want; want = 0 ; } else { # 第二部分,输入缓冲区里有部分字符,但是没有达到fread的size需求,先把已有的拷贝至目标buff if (have > 0 ) { ... memcpy (s, fp->_IO_read_ptr, have); s += have; want -= have; fp->_IO_read_ptr += have; } if (fp->_IO_buf_base && want < (size_t ) (fp->_IO_buf_end - fp->_IO_buf_base)) { ## 第三部分,输入缓冲区里不能满足需求,调用__underflow读入数据 if (__underflow (fp) == EOF) break ; continue ; } ... return n - want; } libc_hidden_def (_IO_file_xsgetn)
_IO_file_xsgetn
是处理fread
读入数据的核心函数,分为三个部分:
第一部分是fp->_IO_buf_base
为空的情况,表明此时的FILE结构体中的指针未被初始化,输入缓冲区未建立,则调用_IO_doallocbuf
去初始化指针,建立输入缓冲区。
第二部分是输入缓冲区里有输入,即fp->_IO_read_ptr
小于fp->_IO_read_end
,此时将缓冲区里的数据直接拷贝至目标buff。
第三部分是输入缓冲区里的数据为空或者是不能满足全部的需求,则调用__underflow
调用系统调用读入数据。
接下来对_IO_file_xsgetn
这三部分进行跟进并分析。
初始化输入缓冲区 首先是第一部分,在fp->_IO_buf_base
为空时,也就是输入缓冲区未建立时,代码调用_IO_doallocbuf
函数去建立输入缓冲区。跟进_IO_doallocbuf
函数,看下它是如何初始化输入缓冲区,为输入缓冲区分配空间的,文件在/libio/genops.c
中:
1 2 3 4 5 6 7 8 9 10 11 void _IO_doallocbuf (_IO_FILE *fp) { if (fp->_IO_buf_base) # 如何输入缓冲区不为空,直接返回 return ; if (!(fp->_flags & _IO_UNBUFFERED) || fp->_mode > 0 ) #检查标志位 if (_IO_DOALLOCATE (fp) != EOF) ## 调用vtable函数 return ; _IO_setb (fp, fp->_shortbuf, fp->_shortbuf+1 , 0 ); } libc_hidden_def (_IO_doallocbuf)
函数先检查fp->_IO_buf_base
是否为空,如果不为空的话表明该输入缓冲区已被初始化,直接返回。如果为空,则检查fp->_flags
看它是不是_IO_UNBUFFERED
或者fp->_mode
大于0,如果满足条件调用FILE的vtable中的_IO_file_doallocate
,跟进去该函数,在/libio/filedoalloc.c
中:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 _IO_file_doallocate (_IO_FILE *fp) { _IO_size_t size; char *p; struct stat64 st ; ... size = _IO_BUFSIZ; ... if (fp->_fileno >= 0 && __builtin_expect (_IO_SYSSTAT (fp, &st), 0 ) >= 0 ) # 调用`_IO_SYSSTAT`获取FILE信息 { ... if (st.st_blksize > 0 ) size = st.st_blksize; ... } p = malloc (size); ... _IO_setb (fp, p, p + size, 1 ); # 调用`_IO_setb`设置FILE缓冲区 return 1 ; } libc_hidden_def (_IO_file_doallocate)
可以看到_IO_file_doallocate
函数是分配输入缓冲区的实现函数,首先调用_IO_SYSSTAT
去获取文件信息,_IO_SYSSTAT
函数是vtable中的 __stat
函数,获取文件信息,修改相应需要申请的size。可以看到在执行完_IO_SYSSTAT
函数后,st结构体的值为:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 pwndbg> p st $4 = { st_dev = 64514 , st_ino = 2248406 , st_nlink = 1 , st_mode = 33204 , st_uid = 1000 , st_gid = 1000 , __pad0 = 0 , st_rdev = 0 , st_size = 7 , st_blksize = 4096 , st_blocks = 8 , st_atim = { tv_sec = 1661346060 , tv_nsec = 403757781 }, st_mtim = { tv_sec = 1661346059 , tv_nsec = 331754338 }, st_ctim = { tv_sec = 1661346059 , tv_nsec = 331754338 }, __glibc_reserved = {0 , 0 , 0 } }
因此size被修改为st.st_blksize
所对应的大小0x1000,接着调用malloc去申请内存,申请出来的堆块如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 pwndbg> heap Allocated chunk | PREV_INUSE Addr: 0x55555555b000 Size: 0x231 Allocated chunk | PREV_INUSE Addr: 0x55555555b230 Size: 0x1011 Top chunk | PREV_INUSE Addr: 0x55555555c240 Size: 0x1fdc1 pwndbg> x/20 gx 0x55555555b230 0x55555555b230 : 0x00007ffff7dd0260 0x0000000000001011 0x55555555b240 : 0x0000000000000000 0x0000000000000000 0x55555555b250 : 0x0000000000000000 0x0000000000000000
空间申请出来后,调用_IO_setb
,跟进去看它干了些啥,文件在/libio/genops.c
中:
1 2 3 4 5 6 7 8 9 void _IO_setb (_IO_FILE *f, char *b, char *eb, int a) { ... f->_IO_buf_base = b; # 设置_IO_buf_base f->_IO_buf_end = eb; # 设置_IO_buf_end ... } libc_hidden_def (_IO_setb)
函数相对比较简单的就是设置了_IO_buf_base
和_IO_buf_end
,可以预料到_IO_setb
函数执行完后,fp的这两个指针被赋上值了:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 pwndbg> p *_IO_list_all $5 = { file = { _flags = -72539000 , _IO_read_ptr = 0x0 , _IO_read_end = 0x0 , _IO_read_base = 0x0 , _IO_write_base = 0x0 , _IO_write_ptr = 0x0 , _IO_write_end = 0x0 , _IO_buf_base = 0x55555555b240 "" , _IO_buf_end = 0x55555555c240 "" , _IO_save_base = 0x0 , _IO_backup_base = 0x0 , _IO_save_end = 0x0 , _markers = 0x0 , _chain = 0x7ffff7dd2540 <_IO_2_1_stderr_>, _fileno = 3 , _flags2 = 0 , _old_offset = 0 , _cur_column = 0 , _vtable_offset = 0 '\000' , _shortbuf = "" , _lock = 0x55555555b0f0 , _offset = -1 , _codecvt = 0x0 , _wide_data = 0x55555555b100 , _freeres_list = 0x0 , _freeres_buf = 0x0 , __pad5 = 0 , _mode = 0 , _unused2 = '\000' <repeats 19 times> }, vtable = 0x7ffff7dd06e0 <_IO_file_jumps> }
到此,初始化缓冲区就完成了,函数返回_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
中:
1 2 3 4 5 6 7 8 9 10 11 12 13 int __underflow (_IO_FILE *fp) { # 额外的检查 ... if (fp->_IO_read_ptr < fp->_IO_read_end) return *(unsigned char *) fp->_IO_read_ptr; ... # 调用_IO_UNDERFLOW return _IO_UNDERFLOW (fp); } libc_hidden_def (__underflow)
函数稍微做一些检查就会调用_IO_UNDERFLOW
函数,其中一个检查是如果fp->_IO_read_ptr
小于fp->_IO_read_end
则表明输入缓冲区里存在数据,可直接返回,否则则表示需要继续读入数据。
检查都通过的话就会调用_IO_UNDERFLOW
函数,该函数是FILE结构体vtable里的_IO_new_file_underflow
,跟进去看,文件在/libio/fileops.c
里:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 int _IO_new_file_underflow (_IO_FILE *fp) { _IO_ssize_t count; ... ## 如果存在_IO_NO_READS标志,则直接返回 if (fp->_flags & _IO_NO_READS) { fp->_flags |= _IO_ERR_SEEN; __set_errno (EBADF); return EOF; } ## 如果输入缓冲区里存在数据,则直接返回 if (fp->_IO_read_ptr < fp->_IO_read_end) return *(unsigned char *) fp->_IO_read_ptr; ... ## 如果没有输入缓冲区,则调用_IO_doallocbuf分配输入缓冲区 if (fp->_IO_buf_base == NULL ) { ... _IO_doallocbuf (fp); } ... ## 设置FILE结构体指针 fp->_IO_read_base = fp->_IO_read_ptr = fp->_IO_buf_base; fp->_IO_read_end = fp->_IO_buf_base; fp->_IO_write_base = fp->_IO_write_ptr = fp->_IO_write_end = fp->_IO_buf_base; ##调用_IO_SYSREAD函数最终执行系统调用读取数据 count = _IO_SYSREAD (fp, fp->_IO_buf_base, fp->_IO_buf_end - fp->_IO_buf_base); ... ## 设置结构体指针 fp->_IO_read_end += count; ... return *(unsigned char *) fp->_IO_read_ptr; } libc_hidden_ver (_IO_new_file_underflow, _IO_file_underflow)
这个_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
。
其中第二步里面的如果fp->_IO_buf_base
位null,则调用_IO_doallocbuf
分配输入缓冲区,似乎有点累赘,因为之前已经分配了,这个原因我在最后会说明。
其中第四步的_IO_SYSREAD
(vtable中的_IO_file_read
函数)的源码比较简单,就是执行系统调用函数read去读取文件数据,文件在libio/fileops.c
,源码如下:
1 2 3 4 5 6 7 _IO_ssize_t _IO_file_read (_IO_FILE *fp, void *buf, _IO_ssize_t size) { return (__builtin_expect (fp->_flags2 & _IO_FLAGS2_NOTCANCEL, 0 ) ? read_not_cancel (fp->_fileno, buf, size) : read (fp->_fileno, buf, size)); }
_IO_file_underflow
函数执行完毕以后,FILE结构体中各个指针已被赋值,且文件数据已读入,输入缓冲区里已经有数据,结构体值如下,其中fp->_IO_read_ptr
指向输入缓冲区数据的开始位置,fp->_IO_read_end
指向输入缓冲区数据结束的位置:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 pwndbg> p *_IO_list_all $4 = { file = { _flags = -72539000 , _IO_read_ptr = 0x55555555b240 "111111\n" , _IO_read_end = 0x55555555b247 "" , _IO_read_base = 0x55555555b240 "111111\n" , _IO_write_base = 0x55555555b240 "111111\n" , _IO_write_ptr = 0x55555555b240 "111111\n" , _IO_write_end = 0x55555555b240 "111111\n" , _IO_buf_base = 0x55555555b240 "111111\n" , _IO_buf_end = 0x55555555c240 "" , _IO_save_base = 0x0 , _IO_backup_base = 0x0 , _IO_save_end = 0x0 , _markers = 0x0 , _chain = 0x7ffff7dd2540 <_IO_2_1_stderr_>, _fileno = 3 , _flags2 = 0 , _old_offset = 0 , _cur_column = 0 , _vtable_offset = 0 '\000' , _shortbuf = "" , _lock = 0x55555555b0f0 , _offset = -1 , _codecvt = 0x0 , _wide_data = 0x55555555b100 , _freeres_list = 0x0 , _freeres_buf = 0x0 , __pad5 = 0 , _mode = -1 , _unused2 = '\000' <repeats 19 times> }, vtable = 0x7ffff7dd06e0 <_IO_file_jumps> }
函数执行完后,返回到_IO_file_xsgetn
函数中,由于while
循环的存在,重新执行第二部分,此时将输入缓冲区拷贝至目标缓冲区,最终返回。
至此,对于fread的源码分析结束。
其他输入函数 完整分析了fread函数之后,还想知道其他一些函数(scanf、gets)等函数时如何通过stdin实现输入的,我编写了源码,并将断点下在了read函数之前,看他们时如何调用去的。
首先是scanf,其最终调用read函数时栈回溯如下:
1 2 3 4 5 6 7 read _IO_new_file_underflow at fileops.c __GI__IO_default_uflow at genops.c _IO_vfscanf_internal at vfscanf.c __isoc99_scanf at at isoc99_scanf.c main () __libc_start_main
可以看到scanf最终也是调用stdin的vtable中的_IO_new_file_underflow
去调用read的。不过它并不是使用_IO_file_xsgetn
,而是使用vtable中的__uflow
,源码如下:
1 2 3 4 5 6 7 8 9 int _IO_default_uflow (_IO_FILE *fp) { int ch = _IO_UNDERFLOW (fp); if (ch == EOF) return EOF; return *(unsigned char *) fp->_IO_read_ptr++; } libc_hidden_def (_IO_default_uflow)
__uflow
函数基本上啥都没干直接就调用了_IO_new_file_underflow
因此最终也是_IO_new_file_underflow
实现的输入。
再看看gets
函数,函数调用栈如下,与scanf基本一致:
1 2 3 4 5 6 read __GI__IO_file_underflow __GI__IO_default_uflow gets main __libc_start_main+240
再试了试fscanf等,仍然是一样的,仍然是最终通过_IO_new_file_underflow
实现的输入。虽然不能说全部的io输入都是通过_IO_new_file_underflow
函数最终实现的输入,但是应该也可以说大部分是使用_IO_new_file_underflow
函数实现的。
但是仍然有一个问题,由于__uflow
直接就调用了_IO_new_file_underflow
函数,那么输入缓冲区是在哪里建立的呢,为了找到这个问题的答案,我在程序进入到fscanf函数后又在malloc
函数下了个断点,然后栈回溯:
1 2 3 4 5 6 7 8 9 malloc __GI__IO_file_doallocate __GI__IO_doallocbuf __GI__IO_file_underflow __GI__IO_default_uflow __GI__IO_vfscanf __isoc99_fscanf main __libc_start_main
原来是在__GI__IO_file_underflow
分配的空间,回到上面看该函数的源码,确实有一段判断输入缓冲区如果为空则调用__GI__IO_doallocbuf
函数建立输入缓冲区的代码,这就解释了__GI__IO_file_underflow
第二步中为啥还会有个输入缓冲区判断的原因了,不得不感慨,代码写的真巧妙。
在结束之前我想总结下fread
在执行系统调用read前对vtable里的哪些函数进行了调用,具体如下:
_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
,它里面有个标志位的判断,是后面构造利用需要注意的一个比较重要条件:
1 2 3 4 5 6 7 ## 如果存在_IO_NO_READS标志,则直接返回 if (fp->_flags & _IO_NO_READS) { fp->_flags |= _IO_ERR_SEEN; __set_errno (EBADF); return EOF; }
fwrite 调试程序如下
1 2 3 4 5 6 7 8 9 #include <stdio.h> int main () { char *data=malloc (0x1000 ); FILE*fp=fopen("test" ,"wb" ); fwrite(data,1 ,0x30 ,fp); return 0 ; }
首先使用进行初步的跟踪,在fwrite
下断点。看到程序首先断在_IO_fwrite
函数中,在开始调试之前,仍然是先把传入的IO FILE fp
值看一看:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 pwndbg> p *_IO_list_all $1 = { file = { _flags = -72540026 , _IO_read_ptr = 0x0 , _IO_read_end = 0x0 , _IO_read_base = 0x0 , _IO_write_base = 0x0 , _IO_write_ptr = 0x0 , _IO_write_end = 0x0 , _IO_buf_base = 0x0 , _IO_buf_end = 0x0 , _IO_save_base = 0x0 , _IO_backup_base = 0x0 , _IO_save_end = 0x0 , _markers = 0x0 , _chain = 0x7ffff7fb76a0 <_IO_2_1_stdout_>, _fileno = 2 , _flags2 = 0 , _old_offset = -1 , _cur_column = 0 , _vtable_offset = 0 '\000' , _shortbuf = "" , _lock = 0x7ffff7fb87d0 <_IO_stdfile_2_lock>, _offset = -1 , _codecvt = 0x0 , _wide_data = 0x7ffff7fb6780 <_IO_wide_data_2>, _freeres_list = 0x0 , _freeres_buf = 0x0 , __pad5 = 0 , _mode = 0 , _unused2 = '\000' <repeats 19 times> }, vtable = 0x7ffff7fb34a0 <_IO_file_jumps> }
以及此时的vtable中的内容:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 pwndbg> p *_IO_list_all->vtable $2 = { __dummy = 0 , __dummy2 = 0 , __finish = 0x7ffff7e59f50 <_IO_new_file_finish>, __overflow = 0x7ffff7e5ad80 <_IO_new_file_overflow>, __underflow = 0x7ffff7e5aa20 <_IO_new_file_underflow>, __uflow = 0x7ffff7e5bf50 <__GI__IO_default_uflow>, __pbackfail = 0x7ffff7e5d680 <__GI__IO_default_pbackfail>, __xsputn = 0x7ffff7e595d0 <_IO_new_file_xsputn>, __xsgetn = 0x7ffff7e59240 <__GI__IO_file_xsgetn>, __seekoff = 0x7ffff7e58860 <_IO_new_file_seekoff>, __seekpos = 0x7ffff7e5c600 <_IO_default_seekpos>, __setbuf = 0x7ffff7e58530 <_IO_new_file_setbuf>, __sync = 0x7ffff7e583c0 <_IO_new_file_sync>, __doallocate = 0x7ffff7e4bc70 <__GI__IO_file_doallocate>, __read = 0x7ffff7e595a0 <__GI__IO_file_read>, __write = 0x7ffff7e58e60 <_IO_new_file_write>, __seek = 0x7ffff7e58600 <__GI__IO_file_seek>, __close = 0x7ffff7e58520 <__GI__IO_file_close>, __stat = 0x7ffff7e58e40 <__GI__IO_file_stat>, __showmanyc = 0x7ffff7e5d810 <_IO_default_showmanyc>, __imbue = 0x7ffff7e5d820 <_IO_default_imbue> }
从图里也看到由于刚经过fopen
初始化,输入输出缓冲区没有建立,此时的所有指针都为空。
_IO_fwrite
函数在文件/libio/iofwrite.c
中:
1 2 3 4 5 6 7 8 9 10 _IO_size_t _IO_fwrite (const void *buf, _IO_size_t size, _IO_size_t count, _IO_FILE *fp) { _IO_size_t request = size * count; ... if (_IO_vtable_offset (fp) != 0 || _IO_fwide (fp, -1 ) == -1 ) written = _IO_sputn (fp, (const char *) buf, request); ... } libc_hidden_def (_IO_fwrite)
没有做过多的操作就调用了_IO_sputn
函数,该函数是vtable
中的__xsputn
(_IO_new_file_xsputn
)在文件/libio/fileops.c中,这里就不一次性把函数的所有源码都贴在这里,而是按部分贴在下面每个部分的开始的地方,不然感觉有些冗余。
如流程所示,源码分析分四个部分进行,与流程相对应,其中下面每部分刚开始的代码都是_IO_new_file_xsputn
函数中的源码。
将目标输出数据拷贝至输出缓冲区 第一部分所包含的代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 _IO_size_t _IO_new_file_xsputn (_IO_FILE *f, const void *data, _IO_size_t n) { _IO_size_t count = 0 ; ... ## 判断输出缓冲区还有多少空间 else if (f->_IO_write_end > f->_IO_write_ptr) count = f->_IO_write_end - f->_IO_write_ptr; ## 如果输出缓冲区有空间,则先把数据拷贝至输出缓冲区 if (count > 0 ) { if (count > to_do) count = to_do; ... memcpy (f->_IO_write_ptr, s, count); f->_IO_write_ptr += count; ## 计算是否还有目标输出数据剩余 s += count; to_do -= count;
主要功能就是判断输出缓冲区还有多少空间,其中像demo
中的程序所示的f->_IO_write_end
以及f->_IO_write_ptr
均为0,此时的输出缓冲区为0。
另一部分则是如果输出缓冲区如果仍有剩余空间的话,则将目标输出数据拷贝至输出缓冲区,并计算在输出缓冲区填满后,是否仍然剩余目标输出数据。
建立输出缓冲区或flush输出缓冲区 第二部分代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 ## 如果还有目标数据剩余,此时则表明输出缓冲区未建立或输出缓冲区已经满了 if (to_do + must_flush > 0 ) { _IO_size_t block_size, do_write; ## 函数实现清空输出缓冲区或建立缓冲区的功能 if (_IO_OVERFLOW (f, EOF) == EOF) return to_do == 0 ? EOF : n - to_do; ## 检查输出数据是否是大块 block_size = f->_IO_buf_end - f->_IO_buf_base; do_write = to_do - (block_size >= 128 ? to_do % block_size : 0 );
经过了上一步骤后,如果还有目标输出数据,表明输出缓冲区未建立或输出缓冲区已经满了,此时调用_IO_OVERFLOW
函数,该函数功能主要是实现刷新输出缓冲区或建立缓冲区的功能,该函数是vtable函数中的__overflow
(_IO_new_file_overflow
),文件在/libio/fileops.c
中:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 int _IO_new_file_overflow (_IO_FILE *f, int ch) { ## 判断标志位是否包含_IO_NO_WRITES if (f->_flags & _IO_NO_WRITES) { f->_flags |= _IO_ERR_SEEN; __set_errno (EBADF); return EOF; } ## 判断输出缓冲区是否为空 if ((f->_flags & _IO_CURRENTLY_PUTTING) == 0 || f->_IO_write_base == NULL ) { if (f->_IO_write_base == NULL ) { ## 分配输出缓冲区 _IO_doallocbuf (f); _IO_setg (f, f->_IO_buf_base, f->_IO_buf_base, f->_IO_buf_base); } ## 初始化指针 if (f->_IO_read_ptr == f->_IO_buf_end) f->_IO_read_end = f->_IO_read_ptr = f->_IO_buf_base; f->_IO_write_ptr = f->_IO_read_ptr; f->_IO_write_base = f->_IO_write_ptr; f->_IO_write_end = f->_IO_buf_end; f->_IO_read_base = f->_IO_read_ptr = f->_IO_read_end; f->_flags |= _IO_CURRENTLY_PUTTING; if (f->_mode <= 0 && f->_flags & (_IO_LINE_BUF | _IO_UNBUFFERED)) f->_IO_write_end = f->_IO_write_ptr; } ## 输出输出缓冲区 if (ch == EOF) return _IO_do_write (f, f->_IO_write_base, f->_IO_write_ptr - f->_IO_write_base); if (f->_IO_write_ptr == f->_IO_buf_end ) if (_IO_do_flush (f) == EOF) ## return EOF; *f->_IO_write_ptr++ = ch; if ((f->_flags & _IO_UNBUFFERED) || ((f->_flags & _IO_LINE_BUF) && ch == '\n' )) if (_IO_do_write (f, f->_IO_write_base, f->_IO_write_ptr - f->_IO_write_base) == EOF) return EOF; return (unsigned char ) ch; } libc_hidden_ver (_IO_new_file_overflow, _IO_file_overflow)
__overflow
函数首先检测IO FILE的_flags
是否包含_IO_NO_WRITES
标志位,如果包含的话则直接返回。
接着判断f->_IO_write_base
是否为空,如果为空的话表明输出缓冲区尚未建立,就调用_IO_doallocbuf
函数去分配输出缓冲区,_IO_doallocbuf
函数源码在上一篇fread
中已经分析过了就不跟过去了,它的功能是分配输入输出缓冲区并将指针_IO_buf_base
和_IO_buf_end
赋值。在执行完_IO_doallocbuf
分配空间后调用_IO_setg
宏,该宏的定义为如下,它将输入相关的缓冲区指针赋值为_IO_buf_base
指针:
1 2 #define _IO_setg(fp, eb, g, eg) ((fp)->_IO_read_base = (eb),\ (fp)->_IO_read_ptr = (g), (fp)->_IO_read_end = (eg))
经过上面这些步骤,此时IO FILE的指针如下图所示,可以看到,_IO_buf_base
和_IO_buf_end
被赋值,且输入缓冲区相关指针被赋值为_IO_buf_base
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 pwndbg> p *_IO_list_all $3 = { file = { _flags = -72539004 , _IO_read_ptr = 0x55555555c250 "" , _IO_read_end = 0x55555555c250 "" , _IO_read_base = 0x55555555c250 "" , _IO_write_base = 0x0 , _IO_write_ptr = 0x0 , _IO_write_end = 0x0 , _IO_buf_base = 0x55555555c250 "" , _IO_buf_end = 0x55555555d250 "" , _IO_save_base = 0x0 , _IO_backup_base = 0x0 , _IO_save_end = 0x0 , _markers = 0x0 , _chain = 0x7ffff7dd2540 <_IO_2_1_stderr_>, _fileno = 3 , _flags2 = 0 , _old_offset = 0 , _cur_column = 0 , _vtable_offset = 0 '\000' , _shortbuf = "" , _lock = 0x55555555c100 , _offset = -1 , _codecvt = 0x0 , _wide_data = 0x55555555c110 , _freeres_list = 0x0 , _freeres_buf = 0x0 , __pad5 = 0 , _mode = -1 , _unused2 = '\000' <repeats 19 times> }, vtable = 0x7ffff7dd06e0 <_IO_file_jumps> }
然后代码初始化其他相关指针,最主要的就是将f->_IO_write_base
以及将f->_IO_write_ptr
设置成f->_IO_read_ptr
指针;将f->_IO_write_end
赋值为f->_IO_buf_end
指针。
接着就执行_IO_do_write
来调用系统调用write
输出输出缓冲区,输出的内容为f->_IO_write_ptr
到f->_IO_write_base
之间的内容。跟进去该函数,函数在/libio/fileops.c
中:
1 2 3 4 5 6 7 int _IO_new_do_write (_IO_FILE *fp, const char *data, _IO_size_t to_do) { return (to_do == 0 || (_IO_size_t) new_do_write (fp, data, to_do) == to_do) ? 0 : EOF; } libc_hidden_ver (_IO_new_do_write, _IO_do_write)
该函数调用了new_do_write
,跟进去,函数在/libio/fileops.c
中:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 static _IO_size_t new_do_write (_IO_FILE *fp, const char *data, _IO_size_t to_do) { _IO_size_t count; ... ## 额外判断 else if (fp->_IO_read_end != fp->_IO_write_base) { _IO_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); ... ## 刷新设置缓冲区指针 _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_SYSWRITE
的地方,进行一个判断,判断fp->_IO_read_end
是否等于fp->_IO_write_base
,如果不等的话,调用_IO_SYSSEEK
去调整文件偏移,这个函数就不跟进去了,正常执行流程不会过去这里。
接着就调用_IO_SYSWRITE
函数,该函数是vtable中的__write
(_IO_new_file_write
)函数,也是最终执行系统调用的地方,跟进去看,文件在/libio/fileops.c
中:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 _IO_ssize_t _IO_new_file_write (_IO_FILE *f, const void *data, _IO_ssize_t n) { _IO_ssize_t to_do = n; while (to_do > 0 ) { ## 系统调用write输出 _IO_ssize_t count = (__builtin_expect (f->_flags2 & _IO_FLAGS2_NOTCANCEL, 0 ) ? write_not_cancel (f->_fileno, data, to_do) : write (f->_fileno, data, to_do)); ... return n; }
执行完_IO_SYSWRITE
函数后,回到new_do_write
函数,刷新设置缓冲区指针并返回。
以块为单位直接输出数据 经历了缓冲区建立以及刷新缓冲区,程序返回到_IO_new_file_xsputn
函数中,进入到如下代码功能块:
1 2 3 4 5 6 7 8 9 10 11 12 13 ## 检查输出数据是否是大块 block_size = f->_IO_buf_end - f->_IO_buf_base; do_write = to_do - (block_size >= 128 ? to_do % block_size : 0 ); if (do_write) { ## 如果是大块的话则不使用输出缓冲区而直接输出。 count = new_do_write (f, s, do_write); to_do -= count; if (count < do_write) return n - to_do; }
运行到此处,此时已经经过了_IO_OVERFLOW
函数(对输出缓冲区进行了初始化或者刷新),也就是说此时的IO FILE缓冲区指针的状态是处于刷新的初始化状态,输出缓冲区中也没有数据。
上面这部分代码检查剩余目标输出数据大小,如果超过输入缓冲区f->_IO_buf_end - f->_IO_buf_base
的大小,则为了提高效率,不再使用输出缓冲区,而是以块为基本单位直接将缓冲区调用new_do_write
输出。new_do_write
函数在上面已经跟过了就是输出,并刷新指针设置。
由于demo程序只输出0x60大小的数据,而它的输出缓冲区大小为0x1000,因此不会进入该部分代码。
剩余目标输出数据放入输出缓冲区中 在以大块为基本单位把数据直接输出后可能还剩余小块数据,IO采用的策略则是将剩余目标输出数据放入到输出缓冲区里面,相关源码如下:
1 2 3 ## 剩余的数据拷贝至输出缓冲区 if (to_do) to_do -= _IO_default_xsputn (f, s+do_write, to_do);
程序调用_IO_default_xsputn
函数对剩下的s+do_write
数据进行操作,跟进去该函数,在/libio/genops.c
中:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 _IO_size_t _IO_default_xsputn (_IO_FILE *f, const void *data, _IO_size_t n) { const char *s = (char *) data; _IO_size_t more = n; if (more <= 0 ) return 0 ; for (;;) { if (f->_IO_write_ptr < f->_IO_write_end) { _IO_size_t count = f->_IO_write_end - f->_IO_write_ptr; if (count > more) count = more; if (count > 20 ) { ## 输出长度大于20 ,则调用memcpy 拷贝 memcpy (f->_IO_write_ptr, s, count); f->_IO_write_ptr += count; #endif s += count; } else if (count) { ## 小于20 则直接赋值 char *p = f->_IO_write_ptr; _IO_ssize_t i; for (i = count; --i >= 0 ; ) *p++ = *s++; f->_IO_write_ptr = p; } more -= count; } ## 如果输出缓冲区为空,则调用`_IO_OVERFLOW`直接输出。 if (more == 0 || _IO_OVERFLOW (f, (unsigned char ) *s++) == EOF) break ; more--; } return n - more; } libc_hidden_def (_IO_default_xsputn)
可以看到函数最主要的作用就是将剩余的目标输出数据拷贝到输出缓冲区里。为了性能优化,当长度大于20时,使用memcpy拷贝,当长度小于20时,使用for循环赋值拷贝。如果输出缓冲区为空,则调用_IO_OVERFLOW
进行输出。
根据源码我们也知道,demo程序中,最终会进入到_IO_default_xsputn
中,并且把数据拷贝至输出缓冲区里,执行完成后,看到IO 结构体的数据如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 pwndbg> p *_IO_list_all $1 = { file = { _flags = -72536956 , _IO_read_ptr = 0x55555555c250 "" , _IO_read_end = 0x55555555c250 "" , _IO_read_base = 0x55555555c250 "" , _IO_write_base = 0x55555555c250 "" , _IO_write_ptr = 0x55555555c280 "" , _IO_write_end = 0x55555555d250 "" , _IO_buf_base = 0x55555555c250 "" , _IO_buf_end = 0x55555555d250 "" , _IO_save_base = 0x0 , _IO_backup_base = 0x0 , _IO_save_end = 0x0 , _markers = 0x0 , _chain = 0x7ffff7dd2540 <_IO_2_1_stderr_>, _fileno = 3 , _flags2 = 0 , _old_offset = 0 , _cur_column = 0 , _vtable_offset = 0 '\000' , _shortbuf = "" , _lock = 0x55555555c100 , _offset = -1 , _codecvt = 0x0 , _wide_data = 0x55555555c110 , _freeres_list = 0x0 , _freeres_buf = 0x0 , __pad5 = 0 , _mode = -1 , _unused2 = '\000' <repeats 19 times> }, vtable = 0x7ffff7dd06e0 <_IO_file_jumps> }
可以看到此时的_IO_write_base
为0x55555555c250
,而_IO_write_ptr
为0x55555555c280
,大小正好是0x30 。
至此源码分析结束。
其他输出函数 fwrite
分析完了,知道它最主要的就是通过vtable函数里面的_IO_new_file_xsputn
实现功能,且最终的建立以及刷新输出缓冲区是在_IO_new_file_overflow
函数里面,最终执行系统调用write对数据进行输出是在new_do_write
函数中。
下面来看一下其他输出函数的栈回溯的情况,应该也都差不多,对于下面的函数,断点下在write
函数,然后查看栈回溯。
首先是printf函数,它的栈回溯为:
1 2 3 4 5 6 7 8 9 write _IO_new_file_write new_do_write+51 __GI__IO_do_write __GI__IO_file_xsputn vfprintf printf main __libc_start_main
也是调用_IO_new_file_overflow
函数进行的实现。但是printf
函数里面情况其实也还挺复杂的,篇幅的限制,就不细说了,其他的输出函数应该也差不多。
结束之前仍然总结下fwrite
在执行系统调用write前对vtable里的哪些函数进行了调用,具体如下:
_IO_fwrite
函数调用了vtable的_IO_new_file_xsputn
。
_IO_new_file_xsputn
函数调用了vtable中的_IO_new_file_overflow
实现缓冲区的建立以及刷新缓冲区。
vtable中的_IO_new_file_overflow
函数调用了vtable的_IO_file_doallocate
以初始化输入缓冲区。
vtable中的_IO_file_doallocate
调用了vtable中的__GI__IO_file_stat
以获取文件信息。
new_do_write
中的_IO_SYSWRITE
调用了vtable_IO_new_file_write
最终去执行系统调用write。
同时,后续如果想通过IO FILE输出缓冲区实现任意读写的话,最关键的函数应是_IO_new_file_overflow
,它里面有个标志位的判断,是后面构造利用需要注意的一个比较重要条件:
1 2 3 4 5 6 7 ## 判断标志位是否包含_IO_NO_WRITES if (f->_flags & _IO_NO_WRITES) { f->_flags |= _IO_ERR_SEEN; __set_errno (EBADF); return EOF; }
Reference https://ray-cp.github.io/page6/