Profile

youngsouk

youngsouk

fread 분석

이번에는 fread를 분석해볼 것입니다. 저번과 마찬가지로 간단한 c 프로그램을 짜고 컴파일 후 디버깅을 하면서 함수를 보고 소스코드를 보는식으로 하겠습니다. 

#include <stdio.h>

int main(){
        char s[100];
        int f = fopen("tmp", "r");

        fread(s,1,10,f);
}

이런식으로 간단하게 짠다음에 디버깅을 해서 함수를 보자면 아래 그림의 과정으로 함수가 호출이 됩니다.

 

먼저 _IO_fread를 보겠습니다.

# define CHECK_FILE(FILE, RET) do { } while (0)

# define _IO_release_lock(_fp) ; } while (0)
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;
}
libc_hidden_def (_IO_fread)

즉_IO_sgetn을 호출하는 모습을 볼 수 있습니다. 

#define _IO_XSGETN(FP, DATA, N) JUMP2 (__xsgetn, FP, DATA, N)
#define JUMP2(FUNC, THIS, X1, X2) (_IO_JUMPS_FUNC(THIS)->FUNC) (THIS, X1, X2)

#if _IO_JUMPS_OFFSET
# define _IO_JUMPS_FUNC(THIS) \
 (*(struct _IO_jump_t **) ((void *) &_IO_JUMPS_FILE_plus (THIS) \
			   + (THIS)->_vtable_offset))
# define _IO_vtable_offset(THIS) (THIS)->_vtable_offset
#else
# define _IO_JUMPS_FUNC(THIS) _IO_JUMPS_FILE_plus (THIS)
# define _IO_vtable_offset(THIS) 0
#endif

#define _IO_JUMPS_FILE_plus(THIS) \
  _IO_CAST_FIELD_ACCESS ((THIS), struct _IO_FILE_plus, vtable)
 
#define _IO_CAST_FIELD_ACCESS(THIS, TYPE, MEMBER) \
  (*(_IO_MEMBER_TYPE (TYPE, MEMBER) *)(((char *) (THIS)) \
				       + offsetof(TYPE, MEMBER)))

#define _IO_MEMBER_TYPE(TYPE, MEMBER) __typeof__ (((TYPE){}).MEMBER)

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

무엇인가 엄청난 매크로 함수들을 사용해서 굉장히 어려워보이지만 간단하게 요약하자면 vtable 의 _IO_xsgetn_t을 call 한다는 것입니다.

일반적으로는 저 vtable 안에 _IO_file_jumps이 저장되어있습니다. 

const struct _IO_jump_t _IO_file_jumps libio_vtable =
{
  JUMP_INIT_DUMMY,
  JUMP_INIT(finish, _IO_file_finish),
  JUMP_INIT(overflow, _IO_file_overflow),
  JUMP_INIT(underflow, _IO_file_underflow),
  JUMP_INIT(uflow, _IO_default_uflow),
  JUMP_INIT(pbackfail, _IO_default_pbackfail),
  JUMP_INIT(xsputn, _IO_file_xsputn),
  JUMP_INIT(xsgetn, _IO_file_xsgetn),
  JUMP_INIT(seekoff, _IO_new_file_seekoff),
  JUMP_INIT(seekpos, _IO_default_seekpos),
  JUMP_INIT(setbuf, _IO_new_file_setbuf),
  JUMP_INIT(sync, _IO_new_file_sync),
  JUMP_INIT(doallocate, _IO_file_doallocate),
  JUMP_INIT(read, _IO_file_read),
  JUMP_INIT(write, _IO_new_file_write),
  JUMP_INIT(seek, _IO_file_seek),
  JUMP_INIT(close, _IO_file_close),
  JUMP_INIT(stat, _IO_file_stat),
  JUMP_INIT(showmanyc, _IO_default_showmanyc),
  JUMP_INIT(imbue, _IO_default_imbue)
};

따라서 _IO_file_xsgetn이 호출이 되게 됩니다. 이 _IO_file_xsgetn의 코드를 보자면

