Tuuna Computer Science

[C 언어] fgets함수 분석과, stdin 모험 본문

C language

[C 언어] fgets함수 분석과, stdin 모험

GuTTe 2019. 3. 31. 17:06

수정) read_ptr은 선언된 버퍼의 크기라기 보단, fgets인자로 준 size값에 따르는 듯?

 

lob풀다가 갑자기 Stdin에 관해서 한 번 분석해보고 싶어서 글을 쓰지만 별로 분석한건 없는 거 같다... 

초짜라 뭘 봐야할지도 잘 모르겠고 ㅋㅋㅋ 

 

stdio.c 이고 

#include "libioP.h"

#include "stdio.h"

#undef stdin

#undef stdout

#undef stderr

FILE *stdin = (FILE *) &_IO_2_1_stdin_;

FILE *stdout = (FILE *) &_IO_2_1_stdout_;

FILE *stderr = (FILE *) &_IO_2_1_stderr_;

stdin에 대한 구조체 정의 부분이다. 

struct _IO_FILE {
  int _flags;           /* High-order word is _IO_MAGIC; rest is flags. */
#define _IO_file_flags _flags


  /* The following pointers correspond to the C++ streambuf protocol. */
  /* Note:  Tk uses the _IO_read_ptr and _IO_read_end fields directly. */
  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;
#if 0
  int _blksize;
#else
  int _flags2;
#endif
  _IO_off_t _old_offset; /* This used to be _offset but it's too small.  */


#define __HAVE_COLUMN /* temporary */
  /* 1+column number of pbase(); 0 is unknown. */
  unsigned short _cur_column;
  signed char _vtable_offset;
  char _shortbuf[1];


  /*  char* _save_gptr;  char* _save_egptr; */


  _IO_lock_t *_lock;
#ifdef _IO_USE_OLD_IO_FILE
};

#include <stdio.h>


int main(void)
{
    char buf[80];
    fgets(buf, 79, stdin);
    return 0;
}

위에 대해서 gdb로 한번 분석해보자. 

 

fgets함수의 프로토타입을 보면 stdin구조체가 먼저 push되어야 함을 알 수 있다. 

 

char *fgets(char *s, int size, FILE *stream);

 

그럼 main+25에 bp를 걸어 edx값을 살펴보자. 

 

0xf7faa5c0이 나온다. 

 

stdin구조체의 시작주소인거 같다. 

 

위에서 정의한 구조체의 첫 번째 멤버는 flag인거 같음. 

 

그 다음은 

 

char* _IO_read_ptr;

char* _IO_read_end; 

char* _IO_read_base; 

 

이렇게 되어 있는데 각 각 입력버퍼의 현재읽고 잇는 주소, 문자열의 끝주소, 시작주소를 의미하는 거같다. 

 

그럼 끝주소 - 시작주소의 값이 내가 쓴 값이 있다는 건데 한번 까보자.

있다! fgets의 특성상 개행표시가 붙는 것도 똑같다. 

 

이걸 잘보면 

 

char* _IO_buf_base;   /* Start of reserve area. */

char* _IO_buf_end;    /* End of reserve area. */

 

이부분에서 서로 같을 빼면 값이 0x400 == 1024가 나오는데 버퍼의 크기임을 알 수 있다. 이때 내가 2048크기의 값을 입력하면 어떻게 될지 

조금 궁금해 졌다. 

 

입력 후, 

 

버퍼의 크기가 1024에서 4096으로 늘어남을 볼 수 있다. 


또 여기서 의문점 우리는 흔히 입력버퍼를 비울 때

getchar(); 를 하는 것을 볼 수 있다.  

 

이것이 입력버퍼를 비운다고 의미할 수 있을까!

 

이렇게 코드를 작성하고 gdb로 열어봤다. 

 

각 각의 stdin구조체의 주소, fgets호출 전, 후 주소, getchar주소에 bp를 걸었다. 

 

일단 stdin의 위치는 0xf7faa5c0으로 확인 됨

 

첫 번째 fgets함수 호출 후의 모습 

 

의아하게도 read_ptr과 read_end의 값이 다르다? 

 

같아야 하는데 read_ptr의 값이 read_end보다 2의 값보다 작다.

 

추측하면 선언된 버퍼의 크기가 10바이트, 입력한 a의 길이가 10개니까 

 

0x0804b150~ 0x0804b169 까지 a가 들어가고 선언된 버퍼를 넘어서면 read_ptr이 움직이지 않고 

 

end_ptr만 입력된 곳까지 가는거 같다. 

 

추측상 read_end의 주소에는 개행이 존재하기 때문에 read_ptr은 read_end주소까지 읽으려 한다. 
하지만 선언된 배열의 크기상, 배열의 크기만큼만 읽게 되고 read_ptr은 read_end까지 읽지 못하게 된다. 
그래서 다음 fgets때는 read_ptr이 read_end까지 자동으로 읽게하게 한다. 왜냐면 fgets는 개행까지 읽어야 하니까!
개행은 read_end에 존재하기 때문이다. 

 

예시 사진 

 

이 상태에서 while(getchar() != '\n');를 호출해보자. 

 

위의 코드는 입력버퍼에서 개행문자를 읽을 때 까지 읽는 코드이다. 

 

현재 read_ptr은 선언한 buf의 끝인 0x0804b169('a')를 가리키고 있다. 

 

코드르 실행 후 

 

read_ptr의 한 주소를 읽고 +1하였다. 

 

처음 getchar()을 했을 때는 0x0804b159에 있는 값을 읽은 거니 'a'가 반환되고 이는 '\n'과 다르다. 그럼 한 번더 getchar()를 호출한다. 

 

호출하고 난 뒤, read_ptr의 값과 read_end의 값이 같아졌다!

 

그리고 '\n'이 반환되어 while문을 pass하게 된다. 

 

그리고 다음 fgets를 호출했는데 여기서 신기한 점이 발견되었다. 

 

바로 전에 쓴 버퍼의 다음주소에 값을 쓰는 것이 아닌 그냥 버퍼의 시작주소에 값을 다시 쓰는 것이다! 

 

정리 :
1. fget를 계속 호출하더라도 처음 버퍼에 값을 덮어 씌운다. 
2. getchar()함수는 엄밀히 입력버퍼를 비우기보다 read_ptr의 값을 read_end의 값과 동일하게 만들어주는 역할을 한다.
(초기화가 아님)
3. fets함수는 개행까지 읽으며 선언된 배열의 크기때문에 다 읽지 못해도 다음 fgets때는 현재의 read_ptr에서부터 자동으로 읽게된다. 
즉, fgets가 개행을 읽었냐 안읽었냐의 확인점은 2가지가 있다. 
fgets함수 내에 현재 읽은 값이 개행인지 아닌지 판단하거나, 이때 판단하지 못했을 때 read_ptr의 값이 read_end의 값과 같으냐 아니냐의 검사로 
개행을 확인하는 것으로 추측이 가능하다. (물론 뇌피셜) 
그리고 처음 read_ptr의 값이 read_end의 값과 같다면 이는 개행까지 읽었다는 판단이 되므로 버퍼의 처음으로 되돌아와 값을 다시 쓰거나 읽게 된다. 
Comments