原文來自安全客,作者:raycp
原文鏈接:https://www.anquanke.com/post/id/177958
相關閱讀:IO FILE 之 fopen 詳解

這是IO FILE系列的第二篇文章,主要寫的是對于fread函數的源碼分析,描述fread讀取文件流的主要流程以及函數對IO FILE結構體以及結構體中的vtable的操作。流程有點小復雜,入坑需謹慎。

總體流程

第一篇文章fopen的分析,講述了系統如何為FILE結構體分配內存并將其鏈接進入_IO_list_all的。

這篇文章則是說在創建了文件FILE以后,fread如何實現從文件中讀取數據的。在開始源碼分析之前,我先把fread的流程圖給貼出來,后面在分析源碼的時候,可以適時的參考下流程圖,增進理解:

從圖中可以看到,整體流程為fread調用_IO_sgetn_IO_sgetn調用vtable中的_IO_XSGETN也就是_IO_file_xsgetn_IO_file_xsgetnfread實現的核心函數。它的流程簡單總結為: 1. 判斷fp->_IO_buf_base輸入緩沖區是否為空,如果為空則調用的_IO_doallocbuf去初始化輸入緩沖區。 2. 在分配完輸入緩沖區或輸入緩沖區不為空的情況下,判斷輸入緩沖區是否存在數據。 3. 如果輸入緩沖區有數據則直接拷貝至用戶緩沖區,如果沒有或不夠則調用__underflow函數執行系統調用讀取數據到輸入緩沖區,再拷貝到用戶緩沖區。

源碼分析

仍然是基于glibc2.23的源碼分析,使用帶符號的glibc對程序進行調試。

fread的函數原型是:

size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream);
The  function fread() reads nmemb items of data, each size bytes long, from the stream pointed to by stream, storing them at the location given by ptr.

demo程序如下:

#include<stdio.h>

int main(){
    char data[20];
    FILE*fp=fopen("test","rb");
    fread(data,1,20,fp);
    return 0;
}

要讓程序可以運行,執行命令echo 111111>test,然后gdb加載程序,斷點下在fread,開始一邊看源碼,一邊動態跟蹤流程。

程序運行起來后,可以看到斷在_IO_fread函數。在開始之前我們先看下FILE結構體fp的內容,從圖里可以看到此時的_IO_read_ptr_IO_buf_base等指針都還是空的,后面的分析一個很重要的步驟也是看這些指針是如何被賦值以及發揮作用的:

vtable中的指針內容如下:

fread實際上是_IO_fread函數,文件目錄為/libio/iofread.c

_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;
  ...
  # 調用_IO_sgetn函數
  bytes_read = _IO_sgetn (fp, (char *) buf, bytes_requested);
  ...
  return bytes_requested == bytes_read ? count : bytes_read / size;
}
libc_hidden_def (_IO_fread)

_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_XSGETN函數,查看它定義:

#define _IO_XSGETN(FP, DATA, N) JUMP2 (__xsgetn, FP, DATA, N)

實際上就是FILE結構體中vtable的__xsgetn函數,跟進去/libio/fileops.c

_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;

  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讀入數據
        break;

          continue;
        }
      ...
  return n - want;
}
libc_hidden_def (_IO_file_xsgetn)

_IO_file_xsgetn是處理fread讀入數據的核心函數,分為三個部分: 第一部分是fp->_IO_buf_base為空的情況,表明此時的FILE結構體中的指針未被初始化,輸入緩沖區未建立,則調用_IO_doallocbuf去初始化指針,建立輸入緩沖區。 第二部分是輸入緩沖區里有輸入,即fp->_IO_read_ptr小于fp->_IO_read_end,此時將緩沖區里的數據直接拷貝至目標buff。 * 第三部分是輸入緩沖區里的數據為空或者是不能滿足全部的需求,則調用__underflow調用系統調用讀入數據。

接下來對_IO_file_xsgetn這三部分進行跟進并分析。

初始化輸入緩沖區

