FSOP(File Stream Oriented Programming)

‍김상빈·2024년 1월 13일

FSOP에 대해서 이해되었다고 생각했으나 막상 설명하려니 걸리는 부분이 많아서 다시 공부하는 마음으로 이 글을 써보려고 한다.

File stream을 통해서 내 마음대로 다시 programming 하겠다는 그런 의미의 기법이다.

그렇다면 File Stream이 무엇인지 설명을 다시 하겠다.

File Stream

Linux에서 정의하기를 모든 데이터 즉 0101011 처럼 되어 있는 모든 정보들은 File이라고 했다. 그렇다면 이런 File들이 memory 또는 hdd에 있을 것이다. 그런 정보들을 stream이라는 가상의 통로를 만들어서 그 통로를 통해서 모니터에 출력해주는 것이다. 아니면 키보드에서 입력한 정보들이 메모리에 들어가서 처리과정을 거처 hdd에 저장할 때 키보드에서 메모리로가는 통로를 stream이라는 가상의 통로를 통해서 전달을 하는 것이다.

C언어로 어떻게 파일들을 열고 닫을까?

이제는 c언어에서 어떻게 파일을 열고 그 데이터를 가져와서 저장을 하고 출력시키는지 또는 파일을 열어서 사용자가 입력한 정보를 파일에 저장하는지 fopen, fread, fwrite, fclose에 대해서 이해해보는 시간을 가져보려고 한다.

fopen

아래는 fopen source code이다.

#include "libioP.h"
#include <fcntl.h>
#include <stdlib.h>
#include <stddef.h>
#include <shlib-compat.h>
FILE *
__fopen_maybe_mmap (FILE *fp)
{
#if _G_HAVE_MMAP
  if ((fp->_flags2 & _IO_FLAGS2_MMAP) && (fp->_flags & _IO_NO_WRITES))
    {
      /* Since this is read-only, we might be able to mmap the contents
	 directly.  We delay the decision until the first read attempt by
	 giving it a jump table containing functions that choose mmap or
	 vanilla file operations and reset the jump table accordingly.  */
      if (fp->_mode <= 0)
	_IO_JUMPS_FILE_plus (fp) = &_IO_file_jumps_maybe_mmap;
      else
	_IO_JUMPS_FILE_plus (fp) = &_IO_wfile_jumps_maybe_mmap;
      fp->_wide_data->_wide_vtable = &_IO_wfile_jumps_maybe_mmap;
    }
#endif
  return fp;
}
FILE *
__fopen_internal (const char *filename, const char *mode, int is32)
{
  struct locked_FILE
  {
    struct _IO_FILE_plus fp;
#ifdef _IO_MTSAFE_IO
    _IO_lock_t lock;
#endif
    struct _IO_wide_data wd;
  } *new_f = (struct locked_FILE *) malloc (sizeof (struct locked_FILE));
  if (new_f == NULL)
    return NULL;
#ifdef _IO_MTSAFE_IO
  new_f->fp.file._lock = &new_f->lock;
#endif
  _IO_no_init (&new_f->fp.file, 0, 0, &new_f->wd, &_IO_wfile_jumps);
  _IO_JUMPS (&new_f->fp) = &_IO_file_jumps;
  _IO_new_file_init_internal (&new_f->fp);
  if (_IO_file_fopen ((FILE *) new_f, filename, mode, is32) != NULL)
    return __fopen_maybe_mmap (&new_f->fp.file);
  _IO_un_link (&new_f->fp);
  free (new_f);
  return NULL;
}
FILE *
_IO_new_fopen (const char *filename, const char *mode)
{
  return __fopen_internal (filename, mode, 1);
}
strong_alias (_IO_new_fopen, __new_fopen)
versioned_symbol (libc, _IO_new_fopen, _IO_fopen, GLIBC_2_1);
versioned_symbol (libc, __new_fopen, fopen, GLIBC_2_1);
# if !defined O_LARGEFILE || O_LARGEFILE == 0
weak_alias (_IO_new_fopen, _IO_fopen64)
weak_alias (_IO_new_fopen, fopen64)
# endif

일단 우선 헤더 파일 중 libioP에 대해서는 조금 있다고 보고 함수들을 우선적으로 보려고 한다.

