IO_FILE基础
https://www.anquanke.com/post/id/164558#h2-1
https://wiki.wgpsec.org/knowledge/ctf/iofile.html|
https://ctf-wiki.org/pwn/linux/user-mode/io-file/introduction/#file_1
IO_FILE
版本差异
- glibc2.23 之前没有保护,之后具有保护。
利用简介
这是一种控制流劫持技术,攻击者可以利用程序中的漏洞覆盖 file 指针指向能够控制的区域,从而改写结构体中重要的数据,或者覆盖 vtable 来控制程序执行流。
IO_FILE 结构体
glibc 中的 FILE 结构体
_IO_FILE_plus
在 libc2.23 版本下,32 位的 vtable 偏移为 0x94,64 位偏移为 0xd8
FILE 实际类型 = struct _IO_FILE_plus
struct _IO_FILE_plus {
struct _IO_FILE file;
const struct _IO_jump_t *vtable;
};
vtable是IO_jump_t类型的指针,IO_jump_t 中保存了一些函数指针,在后面我们会看到在一系列标准IO函数中会调用这些函数指针:
void * funcs[] = {
1 NULL, // "extra word"
2 NULL, // DUMMY
3 exit, // finish
4 NULL, // overflow
5 NULL, // underflow
6 NULL, // uflow
7 NULL, // pbackfail
8 NULL, // xsputn #printf
9 NULL, // xsgetn
10 NULL, // seekoff
11 NULL, // seekpos
12 NULL, // setbuf
13 NULL, // sync
14 NULL, // doallocate
15 NULL, // read
16 NULL, // write
17 NULL, // seek
18 pwn, // close
19 NULL, // stat
20 NULL, // showmanyc
21 NULL, // imbue
};
_IO_FILE
真正的_IO_FILE结构体事实上还会套一层_IO_FILE_plus,下面的是_IO_FILE结构体:
struct _IO_FILE {
int _flags; //这个 32 位整数的“高16位部分”存放一个固定的魔数 _IO_MAGIC,剩下的低位部分才是真正的标志位(flags)
#define _IO_file_flags _flags //别名
/* The following pointers correspond to the C++ streambuf protocol. */
/* Note: Tk uses the _IO_read_ptr and _IO_read_end fields directly. */
char* _IO_read_ptr; /* Current read pointer */
char* _IO_read_end; /* End of get area. */
char* _IO_read_base; /* Start of putback+get area. */
// _IO_read_base <= _IO_read_ptr <= _IO_read_end
char* _IO_write_base; /* Start of put area. */
char* _IO_write_ptr; /* Current put pointer. */
char* _IO_write_end; /* End of put area. */
char* _IO_buf_base; /* Start of reserve area. */
char* _IO_buf_end; /* End of reserve area. */
/* The following fields are used to support backing up and undo. */
char *_IO_save_base; /* Pointer to start of non-current get area. */
char *_IO_backup_base; /* Pointer to first valid character of backup area */
char *_IO_save_end; /* Pointer to end of non-current get area. */
struct _IO_marker *_markers;
struct _IO_FILE *_chain; //全局链表
int _fileno;//底层的文件描述符(fd),是一个序号。
#if 0
int _blksize;
#else
int _flags2;
#endif
_IO_off_t _old_offset; // This used to be _offset but it's too small. //为了 ABI 兼容保留了 _old_offset
#define __HAVE_COLUMN /* temporary */
/* 1+column number of pbase(); 0 is unknown. */
unsigned short _cur_column;
signed char _vtable_offset;
char _shortbuf[1]; //当流处于无缓冲模式、或还没分配正式缓冲区时,至少有 1 字节空间可用
/* char* _save_gptr; char* _save_egptr; */
_IO_lock_t *_lock; //线程锁
#ifdef _IO_USE_OLD_IO_FILE
};
struct _IO_FILE_complete
{
struct _IO_FILE _file;
#endif
#if defined _G_IO_IO_FILE_VERSION && _G_IO_IO_FILE_VERSION == 0x20001
_IO_off64_t _offset;
# if defined _LIBC || defined _GLIBCPP_USE_WCHAR_T
/* Wide character stream stuff. */
struct _IO_codecvt *_codecvt; //编码转换器
struct _IO_wide_data *_wide_data;//宽字符流的缓冲与状态数据
struct _IO_FILE *_freeres_list;//清理机制有关
void *_freeres_buf;
# else
void *__pad1;//无宽字符支持,则使用__pad* 填充,保持结构体大小一致/对齐
void *__pad2;
void *__pad3;
void *__pad4;
size_t __pad5;
int _mode;
/* Make sure we don't get into trouble again. */
char _unused2[15 * sizeof (int) - 4 * sizeof (void *) - sizeof (size_t)];//额外保留空间
#endif
};
进程中的 FILE 结构会通过_chain域彼此连接形成一个链表,链表头部用全局变量_IO_list_all表示,通过这个值我们可以遍历所有的FILE结构。
stdin/stdout/stderr
在标准 I/O 库中,每个程序启动时有三个文件流是自动打开的:stdin、stdout、stderr。因此在初始状态下,_IO_list_all 指向了一个有这些文件流构成的链表,但是需要注意的是这三个文件流位于 libc.so 的数据段。而我们使用fopen创建的文件流是分配在堆内存上的。
glibc 中有:
extern struct _IO_FILE_plus _IO_2_1_stdin_;
extern struct _IO_FILE_plus _IO_2_1_stdout_;
extern struct _IO_FILE_plus _IO_2_1_stderr_;
//内存布局
libc.so:
.text
.rodata
.data
_IO_2_1_stdin_
_IO_2_1_stdout_
_IO_2_1_stderr_
他们位于 libc 的 .data 段,是静态全局变量。由于位于 libc 中可以通过泄露他们的地址来计算 libc。
stat 状态信息
struct stat {
dev_t st_dev; // 设备号
ino_t st_ino; // inode 号
mode_t st_mode; // 文件类型 + 权限
nlink_t st_nlink; // 硬链接数
uid_t st_uid; // 所有者 ID
gid_t st_gid; // 组 ID
off_t st_size; // 文件大小(字节)
blksize_t st_blksize; // 推荐块大小
blkcnt_t st_blocks; // 实际占用块数
time_t st_atime; // 访问时间
time_t st_mtime; // 修改时间
time_t st_ctime; // 状态改变时间
};
IO_FILE 之 fopen
整体流程
FILE *fp = fopen("a.txt","r");
当执行上面的代码后:
malloc一个_IO_FILE_plus_IO_no_init对 File 结构体进行初始化_IO_file_init将结构体链接至_IO_list_all链表中_IO_file_fopen执行系统调用打开文件
(图下为流程图)
这个函数创建的结构体位于堆上,也就是说可以通过堆漏洞对它进行利用。
fopen 实际上是 _IO_new_fopen,它调用的是 __fopen_internal:
_IO_FILE *
__fopen_internal (const char *filename, const char *mode, int is32)//int is32:以 32 位打开,最大支持 2G 文件
{
struct locked_FILE //文件线程锁
{
struct _IO_FILE_plus fp; //本质上包含:struct _IO_FILE,是标准的 FILE 结构体
#ifdef _IO_MTSAFE_IO
_IO_lock_t lock; //文件锁,因为锁是附加功能所以包了一层结构体。
#endif
struct _IO_wide_data wd; //支持宽字符
} *new_f = (struct locked_FILE *) malloc (sizeof (struct locked_FILE)); // 1、 分配内存
...
_IO_no_init (&new_f->fp.file, 0, 0, &new_f->wd, &_IO_wfile_jumps); // 2、 初始化结构体
...//这里 fp 一般是_IO_FILE_plus,fp.file 是里面的 _IO_FILE。最后一个参数是虚函数表。
_IO_JUMPS (&new_f->fp) = &_IO_file_jumps; // 设置 vtable 为_IO_file_jumps
_IO_file_init (&new_f->fp); // 3、 将 file 结构体链接至_IO_list_all
...
// 4、 打开文件
if (_IO_file_fopen ((_IO_FILE *) new_f, filename, mode, is32) != NULL)
return __fopen_maybe_mmap (&new_f->fp.file);
}
具体四个流程的实现如下:
malloc 流程
*new_f = (struct locked_FILE *) malloc (sizeof (struct locked_FILE));
使用 gdb 跟进 fopen,单步过 malloc 函数,可以看到 malloc 函数分配了一个 struct locked_FILE 大小的结构体,并将返回的地址赋给了 new_f 变量。1e1 表示的是结构体的大小,共包含了以下三个结构:_IO_FILE_plus、_IO_lock_t、IO_wide_data,其中 _IO_FILE_plus 为使用的 IO_FILE 结构体。
_IO_no_init 初始化
执行完 malloc 后就是 _IO_no_init 函数:相关代码如下
void
_IO_old_init (_IO_FILE *fp, int flags) //_IO_no_init 是较新的初始化接口。这个是兼容旧版本的入口,两个本质是一样的函数。
{
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; /* Not necessary. */
fp->_IO_save_base = NULL; //链表/备份/marker 等内部结构清空
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; //vtable 偏移 & 锁初始化
#endif
#ifdef _IO_MTSAFE_IO
if (fp->_lock != NULL)
_IO_lock_init (*fp->_lock);
#endif
}
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) //如果涉及宽字符,初始化_wide_data
{# 初始化fp的_wide_data字段
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
/* Cause predictable crash when a wide function is called on a byte
stream. */
fp->_wide_data = (struct _IO_wide_data *) -1L;
#endif
fp->_freeres_list = NULL; //清空 freeres 链表
}
执行完 _IO_no_init 后,函数使用 lea 指令将 _IO_FILE_plus 结构体的 vtable 设置成了 _IO_file_jumps。
IO_file_init
执行完 lea 命令之后就是 IO_file_init 阶段,不同版本的 glibc 函数会有所差异。实现的内容都是通过调用 _IO_file_init 将 _IO_FILE_plus 结构体链接到了 _IO_list_all 链表中,跟进该函数可以看到这个函数的主体是调用了_IO_link_in 函数。
void _IO_new_file_init (struct _IO_FILE_plus *fp)
{
/* POSIX.1 allows another file handle to be used to change the position
of our file descriptor. Hence we actually don't know the actual
position before we do the first fseek (and until a following fflush). */
fp->file._offset = _IO_pos_BAD;
fp->file._IO_file_flags |= CLOSED_FILEBUF_FLAGS;
// 调用_IO_link_in 和设置_fileno
_IO_link_in (fp);
fp->file._fileno = -1;
}
_IO_link_in 函数:把一个 FILE 流对象(glibc 的内部结构)挂到全局链表 _IO_list_all 里,并标记它“已经在链表中”。在没执行 _IO_file_init 函数前,_IO_list_all 指向的是 stderr 结构体。
void _IO_link_in (struct _IO_FILE_plus *fp)
{
if ((fp->file._flags & _IO_LINKED) == 0) //先判断:这个 FILE 是否已经被“链接进全局链表”; 检查 flag 的标志位是否是_IO_LINKED
{
fp->file._flags |= _IO_LINKED; // 设置_IO_LINKED 标志位
#ifdef _IO_MTSAFE_IO //在启用 _IO_MTSAFE_IO(多线程安全 I/O)时编译生效:
_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; //把 fp 插入全局链表头部
_IO_list_all = fp; //让 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
}
}
_IO_file_open
将 FILE 结构体链接到 _IO_list_all 链表后,程序返回到 __fopen_internal 中,最后是调用 _IO_file_open 函数打开文件句柄
_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;
...
}
...
result = _IO_file_open (fp, filename, omode|oflags, oprot, read_write, //调用_IO_file_open 函数打开文件
is32not64);
...
}
下面是 _IO_file_open 函数:
_IO_FILE *
_IO_file_open (_IO_FILE *fp, const char *filename, int posix_mode, int prot,
int read_write, int is32not64)
{
int fdesc;
...
# 调用系统函数open打开文件
fdesc = open (filename, posix_mode | (is32not64 ? 0 : O_LARGEFILE), prot);//fdesc 是文件描述符,是一个索引。
...
fp->_fileno = fdesc; //将文件描述符设置到_fileno 里
...
#再次调用_IO_link_in
_IO_link_in ((struct _IO_FILE_plus *) fp);//注册到 glibc 的全局打开流链表
return fp;
}
执行系统调用 open 打开文件,并将文件描述符赋值给 FILE 结构体的 _fileno 字段,最后再次调用 _IO_link_in 函数,确保该结构体被链接到 _IO_list_all 链表。
IO_FILE 之 fread
前面分析了系统如何为FILE结构体分配内存并将其链接进_IO_list_all,那么这里则是讲述创建文件 FILE之后,fread如何实现从文件中读取数据的。fread 的大致流程如下。
整体流程
整体流程为 fread 调用 vtable 中的 IO_file_xsgetn,其中 IO_file_xsgetn 是 fread 的核心函数,它的流程大致如下:
- 判断
fp->_IO_buf_base输入缓冲区是否为空,如果为空则调用_IO_doalllocbuf去初始化输入缓冲区。 - 在分配完输入缓冲区或输入缓冲区不为空的情况下,判断输入缓冲区是否存在数据。
- 如果输入缓冲区有数据则直接拷贝至用户缓冲区,如果没有或不够则调用
_underflow函数执行系统调用读取数据到输入缓冲区,再拷贝到用户缓冲区。
使用 gdb 调试下面程序:
#include<stdio.h>
int main(){
FILE* fp = fopen("test","rb");
char *ptr = malloc(0x20);
fread(ptr, 1, 20, fp);
return 0;
}
断点到 fread 函数,单步步入查看 _IO_list_all 结构体:此时 _IO_read_ptr 和 _IO_buf_base 等指针都是空的。
查看 vtable:
| 字段 | 作用 |
|---|---|
| __underflow | 读缓冲不够时怎么处理 |
| __overflow | 写缓冲满时怎么处理 |
| __xsgetn | fread 走的批量读代码执行流程(函数调用连)。 |
| __xsputn | fwrite 走的批量写路径 |
| __read | 最终调用 read() |
| __write | 最终调用 write() |
| __close | fclose 时调用 |
| __seek | fseek 时调用 |
_IO_fread
_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);//检查 FILE 指针合法性, 是个宏。
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; //fread 返回的是元素数,不是字节数。
}
libc_hidden_def (_IO_fread)
_IO_sgetn 函数
_IO_size_t
_IO_sgetn (_IO_FILE *fp, void *data, _IO_size_t n)
{
/* FIXME handle putback buffer here! */
return _IO_XSGETN (fp, data, n);
}
libc_hidden_def (_IO_sgetn)
在_IO_sgetn 函数中会调用_IO_XSGETN,而_IO_XSGETN是_IO_FILE_plus.vtable 中的函数指针,在调用这个函数时会首先取出 vtable 中的指针然后再进行调用,在默认情况下函数指针是指向_IO_file_xsgetn 函数的。下面将介绍_IO_file_xsgetn函数。
__IO_file_xsgetn
#define _IO_XSGETN(FP, DATA, N) JUMP2 (__xsgetn, FP, DATA, N)
_IO_XSGETN(FP,DATA,N):从流里读取 N 个字节到 DATA 中,返回实际读取到的字节数。
JUMP2:展开后是(fp->vtable->__xsgetn)(fp, data, n)
_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; //want:想要读取的量
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;
if (want <= have) //输入缓冲区里已经有足够的字符,则直接把缓冲区里的字符给目标 buff
{
memcpy (s, fp->_IO_read_ptr, want);
fp->_IO_read_ptr += want;
want = 0;
}
else
{
if (have > 0) //输入缓冲区里有部分字符,但是没有达到 fread 的 size 需求,先把已有的拷贝至目标 buff
{
...
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))//缓冲区存在&&整个缓冲区的容量
{
if (__underflow (fp) == EOF) //__underflow()向底层要数据,确保读缓冲区里至少有 1 个可读字节, EOF 表示-1
break;
continue;
}
...
return n - want;
}
libc_hidden_def (_IO_file_xsgetn)
_IO_doallocbuf
首先在 fp->_IO_buf_base 为空时,也就是输入缓冲区未建立时,代码调用 _IO_doallocbuf 去建立输入缓冲区。
void _IO_doallocbuf (_IO_FILE *fp)
{
if (fp->_IO_buf_base) //_IO_buf_base 是缓冲区起始指针, 不为 NULL 说明缓冲区已经设置/分配过了。
return;
if (!(fp->_flags & _IO_UNBUFFERED) || fp->_mode > 0) //_IO_UNBUFFERED 表示不缓冲。
if (_IO_DOALLOCATE (fp) != EOF) //调用 vtable 函数
return;
_IO_setb (fp, fp->_shortbuf, fp->_shortbuf+1, 0);//给 fp 设置一个“最小缓冲区”(1 字节)
}
libc_hidden_def (_IO_doallocbuf)
_IO_file_doallocate
如果 fp->_IO_buf_base 为空,则检查 fp->_flags 看它是不是 _IO_UNBUFFERED 或者 fp->_mode 大于 0,如果满足条件则调用 FILE 的 vtable 中的 _IO_file_doallocate。
_IO_file_doallocate (_IO_FILE *fp)
{
_IO_size_t size; //缓冲区大小
char *p; //新分配出来的缓冲区内存起始地址
struct stat64 st; //st:用来存放 fstat 得到打开的文件的文件信息(64 位版本的 stat 结构)。
...
size = _IO_BUFSIZ; //标准 I/O 默认使用的缓冲区大小
...
if (fp->_fileno >= 0 && __builtin_expect (_IO_SYSSTAT (fp, &st), 0) >= 0) //如果这个 FILE 有 fd 且获取 stat 则进入。
{
...
if (st.st_blksize > 0) //文件系统建议的最佳 I/O 块大小
size = st.st_blksize;
...
}
p = malloc (size);
...
_IO_setb (fp, p, p + size, 1);//设置[p, p+size) 这段内存缓冲区
return 1;
}
libc_hidden_def (_IO_file_doallocate)
下面的是 _IO_setb 函数:
void
_IO_setb (_IO_FILE *f, char *b, char *eb, int a)
{
...
f->_IO_buf_base = b;
f->_IO_buf_end = eb;
...
}
libc_hidden_def (_IO_setb)
到此,初始化缓冲区就完成了,函数返回 _IO_file_doallocate 后,接着 _IO_file_doallocate 也返回,回到 _IO_file_xsgetn 函数中。
第二部分
拷贝输入缓冲区数据,如果输入缓冲区存在已输入的数据,则把它直接拷贝到目标缓冲区里。 需要说明下的是从这里可以看出来 fp->_IO_read_ptr 指向的是输入缓冲区的起始地址,fp->_IO_read_end 指向的是输入缓冲区的结束地址。将 fp->_IO_read_ptr 到 fp->_IO_read_end 之间的数据通过 memcpy 拷贝到目标缓冲区中。
在输入缓冲区为 0 或者是不能满足需求的时候则会执行到最后一步 __underflow 去执行系统调用 read 读取数据,并放入到输入缓冲区里。因为我们的这个示例程序是第一次读取数据,此时的 fp->_IO_read_end和fp->_IO_read_ptr 都是 0,因此会进入到 __underflow。
__underflow
int __underflow (_IO_FILE *fp)
{
//额外的检查
...
if (fp->_IO_read_ptr < fp->_IO_read_end) //读缓冲区里还有至少 1 个字节可读
return *(unsigned char *) fp->_IO_read_ptr; //把 read_ptr 指向的那个字节取出来
...
return _IO_UNDERFLOW (fp);//当缓冲区没有数据,调用这个,是一个宏/间接调用,通常会走到 fp 的 vtable(虚表)里对应的 underflow 方法。对普通文件流,这一步通常最终会触发一次 read()/recv() 等系统调用继续读取数据,把数据读进缓冲区,并更新:`fp->_IO_read_base`,`fp->_IO_read_ptr`,`fp->_IO_read_end`
}
libc_hidden_def (__underflow)
_IO_new_file_underflow
_IO_UNDERFLOW函数,实际是FILE结构体vtable里的_IO_new_file_underflow。
int _IO_new_file_underflow (_IO_FILE *fp) //通常挂在普通文件流的 vtable underflow 槽位上。
{
_IO_ssize_t count;
...
if (fp->_flags & _IO_NO_READS) //是否存在_IO_NO_READS ,表示这个流不允许读。
{
fp->_flags |= _IO_ERR_SEEN; //标记已发生错误
__set_errno (EBADF); //把 errno 设为 EBADF
return EOF;
}
if (fp->_IO_read_ptr < fp->_IO_read_end) //如果输入缓冲区里存在数据,则直接返回
return *(unsigned char *) fp->_IO_read_ptr;
...
if (fp->_IO_buf_base == NULL) //如果没有输入缓冲区,则调用_IO_doallocbuf分配输入缓冲区
{
...
_IO_doallocbuf (fp); //内部会决定用 malloc 分配多大
}
...
fp->_IO_read_base = fp->_IO_read_ptr = fp->_IO_buf_base; //设置FILE结构体指针。
fp->_IO_read_end = fp->_IO_buf_base; //end == base,表示缓冲区没有数据。
fp->_IO_write_base = fp->_IO_write_ptr = fp->_IO_write_end
= fp->_IO_buf_base; //write_base/write_ptr/write_end 全等于 base,全为0。
count = _IO_SYSREAD (fp, fp->_IO_buf_base,
fp->_IO_buf_end - fp->_IO_buf_base); // 调用_IO_SYSREAD函数最终执行系统调用读取数据
...
fp->_IO_read_end += count; //之前 read_end == buf_base,现在添加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去读取文件数据:
_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)); //fp->_fileno:底层文件描述符 fd
}
__builtin_expect(条件, 0):提示编译器分支预测——“这个条件大概率为假(0)”。
IO_FILE之fwrite
整体流程
fwrite的主要实现在_IO_xsputn中,根据前面对_IO_FILE_plus的介绍,可知_IO_XSPUTN位于_IO_FILE_plus的 vtable中,调用这个函数需要首先取出 vtable 中的指针,再跳过去进行调用。该函数内部整体流程包含四个部分。
- 首先判断输出缓冲区还有多少剩余,如果有剩余则将目标输出数据拷贝到输出缓冲区。
- 如果输出缓冲区没有剩余(输出缓冲区未建立也是没有剩余)或输出缓冲区不够则调用
_IO_OVERFLOW建立输出缓冲区或刷新输出缓冲区。 - 输出缓冲区刷新后判断剩余的目标输出数据是否超过块的
size,如果超过块的size,则不通过输出缓冲区直接以块为单位,使用sys_write输出目标数据。 - 如果按块输出数据后还剩一点数据则调用
_IO_default_xsputn将数据拷贝到输出缓冲区。
接着介绍一下其中涉及的几个IO_FILE结构体的指针。
| 指针 | 描述 |
|---|---|
| _IO_buf_base | 输入输出缓冲区基地址 |
| _IO_buf_end | 输入输出缓冲区结束地址 |
| _IO_write_base | 输出缓冲区基地址 |
| _IO_write_ptr | 输入缓冲区当前地址 |
| _IO_write_end | 输出缓冲区结束地址 |
fwrite函数的原型:
size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream)
# ptr-- 这是指向要被写入的元素数组的指针。
# size-- 这是要被写入的每个元素的大小,以字节为单位。
# nmemb-- 这是元素的个数,每个元素的大小为 size 字节。
# stream-- 这是指向 FILE 对象的指针,该 FILE 对象指定了一个输出流。
示例程序:使用gdb进行调试,在fwrite处下断点,此时_IO_list_all经过fopen初始化。经过fopen初始化,输入输出缓冲区没有建立,此时所有指针都为空。
#include<stdio.h>
int main(){
FILE* fp = fopen("test","wb");
char *ptr = malloc(0x20);
fwrite(ptr, 1, 0x20, fp);
return 0;
}
fwrite
下面看一下fwrite函数:
_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; //request:期望写入的总字节数。
...
if (_IO_vtable_offset (fp) != 0 || _IO_fwide (fp, -1) == -1)//_IO_vtable_offset跟兼容/虚表访问等有关。
//_IO_fwide 用来管理流走宽字符还是普通char字节。
written = _IO_sputn (fp, (const char *) buf, request);//stdio 内部的“写入 request 个字节”的入口函数。
//这里buf转换为char类型,就是写普通char字节。
...
}
libc_hidden_def (_IO_fwrite)//这是glibc的宏,用于把 _IO_fwrite 标记成 libc 内部可见/隐藏符号。
_IO_fwrite函数调用了_IO_sputn 函数,_IO_sputn 等价于通过 vtable 调度到 _IO_xsputn。
_IO_xsputn
在_IO_XSPUTN对应的默认函数_IO_new_file_xsputn ,该函数源码从四部分讨论
第一部分源码:
_IO_size_t _IO_new_file_xsputn (_IO_FILE *f, const void *data, _IO_size_t n)
{
_IO_size_t count = 0; //本次能处理的字节数,先置 0,后面根据缓冲区剩余空间计算。
...
else if (f->_IO_write_end > f->_IO_write_ptr) //缓冲末尾-缓冲当前位置
count = f->_IO_write_end - f->_IO_write_ptr; /* Space available. */
if (count > 0) //如果输出缓冲区有空间,则先把数据拷贝至输出缓冲区
{
if (count > to_do)
count = to_do; //to_do:还剩多少字节需要写
...
memcpy (f->_IO_write_ptr, s, count); //计算是否还有目标输出数据剩余
f->_IO_write_ptr += count; //向后移动count表示已占用。
s += count; //s表示 “还没写出去的那部分用户数据”的当前指针
to_do -= count;
第二部分:
//如果还有目标数据剩余,此时则表明输出缓冲区未建立或输出缓冲区已经满了
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_new_file_overflow
_IO_file_xsputn---调用>>>IO_new_file_overflow---调用>>> _IO_doallocbuf---调用>>>_IO_file_doallocate---调用>>>_IO_setb
int _IO_new_file_overflow (_IO_FILE *f, int ch)//ch是要写入的字符,约定ch==EOF时,触发flush。
{
if (f->_flags & _IO_NO_WRITES) // 判断标志位是否包含_IO_NO_WRITES,该流不允许写(例如以只读方式打开)。
{
f->_flags |= _IO_ERR_SEEN; //在流对象上设置“发生错误”,(ferror() 会据此返回非 0)
__set_errno (EBADF);//errno = EBADF:坏文件描述符 / 不合适的访问模式
return EOF;//按 stdio 约定返回 EOF 表示失败
}
//_IO_CURRENTLY_PUTTING:表示当前流处于写入模式;判断输出缓冲区是否为空。
if ((f->_flags & _IO_CURRENTLY_PUTTING) == 0 || f->_IO_write_base == NULL)
{
if (f->_IO_write_base == NULL)//写缓冲基址为空(_IO_write_base == NULL)——尚未建立写缓冲结构
{
_IO_doallocbuf (f);//为该流分配 stdio 缓冲区
_IO_setg (f, f->_IO_buf_base, f->_IO_buf_base, f->_IO_buf_base);//设置read和buf的缓冲区地址
}
//初始化指针
if (f->_IO_read_ptr == f->_IO_buf_end)
f->_IO_read_end = f->_IO_read_ptr = f->_IO_buf_base;//如果 read_ptr 刚好在 buf_end(读到缓冲尾),则把 read_ptr/read_end 拉回到 buf_base,表示读区空。
f->_IO_write_ptr = f->_IO_read_ptr; //把写指针基于当前 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;//标记流已进入“putting”(写入)模式
if (f->_mode <= 0 && f->_flags & (_IO_LINE_BUF | _IO_UNBUFFERED))//_mode <= 0:通常表示“窄字符
//_IO_LINE_BUF:行缓冲(遇到 \n 刷新),_IO_UNBUFFERED:无缓冲(每次写都直接下沉到内核)。
f->_IO_write_end = f->_IO_write_ptr;//实现层面等价于“缓冲容量为 0”,于是下一次写入会立刻触发 flush/write,(因为看起来缓冲“已经满了/不能缓存”)。
}
if (ch == EOF)//约定表示只 flush,不写字符。
return _IO_do_write (f, f->_IO_write_base,f->_IO_write_ptr - f->_IO_write_base);
//_IO_do_write(f, base, len):把 [write_base, write_ptr) 这段数据真正写到底层(通常最终到 write() 系统调用或类似层)
if (f->_IO_write_ptr == f->_IO_buf_end ) /* Buffer is really full */
if (_IO_do_flush (f) == EOF) //缓冲真的满了:先 flush 再写
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)
#define _IO_setg(fp, eb, g, eg)
((fp)->_IO_read_base = (eb),// 读缓冲区起始位置
(fp)->_IO_read_ptr = (g),// 当前读取位置
(fp)->_IO_read_end = (eg)) // 读缓冲区结束位置
| 参数 | 含义 |
|---|---|
fp |
指向 FILE 结构体的指针 |
eb |
read buffer 起始地址(base) |
g |
当前读取位置(get pointer) |
eg |
read buffer 结束地址(end) |
执行结束后则buf和read指针地址相同,如下图:
_IO_new_do_write
__xsputn--> _IO_new_do_write
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;//成功书写就返回,失败返回EOF
}
libc_hidden_ver (_IO_new_do_write, _IO_do_write)
new_do_write函数:
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);
...
//写完之后 把 FILE 的缓冲状态重置成一个干净的状态
_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_new_file_write
► 0 0x7ffff7c93940 _IO_file_write
1 0x7ffff7c92571 _IO_do_write+177
_IO_ssize_t
_IO_new_file_write (_IO_FILE *f, const void *data, _IO_ssize_t n)
{
_IO_ssize_t to_do = n; //to_do需要写的数据量
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_new_file_xsputn
new_do_write函数返回到_IO_new_file_xsputn函数,继续执行下面:
//检查输出数据是否是大块
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采用的策略是将剩余目标输出数据放入到输出缓冲区里面,相关源码如下。
//剩余的数据拷贝至输出缓冲区
if (to_do)
to_do -= _IO_default_xsputn (f, s+do_write, to_do);
程序调用_IO_default_xsputn函数对剩下的s + do_write数据进行操作
_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; //more表示还剩多少字节没写,初始值为n。
if (more <= 0)
return 0;
for (;;)//无限循环直到break
{
/* Space available. */
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); //把用户数据 s 拷贝到缓冲区当前位置
f->_IO_write_ptr += count; //写指针前移,表示新增了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进行输出。
IO_FILE之fclose
fclose 是标准 IO 库中用于关闭已打开文件的函数,其作用与 fopen 相反。
int fclose(FILE *stream)
功能:关闭一个文件流,使用 fclose 就可以把缓冲区内最后剩余的数据输出到磁盘文件中,并释放文件指针和有关的缓冲区
整体流程
第一步:fclose 首先会调用_IO_unlink_it 将指定的 FILE 从_chain链表中脱链
if (fp->_IO_file_flags & _IO_IS_FILEBUF)
_IO_un_link ((struct _IO_FILE_plus *) fp);
第二步:调用_IO_file_close_it 函数,_IO_file_close_it 会调用系统接口close 关闭文件
if (fp->_IO_file_flags & _IO_IS_FILEBUF)
status = _IO_file_close_it (fp);
第三步:调用vtable 中的_IO_FINISH,其对应的是_IO_file_finish 函数,其中会调用free函数释放之前分配的 FILE 结构
_IO_FINISH (fp);
IO_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实现
vfprintf+11
_IO_file_xsputn
_IO_file_overflow
funlockfile
_IO_file_write
write


浙公网安备 33010602011771号