重要概念:fopen()返回的是一个结构体的指针
_IO_FILE 结构体在什么时候被创建?
 
_IO_FILE 结构体的实例是在程序使用标准 I/O 函数(如 fopen、fclose、fread、fwrite 等)时创建和管理的。这个结构体实际上是 GNU C Library (glibc) 用于处理文件流的底层实现细节。当你在程序中打开一个文件或者创建一个流时,glibc 会在后台分配和初始化一个 _IO_FILE 结构体,并返回一个指向它的指针(即 FILE* 类型)。
下面是一些常见场景,说明 _IO_FILE 结构体是如何被创建和使用的:
 
-  使用 fopen打开文件:
 当你调用fopen打开一个文件时,glibc 会分配一个_IO_FILE结构体并进行初始化。例如:FILE *file = fopen("example.txt", "r");在这个例子中, fopen函数会创建一个_IO_FILE结构体实例来管理example.txt文件的读操作,并返回一个指向该结构体的指针file。
-  使用 fdopen关联文件描述符和文件流:
 如果你有一个现有的文件描述符,并希望将其与一个标准 I/O 流关联,可以使用fdopen函数:int fd = open("example.txt", O_RDONLY); FILE *file = fdopen(fd, "r");fdopen会创建一个新的_IO_FILE结构体实例,并将文件描述符fd关联到这个结构体上。
-  标准输入输出: 
 标准输入(stdin)、标准输出(stdout)和标准错误(stderr)也是通过_IO_FILE结构体来管理的。它们在程序启动时由运行时库自动初始化。fprintf(stdout, "Hello, World!\n");
_IO_FILE 结构体的创建过程
 
当函数如 fopen 被调用时,glibc 内部会进行以下步骤:
-  分配内存: 
 glibc 会调用内存分配函数(如malloc)为_IO_FILE结构体分配内存。
-  初始化结构体: 
 分配内存后,glibc 会初始化_IO_FILE结构体的各个字段。例如,它会设置缓冲区指针、文件描述符、文件模式等。
-  返回指针: 
 初始化完成后,glibc 会返回一个指向这个_IO_FILE结构体的指针,即FILE*类型的指针。
_IO_FILE 在文件流操作中的生命周期
 
-  创建: 
 当你使用标准 I/O 函数(如fopen、fdopen)打开或创建一个文件流时,glibc 会创建一个_IO_FILE结构体实例。
-  使用: 
 在文件流的生命周期内,所有对该文件流的读写操作(如fread、fwrite、fgets、fputs等)都会通过这个_IO_FILE结构体来管理缓冲区、文件描述符和流的状态。
-  销毁: 
 当你调用fclose关闭文件流时,glibc 会执行以下操作:- 刷新缓冲区中的数据(如果有需要)。
- 释放与文件流关联的资源(如缓冲区内存)。
- 关闭文件描述符。
- 最后,释放 _IO_FILE结构体的内存。
 
示例代码
以下是一个简单的示例代码,展示了 _IO_FILE 结构体实例的创建和使用过程:
#include <stdio.h>int main() {// 打开文件,创建一个 _IO_FILE 结构体实例FILE *file = fopen("example.txt", "w");if (file == NULL) {perror("Failed to open file");return 1;}// 使用文件流进行写操作fprintf(file, "Hello, World!\n");// 关闭文件,销毁 _IO_FILE 结构体实例fclose(file);return 0;
}
在这个示例中,当调用 fopen 时,glibc 会创建并初始化一个 _IO_FILE 结构体实例。当调用 fclose 时,glibc 会销毁这个实例并释放相关资源。
_IO_FILE 结构体
 