size_t
_IO_file_xsgetn (FILE *fp, void *data, size_t n)
{
  size_t want, have;
  ssize_t count;
  char *s = data;
  want = n;
  if (fp->_IO_buf_base == NULL) //예약된 버퍼가 없다면
    {
      /* 아마 우리는 이미 돌아갈 포인터를 가지고 있다. */
      if (fp->_IO_save_base != NULL) // 백업이 있다면 삭제한다.
        {
          free (fp->_IO_save_base);
          fp->_flags &= ~_IO_IN_BACKUP;
        }
      _IO_doallocbuf (fp); //버퍼를 할당해주는 함수이다.
    }
  while (want > 0) // 읽어들일 갯수가 양수이면 참이 된다.
    {
      have = fp->_IO_read_end - fp->_IO_read_ptr; // 실제로 읽어들일 수 있는 바이트 수를 구한다.
      if (want <= have)
        {
          memcpy (s, fp->_IO_read_ptr, want); // 원하는 바이트 수 만큼 memcpy를 통해 값 복사를 한다.
          fp->_IO_read_ptr += want; // 현재 읽기 포인터를 갱신해준다.
          want = 0;
        }
      else
        {
          if (have > 0) // want > have 즉 읽어들일 바이트가 부족할 때 실행된다.
            {
              s = __mempcpy (s, fp->_IO_read_ptr, have);
              want -= have;
              fp->_IO_read_ptr += have;
            }
          /* 백업과 반복을 위해 확인한다. */
          if (_IO_in_backup (fp))
            {
              _IO_switch_to_main_get_area (fp);
              continue;
            }
          /* 만약 우리가 현재 버퍼(예약된 영역)보다 적게 원한다면, underflow를 일으키고 복사를 반복한다.
             그렇지 않으면, 유저 버퍼에 직접 _IO_SYSREAD를 실행한다.
          */
          if (fp->_IO_buf_base
              && want < (size_t) (fp->_IO_buf_end - fp->_IO_buf_base)) 
            {
              if (__underflow (fp) == EOF)
                break;
              continue;
            }
          /* 이것들은 우리가 입력을 위해 기다리기위해 longjmp를 하기 때문에 sysread이전에 설정이 되어야만 한다.*/
          _IO_setg (fp, fp->_IO_buf_base, fp->_IO_buf_base, fp->_IO_buf_base);
          _IO_setp (fp, fp->_IO_buf_base, fp->_IO_buf_base);
          /* 정렬을 유지하기 위해 시도한다. : 블럭의 모든 영역을 읽어들인다.  */
          count = want;
          if (fp->_IO_buf_base)
            {
              size_t block_size = fp->_IO_buf_end - fp->_IO_buf_base; // 예약된 영역의 크기를 저장한다.
              if (block_size >= 128)
                count -= want % block_size;
            }
          count = _IO_SYSREAD (fp, s, count);
          if (count <= 0) // 제대로 읽어들이지 못했을 경우이다. 
            {
              if (count == 0)
                fp->_flags |= _IO_EOF_SEEN;
              else
                fp->_flags |= _IO_ERR_SEEN;
              break;
            }
          s += count; // s와 want를 갱신한다.
          want -= count;
          if (fp->_offset != _IO_pos_BAD)
            _IO_pos_adjust (fp->_offset, count);
        }
    }
  return n - want;
}
libc_hidden_def (_IO_file_xsgetn)

이런식으로 구성이 돼있습니다. 여기서 우리는 제일 처음 fread를 할 때 실행이 되는 _IO_doallocbuf를 보겠습니다.

#define _IO_DOALLOCATE(FP) JUMP0 (__doallocate, FP)
#define JUMP0(FUNC, THIS) (_IO_JUMPS_FUNC(THIS)->FUNC) (THIS)