우선 상황을 보면 _IO_new_fopen 함수를 실행시키면 __fopen_internal 함수가 실행되는 것을 알수가 있다.

int is32 = 1로 이미 값이 정해져 있기에 어떤 의미를 가져갔는지 알수가 없다.

mode에는 r, w 등등 이 존재한다.

__fopen_internal 함수를 실행하면 locked_file 구조체를 선언하고 malloc으로 구조체를 생성한다. 그 구조체 chumk의 주소를 new_f 포인터 변수에 저장한다. 만약 new_f가 null이라면 null을 return시킨다. 만약 제대로 할당되서 주소가 new_f에 저장이 된다면 lock을 걸어버린다. 다른 process에서 사용하면 안되니까 그런 것이다. 그 다음 보이는 함수인 _IO_no_init() 함수가 어떤 함수인지에 대해서 알아봐야한다.

void
_IO_no_init (FILE *fp, int flags, int orientation,
	     struct _IO_wide_data *wd, const struct _IO_jump_t *jmp)
{
  _IO_old_init (fp, flags);
  fp->_mode = orientation;
  if (orientation >= 0)
    {
      fp->_wide_data = wd;
      fp->_wide_data->_IO_buf_base = NULL;
      fp->_wide_data->_IO_buf_end = NULL;
      fp->_wide_data->_IO_read_base = NULL;
      fp->_wide_data->_IO_read_ptr = NULL;
      fp->_wide_data->_IO_read_end = NULL;
      fp->_wide_data->_IO_write_base = NULL;
      fp->_wide_data->_IO_write_ptr = NULL;
      fp->_wide_data->_IO_write_end = NULL;
      fp->_wide_data->_IO_save_base = NULL;
      fp->_wide_data->_IO_backup_base = NULL;
      fp->_wide_data->_IO_save_end = NULL;
      fp->_wide_data->_wide_vtable = jmp;
    }
  else
    /* Cause predictable crash when a wide function is called on a byte
       stream.  */
    fp->_wide_data = (struct _IO_wide_data *) -1L;
  fp->_freeres_list = NULL;
}
_IO_no_init (&new_f->fp.file, 0, 0, &new_f->wd, &_IO_wfile_jumps);

라고 함수를 실행했기 때문에 fp mode는 0이고 orientation이 0이기 때문에 wide_data 값들을 null로 초기화 시킨다.
그러니 이 함수는 초기화시키는 함수로 볼수가 있다.

그 다음 함수인 _IO_jumps에 대해서 알아보겠다.

#define _IO_JUMPS(THIS) (THIS)->vtable

메크로 함수이다.

_IO_JUMPS (&new_f->fp) = &_IO_file_jumps;

보면 new_f->fp를 THIS에 넣고 vtable 주소 위치에 _IO_file_jumps 주소를 넣는 것을 알수가 있다. 그러니 이 메크로 함수는 vtable이 어디있는지 찾아서 저장시키려고 하는 것을 알수가 있다.

다음 함수인 _IO_new_file_init_internal 함수를 보려고 한다.

#define _IO_pos_BAD ((off64_t) -1)
#define CLOSED_FILEBUF_FLAGS \
  (_IO_IS_FILEBUF+_IO_NO_READS+_IO_NO_WRITES+_IO_TIED_PUT_GET)

void
_IO_new_file_init_internal (struct _IO_FILE_plus *fp)
{
  /* POSIX.1 allows another file handle to be used to change the position
     of our file descriptor.  Hence we actually don't know the actual
     position before we do the first fseek (and until a following fflush). */
  fp->file._offset = _IO_pos_BAD;
  fp->file._flags |= CLOSED_FILEBUF_FLAGS;
  _IO_link_in (fp);
  fp->file._fileno = -1;
}

void
_IO_link_in (struct _IO_FILE_plus *fp)
{
  if ((fp->file._flags & _IO_LINKED) == 0)
    {
      fp->file._flags |= _IO_LINKED;
#ifdef _IO_MTSAFE_IO
      _IO_cleanup_region_start_noarg (flush_cleanup);
      _IO_lock_lock (list_all_lock);
      run_fp = (FILE *) fp;
      _IO_flockfile ((FILE *) fp);
#endif
      fp->file._chain = (FILE *) _IO_list_all;
      _IO_list_all = fp;
#ifdef _IO_MTSAFE_IO
      _IO_funlockfile ((FILE *) fp);
      run_fp = NULL;
      _IO_lock_unlock (list_all_lock);
      _IO_cleanup_region_end (0);
#endif
    }
}

