前言:
这几天做IO类的题,发现自己是真的菜,决定暂时放一放apple1的学习,重新学习一下IO流。
本篇源码还是glibc-2.35。
FILE结构
_IO_FILE_plus
我们先来看一下查看结构体_IO_FILE_plus的组成:
struct _IO_FILE_plus
{
FILE file;
const struct _IO_jump_t *vtable;
};
分别看一下他的两个组成成分:
FILE
typedef struct _IO_FILE FILE;
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
};
struct _IO_FILE_complete
{
struct _IO_FILE _file;
#endif
__off64_t _offset;
/* Wide character stream stuff. */
struct _IO_codecvt *_codecvt;
struct _IO_wide_data *_wide_data;
struct _IO_FILE *_freeres_list;
void *_freeres_buf;
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)];
};
_IO_jump_t
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);
};
现在我们已经对这个结构体有了深刻认识了,接下来就是认识一下_IO_2_1_stdout以及和他相关联的结构体了:
程序进程中的FILE结构会通过_chain域彼此连接形成一个链表,链表头部用全局变量_IO_list_all表示,通过这个值可以遍历所有的FILE结构,大致的链表结构如下图:
在标准I/O库中,每个程序启动时这三个文件流是自动打开的:stdin、stdout、stderr。三个文件流位于的是libc.so的数据段,有的时候也会位于bss段,根据经验观察似乎在使用setvbuf时会出现这种情况,个人理解是因为stdin等三个变量是未初始化的全局变量, 所以他们被分配到bss段. 在执行setvbuf时, 经过两次重定位将libc地址填入其中。也就是说在不在bss段并不影响他的值和遍历过程。
_flags规则
这里我们将深入的讲解一下FILE结构体中的第一个成员变量_flag,这个成员变量在后续利用_IO_2_1_stdout泄露libc的时候具有重要作用。
先简介一下_flag的规则,_flag的高两位字节是由libc固定的,不同的libc可能存在差异,但是基本上都为0xfbad0000。高两位字节其实就是作为一个标识,标志这是一个什么文件。而低两位字节的位数规则决定了程序的执行状态,低两位的规则如下:
#define _IO_MAGIC 0xFBAD0000 /* Magic number */
#define _OLD_STDIO_MAGIC 0xFABC0000 /* Emulate old stdio. */
#define _IO_MAGIC_MASK 0xFFFF0000
#define _IO_USER_BUF 1 /* User owns buffer; don't delete it on close. */
#define _IO_UNBUFFERED 2
#define _IO_NO_READS 4 /* Reading not allowed */
#define _IO_NO_WRITES 8 /* Writing not allowd */
#define _IO_EOF_SEEN 0x10
#define _IO_ERR_SEEN 0x20
#define _IO_DELETE_DONT_CLOSE 0x40 /* Don't call close(_fileno) on cleanup. */
#define _IO_LINKED 0x80 /* Set if linked (using _chain) to streambuf::_list_all.*/
#define _IO_IN_BACKUP 0x100
#define _IO_LINE_BUF 0x200
#define _IO_TIED_PUT_GET 0x400 /* Set if put and get pointer logicly tied. */
#define _IO_CURRENTLY_PUTTING 0x800
#define _IO_IS_APPENDING 0x1000
#define _IO_IS_FILEBUF 0x2000
#define _IO_BAD_SEEN 0x4000
#define _IO_USER_LOCK 0x8000
在执行流程中会将_flag和定义常量进行按位与运算,并根据与运算的结构进行判断如何执行。
puts()函数执行流程
在栈的时候,我们经常使用puts去泄露地址,这里我们通过源码来明确一下puts是如何达到输出数据的功能的:
_IO_puts --> _IO_new_file_xsputn
puts()函数在源码中的表现形式为_IO_puts,我们一起来看一下源码:
int
_IO_puts (const char *str)
{
int result = EOF;
size_t len = strlen (str);
_IO_acquire_lock (stdout);
if ((_IO_vtable_offset (stdout) != 0
|| _IO_fwide (stdout, -1) == -1)
&& _IO_sputn (stdout, str, len) == len
&& _IO_putc_unlocked ('\n', stdout) != EOF)
result = MIN (INT_MAX, len + 1);
_IO_release_lock (stdout);
return result;
}
这里可以看到_IO_puts在过程当中调用了一个叫做_IO_sputn函数(_IO_fwrite也会调用这个),
_IO_sputn其实是一个宏,它的作用就是调用_IO_2_1_stdout_中的vtable所指向的_xsputn成员,也就是_IO_new_file_xsputn函数
_IO_new_file_xsputn --> _IO_OVERFLOW
size_t
_IO_new_file_xsputn (FILE *f, const void *data, size_t n)
{
const char *s = (const char *) data;
size_t to_do = n;
int must_flush = 0;
size_t count = 0;
if (n <= 0)
return 0;
/* This is an optimized implementation.
If the amount to be written straddles a block boundary
(or the filebuf is unbuffered), use sys_write directly. */
/* First figure out how much space is available in the buffer. */
if ((f->_flags & _IO_LINE_BUF) && (f->_flags & _IO_CURRENTLY_PUTTING))
{
count = f->_IO_buf_end - f->_IO_write_ptr;
if (count >= n)
{
const char *p;
for (p = s + n; p > s; )
{
if (*--p == '\n')
{
count = p - s + 1;
must_flush = 1;
break;
}
}
}
}
else if (f->_IO_write_end > f->_IO_write_ptr)
count = f->_IO_write_end - f->_IO_write_ptr; /* Space available. */
/* Then fill the buffer. */
if (count > 0)
{
if (count > to_do)
count = to_do;
f->_IO_write_ptr = __mempcpy (f->_IO_write_ptr, s, count);
s += count;
to_do -= count;
}
if (to_do + must_flush > 0)
{
size_t block_size, do_write;
/* Next flush the (full) buffer. */
if (_IO_OVERFLOW (f, EOF) == EOF)
/* If nothing else has to be written we must not signal the
caller that everything has been written. */
return to_do == 0 ? EOF : n - to_do;
/* Try to maintain alignment: write a whole number of blocks. */
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;
}
/* Now write out the remainder. Normally, this will fit in the
buffer, but it's somewhat messier for line-buffered files,
so we let _IO_default_xsputn handle the general case. */
if (to_do)
to_do -= _IO_default_xsputn (f, s+do_write, to_do);
}
return n - to_do;
}
耐着性子去解读一下:
if ((f->_flags & _IO_LINE_BUF) && (f->_flags & _IO_CURRENTLY_PUTTING))
{
count = f->_IO_buf_end - f->_IO_write_ptr;
if (count >= n)
{
const char *p;
for (p = s + n; p > s; )
{
if (*--p == '\n')
{
count = p - s + 1;
must_flush = 1;
break;
}
}
}
}
这段代码先判断检查两个标志位。首先,_IO_LINE_BUF标志位表示流是以行缓冲模式打开的,即输出会在遇到换行符(\n)时自动刷新到目标设备。其次,_IO_CURRENTLY_PUTTING标志位表示当前流正在执行写入操作。
count = f->_IO_buf_end - f->_IO_write_ptr;:这行代码计算了当前流内部缓冲区中剩余的空间量。_IO_buf_end是指向缓冲区末尾的指针,而_IO_write_ptr是指向下一个写入位置的指针。两者之差即为剩余空间的大小。
接下来的if语句检查剩余空间是否足够大,能够容纳即将写入的n个字节(count >= n)。如果是,那么代码会进一步检查这些即将写入的字节中是否包含换行符(\n),因为如果是行缓冲模式,遇到换行符时应该刷新缓冲区。
for (p = s + n; p > s; ):这个循环从即将写入的字节序列的末尾开始向前遍历,直到序列的开头。在循环体内,if (*--p == '\n')检查当前字符是否为换行符。如果是,则执行以下操作count = p - s + 1;:更新count为从原始字符串s到换行符(包括换行符)之间的字节数。这意味着,如果找到换行符,则只将换行符之前的字节写入缓冲区,因为换行符会触发缓冲区的刷新。must_flush = 1;:设置must_flush标志为1,表示需要刷新缓冲区。注意,这里假设must_flush在之前的代码段中已经被声明并初始化为0或适当的值。break;:跳出循环,因为已经找到了换行符,无需继续检查。
else if (f->_IO_write_end > f->_IO_write_ptr)
count = f->_IO_write_end - f->_IO_write_ptr; /* Space available. */
检查在当前的缓冲区中,从 _IO_write_ptr
到 _IO_write_end
之间有多少空间是可用的,即还有多少字节可以被写入缓冲区而不需要刷新(即不需要实际写入到文件中)。这个空间量被存储在 count
变量中。
if (count > 0)
{
if (count > to_do)
count = to_do;
f->_IO_write_ptr = __mempcpy (f->_IO_write_ptr, s, count);
s += count;
to_do -= count;
}
首先,通过 if (count > 0) 检查当前缓冲区中是否还有空间可用于写入。如果没有空间(count 不大于 0),则不执行任何操作。
接着,通过 if (count > to_do) 检查缓冲区中剩余的空间量是否大于还需要写入的数据量。如果是,那么实际写入的数据量应该是 to_do,而不是 count,因为没有必要写入比需要更多的数据。因此,将 count 设置为 to_do。
然后,使用 __mempcpy 函数将 s 指向的数据复制到缓冲区的当前写入位置(f->_IO_write_ptr 指向的位置)。__mempcpy 函数类似于 memcpy,但它返回目标缓冲区中最后一个被复制字节之后的位置,这里正是更新 f->_IO_write_ptr 所需要的,以便下一次写入知道从哪里开始。
随后,更新 s 指针,使其指向已经复制到缓冲区中的数据的下一个字节,为下一次可能的写入操作做准备。
最后,更新 to_do 变量,减去已经写入缓冲区的数据量,以反映还需要写入多少数据。
if (to_do + must_flush > 0)
{
size_t block_size, do_write;
/* Next flush the (full) buffer. */
if (_IO_OVERFLOW (f, EOF) == EOF)
/* If nothing else has to be written we must not signal the
caller that everything has been written. */
return to_do == 0 ? EOF : n - to_do;
/* Try to maintain alignment: write a whole number of blocks. */
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;
}
/* Now write out the remainder. Normally, this will fit in the
buffer, but it's somewhat messier for line-buffered files,
so we let _IO_default_xsputn handle the general case. */
if (to_do)
to_do -= _IO_default_xsputn (f, s+do_write, to_do);
}
return n - to_do;
- 条件判断:首先,通过if (to_do + must_flush > 0)判断是否有数据需要写入或者必须刷新缓冲区。to_do表示还有多少字节的数据需要写入。
- 缓冲区刷新:如果条件满足,代码首先尝试刷新缓冲区。通过_IO_OVERFLOW(f, EOF)尝试刷新缓冲区。如果返回EOF(表示失败),则根据to_do的值决定是否返回EOF。如果to_do为0(即没有数据需要写入),则返回EOF以指示失败;否则,返回已写入字节的补数(n - to_do),这里n可能是原始需要写入的字节总数。_IO_OVERFLOW就是vtable中的__overflow。
- 计算写入块大小:接下来,他尝试通过保持对齐来优化写入操作。它首先计算缓冲区的大小(block_size),然后计算一个do_write值,这个值是基于to_do和block_size的,目的是尽可能写入一个完整的块(如果block_size大于或等于128,则do_write是to_do减去to_do对block_size的余数)。
- 写入部分数据:如果do_write不为0,即有需要写入的数据块,则通过new_do_write函数(可能是自定义的或特定于实现的)尝试写入这些数据。如果实际写入的字节数少于do_write指定的字节数,则函数返回已写入的字节数的补数(n - to_do)。
- 写入剩余数据:最后,如果还有剩余的数据(to_do不为0),则通过_IO_default_xsputn函数写入剩余的数据。这个函数负责处理一般情况的写入,包括线缓冲(line-buffered)文件的复杂情况。
- 函数返回:无论写入操作是否完全成功,函数最后都返回已写入字节的补数(n - to_do),这表示了原始请求中成功写入的字节数。
_IO_new_file_overflow --> _IO_do_write
调用 __overflow确实是去调用 _IO_new_file_overflow,位置在:glibc/libio/fileops.c:
int
_IO_new_file_overflow (FILE *f, int ch)
{
if (f->_flags & _IO_NO_WRITES) /* SET ERROR */
{
f->_flags |= _IO_ERR_SEEN;
__set_errno (EBADF);
return EOF;
}
/* If currently reading or no buffer allocated. */
if ((f->_flags & _IO_CURRENTLY_PUTTING) == 0 || f->_IO_write_base == NULL)
{
/* Allocate a buffer if needed. */
if (f->_IO_write_base == NULL)
{
_IO_doallocbuf (f);
_IO_setg (f, f->_IO_buf_base, f->_IO_buf_base, f->_IO_buf_base);
}
/* Otherwise must be currently reading.
If _IO_read_ptr (and hence also _IO_read_end) is at the buffer end,
logically slide the buffer forwards one block (by setting the
read pointers to all point at the beginning of the block). This
makes room for subsequent output.
Otherwise, set the read pointers to _IO_read_end (leaving that
alone, so it can continue to correspond to the external position). */
if (__glibc_unlikely (_IO_in_backup (f)))
{
size_t nbackup = f->_IO_read_end - f->_IO_read_ptr;
_IO_free_backup_area (f);
f->_IO_read_base -= MIN (nbackup,
f->_IO_read_base - f->_IO_buf_base);
f->_IO_read_ptr = f->_IO_read_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 ) /* Buffer is really full */
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;
}
我们希望利用的便是下面这一段代码中的_IO_do_write函数, 这个函数执行后会调用系统调用write输出输出缓冲区,
if (ch == EOF)
return _IO_do_write (f, f->_IO_write_base,
f->_IO_write_ptr - f->_IO_write_base);
传入_IO_do_write函数的参数为:stdout结构体、_IO_write_base(输出缓冲区起始地址)和size(_IO_write_end - _IO_write_base计算得来)
那么显然易见,只要我们事先在stdout的_IO_write_base的位置部署要输出的起始地址,那么在去利用_IO_do_write函数,即可打印部分内存地址,打印出来的内容就包含我们所需要泄露的libc
_IO_new_do_write --> _new_do_write
int
_IO_new_do_write (FILE *fp, const char *data, size_t to_do)
{
return (to_do == 0
|| (size_t) new_do_write (fp, data, to_do) == to_do) ? 0 : EOF;
}
可以看到_IO_new_do_write并没有做太多的操作,就调用了new_do_write函数,并且new_do_write函数的参数和传入的参数是一样的,第一个参数是stdout结构体,第二个参数是输出缓冲区起始地址,第三个参数是输出长度
new_do_write --> _IO_SYSWRITE
static size_t
new_do_write (FILE *fp, const char *data, size_t to_do)
{
size_t count;
if (fp->_flags & _IO_IS_APPENDING)
/* On a system without a proper O_APPEND implementation,
you would need to sys_seek(0, SEEK_END) here, but is
not needed nor desirable for Unix- or Posix-like systems.
Instead, just indicate that offset (before and after) is
unpredictable. */
fp->_offset = _IO_pos_BAD;
else if (fp->_IO_read_end != fp->_IO_write_base)
{
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);
if (fp->_cur_column && count)
fp->_cur_column = _IO_adjust_column (fp->_cur_column - 1, data, count) + 1;
_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_puts -->> _IO_new_file_xsputn -->> _IO_OVERFLOW
-->> _IO_do_write(_IO_new_do_write) -->> _new_do_write
-->> _IO_SYSWRITE
那我们从头来看一下需要绕过的检查:
_IO_puts --> _IO_new_file_xsputn
if ((_IO_vtable_offset (stdout) != 0
|| _IO_fwide (stdout, -1) == -1)
&& _IO_sputn (stdout, str, len) == len
&& _IO_putc_unlocked ('\n', stdout) != EOF)
第一个(_IO_vtable_offset (stdout) != 0 || _IO_fwide (stdout, -1) == -1) 我们需要他判断为真,但是_IO_vtable_offset (stdout) != 0 没法成立,_IO_vtable_offset这个宏强制将结果赋值成0:
那只能去看_IO_fwide这个宏操作:
({ int __result = (-1);
if (__result < 0 && ! (0))
{ if ((stdout)->_mode == 0)
(stdout)->_mode = -1;
__result = (stdout)->_mode;
}
else
if (__builtin_constant_p (-1) && (-1) == 0)
__result = (0) ? -1 : (stdout)->_mode;
else
__result = _IO_fwide (stdout, __result); __result;
})
这个宏操作其实就是去检查了 (stdout)->_mode == 0 为0就把他赋值成-1,之后result==(stdout)->_mode,也就是说要想result=-1,我们需要将_mode=0或者-1。
_IO_new_file_xsputn --> _IO_OVERFLOW
这一步没有什么需要注意的,只需要to_do>0即可,
_IO_new_file_overflow --> _IO_do_write
这里一共有两个绕过点:
一、f->_flags & _IO_NO_WRITES == 0
即:
#define _IO_MAGIC 0xFBAD0000
#define _IO_NO_WRITES 8
_flags & _IO_NO_WRITES = 0
_flags = 0xfbad0000
二、f->_flags & _IO_CURRENTLY_PUTTING != 0 && f->_IO_write_base != NULL
让这个条件为假的原因是,如果判断成功,也就是检查输出缓冲区为空,便会进行分配空间,并且会初始化指针。一旦进行初始化操作,那么就会覆盖掉我们事先在stdout的_IO_write_base的数据,导致无法掌控输出内容。
至于后半部分f->_IO_write_base == NULL的判断,由于我们会在_IO_write_base中部署数据,所以后半部分的条件判断一定为假。那么这样一来我们将前半部分也为假,即f->_flags & _IO_CURRENTLY_PUTTING = 1,则整个判断就为假:
即:
#define _IO_MAGIC 0xFBAD0000
#define _IO_CURRENTLY_PUTTING 0x800
f->_flags & _IO_CURRENTLY_PUTTING = 1
_flags = 0xfbad0800
_IO_new_do_write --> new_do_write
因为to_do是我们要输出的内容的长度,所以一定不为0,也就是会正常走到new_do_write函数。
new_do_write --> _IO_SYSWRITE
这里面一共有两个判断:
else这条分支我们尽可能的不碰,原因是因为
一般在做这种题的时候都会伴随着随机化保护的开启,进行攻击的时候,我们一般采用的都是覆盖末位字节的方式造成偏移,因为即使随机化偏移也会存在0x1000对齐。但是这时候就会遇到一个很尴尬的情况,_IO_read_end和_IO_write_base存放的地址是由末位字节和其他高字节共同组成的,其他高字节由于随机化的缘故无法确定,基本上这个地方很难绕过。而一旦进行到else if 内那随后执行的_IO_SYSSEEK函数会因为fp->_IO_write_base - fp->_IO_read_end难以控制导致执行失败。
而if分支相对来说造成的影响就比较小了,内部仅仅将偏移设置为标准值,不会影响后续的输出流程。并且if判断的条件也很容易满足,我们只需要将fp->_flags & _IO_IS_APPENDING != 0即可:
#define _IO_MAGIC 0xFBAD0000
#define _IO_IS_APPENDING 0x1000
fp->_flags & _IO_IS_APPENDING = 1
_flags = 0xfbad1000
总结:
综上所述,我们需要绕过的点有:
- 设置_flags & _IO_NO_WRITES = 0
- 设置_flags & _IO_CURRENTLY_PUTTING = 1
- 设置_flags & _IO_IS_APPENDING = 1
- 设置_flags = 0xFBAD1800
- 设置_IO_write_base指向想要泄露的位置,_IO_write_ptr指向泄露结束的地址(不需要一定设置指向结尾,stdout结构中自带地址也足够泄露libc)
- _mode=0或者-1
- _fileno为1(因为最后要输出内容到屏幕,所以文件描述符应该为1)