void
_IO_doallocbuf (FILE *fp)
{
  if (fp->_IO_buf_base) // 할당이 이미 되어있다면 종료한다.
    return;
  if (!(fp->_flags & _IO_UNBUFFERED) || fp->_mode > 0) 
    if (_IO_DOALLOCATE (fp) != EOF)
      return;
  _IO_setb (fp, fp->_shortbuf, fp->_shortbuf+1, 0);
}
libc_hidden_def (_IO_doallocbuf)

_IO_DOALLOCATE를 통해 버퍼를 할당해주는 함수입니다. _IO_DOALLOCATE는 위의 _IO_xsgetn_t과 같이 vtable의 __doallocate를 호출하는 것입니다. 위의 점프 테일블을 참고해서 보게되면 __doallocate는 _IO_file_doallocate를 호출하게 된다는 것을 알 수 있습니다. 

int
_IO_file_doallocate (FILE *fp)
{
  size_t size;
  char *p;
  struct stat64 st;

  size = BUFSIZ;
  if (fp->_fileno >= 0 && __builtin_expect (_IO_SYSSTAT (fp, &st), 0) >= 0)
    {
      if (S_ISCHR (st.st_mode))
	{
	  /* Possibly a tty.  */
	  if (
#ifdef DEV_TTY_P
	      DEV_TTY_P (&st) ||
#endif
	      local_isatty (fp->_fileno))
	    fp->_flags |= _IO_LINE_BUF;
	}
#if defined _STATBUF_ST_BLKSIZE
      if (st.st_blksize > 0 && st.st_blksize < BUFSIZ)
	size = st.st_blksize;
#endif
    }
  p = malloc (size);
  if (__glibc_unlikely (p == NULL))
    return EOF;
  _IO_setb (fp, p, p + size, 1);
  return 1;
}
libc_hidden_def (_IO_file_doallocate)

조금 복잡하게 함수가 보이지만 간단히 요약하자면 블록 크기만큼 malloc()을 통해 동적 할당을 하고 _IO_setb를 호출하게 됩니다.

void
_IO_setb (FILE *f, char *b, char *eb, int a)
{
  if (f->_IO_buf_base && !(f->_flags & _IO_USER_BUF)) // 예약된 영역이 있지만 USER_BUF를 사용하지 않는다면
    free (f->_IO_buf_base);
  f->_IO_buf_base = b;
  f->_IO_buf_end = eb;
  if (a)
    f->_flags &= ~_IO_USER_BUF;
  else
    f->_flags |= _IO_USER_BUF;
}
libc_hidden_def (_IO_setb)

이 _IO_setb는 아까 블록 크기만큼 할당한 포인터의 시작과 끝을 _IO_buf_base와 _IO_buf_end에 저장을 해주는 함수입니다. 그래서 버퍼가 없다면 

이런식으로 진행이 되게 됩니다.  이렇게 버퍼할당과 관련된 부분은 끝이 났습니다.


          /* 만약 우리가 현재 버퍼(예약된 영역)보다 적게 원한다면, underflow를 일으키고 복사를 반복한다.
             그렇지 않으면, 유저 버퍼에 직접 _IO_SYSREAD를 실행한다.
          */
          if (fp->_IO_buf_base
              && want < (size_t) (fp->_IO_buf_end - fp->_IO_buf_base)) 
            {
              if (__underflow (fp) == EOF)
                break;
              continue;
            }

그 다음으로 분석할 함수는 처음 fread를 실행하여 have가 0일 때 실행이되는 __underflow부분을 보겠습니다. 

