Profile

youngsouk

youngsouk

fwrite 분석

2019/09/13 - [힙(heap)/house_of_orange] - house_of_orange 번외 fread 분석 (glibc 2.23)

저번에는 fread에 대해 분석을 해보았는데 이제는 fwrite에 대해 분석을 진행해 볼 것입니다.

#include <stdio.h>

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

        fwrite(s,1,9,f);
}

이런식으로 간단한 프로그램을 짜주시고 컴파일 후 디버깅을 이용해 함수를 보시면 됩니다.

fwrite는 iofwrite랑 같습니다. 즉 iofwrite를 분석하면 fwrite를 분석하는 것입니다.

_IO_size_t
_IO_fwrite (const void *buf, _IO_size_t size, _IO_size_t count, _IO_FILE *fp)
{
  _IO_size_t request = size * count; //출력할 크기 저장
  _IO_size_t written = 0; 
  CHECK_FILE (fp, 0);
  if (request == 0) //출력할 것이 없으면 종료
    return 0;
  _IO_acquire_lock (fp);
  if (_IO_vtable_offset (fp) != 0 || _IO_fwide (fp, -1) == -1)
    written = _IO_sputn (fp, (const char *) buf, request);
  _IO_release_lock (fp);
  /* 우리는 모든 입력을 출력을 해서, 반환 값은 이것 또는 EOF가 반환된다.
  	 후자(EOF)는 우리가 간단히 버퍼를 비울 수 없게 하는 특별한 경우이다.
     하지만, 그 데이터가 버퍼 안에 있고, 그러므로 fwrite가 신경쓰일 정도 멀리 쓰였다.
  */
  if (written == request || written == EOF)
    return count;
  else
    return written / size;
}
libc_hidden_def (_IO_fwrite)

여기서 본격적으로 분석해볼 함수는 바로 _IO_sputn입니다. 

#define _IO_sputn(__fp, __s, __n) _IO_XSPUTN (__fp, __s, __n)
#define _IO_XSPUTN(FP, DATA, N) JUMP2 (__xsputn, FP, DATA, N)
#define JUMP2(FUNC, THIS, X1, X2) (_IO_JUMPS_FUNC(THIS)->FUNC) (THIS, X1, X2)

이렇게 vtable의 _IO_XSPUTN을 실행시킵니다. 그런데 여기에는 아래 두개의 글에서 보셨다시피

2019/09/13 - [힙(heap)/house_of_orange] - house_of_orange 번외 fread 분석 (glibc 2.23)

2019/09/12 - [힙(heap)/house_of_orange] - house_of_orange 번외 - fopen함수 분석(glibc 2.23)

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

이 구조체의 주소가 vtable에 담겨 있습니다. 즉 _IO_file_xsputn함수를 호출하게 되는것입니다. 그런데 _IO_file_xsputn는 _IO_new_file_xsputn 함수의 약어입니다. 즉 진짜로 호출 되는 함수는 _IO_new_file_xsputn가 됩니다.

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;
  /* 이것은 최적화된 삽입이다.
     만약 출력될 크기가 블록 경계를 침범하면(또는 파일버퍼가 버퍼링을 하지 않을때)
	 sys_write를 직접 사용한다. */
  /* 첫번쨰로 버퍼안에 얼마나 많은 공간이 이용가능한지 알아내야 한다.  */
  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; /* 이용가능한 공간. */
  /* 버퍼를 채운다. */
  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;
      /* 다음으로 버퍼를 비운다. */
      if (_IO_OVERFLOW (f, EOF) == EOF)
        /* 만약 쓰일 것이 아무것도 없다면, 우리는 모든 것이 쓰여지는 함수을 호출하면 안된다. */
        return to_do == 0 ? EOF : n - to_do;
      /* 정렬을 유지하려 시도한다. : 블록의 전체를 출력한다.  */
      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;
        }
      /* 이제 나머지를 출력해준다. 일반적으로, 이것은 버퍼안에서 잘 맞는다.
         하지만, 그것은 어느정도 line-buffered 파일에게는 지저분하다.
         그래서 우리는 _IO_default_xsputn가 일반적인 경우를 맡도록 허용한다. */
      if (to_do)
        to_do -= _IO_default_xsputn (f, s+do_write, to_do);
    }
  return n - to_do;
}
libc_hidden_ver (_IO_new_file_xsputn, _IO_file_xsputn)