이 함수는 file의 offset을 구해서 저장하고 flag를 성정한 다음 fp를 인자로 _IO_link_in 함수를 실행시킨다. 그 함수에서는 우선 _flags를 확인하고 없다면 다 flush해서 clean 시킨다. 그 다음 chain 변수에 _IO_list_all 주소를 넣고 그 주소의 값에는 fp를 넣는다. 즉 chain을 시킨것으로 볼수가 있다.

그리고 거의 마지막인 코드인 _IO_file_open을 보려고 한다.

_IO_file_open (FILE *fp, const char *filename, int posix_mode, int prot,
	       int read_write, int is32not64)
{
  int fdesc;
  if (__glibc_unlikely (fp->_flags2 & _IO_FLAGS2_NOTCANCEL))
    fdesc = __open_nocancel (filename,
			     posix_mode | (is32not64 ? 0 : O_LARGEFILE), prot);
  else
    fdesc = __open (filename, posix_mode | (is32not64 ? 0 : O_LARGEFILE), prot);
  if (fdesc < 0)
    return NULL;
  fp->_fileno = fdesc;
  _IO_mask_flags (fp, read_write,_IO_NO_READS+_IO_NO_WRITES+_IO_IS_APPENDING);
  /* For append mode, send the file offset to the end of the file.  Don't
     update the offset cache though, since the file handle is not active.  */
  if ((read_write & (_IO_IS_APPENDING | _IO_NO_READS))
      == (_IO_IS_APPENDING | _IO_NO_READS))
    {
      off64_t new_pos = _IO_SYSSEEK (fp, 0, _IO_seek_end);
      if (new_pos == _IO_pos_BAD && errno != ESPIPE)
	{
	  __close_nocancel (fdesc);
	  return NULL;
	}
    }
  _IO_link_in ((struct _IO_FILE_plus *) fp);
  return fp;
}

드디어 system call을 이용해서 __open 함수를 이용해서 파일을 연다. 그 후 _fileno에 fdesc 값을 넣고 알맞는 flags 값으로 조정을 한다.

그러니까 간단히 fopen을 설명하자면 fopen을 실행을 시키면 구조체를 먼저 초기화를 시킨후 vtable offset을 구해서 구조체에서 vtable offset을 넣어주고 system call을 이용해서 _open으로 파일을 열어서 그 주소를 가져온 후 어떤 모드인지 확인해서 _flags 값을 맞춰서 updates를 한다. 이렇게 _IO_FILE 구조체의 값이 다 설정되는 것이다. vtable offset도 마찬가지고.
그래서 fsop 예제 문제를 풀 때 read(0, *fp, 0x100) 이렇게 되어있으면 그 위치에 저장되어있는 _IO_FILE과 vtable에 접근해서 값을 바꿀수가 있는 것이다.

이제는 여기서 이야기한 _IO_FILE과 vtable이 무엇인지에 대해서 알아보도록 하겠다.

_IO_FILE

struct _IO_FILE_plus
{
  FILE file;
  const struct _IO_jump_t *vtable; /* 파일 관련 작업을 수행하는 가상 함수 테이블입니다. */
};

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;	/* 파일 읽기 버퍼에 대한 포인터입니다. */
  char *_IO_read_end;	/* 파일 읽기 버퍼 주소의 끝을 가리키는 포인터입니다. */
  char *_IO_read_base;	/* 파일 읽기 버퍼 주소의 시작을 가리키는 포인터입니다. */
  char *_IO_write_base;	/* 파일 쓰기 버퍼 주소의 시작을 가리키는 포인터입니다. */
  char *_IO_write_ptr;	/* 쓰기 버퍼에 대한 포인터입니다. */
  char *_IO_write_end;	/* 파일 쓰기 버퍼 주소의 끝을 가리키는 포인터입니다. */
  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; /* 프로세스의 _IO_FILE 구조체는 _chain 필드를 통해 링크드 리스트를 만듭니다. 링크드 리스트의 헤더는 라이브러리의 전역 변수인 _IO_list_all 에 저장됩니다. */
  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
};