int
__underflow (_IO_FILE *fp)
{
#if defined _LIBC || defined _GLIBCPP_USE_WCHAR_T
  if (_IO_vtable_offset (fp) == 0 && _IO_fwide (fp, -1) != -1)
    return EOF;
#endif

  if (fp->_mode == 0)
    _IO_fwide (fp, -1);
  if (_IO_in_put_mode (fp))
    if (_IO_switch_to_get_mode (fp) == EOF)
      return EOF;
  if (fp->_IO_read_ptr < fp->_IO_read_end)
    return *(unsigned char *) fp->_IO_read_ptr;
  if (_IO_in_backup (fp))
    {
      _IO_switch_to_main_get_area (fp);
      if (fp->_IO_read_ptr < fp->_IO_read_end)
	return *(unsigned char *) fp->_IO_read_ptr;
    }
  if (_IO_have_markers (fp))
    {
      if (save_for_backup (fp, fp->_IO_read_end))
	return EOF;
    }
  else if (_IO_have_backup (fp))
    _IO_free_backup_area (fp);
  return _IO_UNDERFLOW (fp);
}
libc_hidden_def (__underflow)

저기서 유심히 보아야될 부분은 _IO_switch_to_get_mode와 _IO_UNDERFLOW 부분입다. 먼저 _IO_switch_to_get_mode부터 보겠습니다.

int
_IO_switch_to_get_mode (FILE *fp)
{
  if (fp->_IO_write_ptr > fp->_IO_write_base) 
    if (_IO_OVERFLOW (fp, EOF) == EOF)
      return EOF;
  if (_IO_in_backup (fp))
    fp->_IO_read_base = fp->_IO_backup_base;
  else
    {
      fp->_IO_read_base = fp->_IO_buf_base;
      if (fp->_IO_write_ptr > fp->_IO_read_end) // 현재 포인터가 읽을 위치의 끝보다 크다면 갱신해준다.
	fp->_IO_read_end = fp->_IO_write_ptr;
    }
  fp->_IO_read_ptr = fp->_IO_write_ptr;

  fp->_IO_write_base = fp->_IO_write_ptr = fp->_IO_write_end = fp->_IO_read_ptr;

  fp->_flags &= ~_IO_CURRENTLY_PUTTING;
  return 0;
}
libc_hidden_def (_IO_switch_to_get_mode)

read_ptr을 갱신하고 write포인터를 초기화 시켜주는 함수입니다. 처음 fread()를 할 때에는 유의미한 기능은 업습니다.

 

_IO_UNDERFLOW 함수는 vtable의 underflow를 실행시킵니다. 즉 처음 fread를 하게 되면 _IO_file_underflow가 호출이 되게 됩니다. 그런데 _IO_file_underflow는 _IO_new_file_underflow와 같습니다. 즉 _IO_new_file_underflow를 호출하는 것입니다.

versioned_symbol (libc, _IO_new_file_underflow, _IO_file_underflow, GLIBC_2_1);