2019/09/13 - [힙(heap)/house_of_orange] - house_of_orange 번외 fread 분석 (glibc 2.23)

기본적으로 블록 단위로 출력을 하는점에서 fread와 동작 방식이 약간 비슷합니다.

이제 우리가 봐야할것은 _IO_OVERFLOW라는 함수입니다.

#define _IO_OVERFLOW(FP, CH) JUMP1 (__overflow, FP, CH)
#define JUMP1(FUNC, THIS, X1) (_IO_JUMPS_FUNC(THIS)->FUNC) (THIS, X1)

vtable안의 overflow함수를 찾아가게 됩니다. 즉 이 상황에서는 _IO_file_overflow가 호출이 되지만, _IO_file_overflow는 _IO_new_file_overflow의 약자이므로 실제로는 _IO_new_file_overflow가 호출이 되게 됩니다.

int
_IO_new_file_overflow (FILE *f, int ch)
{
  if (f->_flags & _IO_NO_WRITES) /* 에러를 셋팅한다. */
    {
      f->_flags |= _IO_ERR_SEEN;
      __set_errno (EBADF);
      return EOF;
    }
  /* 만약 현재 읽기 또는 버퍼가 할당되지 않았다면 */
  if ((f->_flags & _IO_CURRENTLY_PUTTING) == 0 || f->_IO_write_base == NULL)
    {
      /* 만약 필요하다면 버퍼를 할당하게 된다. */
      if (f->_IO_write_base == NULL)
        {
          _IO_doallocbuf (f);
          _IO_setg (f, f->_IO_buf_base, f->_IO_buf_base, f->_IO_buf_base);
        }
      /* 그렇지 않으면 현재 읽기가 되어야한다.
      	 만약 _IO_read_ptr(그리고 또한 _IO_read_end) 가 버퍼의 끝에 위치한다면, 
         논리적으로 버퍼를 앞으로 한 블록 옮긴다.(읽기 관련 포인터들을 블록의 시작을 가리키게 만듦으로서)
         이것은 연속된 출력을 위한 공간을 만든다.
         그렇지 않은면, 읽기 포인터들을 _IO_read_end로 설정한다.(그것을 혼자 둠으로서, 그것은
         외부 위치와 계속하여 대응할 수 있다.
      */
      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 ) /* 버퍼가 진짜로 다찼을 때이다. */
    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;
}
libc_hidden_ver (_IO_new_file_overflow, _IO_file_overflow)

#define _IO_do_flush(_f) \
  ((_f)->_mode <= 0                                                              \
   ? _IO_do_write(_f, (_f)->_IO_write_base,                                      \
                  (_f)->_IO_write_ptr-(_f)->_IO_write_base)                      \
   : _IO_wdo_write(_f, (_f)->_wide_data->_IO_write_base,                      \
                   ((_f)->_wide_data->_IO_write_ptr                              \
                    - (_f)->_wide_data->_IO_write_base)))

_IO_do_flush함수에 관한 자세한 내용은 아래 글에서 볼 수 있습니다.

2019/09/13 - [힙(heap)/house_of_orange] - house_of_orange 심화준비 - fclose 분석

_IO_new_file_overflow함수에서는 읽기와 쓰기 관련 포인터들을 설정해주고 _IO_do_write를 호출해주고 있습니다. 그런데 _IO_do_write은 _IO_new_do_write의 약어이므로 _IO_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;
}
libc_hidden_ver (_IO_new_do_write, _IO_do_write)