이렇게 FILE *fp를 하면 저 struct를 통해서 값을 설정할 수 있다 예를 들면 fp->flag = x 이런 느낌으로 할 수 있다. 그렇다면 구조체에 있는 변수들은 어떤 의미를 담고 있는지 알아보려고 한다.

int _flags

flags는 파일에서 어떤걸 허용하는지에 대해서 알려주는 변수이다.

/* Magic number and bits for the _flags field.  The magic number is
   mostly vestigial, but preserved for compatibility.  It occupies the
   high 16 bits of _flags; the low 16 bits are actual flag bits.  */

#define _IO_MAGIC         0xFBAD0000 /* Magic number */
#define _IO_MAGIC_MASK    0xFFFF0000
#define _IO_USER_BUF          0x0001 /* Don't deallocate buffer on close. */
#define _IO_UNBUFFERED        0x0002
#define _IO_NO_READS          0x0004 /* Reading not allowed.  */
#define _IO_NO_WRITES         0x0008 /* Writing not allowed.  */
#define _IO_EOF_SEEN          0x0010
#define _IO_ERR_SEEN          0x0020
#define _IO_DELETE_DONT_CLOSE 0x0040 /* Don't call close(_fileno) on close.  */
#define _IO_LINKED            0x0080 /* In the list of all open files.  */
#define _IO_IN_BACKUP         0x0100
#define _IO_LINE_BUF          0x0200
#define _IO_TIED_PUT_GET      0x0400 /* Put and get pointer move in unison.  */
#define _IO_CURRENTLY_PUTTING 0x0800
#define _IO_IS_APPENDING      0x1000
#define _IO_IS_FILEBUF        0x2000
                           /* 0x4000  No longer used, reserved for compat.  */
#define _IO_USER_LOCK         0x8000

위와 같은 options들이 존재를 한다. 이를 통해서 스트림의 동작을 제어하는데 사용하는 변수가 바로 flags인 것이다.

_IO_MAGIC - IO 파일인지 check하는 option

_IO_USER_BUF - 파일 스트림이 사용자가 제공한 버퍼를 사용하고 있을 때인지 확인하는 option

_IO_NO_READS - 읽기 작업이 허용되지 않는 option

_IO_NO_WRITES - 쓰기 작업이 허용되지 않는 option

_IO_EOF_SEEN - 파일의 끝에 도달했다는 것을 알리는 option

_IO_ERR_SEEN - 파일 I/O 작업 중에 오류가 발생했다는 것을 알리는 option

_IO_DELETE_DONT_CLOSE - 파일을 닫을 때 _fileno에 대한 close 함수를 호출하지 않도록 지정하는 option

_IO_LINKED - 현재 열려 있는 파일 목록에 포함되어 있다는 것을 알려주는 option

_IO_IN_BACKUP - 백업 작업 중에 있는지 알려주는 option

_IO_LINE_BUF - 개행문자를 check하여 작업하고 개행문자가 있으면 입력혹은 출력하라는 option

_IO_TIED_PUT_GET - put과 get 포인터가 같은지 확인하는 option

_IO_CURRENTLY_PUTTING - 현재 파일에 데이터를 쓰는 중임을 나타내는 option

위 option들을 보면 버퍼를 처리할 때 조건들이 많다. 그런 조건들을 stdio.h에서 setvbuf에서 어떻게 buffer를 처리 할 것인지에 대해서 설정을 한다. 버퍼링을 제어하는 함수라고 설명이 되어있는데 여기서 말하는 버퍼링은 버퍼에 데이터를 저장하고 처리하는 그런 과정을 이야기한다.

위와 같은 option들을 사용하여 flag의 조건들을 세팅하여 stream들을 제어하는 것이다.

이 다음으로는 vtable에 대해서 설명하려고 한다

vtable

vtable은 vitual table로 예를들면 class안에 있는 함수들처럼 vitual function의 주소를 table에 저장시킨 table을 vtable이라고 한다.

struct _IO_FILE_plus
{
  FILE file;
  const struct _IO_jump_t *vtable;
};

이 구조에서 struct _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);
};
#define JUMP_FIELD(TYPE, NAME) TYPE NAME

이 JUMP_FIELF는 메크로 함수로 변수를 선언하는 것이다. 그 변수에는 각각 함수의 주소가 저장되어있다. 이 구조를 이용해서 fread, fclose, fwrite에서 _IO_file_jump에서 이용해서 그 함수를 실행시키게 되는것이다.

