重要概念: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 标准库初始化,用于管理标准输入、输出和错误流。