여기에서는 또 new_do_write를 호출하게 됩니다. 

static size_t
new_do_write (FILE *fp, const char *data, size_t to_do)
{
  size_t count;
  if (fp->_flags & _IO_IS_APPENDING)
    /* 적절한 O_APPEND 선언 없이 시스템에서, 당신은 sys_seek(0, SEEK_END)가
       여기에서 필요하다. 하지만 Unix 또는 Posix같은 시스템에서는 필요하지도 원하지도 않는다.
       대신에, 오프셋이 예상치 못하다는 것을 나타나게 한다.
    */
    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;
}

offset을 찾은 뒤 _IO_SYSWRITE가 호출이 되는데 이 함수는 vtable의 write부분을 호출하는 함수입니다. 즉 이 상황에서는 _IO_new_file_write가 호출이 되게 됩니다.

ssize_t
_IO_new_file_write (FILE *f, const void *data, ssize_t n)
{
  ssize_t to_do = n;
  while (to_do > 0)
    {
      ssize_t count = (__builtin_expect (f->_flags2
                                         & _IO_FLAGS2_NOTCANCEL, 0)
			   ? __write_nocancel (f->_fileno, data, to_do)
			   : __write (f->_fileno, data, to_do));
      if (count < 0)
	{
	  f->_flags |= _IO_ERR_SEEN;
	  break;
	}
      to_do -= count;
      data = (void *) ((char *) data + count);
    }
  n -= to_do;
  if (f->_offset >= 0)
    f->_offset += n;
  return n;
}

여기에서는 실제 크기만큼 파일에 출력을 해주고 있습니다. 그래서 지금까지의 흐름을 그림으로 나타내자면

이렇게 됩니다. 

이제 분석해볼 함수는 _IO_default_xsputn함수입니다. 이 함수는 _IO_new_file_xsputn에서 출력하고 남는 나머지를 출력해주는 용도로 쓰입니다.

_IO_size_t
_IO_default_xsputn (_IO_FILE *f, const void *data, _IO_size_t n)
{
  const char *s = (char *) data;
  _IO_size_t more = n;
  if (more <= 0)
    return 0;
  for (;;)
    {
      /* 이용가능한 공간 */
      if (f->_IO_write_ptr < f->_IO_write_end)
	{
	  _IO_size_t count = f->_IO_write_end - f->_IO_write_ptr;
	  if (count > more)
	    count = more;
	  if (count > 20)
	    {
#ifdef _LIBC
	      f->_IO_write_ptr = __mempcpy (f->_IO_write_ptr, s, count);
#else
	      memcpy (f->_IO_write_ptr, s, count);
	      f->_IO_write_ptr += count;
#endif
	      s += count;
	    }
	  else if (count)
	    {
	      char *p = f->_IO_write_ptr;
	      _IO_ssize_t i;
	      for (i = count; --i >= 0; )
		*p++ = *s++;
	      f->_IO_write_ptr = p;
	    }
	  more -= count;
	}
      if (more == 0 || _IO_OVERFLOW (f, (unsigned char) *s++) == EOF)
	break;
      more--;
    }
  return n - more;
}
libc_hidden_def (_IO_default_xsputn)

버퍼에 출력할 문자들을 복사해놓은 뒤 _IO_OVERFLOW를 호출하는 모습을 볼 수 있습니다. 그런데 이 함수까지 진입 하게 되면 출력할 크기가 블록 크기의 배수가 됩니다. 왜냐하면 

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

이 과정으로 나머지를 출력해버렸기 때문입니다. 그래서 _IO_default_xsputn에서 블록크기만큼 버퍼에 복사한 뒤 _IO_OVERFLOW를 통해 파일에 출력을 하게 되는 것입니다. 이렇게 fwrite에 쓰이는 함수 분석은 끝났습니다. 마지막으로 이 출력과정을 그림으로 나타내보겠습니다.

'FSOP' 카테고리의 다른 글

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