1. FILE 结构
下面给出 stdin/stdout/stderr 的定义,它们都是一个 FILE 结构体:
// https://elixir.bootlin.com/glibc/glibc-2.31/source/libio/stdio.c#L33
FILE *stdin = (FILE *) &_IO_2_1_stdin_;
FILE *stdout = (FILE *) &_IO_2_1_stdout_;
FILE *stderr = (FILE *) &_IO_2_1_stderr_;
// https://elixir.bootlin.com/glibc/glibc-2.31/source/libio/libio.h#L149
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_;
::: tip 等等,怎么有两个不同的定义?stdin/stdout/stderr 究竟是 FILE 结构体,还是 _IO_FILE_plus 结构体?
实际上,在 glibc 的内部实现中,这三个流确实是 struct _IO_FILE_plus。但是由于 FILE 位于 _IO_FILE_plus 结构体的起始位置,_IO_FILE_plus 结构体的地址和它内部 FILE 成员的地址在数值上是完全一样的。因此,glibc 可以安全地将 &IO_2_1_stdin 强制转换为 FILE *。
虽然 stdin/stdout/stderr 拿到的是一个 FILE * 指针,看起来好像只能访问 FILE 内部的成员(如缓冲区指针、标志位等),但实际上:在内存里,FILE 成员的后面紧跟着一个 vtable 指针。当你调用 fprintf(stdout, …) 或 fwrite(…) 时,glibc 内部会将这个 FILE * 重新转换回内部使用的结构体类型,并跳转到 vtable 指向的函数实现(如 _IO_file_write)。
这种设计类似于 C++ 的类继承:FILE (struct _IO_FILE):相当于“基类”,包含了基础的数据成员。_IO_FILE_plus:相当于“派生类”,它在基类的基础上增加了虚函数表 (vtable)。
封装性:对用户屏蔽了底层的虚函数表实现。用户只需要知道 FILE * 怎么用,不需要关心底层的 I/O 是如何跳转的。多态性:不同的文件类型(普通文件、字符串流 sscanf、终端)可以共享相同的 FILE 结构前端,但通过替换 vtable 来实现不同的读写行为。 :::
::: warning 这种 vtable 机制在过去曾是 Linux 提权攻击的一个重灾区(后面会讲到的 FSOP, File Stream Oriented Programming),攻击者通过伪造 vtable 来控制程序执行流。因此,现代 glibc 对 vtable 做了很多安全校验。 :::
下面给出 _IO_FILE_plus 和 FILE 的定义:
// https://elixir.bootlin.com/glibc/glibc-2.31/source/libio/libioP.h#L324
struct _IO_FILE_plus
{
FILE file;
const struct _IO_jump_t *vtable;
};
// https://elixir.bootlin.com/glibc/glibc-2.31/source/libio/bits/types/FILE.h#L7
typedef struct _IO_FILE FILE;
// https://elixir.bootlin.com/glibc/glibc-2.31/source/libio/bits/types/struct_FILE.h#L49
struct _IO_FILE
{
int _flags; /* High-order word is _IO_MAGIC; rest is flags. */
/* The following pointers correspond to the C++ streambuf protocol. */
char *_IO_read_ptr; /* Current read pointer */
char *_IO_read_end; /* End of get area. */
char *_IO_read_base; /* Start of putback+get area. */
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; //文件描述符
int _flags2;
__off_t _old_offset; /* This used to be _offset but it's too small. */
/* 1+column number of pbase(); 0 is unknown. */
unsigned short _cur_column;
signed char _vtable_offset;
char _shortbuf[1];
_IO_lock_t *_lock; //同步用
#ifdef _IO_USE_OLD_IO_FILE
};
// 可以稍微了解一下 _flags 的定义
// https://elixir.bootlin.com/glibc/glibc-2.31/source/libio/libio.h#L67
/* Magic number and bits for the _flags field. The magic number is
mostly vestigial, but preserved for compatibility. It occupies the
high 16 bits of _flags; the low 16 bits are actual flag bits. */
#define _IO_MAGIC 0xFBAD0000 /* Magic number */
#define _IO_MAGIC_MASK 0xFFFF0000
#define _IO_USER_BUF 0x0001 /* Don't deallocate buffer on close. */
#define _IO_UNBUFFERED 0x0002
#define _IO_NO_READS 0x0004 /* Reading not allowed. */
#define _IO_NO_WRITES 0x0008 /* Writing not allowed. */
#define _IO_EOF_SEEN 0x0010
#define _IO_ERR_SEEN 0x0020
#define _IO_DELETE_DONT_CLOSE 0x0040 /* Don't call close(_fileno) on close. */
#define _IO_LINKED 0x0080 /* In the list of all open files. */
#define _IO_IN_BACKUP 0x0100
#define _IO_LINE_BUF 0x0200
#define _IO_TIED_PUT_GET 0x0400 /* Put and get pointer move in unison. */
#define _IO_CURRENTLY_PUTTING 0x0800
#define _IO_IS_APPENDING 0x1000
#define _IO_IS_FILEBUF 0x2000
/* 0x4000 No longer used, reserved for compat. */
#define _IO_USER_LOCK 0x8000
下面给出 vtable 的定义。当要寻找对应函数的时候,就会来这里查表:
// https://elixir.bootlin.com/glibc/glibc-2.31/source/libio/libioP.h#L293
struct _IO_jump_t
{
JUMP_FIELD(size_t, __dummy);
JUMP_FIELD(size_t, __dummy2);
JUMP_FIELD(_IO_finish_t, __finish);
JUMP_FIELD(_IO_overflow_t, __overflow);
JUMP_FIELD(_IO_underflow_t, __underflow);
JUMP_FIELD(_IO_underflow_t, __uflow);
JUMP_FIELD(_IO_pbackfail_t, __pbackfail);
/* showmany */
JUMP_FIELD(_IO_xsputn_t, __xsputn);
JUMP_FIELD(_IO_xsgetn_t, __xsgetn);
JUMP_FIELD(_IO_seekoff_t, __seekoff);
JUMP_FIELD(_IO_seekpos_t, __seekpos);
JUMP_FIELD(_IO_setbuf_t, __setbuf);
JUMP_FIELD(_IO_sync_t, __sync);
JUMP_FIELD(_IO_doallocate_t, __doallocate);
JUMP_FIELD(_IO_read_t, __read);
JUMP_FIELD(_IO_write_t, __write);
JUMP_FIELD(_IO_seek_t, __seek);
JUMP_FIELD(_IO_close_t, __close);
JUMP_FIELD(_IO_stat_t, __stat);
JUMP_FIELD(_IO_showmanyc_t, __showmanyc);
JUMP_FIELD(_IO_imbue_t, __imbue);
};
2. FILE 结构体相关的操作函数
2.1. fopen
extern FILE *fopen (const char *__restrict __filename,
const char *__restrict __modes) __wur;
FILE *
_IO_new_fopen (const char *filename, const char *mode)
{
return __fopen_internal (filename, mode, 1);
}
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
_IO_no_init (&new_f->fp.file, 0, 0, &new_f->wd, &_IO_wfile_jumps);
_IO_JUMPS (&new_f->fp) = &_IO_file_jumps;
_IO_new_file_init_internal (&new_f->fp);
if (_IO_file_fopen ((FILE *) new_f, filename, mode, is32) != NULL)
return __fopen_maybe_mmap (&new_f->fp.file);
_IO_un_link (&new_f->fp);
free (new_f);
return NULL;
}
大致流程如下:
- malloc 分配 FILE + vtable + lock 空間
- 初始化 FILE,將 vtable 設置為 glibc 中已經寫好的各個函數
- 將 FILE 鏈入 _IO_list_all
- 系統調用 open 打開文件,並設置文件描述符號
2.2. fread
extern size_t fread (void *__restrict __ptr, size_t __size,
size_t __n, FILE *__restrict __stream) __wur;
size_t
_IO_fread (void *buf, size_t size, size_t count, FILE *fp)
{
size_t bytes_requested = size * count;
size_t bytes_read;
CHECK_FILE (fp, 0);
if (bytes_requested == 0)
return 0;
_IO_acquire_lock (fp);
bytes_read = _IO_sgetn (fp, (char *) buf, bytes_requested);
_IO_release_lock (fp);
return bytes_requested == bytes_read ? count : bytes_read / size;
}
總結
- 調用 vtable 中的 _IO_XSGETN (_IO_file_xsgetn)。
- 如果沒有 buffer,則調用 vtable 中的 _IO_DOALLOCATE (_IO_file_doallocate) 進行分配。
- 調用 vtable 中的 _IO_SYSSTAT (_IO_file_stat) 查看文件資訊,設置 buffer 大小。
- 使用 malloc 分配一塊區域作為 buffer。
- 開始讀取 buffer
2.3. fwrite
size_t
_IO_fwrite (const void *buf, size_t size, size_t count, FILE *fp)
{
size_t request = size * count;
size_t written = 0;
CHECK_FILE (fp, 0);
if (request == 0)
return 0;
_IO_acquire_lock (fp);
if (_IO_vtable_offset (fp) != 0 || _IO_fwide (fp, -1) == -1)
written = _IO_sputn (fp, (const char *) buf, request);
_IO_release_lock (fp);
/* We have written all of the input in case the return value indicates
this or EOF is returned. The latter is a special case where we
simply did not manage to flush the buffer. But the data is in the
buffer and therefore written as far as fwrite is concerned. */
if (written == request || written == EOF)
return count;
else
return written / size;
}
libc_hidden_def (_IO_fwrite)
n <= 0: 當data n 小於等於 0 時,程式直接返回 0,不會進行任何操作。
data n 小於等於 buffer剩餘空間 (count)
- buffer剩餘空間 (count): 由 f->_IO_write_end - f->_IO_write_ptr 決定buffer剩餘空間大小。
- 當data小於等於剩餘的buffer空間時,程式將資料直接寫入buffer,沒有超出buffer的部分,因此不需要進行syscall write或flush。
- 程式執行流程:
- 計算剩餘的buffer空間 count。
- 將資料完全寫入buffer (__mempcpy 複製資料到 f->_IO_write_ptr)。
- 更新寫指針 f->_IO_write_ptr。
- 返回 n,表示成功寫入的資料大小。
- data n 大於 buffer剩餘空間 (count)
- 當data超過buffer剩餘空間時,程式會首先嘗試填滿buffer,然後將剩餘的部分進行syscall write。
- 步驟:
- 計算剩餘的buffer空間 count,將 count 字節的資料寫入buffer。
- 如果資料剩餘 (to_do > 0),會進行buffer刷新(即調用 _IO_OVERFLOW),並將資料寫入底層文件。
- 嘗試寫入剩餘資料,並根據文件的塊對齊要求優化寫入(維持整塊大小的寫入以提高效能)。
- 剩餘資料由 _IO_default_xsputn 處理,進行最後的寫入操作。
- 返回總共寫入的資料大小。
- data n 小於等於 buffer剩餘空間,但啟用行緩衝模式 (_IO_LINE_BUF)
- 行緩衝模式: 如果啟用行緩衝模式且當前正在寫入 (_IO_CURRENTLY_PUTTING),程式會檢查資料中是否包含換行符 (‘\n’)。
- 步驟:
- 計算buffer剩餘空間 count。
- 如果data小於等於剩餘空間,則檢查資料中是否包含換行符。
- 如果遇到換行符,設置 must_flush = 1,表示需要立即刷新buffer。
- 將資料寫入buffer直到換行符,然後執行刷新操作。
- 剩餘資料寫入後,返回成功寫入的資料大小。
- data n 大於 buffer剩餘空間,啟用行緩衝模式
- 步驟:
- 先將部分資料寫入buffer直到滿。
- 當buffer空間不足時,觸發刷新,並嘗試將資料直接寫入文件。
- 檢查剩餘資料中是否有換行符,遇到換行符時觸發buffer刷新。
- 將剩餘資料按塊大小(block_size)對齊寫入。
- 返回總共寫入的資料大小。
- data n 超過 block_size
- block_size = f->_IO_buf_end - f->_IO_buf_base,這個大小會決定每次可以寫入的最大數據量。
- 當data超過buffer塊大小時,程式會分成多個塊進行寫入,以保持區塊對齊,並優化 I/O 操作。
- 步驟:
- 先將部分資料寫入buffer。
- buffer滿後,進行buffer刷新。
- 將剩餘資料按照塊大小進行對齊寫入(即寫入一整數塊的資料,保證效率)。
- 最後寫入剩下的資料,並更新寫指針。
- 返回寫入的總資料大小。
2. 常见用法
setvbuf(stdin, 0, 2, 0);
setvbuf(stdout, 0, 2, 0);
/*
* 0 (即 NULL):不使用自定义的缓冲区,让系统处理。
* 2 (即 _IONBF):No Buffering(无缓冲)。
* 0:缓冲区的大小。既然设置了无缓冲,大小自然就是 0。
*/
有这两行,是设定 stdin/stdout 无缓冲区(具体实现方法:缓冲区为 1 字节)。
PWN 常见。用来确保在不稳定的网络环境下,stdin/stdout 能够第一时间发送。
在 IO_FILE 攻击中更是好:这意味着程序一启动,就能令 _IO_read_ptr == _IO_read_end。
直接修改 _IO_buf_base,再消耗掉发送的字节就行。不需要像默认情况一样,要发送 4096 个字节填满缓冲区才能使得修改的 _IO_buf_base 生效。
Loading Comment Component...