int
_IO_new_file_underflow (_IO_FILE *fp)
{
  _IO_ssize_t count;
#if 0
  /* SysV 는 이 검사를 만들지 않는다.; 호환성을 위해서 뻈다. */
  if (fp->_flags & _IO_EOF_SEEN)
    return (EOF);
#endif

  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;

  if (fp->_IO_buf_base == NULL)
    {
      /* 아마 우린는 돌아갈 포인터를 이미 가지고 있다. */
      if (fp->_IO_save_base != NULL) // 백업이 있다면 free 시킨다.
	{
	  free (fp->_IO_save_base);
	  fp->_flags &= ~_IO_IN_BACKUP;
	}
      _IO_doallocbuf (fp); // 버퍼가 없다면 할당해준다.
    }

  /* 읽기전에 전체 파일들을 초기화 시킨다. */
  /* FIXME 이것이 genops로 이동되어야 할까?? */
  if (fp->_flags & (_IO_LINE_BUF|_IO_UNBUFFERED))
    {
#if 0
      _IO_flush_all_linebuffered ();
#else
      /* 우리는 line buffer인 모든 스트림을 초기화 시키곤 했다. 이것은 실제로 아무 표준에서나 요구되었다.
	 전통적인 Unix 시스템은 stdout을 초기화 시킨다. stderr는 line buffer가 되지 않는 것이 낫다.
     그래서 우리는 여기에서 명확하게 한다. */
      _IO_acquire_lock (_IO_stdout);

      if ((_IO_stdout->_flags & (_IO_LINKED | _IO_NO_WRITES | _IO_LINE_BUF))
	  == (_IO_LINKED | _IO_LINE_BUF))
	_IO_OVERFLOW (_IO_stdout, EOF);

      _IO_release_lock (_IO_stdout);
#endif
    }

  _IO_switch_to_get_mode (fp);

  /* 이것은 매우 까다롭다. 우리는 우리가 입력을 기다리는
  	 동안에 longjump()를 해야하기 때문에 _IO_SYSREAD()을 호출
  	 하기 전에 저러한 포인터들을 조정해야만 한다. 저러한 포인터들은
     아마 섞여 있다.
  */
  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;

  count = _IO_SYSREAD (fp, fp->_IO_buf_base,
		       fp->_IO_buf_end - fp->_IO_buf_base); // _IO_SYSREAD를 통해 _IO_buf_base에 읽어들인다.
  if (count <= 0)
    {
      if (count == 0)
	fp->_flags |= _IO_EOF_SEEN;
      else
	fp->_flags |= _IO_ERR_SEEN, count = 0;
  }
  fp->_IO_read_end += count;
  if (count == 0)
    {
      /* 만약 스트림이 EOF라면, 호출이 헨들로 바꿔질 수 있다. 그 결과로서, 우리의 오프셋 캐쉬는
      더 이상 유효하지 못하게 된다. 그래서 그것을 셋팅하지 않는다.
      */
      fp->_offset = _IO_pos_BAD;
      return EOF;
    }
  if (fp->_offset != _IO_pos_BAD)
    _IO_pos_adjust (fp->_offset, count);
  return *(unsigned char *) fp->_IO_read_ptr;
}
libc_hidden_ver (_IO_new_file_underflow, _IO_file_underflow)

#define _IO_pos_adjust(pos, delta) ((pos) += (delta))

이렇게 _IO_new_file_underflow에서는 _IO_sysread를 통해 실제 파일을 읽고 fp의 read와 write에 관한 포인터들을 셋팅해줍니다.

          _IO_setg (fp, fp->_IO_buf_base, fp->_IO_buf_base, fp->_IO_buf_base);
          _IO_setp (fp, fp->_IO_buf_base, fp->_IO_buf_base);

그 다음으로 살펴볼 부분은 sysread를 하기 전에 셋팅을 하는 위의 두 함수입니다.

#define _IO_setg(fp, eb, g, eg)  ((fp)->_IO_read_base = (eb),\
        (fp)->_IO_read_ptr = (g), (fp)->_IO_read_end = (eg))
#define _IO_setp(__fp, __p, __ep) \
       ((__fp)->_IO_write_base = (__fp)->_IO_write_ptr \
        = __p, (__fp)->_IO_write_end = (__ep))

두 함수 모두 read와 write와 관련된 포인터를 초기화 시켜준다는 것을 알 수 있습니다. 이렇게 fread()함수에 대한 분석은 끝났습니다. 

그래서 fread()함수의 특성에 대해서 정리하자면 이렇게 됩니다.

1. 버퍼가 없으면 버퍼를 할당받는다.

2. 기본적으로 한 블록 크기만큼 파일에서 읽어와서 버퍼에 저장해놓는다.

3. 만약 읽어야되는 데이터가 블록 크기 미만이고, 버퍼에 저장해둔 크기가 부족하다면 버퍼의 내용을 또다시 한 블럭 만큼 읽어서 갱신한다.

4. 만약 읽을 데이터의 크기가 블록 크기 이상이면 버퍼를 이용하지 않고 직접 fread()인자 버퍼에 데이터를 복사한다.

그리고 fread함수의 진행 과정을 그림으로 나타내면 이렇게 됩니다.

'FSOP' 카테고리의 다른 글

setvbuf 함수 분석  (0) 2019.09.25
FSOP - seethefile(pwnable.tw)  (0) 2019.09.20
fclose 분석  (0) 2019.09.13
fwrite 분석  (0) 2019.09.13
fopen함수 분석  (0) 2019.09.12