이제는 fclose에 대해서 설명해 보려고 한다

fclose

아래는 fclose 함수 source code이다.

int
_IO_new_fclose (FILE *fp)
{
  int status;
  CHECK_FILE(fp, EOF);
#if SHLIB_COMPAT (libc, GLIBC_2_0, GLIBC_2_1)
  /* We desperately try to help programs which are using streams in a
     strange way and mix old and new functions.  Detect old streams
     here.  */
  if (_IO_vtable_offset (fp) != 0)
    return _IO_old_fclose (fp);
#endif
  /* First unlink the stream.  */
  if (fp->_flags & _IO_IS_FILEBUF)
    _IO_un_link ((struct _IO_FILE_plus *) fp);
  _IO_acquire_lock (fp);
  if (fp->_flags & _IO_IS_FILEBUF)
    status = _IO_file_close_it (fp);
  else
    status = fp->_flags & _IO_ERR_SEEN ? -1 : 0;
  _IO_release_lock (fp);
  _IO_FINISH (fp);
  if (fp->_mode > 0)
    {
      /* This stream has a wide orientation.  This means we have to free
	 the conversion functions.  */
      struct _IO_codecvt *cc = fp->_codecvt;
      __libc_lock_lock (__gconv_lock);
      __gconv_release_step (cc->__cd_in.step);
      __gconv_release_step (cc->__cd_out.step);
      __libc_lock_unlock (__gconv_lock);
    }
  else
    {
      if (_IO_have_backup (fp))
	_IO_free_backup_area (fp);
    }
  _IO_deallocate_file (fp);
  return status;
}

한줄 씩 이해해 보도록 하겠다.

상태를 알려주는 정수형 변수를 선언한다. 그 다음 check_file 함수를 실행한다.

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

메크로 함수로 이 FILE이 유효한지 check를 하고 그렇지 않다면 RET을 return하는 동작을 한다.

그리고 if문으로

if (fp->_flags & _IO_IS_FILEBUF)
    _IO_un_link ((struct _IO_FILE_plus *) fp);

flags 값이 _IO_IS_FILEBUF 즉 0x2000인지 확인을 한다. 참이면 _IO_un_link 함수를 실행한다.

void _IO_un_link (struct _IO_FILE_plus *fp)
{
  if (fp->file._flags & _IO_LINKED)
    {
      FILE **f;
#ifdef _IO_MTSAFE_IO
      _IO_cleanup_region_start_noarg (flush_cleanup);
      _IO_lock_lock (list_all_lock);
      run_fp = (FILE *) fp;
      _IO_flockfile ((FILE *) fp);
#endif
      if (_IO_list_all == NULL)
	;
      else if (fp == _IO_list_all)
	_IO_list_all = (struct _IO_FILE_plus *) _IO_list_all->file._chain;
      else
	for (f = &_IO_list_all->file._chain; *f; f = &(*f)->_chain)
	  if (*f == (FILE *) fp)
	    {
	      *f = fp->file._chain;
	      break;
	    }
      fp->file._flags &= ~_IO_LINKED;
#ifdef _IO_MTSAFE_IO
      _IO_funlockfile ((FILE *) fp);
      run_fp = NULL;
      _IO_lock_unlock (list_all_lock);
      _IO_cleanup_region_end (0);
#endif
    }
}

code를 보면 flags의 값이 0x80이면 조건문을 수행한다. fp가 _IO_list_all과 같다면 chain 값을 넣는다. 그리고 fopen으로 lock 걸었던 것을 해제를 하고 _IO_FILE 구조체를 cleanup한다.
이 조건문에 해당이 안되어도 lock은 해제하기 위해서 메크로 함수인 _IO_acquire_lock (fp);를 이용해서 lock을 해제한다.

그리고 보면 _IO_FINISH (fp);을 실행한다.

#define _IO_FINISH(FP) JUMP1 (__finish, FP, 0)

메크로 함수로 vtable에 있는 __finish에 저장되어 있는 값으로 jump를 한다.

그러니 __finish에 저장되어 있는 값을 바꾼다면 그 함수로 jump를 할 수 있다는 것이다.

profile
nickname: pwn_newbe

0개의 댓글