在 Linux 系统中,_IO_FILE 结构体是 GNU C Library (glibc) 中实现标准 I/O (stdio) 的核心数据结构之一。它用于描述文件流(FILE*)的内部状态和缓冲区信息。理解 _IO_FILE 结构体对于某些高级的漏洞利用技术(如利用格式字符串漏洞或缓冲区溢出漏洞)非常重要。
以下是 _IO_FILE 结构体的一般布局(具体布局可能会随着 glibc 版本的不同而变化):
struct _IO_FILE {int _flags;                // 文件流的状态标志char* _IO_read_ptr;        // 缓冲区读取指针char* _IO_read_end;        // 缓冲区读取结束指针char* _IO_read_base;       // 缓冲区读取基地址char* _IO_write_base;      // 缓冲区写入基地址char* _IO_write_ptr;       // 缓冲区写入指针char* _IO_write_end;       // 缓冲区写入结束指针char* _IO_buf_base;        // 缓冲区基地址char* _IO_buf_end;         // 缓冲区结束地址char *_IO_save_base;       // 保存的缓冲区基地址char *_IO_backup_base;     // 备份的缓冲区基地址char *_IO_save_end;        // 保存的缓冲区结束地址struct _IO_marker *_markers; // 标记链表struct _IO_FILE *_chain;   // 文件流链表int _fileno;               // 文件描述符int _flags2;               // 额外的标志__off_t _old_offset;       // 旧的偏移量unsigned short _cur_column;// 当前列号signed char _vtable_offset;// 虚表偏移char _shortbuf[1];         // 短缓冲区_IO_lock_t *_lock;         // 锁__off64_t _offset;         // 偏移量void *__pad1;              // 填充void *__pad2;              // 填充void *__pad3;              // 填充void *__pad4;              // 填充size_t __pad5;             // 填充int _mode;                 // 模式char _unused2[15 * sizeof (int) - 4 * sizeof (void *) - sizeof (size_t)];// 未使用的填充
};
关键字段
- _flags: 用于描述文件流的状态标志,例如是否为读模式、写模式等。
- _IO_read_ptr, _IO_read_end, _IO_read_base: 分别指向当前读取的位置、读取的结束位置和读取缓冲区的基地址。
- _IO_write_base, _IO_write_ptr, _IO_write_end: 分别指向当前写入的位置、写入的结束位置和写入缓冲区的基地址。
- _IO_buf_base, _IO_buf_end: 分别指向缓冲区的基地址和结束地址。
- _IO_save_base, _IO_backup_base, _IO_save_end: 用于保存缓冲区状态的指针。
- _markers: 指向标记结构的链表,用于支持多种流操作。
- _chain: 指向下一个文件流的指针,形成一个文件流链表。
- _fileno: 文件描述符。
- _flags2: 额外的标志位。
- _old_offset: 用于记录偏移量。
- _cur_column: 当前列号,主要用于格式化输出。
- _vtable_offset: 虚表偏移,用于支持面向对象的操作。
- _shortbuf: 一个短缓冲区。
- _lock: 指向用于同步的锁。
- _offset: 文件流的位置偏移量。
- 填充字段: 用于对齐和扩展。
stdin、stdout 和 stderr指针
 
这些指针是程序的标准输入、标准输出和标准错误流(stdin、stdout 和 stderr)在内存中的地址。它们是全局变量,通常在程序启动时被初始化,以指向相应的 FILE 结构体。
解释每个指针
-  stdout(标准输出)- 地址:0x602020
- 指向的地址:0x00007fe6e8e03620
 
- 地址:
-  stdin(标准输入)- 地址:0x602030
- 指向的地址:0x00007fe6e8e028e0
 
- 地址:
-  stderr(标准错误)- 地址:0x602040
- 指向的地址:0x00007fe6e8e03540
 
- 地址:
每个地址如 0x602020 是全局变量的地址,而对应的值(如 0x00007fe6e8e03620)是这些全局变量指向的 FILE 结构体实例的地址。
内存布局和用途
-  stdout:- 地址:0x602020
- 指向的地址:0x00007fe6e8e03620
- 用途:标准输出通常用于打印普通输出信息,默认连接到终端的显示设备。
 
- 地址:
-  stdin:- 地址:0x602030
- 指向的地址:0x00007fe6e8e028e0
- 用途:标准输入用于读取输入数据,默认连接到终端的键盘输入。
 
- 地址:
-  stderr:- 地址:0x602040
- 指向的地址:0x00007fe6e8e03540
- 用途:标准错误用于打印错误信息,默认也连接到终端的显示设备。
 
- 地址:
背后的机制
在程序启动时,C 标准库(如 glibc)会初始化这几个标准流。具体来说,它们会分配相应的 FILE 结构体,并将 stdin、stdout 和 stderr 这些全局变量指向这些结构体。
以下是一个简化的示意图,展示了这些指针和 FILE 结构体的关系:
+----------------+           +----------------+
|  0x602020      | --------> | FILE for stdout|
|  (stdout)      |           | 0x00007fe6e8e03620 |
+----------------+           +----------------++----------------+           +----------------+
|  0x602030      | --------> | FILE for stdin |
|  (stdin)       |           | 0x00007fe6e8e028e0 |
+----------------+           +----------------++----------------+           +----------------+
|  0x602040      | --------> | FILE for stderr|
|  (stderr)      |           | 0x00007fe6e8e03540 |
+----------------+           +----------------+
示例代码验证
下面是一些示例代码,可以用来验证这些指针的地址:
#include <stdio.h>int main() {printf("Address of stdout: %p\n", (void*)&stdout);printf("Address of stdin: %p\n", (void*)&stdin);printf("Address of stderr: %p\n", (void*)&stderr);printf("Pointer value of stdout: %p\n", (void*)stdout);printf("Pointer value of stdin: %p\n", (void*)stdin);printf("Pointer value of stderr: %p\n", (void*)stderr);return 0;
}
运行这段代码,你应该会看到标准流指针的地址和它们指向的 FILE 结构体的地址,这与你提供的内存地址应该是一致的。
总结
这些指针(stdout、stdin 和 stderr)是全局变量,指向标准 I/O 流的 FILE 结构体实例。这些实例在程序启动时由 C 标准库初始化,用于管理标准输入、输出和错误流。