首先是第一部分,在fp->_IO_buf_base為空時,也就是輸入緩沖區未建立時,代碼調用_IO_doallocbuf函數去建立輸入緩沖區。跟進_IO_doallocbuf函數,看下它是如何初始化輸入緩沖區,為輸入緩沖區分配空間的,文件在/libio/genops.c中:

void
_IO_doallocbuf (_IO_FILE *fp)
{
  if (fp->_IO_buf_base) # 如何輸入緩沖區不為空,直接返回
    return;
  if (!(fp->_flags & _IO_UNBUFFERED) || fp->_mode > 0) #檢查標志位
    if (_IO_DOALLOCATE (fp) != EOF) ## 調用vtable函數
      return;
  _IO_setb (fp, fp->_shortbuf, fp->_shortbuf+1, 0);
}
libc_hidden_def (_IO_doallocbuf)

函數先檢查fp->_IO_buf_base是否為空,如果不為空的話表明該輸入緩沖區已被初始化,直接返回。如果為空,則檢查fp->_flags看它是不是_IO_UNBUFFERED或者fp->_mode大于0,如果滿足條件調用FILE的vtable中的_IO_file_doallocate,跟進去該函數,在/libio/filedoalloc.c中:

_IO_file_doallocate (_IO_FILE *fp)
{
  _IO_size_t size;
  char *p;
  struct stat64 st;

  ...
  size = _IO_BUFSIZ;
  ...
  if (fp->_fileno >= 0 && __builtin_expect (_IO_SYSSTAT (fp, &st), 0) >= 0) # 調用`_IO_SYSSTAT`獲取FILE信息
   {
     ... 
     if (st.st_blksize > 0)
         size = st.st_blksize;
     ...
   }
 p = malloc (size);
 ...
 _IO_setb (fp, p, p + size, 1); # 調用`_IO_setb`設置FILE緩沖區
  return 1;
}
libc_hidden_def (_IO_file_doallocate)

可以看到_IO_file_doallocate函數是分配輸入緩沖區的實現函數,首先調用_IO_SYSSTAT去獲取文件信息,_IO_SYSSTAT函數是vtable中的__stat函數,獲取文件信息,修改相應需要申請的size。可以看到在執行完_IO_SYSSTAT函數后,st結構體的值為:

因此size被修改為st.st_blksize所對應的大小0x1000,接著調用malloc去申請內存,申請出來的堆塊如下:

空間申請出來后,調用_IO_setb,跟進去看它干了些啥,文件在/libio/genops.c中:

void
_IO_setb (_IO_FILE *f, char *b, char *eb, int a)
{
  ...
  f->_IO_buf_base = b; # 設置_IO_buf_base 
  f->_IO_buf_end = eb; # 設置_IO_buf_end
  ...
}
libc_hidden_def (_IO_setb)

函數相對比較簡單的就是設置了_IO_buf_base_IO_buf_end,可以預料到_IO_setb函數執行完后,fp的這兩個指針被賦上值了:

到此,初始化緩沖區就完成了,函數返回_IO_file_doallocate后,接著_IO_file_doallocate也返回,回到_IO_file_xsgetn函數中。

拷貝輸入緩沖區數據

初始化緩沖區完成之后,代碼返回到_IO_file_xsgetn函數中,程序就進入到第二部分:拷貝輸入緩沖區數據,如果輸入緩沖區里存在已輸入的數據,則把它直接拷貝到目標緩沖區里。

這部分比較簡單,需要說明下的是從這里可以看出來fp->_IO_read_ptr指向的是輸入緩沖區的起始地址,fp->_IO_read_end指向的是輸入緩沖區的結束地址。

fp->_IO_read_end-fp->_IO_read_ptr之間的數據通過memcpy拷貝到目標緩沖區里。

執行系統調用讀取數據

在輸入緩沖區為0或者是不能滿足需求的時候則會執行最后一步__underflow去執行系統調用read讀取數據,并放入到輸入緩沖區里。

因為demo里第一次讀取數據,此時的fp->_IO_read_end以及fp->_IO_read_ptr都是0,因此會進入到__underflow,跟進去細看,文件在/libio/genops.c中:

int
__underflow (_IO_FILE *fp)
{

  # 額外的檢查
  ...
  if (fp->_IO_read_ptr < fp->_IO_read_end)
    return *(unsigned char *) fp->_IO_read_ptr;
  ...
  # 調用_IO_UNDERFLOW
  return _IO_UNDERFLOW (fp);
}
libc_hidden_def (__underflow)

函數稍微做一些檢查就會調用_IO_UNDERFLOW函數,其中一個檢查是如果fp->_IO_read_ptr小于fp->_IO_read_end則表明輸入緩沖區里存在數據,可直接返回,否則則表示需要繼續讀入數據。

檢查都通過的話就會調用_IO_UNDERFLOW函數,該函數是FILE結構體vtable里的_IO_new_file_underflow,跟進去看,文件在/libio/fileops.c里:

int
_IO_new_file_underflow (_IO_FILE *fp)
{
  _IO_ssize_t count;
  ...
  ## 如果存在_IO_NO_READS標志,則直接返回
  if (fp->_flags & _IO_NO_READS)
    {
      fp->_flags |= _IO_ERR_SEEN;
      __set_errno (EBADF);
      return EOF;
    }
  ## 如果輸入緩沖區里存在數據,則直接返回
  if (fp->_IO_read_ptr < fp->_IO_read_end)
    return *(unsigned char *) fp->_IO_read_ptr;
  ...
  ## 如果沒有輸入緩沖區,則調用_IO_doallocbuf分配輸入緩沖區
  if (fp->_IO_buf_base == NULL)
    {
      ...
      _IO_doallocbuf (fp);
    }
  ...
  ## 設置FILE結構體指針
  fp->_IO_read_base = fp->_IO_read_ptr = fp->_IO_buf_base;
  fp->_IO_read_end = fp->_IO_buf_base;
  fp->_IO_write_base = fp->_IO_write_ptr = fp->_IO_write_end
    = fp->_IO_buf_base;
  ##調用_IO_SYSREAD函數最終執行系統調用讀取數據
  count = _IO_SYSREAD (fp, fp->_IO_buf_base,
               fp->_IO_buf_end - fp->_IO_buf_base);
  ...
  ## 設置結構體指針
  fp->_IO_read_end += count;
  ...
  return *(unsigned char *) fp->_IO_read_ptr;
}
libc_hidden_ver (_IO_new_file_underflow, _IO_file_underflow)

這個_IO_new_file_underflow函數,是最終調用系統調用的地方,在最終執行系統調用之前,仍然有一些檢查,整個流程為: 1. 檢查FILE結構體的_flag標志位是否包含_IO_NO_READS,如果存在這個標志位則直接返回EOF,其中_IO_NO_READS標志位的定義是#define _IO_NO_READS 4 /* Reading not allowed */。 2. 如果fp->_IO_buf_base位null,則調用_IO_doallocbuf分配輸入緩沖區。 3. 接著初始化設置FILE結構體指針,將他們都設置成fp->_IO_buf_base 4. 調用_IO_SYSREAD(vtable中的_IO_file_read函數),該函數最終執行系統調用read,讀取文件數據,數據讀入到fp->_IO_buf_base中,讀入大小為輸入緩沖區的大小fp->_IO_buf_end - fp->_IO_buf_base。 5. 設置輸入緩沖區已有數據的size,即設置fp->_IO_read_endfp->_IO_read_end += count

其中第二步里面的如果fp->_IO_buf_base位null,則調用_IO_doallocbuf分配輸入緩沖區,似乎有點累贅,因為之前已經分配了,這個原因我在最后會說明。

其中第四步的_IO_SYSREAD(vtable中的_IO_file_read函數)的源碼比較簡單,就是執行系統調用函數read去讀取文件數據,文件在libio/fileops.c,源碼如下:

_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));
 }

_IO_file_underflow函數執行完畢以后,FILE結構體中各個指針已被賦值,且文件數據已讀入,輸入緩沖區里已經有數據,結構體值如下,其中fp->_IO_read_ptr指向輸入緩沖區數據的開始位置,fp->_IO_read_end指向輸入緩沖區數據結束的位置:

函數執行完后,返回到_IO_file_xsgetn函數中,由于while循環的存在,重新執行第二部分,此時將輸入緩沖區拷貝至目標緩沖區,最終返回。

至此,對于fread的源碼分析結束。

其他輸入函數

完整分析了fread函數之后,還想知道其他一些函數(scanf、gets)等函數時如何通過stdin實現輸入的,我編寫了源碼,并將斷點下在了read函數之前,看他們時如何調用去的。

首先是scanf,其最終調用read函數時棧回溯如下:

read
_IO_new_file_underflow at fileops.c
__GI__IO_default_uflow at genops.c
_IO_vfscanf_internal at vfscanf.c
__isoc99_scanf at  at isoc99_scanf.c
main ()
__libc_start_main

可以看到scanf最終也是調用stdin的vtable中的_IO_new_file_underflow去調用read的。不過它并不是使用_IO_file_xsgetn,而是使用vtable中的__uflow,源碼如下:

int
_IO_default_uflow (_IO_FILE *fp)
{
  int ch = _IO_UNDERFLOW (fp);
  if (ch == EOF)
    return EOF;
  return *(unsigned char *) fp->_IO_read_ptr++;
}
libc_hidden_def (_IO_default_uflow)

__uflow函數基本上啥都沒干直接就調用了_IO_new_file_underflow因此最終也是_IO_new_file_underflow實現的輸入。

再看看gets函數,函數調用棧如下,與scanf基本一致:

read
__GI__IO_file_underflow
__GI__IO_default_uflow
gets
main
 __libc_start_main+240

再試了試fscanf等,仍然是一樣的,仍然是最終通過_IO_new_file_underflow實現的輸入。雖然不能說全部的io輸入都是通過_IO_new_file_underflow函數最終實現的輸入,但是應該也可以說大部分是使用_IO_new_file_underflow函數實現的。

但是仍然有一個問題,由于__uflow直接就調用了_IO_new_file_underflow函數,那么輸入緩沖區是在哪里建立的呢,為了找到這個問題的答案,我在程序進入到fscanf函數后又在malloc函數下了個斷點,然后棧回溯:

malloc
__GI__IO_file_doallocate
__GI__IO_doallocbuf
__GI__IO_file_underflow
__GI__IO_default_uflow
__GI__IO_vfscanf
__isoc99_fscanf
main
__libc_start_main

原來是在__GI__IO_file_underflow分配的空間,回到上面看該函數的源碼,確實有一段判斷輸入緩沖區如果為空則調用__GI__IO_doallocbuf函數建立輸入緩沖區的代碼,這就解釋了__GI__IO_file_underflow第二步中為啥還會有個輸入緩沖區判斷的原因了,不得不感慨,代碼寫的真巧妙。

小結

在結束之前我想總結下fread在執行系統調用read前對vtable里的哪些函數進行了調用,具體如下: _IO_sgetn函數調用了vtable的_IO_file_xsgetn _IO_doallocbuf函數調用了vtable的_IO_file_doallocate以初始化輸入緩沖區。 vtable中的_IO_file_doallocate調用了vtable中的__GI__IO_file_stat以獲取文件信息。 __underflow函數調用了vtable中的_IO_new_file_underflow實現文件數據讀取。 * vtable中的_IO_new_file_underflow調用了vtable__GI__IO_file_read最終去執行系統調用read。

先提一下,后續如果想通過IO FILE實現任意讀的話,最關鍵的函數應是_IO_new_file_underflow,它里面有個標志位的判斷,是后面構造利用需要注意的一個比較重要條件:

  ## 如果存在_IO_NO_READS標志,則直接返回
  if (fp->_flags & _IO_NO_READS)
    {
      fp->_flags |= _IO_ERR_SEEN;
      __set_errno (EBADF);
      return EOF;
    }

終于結束了,寫的有些凌亂,將就看看吧。


Paper 本文由 Seebug Paper 發布,如需轉載請注明來源。本文地址:http://www.bjnorthway.com/925/