2014년 3월 1일 토요일

gets 함수는 사용하지 말아야 한다.

C 프로그래밍을 하는 고수 프로그래머의 대부분(아니 거의 전부) 는 다들 gets 함수를 사용하지 말라고 조언한다. 그 이유에 대해서 1988년 엄청난 피해를 입혔던 모리스 웜(Morris Worm) 에 대해 언급하고자 한다.

컴퓨터 해킹의 역사에서 엄청나게 굵고 큰 한 획을 그었던 Morris Worm 의 컴퓨터 파괴 원리는 바로 버퍼 오버플로우이다. 요즘의 언어인 JAVA나 Python 같은 언어는 메모리를 자체적으로 관리하기 때문에 버퍼 오버플로우에 대해서는 안전할 수 있겠지만, 프로그래머에게 엄청난 자유(?)를 선사하는 어셈블리어, C, C++ 등은 시스템에 매우 밀접하게 동작하므로 버퍼 오버플로우에 대한 고려를 프로그래머가 직접 해야 한다. Morris Worm은 바로 C 언어의 표준 함수 중 버퍼 오버플로우에 노출된 strcpy, strcat, gets, sscanf, sprintf 함수 등의 취약점을 노린 것이며 이 worm으로 인하여 전 세계 UNIX 머신의 10%가 공격을 당하여 천문학적인 피해를 입었다.

(여담.. 이 Worm을 만든 해커 모리스는 미국의 컴퓨터 범죄법에 따라 유죄를 받고 집행유예 3년, 사회 봉사 400시간에 10000달러가 넘는 벌금을 받았다.)

버퍼 오버플로우는 웹과 애플리케이션 환경에 맞는 새로운 언어들이 많이 나와 쓰이게 되고, 그리하여 C, C++ 프로그램의 구현 비율이 상대적으로 낮아짐에 따라 그 심각성은 점차 줄어들고 있으나, 아직도 시스템을 다루는 프로그램은 C, C++이 가장 많이 쓰이고 있으므로 위험성까지 줄어든 것은 아니다.

그렇다면, 버퍼 오버플로우가 무엇인지, 백문이 불여일견, 다음 코드를 보자.

#include <stdio.h>
#include <string.h>

int main( int argc, char *argv[] )
{
    char buff[200];
    strcpy( buff, argv[1] );
    printf("User Input : [%s]\n", buff);
    return 0;
}

사용자의 문자열 입력을 받아 buff 에 저장하고 출력하는 프로그램으로서 얼핏 보면 별 문제가 없어 보이지만, 만일 사용자가 200자가 넘는 문자를 입력했을 경우에는 이 프로그램은 정상 동작을 보장할 수 없을 뿐 아니라 치명적 문제가 발생할 수 있다. 200자 이후의 문자가 메모리 어느 영역에 저장될 지 알 수 없기 때문이다.

문제는 strcpy 이다. strcpy는 source 문자열에서 null 문자가 나올 때까지 멈추지 않고 계속 복사를 진행한다. 문자열을 받았다면 언젠가는 종료가 되겠지만 만일 source 문자열 포인터에 임의로 문자열이 아닌 다른 것을 넣어 null 문자가 없는 data를 가리킬 경우 따로 구현해보지 않아도 심각한 문제를 발생할 수 있을 것이다.

같은 이유로 gets 함수 또한 마찬가지이다.

#include <stdio.h>

int main()
{
    char buff[1000];
    printf("Input string : ");
    gets(buff);
    printf("[%s]\n", buff);
    return 0;
}

역시 앞선 코드와 마찬가지로 buffer overflow를 유발할 수 있다. (당연한 이야기지만 buff 를 아무리 충분히 크게 잡는다고 해서 이 문제가 해결되지는 않을 뿐더러 그 방법은 바람직하지도 않다.)

그렇다면 이러한 버퍼 오버플로우를 유발하는 함수는 모두 사용하지 말아야 하는가?

C 표준 함수를 살펴보면 이러한 함수를 대체할 수 있는 함수들이 몇몇 존재한다. 즉 strcpy 대신 strncpy를, strcat 대신 strncat 함수를 사용하여, 혹은 sscanf, sprintf 에서 포맷을 더욱 신중히 정형화하여 복사 혹은 붙여넣기의 횟수를 제한하는 방법이 있다. 그리고 strcpy, strcat, sprintf, sscanf 함수를 user input 에 그대로 사용하지 않고 제한된 경우에 조심스럽게 사용을 한다면 괜찮을 수 있겠다..

하지만 gets의 경우는 다르다. gets는 온전히 user input에 관련한 함수이므로 gets를 그대로 사용하면서 버퍼 오버플로우를 방지할 수 있는 방법은 없다. (일부 라이브러리에서 gets를 비표준으로 재정의하여 버퍼입력을 제한하도록 다시 설계해 놓았다면 모를까. 하지만 그러한 컴파일러는 극히 일부에 불과하다고 봐야 한다.)

따라서 gets 대신 입력 크기를 제한할 수 있는 fgets를 사용하라고 강력하게 권장하는 것이다.

다만 fgets는 gets와 달리 user input 끝에 오는 '\n'을 제거하지 않는다. 따라서 fgets로 문자열을 입력받은 다음 '\n'을 제거하는 구문을 추가하면 되며 그 방법은 아래와 같이 처리할 수 있다.

#include <stdio.h>
#include <string.h>

int main()
{
    char buff[1000] = {0};
    char *p;
    printf("Input string : ");
    fgets(buff, sizeof(buff)-1, stdin);
    if( (p=strchr(buff, '\n')) != NULL ) *p = '\0';
    printf("[%s]\n", buff);
    return 0;
}

C 언어는 사용자에게 상당히 많은 자유를 주는 언어지만 크만큼 사용자가 책임을 져야 하는 부분 또한 많다. 하지만 이 부분에서 신중을 기하여 여러 위험요소를 직접 예방해 가며 차근 차근 코드를 작성해 나간다면 다른 언어보다 훨씬 더 robust한 프로그램이 될 수 있다는 것은 C언어의 큰 매력이다.

### Wafting ....... Done !!!
### ...
### ;;

2014년 2월 27일 목요일

#pragma once 와 #pragma pack 에 관하여.

C 언어의 전처리 지시자로서 #pragma 라는 것이 있다. 이것은 다른 전처리 지시자와 달리 컴파일하고자 하는 컴파일러에 의존적인 내용들이 들어가 있다. 즉, #pragma 뒤에 오는 구문은 컴파일러마다 다르게 정의되어 있다.

ISO C (표준 C)에서는 C90(ISO/IEC 9899:1990) 까지는 #pragma 의 표준 구문이 존재하지 않았다. 즉, 순전히 기능을 컴파일러에 맡겼다. 그런데 C99(ISO/IEC 9899:1999) 에서 #pragma에 표준 구문이 추가 되었다. 그 표준 구문은 #pragma STDC .... 라고 입력하면 되는데. 이 글에서 이를 다룰 것이 아니므로 여기까지만 언급하고 넘어가겠다.

#pragma 구문의 가장 대표적인 예는 바로 이것이다.

#pragma once

결론적으로 말하면 일반적으로 header 파일(.h) 의 맨 위에 저 구문을 집어 넣으며, 중복 include 를 방지하는 데에 사용한다. 아래의 예를 보자.

[info.h]
#pragma once

typedef struct _information {
    int age;
    double height;
} INFO;

INFO info = { 80, 158.5 };


[isold.h]
#include "info.h"

int is_old( int age )
{
    if( info.age >= age ) return 1;
    return 0;
}


[istall.h]
#include "info.h"

int is_tall( double height )
{
    if( info.height >= height ) return 1;
    return 0;
}


[main.c]
#include "isold.h"
#include "istall.h"
#include <stdio.h>

int main()
{
    int old = is_old(35);
    int tall = is_tall(193.2);

    if(old) printf("old ");
    if(tall) printf("tall ");

    putchar('\n');
    return 0;
}


위의 main.c 를 build 한다고 하자. main.c 는 stdio.h 외에 두 개의 파일을 include 하고 있는데, 그 두 개의 파일은 모두 info.h 를 include 하고 있다. 즉 main.c의 입장에서 보면 info.h가 두 번 include 되므로 struct _information 의 타입 재정의와 struct 의 전역 변수가 중복정의된다. 이럴 경우 info.h 의 맨 위에 #pragma once 를 넣으면 한 번만 include 되게 해 준다는 것인데....

애석하게도 #pragma once 는 모든 표준 C compiler 에서 사용 가능한 것이 아니다. 특히 대부분의 UNIX 와 Linux 에서는 지원하지 않는다. 따라서 프로그램의 이식성을 위해 "아직까지는" #pragma once 대신 아래와 같이 전통적인 방법(#ifndef, #define, #endif)으로 info.h 를 작성하는 것이 좋다.

[info.h 수정]
#ifndef _INFO_H_
#define _INFO_H_

typedef struct _information {
    int age;
    double height;
} INFO;

INFO info = { 80, 158.5 };

#endif


마지막으로 위에서 본인이 "아직까지는" 이라는 곳에 포인트를 둔 이유를 이야기하고자 한다. 비록 #pragma once 가 C 표준은 아니지만 현재 여러 표준 C 컴파일러에서 #pragma once 의 기능 채택 비율이 낮지 않다는 것이다. 최근에 Linux GCC 컴파일러는 #pragma once 의 기능을 한 때 없애버렸다가 다시 채택하였다(버전 3.4).  이런 추세라면 #pragma once 도 결국엔 범용적으로 쓰이지 않을까 조심스럽게 예상은 하지만.... 아직은 전통적인 방법을 추천하고 싶다.

참고 : http://en.wikipedia.org/wiki/Pragma_once


다음............
또 하나의 많이 사용되는 구문으로 #pragma pack 이라는 것이 있다. 일단 이 것이 왜 나왔는지를 살짝 이야기하겠다.

32비트 OS를 예를 들어 설명하면, CPU는 메모리를 4바이트씩 끊어서 읽는다. 따라서 메모리에서 변수 access를 빠르게 하기 위하여 중간에 1, 2바이트 씩을 비우고 할당하는 경우가 생긴다. 다음을 보자.

struct A {
    char c;
    int i;
};

위의 경우 sizeof( struct A ) 는 5가 예상되지만 실제로는 8 (혹은 OS의 종류에 따라서 그 이상 또는 그 이하)이 된다 . 이런 경우는 위의 struct 구문을 그대로 네트워크에 실어보낸다든지 했을 때에 변수 디코딩에 약간의 문제가 발생할 수 있다.

메모리 access 에 관계 없이 sizeof( struct A ) 가 5가 나오도록 하려고 사용하는 구문이 #pragma pack 이다. 보통 구조체와 공용체(, 그리고 C++의 경우 클래스) 사이즈의 조정이 필요한 구문의 위와 아래에 사용을 한다. 하지만 이 또한 표준이 아니므로 컴파일러 마다 #pragma pack 사용법이 다르다.

Visual C 컴파일러는 다음과 같이 사용한다.

// 현재의 pack 사이즈를 저장하지 않고 사이즈를 2로 설정
#pragma pack(2)

// 현재의 pack 사이즈를 저장하고 pack 사이즈를 1로 설정
#pragma pack(push, 1)
 . . . . .
// 저장한 pack 사이즈로 변경
#pragma pack(pop)


Linux GCC 컴파일러는 다음과 같이 사용한다.

// 현재의 pack 사이즈를 1로 설정
#pragma pack(1)
 . . . . .
// 최초의 pack 사이즈로 변경
#pragma pack()

단 최근의 GCC 컴파일러는 (4.0 이상) Visual C 컴파일러의 스타일도 호환 가능하다고 언급이 되어 있다.

결론은....

C 프로그래밍을 할 때 예전처럼 OS dependent 한 코드만을 작성한다면 모르겠지만, 오늘날처럼 이식성이 중요한 코드를 작성하는 경우에는 C 표준, 그리고 각 컴파일러의 지원 여부 등을 고려하여 좀 더 범용성 있는 코드를 작성하려는 노력이 필요하다.

### Wafting ....... Done !!!
### ...
### ;;

2013년 11월 12일 화요일

[C++] C++의 난감한 상황...

C++의 private 접근제어 키워드에 대하여 이야기를 한 바 있다.
이번에는 어찌 보면 좀 황당한(?) 소스코드를 이야기하고자 한다.

참고로 지금 언급하고자 하는 개념은, 어떤 블로그에서는 당당하게 "C++ 클래스의 private 멤버 변수에 접근하고 값을 변경하는 방법!!" 이라고 소개되기도 하였다. (개인적으로 참 난감한 글이었다.)

아래 소스코드를 보자.

#include <iostream>

class TempClass
{
private:
    const int a;
    const int b;
public:
    TempClass() : a(100), b(200) {}
    TempClass(int i, int j) : a(i), b(j) {}
    int get_a() const { return a; }
    int get_b() const { return b; }
};

int main()
{
    TempClass c;

    int &aa = *reinterpret_cast<int*>(&c);      // int &aa = *(int*)(&c);
    int &bb = *(reinterpret_cast<int*>(&c)+1);  // int &bb = *((int*)(&c)+1);
    std::cout << "before c.a : " << c.get_a() << "   c.b : " << c.get_b() << std::endl;
    aa = 300;
    bb = 400;
    std::cout << "after  c.a : " << c.get_a() << "   c.b : " << c.get_b() << std::endl;

    return 0;
}

일반적인 class인 TempClass를 정의하였고, private 멤버변수 a, b는 const 이므로 생성자에 의해서만 값이 초기화된다.

그런데 main 함수에서 생성된 클래스 인스턴스의 주소를 받았고 이를 이용하여 reinterpret_cast 를 써서 "억지로" private 멤버에 접근을 하여 참조자를 부여하였다.
(주석처리된 C 스타일의 casting 또한 결국 동일한 의미이다.)

결론은... 역시 정상적으로 컴파일되며 값 또한 변경이 된다.

....................

C, C++ (뿐만 아니라 어느 언어로 프로그래밍을 하든 비슷한 이야기지만) 프로그래밍에서 사용되는 키워드의 의미를 망각하거나 잘못 이해하면 안되겠다.

C, C++ 에서 사용되는 키워드인 const, private 등은 어디까지나 프로그래머를 위한 키워드이지 프로그램을 사용하는 사용자를 위한 키워드가 아니다. 즉, 프로그래머가 위와 같은 키워드를 적절히 사용하여 프로그래밍을 하면, 나중에 그 소스코드를 이용할 때, 혹은 다른 사람이 그 소스코드를 활용할 때 이러한 키워드를 보고 해당 변수, 함수, 객체의 의미를 잘 정립할 수 있으며, 또한 혹시 모를 프로그램상의 오류를 미연에 방지할 수 있다는 데에 의미를 두어야 한다.

결국, 위와 같이 소스코드를 짜 보는 것은 프로그래머의 자유이고, 분명히 한번 궁금해서 실험적으로 해 볼 수 있는 시도임엔 틀림 없다. 무언가를 제한하는 키워드를 사용하는 의의를 역설적으로 음미해 보는 데에는 데에는 분명 유익한 코드이니까.

실전에서 저런 식으로 프로그래밍을 하는 것 만큼 어리석은 짓은 없을 것이다.

저것은 결코 대단한 기술도 아니며, 저걸 "private 변수에 접근할 수 있는 비법" 으로써 안다고 자랑할 만한 거리는 더더욱 아닌 것이다.

마치 뭐랄까, 박물관에 전시된 골동품에 "손대지 마시오!" 라고 적혀진 경고문구를 무시하고 자기 손때를 묻힌 후 만졌다고 자랑하는 거나 다를게 없어보인다는....

적어도 C/C++ 프로그래밍을 할때 const 와 private 등을 제대로 사용할 줄 아는 사람들에게는 저 코드는 정말 stupid한 소스코드이기에.

### Wafting ....... Done !!!
### ...
### ;;

[C++] 클래스의 private 접근지정자에 대한 고찰

이번엔 C++ 에 대한 글이다.
C++는 객체지향을 도입한 언어로서, 클래스를 정의할 수 있고 클래스에 대한 멤버 변수에 접근지정자를 부여할 수 있다. (private, protected, public)

private, protected, public의 의미는 알고 있다는 가정하에 이곳에서는 설명을 생략한다.

본론으로 바로 들어가서, 다음의 소스코드를 보면...

#include <iostream>

class Point
{
private:
    int x_;
    int y_;
public:
    Point() : x_(0), y_(0) {}
    Point(int x, int y) : x_(x), y_(y) {}
    Point(const Point &ref) : x_(ref.x_), y_(ref.y_) {}
    ~Point() {}

    void add_this(Point &ref)
    {
        x_ += ref.x_;
        y_ += ref.y_;
    }

    static Point add_new(Point &ref_a, Point &ref_b)
    {
        Point result;
        result.x_ = ref_a.x_ + ref_b.x_;
        result.y_ = ref_a.y_ + ref_b.y_;
        return result;
    }

    void show( const char *str )
    {
        std::cout << str << " : x[" << x_ << "] y[" << y_ << "]" << std::endl;
    }
};

int main()
{
    Point p1(100, 200);
    Point p2(300, 400);

    p1.show("p1");
    p2.show("p2");
    std::cout << std::endl;

    p1.add_this(p2);
    p1.show("p1");
    std::cout << std::endl;

    Point p3 = Point::add_new(p1, p2);
    p3.show("p3");
    std::cout << std::endl;

    return 0;
}

Point 클래스의 멤버 변수인 x_, y_ 는 private 변수이며, 자신의 멤버 함수들에 의해 접근하는 것은 당연히 허용한다. 그런데 문제는 멤버 함수 내부에서 다른 Point 클래스 객체를 생성하고 그 생성한 객체의 private 멤버 변수에 접근이 가능한지, 또는 멤버함수에서 다른 Point 클래스를 참조자로 받아 그 참조자로서 private 멤버 변수에 접근이 가능한지가 헷갈린다는 것이다.

위의 소스를 보면 add_new 함수는 자체적으로 Point 의 인스턴스를 생성한 후 그 인스턴스의 private 멤버변수에 바로 접근한다. 이 것이 컴파일이 가능하며 실행도 제대로 될까?

이상할 지도 모르지만, 위 소스는 아무런 문제 없이 컴파일 및 실행이 된다. 실행 결과는 다음과 같다.

p1 : x[100] y[200]
p2 : x[300] y[400]

p1 : x[400] y[600]

p3 : x[700] y[1000]

p3 클래스가 아무런 문제 없이 p1, p2의 각 멤버 변수의 합이 저장된 인스턴스로 생성이 되었다.

여기서 알 수 있는 사실은, 우리가 사용하는 접근제어자의 접근 제어 기준은 그때 그때 생성된 인스턴스의 기준이 아니라 클래스 기준이라는 것이다. 즉, 서로 다른 인스턴스이더라도 그 인스턴스가 동일한 클래스 타입이면 각 멤버는 서로의 private 멤버 변수에 접근이 가능하다는 이야기가 된다.

이것이 무언가 대단한 이야기 같지만, 가만히 생각해보면 당연한 이야기이다. C++에서 사용할 수 있는 friend 키워드도 그때그때 생성되는 인스턴스 기준이 아닌 틀 (클래스, 혹은 함수) 기준이다. 상속 또한 인스턴스가 아닌 클래스 기준이다.

다른 관점에서 접근하더라도 마찬가지이다. 만일 private 접근지정자가 클래스가 아닌 인스턴스 기준이라면 C++에서 기본적으로 제공해 주는(그래고 사용자 정의도 가능한) 복사생성자와 대부분의 연산자 오버로딩은 애초에 존재할 수가 없었을 것이다. ^^

뭔가 private이 private 같지 않아보일 지 모르지만, 이 사실은 우리가 private 에 대하여 좀 더 정확한 개념을 짚고 넘어가는 데에 도움이 될 것이다.

그렇다면 C++보다 객체지향성이 더욱 강한 JAVA는 어떨까?
결론부터 말하면... JAVA도 마찬가지이다. 아래 소스를 보자.

class A {
    private int val;
    public A() {
        this.val = 12345678;
    }
    public A(int i) {
        this.val = i;
    }
    
    public void test() {
        A a = new A();
        a.val = this.val;
        System.out.println("val a = " + a.val);
    }
    
    public void show() {
        System.out.println("val = " + this.val);
    }
}

public class Test {
    public static void main( String[] arags ) {
        System.out.println("Test !!!");
        A mainA = new A(87654321);
        mainA.show();
        mainA.test();
    }
    
}

class A 의 멤버 함수 test 안에서 다른 인스턴스 a를 선언한 후 a의 private 멤버 val에 직접 접근하여 값을 변경한다.
역시 정상 작동하며 결과는 다음과 같다.

Test !!!
val = 87654321
val a = 87654321

물론 JAVA에서 저런 형태의 소스코드를 작성할 경우가 드물 수도 있지만, private의 대상 범주를 좀 더 확실히 하는 데에 도움이 될 만한 소스코드일 것 같다.

결론은... 이 한마디로 갈음할 수 있을 듯 하다.

C++의 클래스는 자기 자신을 friend 클래스로 간주한다.

### Wafting ....... Done !!!
### ...
### ;;

2013년 10월 1일 화요일

함수의 인자로 배열이 올 때

C언어의 대부분의 기본 서적에는 함수에 대하여, 그리고 배열에 대하여 자세히 설명이 되어 있으나 함수의 인자(parameter)로 배열이 오는 경우에 대하여 설명이 되어 있는 곳이 거의 없어 이 곳에서 이야기하고자 한다.

다음 소스를 보자.

#include <stdio.h>

void array_swap( char a[5], char b[5] )
{
    char temp[5];
    int i;

    printf("Swap before : a[%s][%p] b[%s][%p]\n", a, a, b, b);

    for( i=0; i<5; i++ )
    {
        temp[i] = a[i];
        a[i] = b[i];
        b[i] = temp[i];
    }

    printf("Swap after  : a[%s][%p] b[%s][%p]\n", a, a, b, b);

}

int main()
{
    char one[5] = "abcd";
    char two[5] = "1234";

    printf("Main before : one[%s][%p] two[%s][%p]\n", one, one, two, two);
    array_swap(one, two);
    printf("Main after  : one[%s][%p] two[%s][%p]\n", one, one, two, two);

    return 0;
}

위의 소스를 컴파일하여 실행하면 main 함수의 one, two 의 내용이 서로 바뀌어 있는 것을 알 수 있다. 분명 array_swap 함수의 인자인 a, b 는 main의 one, two 의 주소를 받는 것이다. 위 소스코드의 실행 결과는 다음과 같다.

Main before : one[abcd][0xbf990e82] two[1234][0xbf990e87]
Swap before : a[abcd][0xbf990e82] b[1234][0xbf990e87]
Swap after  : a[1234][0xbf990e82] b[abcd][0xbf990e87]
Main after  : one[1234][0xbf990e82] two[abcd][0xbf990e87]

그런데 사실 array_swap 함수의 a, b 가 주소를 받는 것이므로 a, b 뒤에 있는 [5] 의 5는 의미가 없다. 다시 말해서 a, b 는 array_swap을 호출할 때 넣어주는 배열의 주소만을 받는 것이다.

실제로 배열을 선언하면 그 배열의 주소는 바뀌지 않는다. 즉 다음과 같은 경우는 compile error 가 된다.

char c[100];
char *pc;
pc = c;      // OK!
c = pc;      // ERROR!

하지만, 함수의 인자로 선언된 배열은 그렇지 않다. 다음의 경우는 error 가 아니다.

void func( char c[] )
{
    char *pc;
    c = pc;   // NO ERROR!
}

결론은.. C언어에서 함수의 인자로 선언된 배열은 포인터로 동작한다.

이를 확인할 수 있는 방법이 있다.

위의 소스코드의 array_swap 에서 a, b 의 sizeof 는 4(32비트 OS기준) 이다. 이는 어떠한 크기의 배열을 받아도 변함이 없다.

즉, 함수 선언시 인자가 배열인 경우에도 call by value를 생각하여 배열 내부의 data가 모두 copy 되어 들어오는 게 아니라는 것이 중요하다.


2013년 9월 12일 목요일

fflush 에 대한 고찰

C언어에서 fflush 함수는 stdio.h 에 선언되어 있는 C 표준 함수이다.
형식은 다음과 같다.

int fflush( FILE *stream );

stream 에 대한 버퍼의 데이터를 꺼내어 해당 스트림에 쏟아붓는(?) 함수이며 return 값은 작업에 성공하면 0, 버퍼에 아무것도 없는 상태라면 EOF, 실패하면 그 실패에 대한 errno 값이다.

이 fflush 함수의 매뉴얼을 잘 살펴보면, 표준 C 라이브러리에서는 출력 스트림에 대해서만 정의되어 있다. 즉, 우리가 자주 사용하는 출력 스트림인 stdout 의 버퍼에 있는 데이터를 꺼내어 출력한 후 버퍼를 비우는 데에도 사용할 수 있다. 이런 경우 통상적으로 "출력 스트림의 버퍼를 비운다." 라고 이야기하기도 하는데 이 뜻은 정확하게는 출력 스트림 버퍼에 남아 있는 데이터를 꺼내어 모두 출력해 버린다는 것이다.

출력 스트림의 버퍼가 비워지는 시점은 OS마다 다를 수 있다.  즉 어떤 OS에서는 출력 스트림 버퍼에 데이터가 들어오자 마자 꺼내어 출력하는가 하면, 또 다른 OS에서는 줄바꿈 캐릭터가 들어와야 출력을 하기도 한다.

C 언어에서 정의하는 대표적인 출력 스트림은 stdout, stderr 가 있다.

본인이 기억하기로는 Windows 에서는 stdout와 stderr가 동일하며 출력 스트림에 데이터가 들어오면 바로 출력하지만 (즉 버퍼를 즉시즉시 비우지만), Linux 는 stdout 과 stderr가 별도의 버퍼를 가지고 있으며, stderr는 즉시 비우는 반면, stdout는 개행을 의미하는 문자가 나타날 때까지, 혹은 표준 입출력 버퍼에 다른 이벤트가 벌어질 때까지 데이터를 출력하지 않고 버퍼에 그대로 쌓는다.

fflush의 사용 예는 다음 코드로 확인해본다.

#include <stdio.h>

int main()
{
    int i;

    printf("12345");
    for(i=0; i>=0; i++);
    printf("67890\n");

    return 0;
}

소스를 그대로 해석해 보면 우선 12345 를 출력하고 i에 대한 for 루프를 돈 후 67890 을 출력하는 것을 예상할 수 있다 (위의 for 루프는 무한루프가 아니다!!! 왜일까?). 그런데 일부 Linux에서는 for 루프를 모두 돈 다음 1234567890 을 한꺼번에 출력한다.

이럴 때, 소스의 의도 대로 12345 출력 후 루프를 돌고 67890을 출력하게끔 하는 것이 fflush 함수이다.

#include <stdio.h>

int main()
{
    int i;

    printf("12345");
    fflush(stdout);
    for(i=0; i>=0; i++);
    printf("67890\n");

    return 0;
}

이렇게 fflush 를 추가하면 된다. 12345가 출력 버퍼에 담긴 후 fflush 함수에 의해 12345가 버퍼에서 나와 출력된다.

한편...

fflush가 C 표준 라이브러리에 정의된 함수임은 분명하나, 함정이 하나 있다.
fflush에 인자로 들어가는 stream의 값은 출력 스트림에 한정된다는 것이다.
즉 stream 에 입력 스트림이 들어갈 경우 C 표준에 정의되어 있지 않기 때문에 컴파일러마다, OS 마다 동작하는 양상이 전혀 다르다.

입력 스트림을 비운다는 뜻은, 입력 스트림 버퍼에 남아 있는 데이터를 깔끔히 청소하고 없앤다는 뜻으로 해석할 수도 있으며, Windows 계열과 POSIX UNIX의 일부 계열에서 이렇게 컴파일되어 동작한다.

하지만 Linux 등 다른 OS에서는 위와 같이 동작하지 않는다. Linux에서는 fflush의 인자로 표준 입력 스트림이 들어오면 아무런 동작도 하지 않는다.

아래의 예를 보자.

#include <stdio.h>

int main()
{
    char c[100];
    int i;

    scanf("%d", &i);
    fflush(stdin);
    fgets(c, sizeof(c), stdin);

    printf("[%d][%s]\n", i, c);
    return 0;
}

이를 실행하고 기대하는 것은
처음에 1234 를 입력하고 Enter, 그 다음 ABCDEFG 를 입력하고 Enter 를 누르면 다음과 같은 결과를 도출할 것인데

[1234][ABCDEFG
]

이를 Linux에서 실행하면 fflush 가 아무런 동작을 하지 않으므로, 실행한 후 1234 만 입력해도

[1234][
]

이렇게 출력하고 끝나버린다. 즉 Linux에서는 fflush에 의하여 '\n' 이 버퍼에서 비워지지 않는다. fflush라는 뜻이 버퍼를 꺼내어 미루었던 작업을 모두 수행해버리는 것이므로, 그냥 버퍼를 비우는 작업은 약간 다른 작업일 수도 있다. 즉, 추후에 합당한 동작이 정의될 때까지 stdin에 대한 fflush 기능은 일단 정의하지 않는다는 의미로 해석해 본다.

Linux에서는 대신 __fpurge, 혹은 fpurge 라는 함수를 소개하고 있다. __fpurge는 Solaris 라는 OS에서 소개되었으며 리눅스의 glibc에서 채택하였다. fpurge는 BSD에서 소개되었다. (하지만 리눅스에서는 아직 지원하지 않는다.) 이 함수는 말 그대로 해당 버퍼를 비워 없앤다는 뜻이다. stdio.h (혹은 stdio_ext.h) 에 다음과 같이 선언되어 있다.

void __fpurge( FILE *stream );   // 현재 리눅스(glibc) 에서 지원함. fpurge 앞에 _ 가 두개임.
int fpurge( FILE *stream );   // 현재 리눅스에서 지원하지 않음.

리눅스에서는 위의 예에서

fflush(stdin);



__fpurge(stdin);

으로 바꾸면 기대했던 결과를 볼 수 있다.

하지만, fflush와 __fpurge는 분명 전혀 다른 동작을 하는 함수이다.
__fpurge 함수는 말 그대로 해당 스트림 버퍼를 말끔히 비우는 기능을 한다. 즉, fflush(stdout) 과 __fpurge(stdout) 은 완전히 다르게 동작한다. fflush(stdout) 이 표준 출력 버퍼에 있는 데이터를 꺼내어 출력을 하는 반면, __fpurge(stdout) 은 표준 출력 버퍼에 있는 데이터를 그냥 없애버린다는 사실을 주의해야 한다.

다시 stdin 으로 돌아와서... 그렇다면 Windows 든 Linux 든 같은 결과를 얻을 수 있게 하는 방법은 없을까? ( __fpurge 함수는 Windows 에서 사용할 수 없다. )

표준 입력 버퍼에 데이터가 채워지는 순간이 키보드의 '\n' (Enter) 가 입력되는 때임을 감안한다면 다음과 같은 방법으로 버퍼에 남아 있는 데이터를 없앨 수 있다.

while( getchar() != '\n' );

보기에 깔끔한 방법은 좀 아닌 것 같지만, OS 호환성을 감안해야 한다면 저 방법이 최선인 듯 하다.

----------------------

여담이지만 C 프로그래밍 교재로 매우 유명한 책인
윤성우 저 열혈(강의) C 프로그래밍의 초판에서는
fflush(stdin) 이 표준 라이브러리로서 표준 입력 스트림 버퍼를 비워 청소한다고 설명하였다가 뭇매를 맞았다. 2010년 발간된 개정판에서는 이에 대해 수정하여 기술되어있다 (개정판 430~432 페이지 및 508 페이지 참고). 그만큼 fflush에 대한 기능은 프로그램의 고수들도 실수하기 쉬운 부분이기도 하다.


2013년 9월 6일 금요일

프로그래밍에 철학을 입히다.

난데없이 C 프로그래밍 언어에 대한 기술적인 이야기가 아닌 프로그래밍의 철학이니 아트니 하는 이야기를 하고 싶은 이유를 굳이 말하자면, 대한민국에서 소프트웨어 엔지니어로서 신념을 갖고 살아가는 것이 얼마나 비참하고 힘든 것인지를 조금이라도 표현하고 싶어서... 정도로 해 두겠다.

현재 본인이 하고 있는 과제 중 하나의 파트를 인도인 외부 개발자에게 아웃소싱하였다. 우리가 필요한 스펙과 요구사항을 이야기해 주고 이에 따른 설계와 개발을 맡겼다. (인도사람의 영어발음은 정말 난해하기 그지없었지만, 어쨌든 가능한 모든 수단(?)을 동원해가며 커뮤니케이션을 하는 데에 성공했다.)

그러고 2개월쯤 뒤, D-Day가 다가왔다. 물론 중간중간에도 중간 점검식으로 미팅을 갖고 모여서 개발 사항에 대해서 공유를 한 바 있었지만, 나 또한 내가 맡아 하는 Task가 워낙 정신적인 Load가 많이 걸리는 일이라, 거의 모든 점검을 PM에게 맡기고, 나는 "뭐.. 알아서 잘 했겠지." 모드로 관조적으로 임했었다......................는게 후폭풍이 이리 크게 다가올 줄은 몰랐다. -_-;;;;;;

D-Day 가 되고 나서야 그 인도 사람이 짜 놓은 C++ 프로그램 소스를 조금이나마 자세히 볼 수 있는 여유가 생겼다. 가장 핵심적이라 할 수 있는 부분의 프로그램 소스를 몇줄 보기도 전에 .... 아뿔싸... 하는 불길한 느낌이 확 와닿았다. 이런!!!

그 프로그램 소스는 그냥 하나의 깡통로봇 같았다. 머리를 톡 하고 치면 땡 하고 울리는 그냥 깡통 로봇.

그는 자신이 짜 놓은 프로그램 소스에 대한 데모 또한 시연했다. 얼핏 보았을 때엔 완벽하게 잘 돌아가는 듯 하였다. 하지만 그 프로그램을 만드는 동안 어떠한 고민도 하지 않은 것 같았다. 즉, 자신이 짠 프로그램이 어디에 사용될 것이므로 그곳에서 사용하려면 어떻게 돌아갈 것이니 이러이러한 부분에 대해서는 조금 더 신경을 써야 하고... 뭐 이런 식의 사고는 전혀 하지 않았으며, 그냥 그저 주어진 요구사항을 듣고 기계처럼, 컨베이어 벨트에서 부품을 조립하는 사람처럼 뚝딱뚝딱 만들고 끝내버린 것이다.

이제 본인은 C 프로그래밍에서의 하나의 매우 사소한 예를 들려고 한다. 아래에 예를 들어 놓은 함수의 기능은 간단하다. 주어진 문자열을 받아 그 문자열의 길이를 반환하는 함수이다. (C 표준 라이브러리에도 존재하는 strlen 함수, 그것이다.)

물론 C 표준 라이브러리 내에서 strlen은 충분히 optimized되었고 robust한 함수일 것이다. 하지만 어떤 환경에서 표준 라이브러리를 사용할 수가 없어 수고스럽게 strlen과 같은 기능을 하는 함수를 직접 짜야 한다고 설정을 가정해도 좋다. 어쨌든 예를 보자.

[예 1]

int my_strlen_1( char *s )
{
    int i=0;
    while( *s )
    {
        i++;
        s++;
    }
    return i;
}

[예 2]

size_t my_strlen_1( const char *s )
{
    size_t i=0;
    while( *s )
    {
        i++;
        s++;
    }
    return i;
}

예 1, 예 2를 이용한 결과는 일반적인 경우 정확히 동일하다. 하지만 프로그래머가 보기엔 예 1 은 예 2 보다 왠지 세련되어 보이지가 않다.

뭐.. 결과만을 따진다면 예 1 이나 예 2 나 같은 점수를 받았을 것이다. 아니, 예 2 는 예 1 보다 조금이라도 고민을 더 했을 것이므로, 생산성(?)을 따지면 오히려 예 1 보다 예 2가 더 낮은 점수를 받았을 지도 모른다.

여러분은 이에 동의하는가?

이런 단순한 것을 프로그래밍의 철학에 비유하는 것이 매우 거창할 수도 있겠지만, 이러한 사고가 모이고 모여 거대한 프로그램 결정체가 형성됨을 생각한다면, 뭐.. 철학이라 해도 과언은 아닐 것이다.

하지만... 더욱 중요한 것은... 저 예가 고상하게 철학이나 운운하는 정도의 것이 아니라는 것이다. 즉.. 저 프로그램 코드가 일회성으로 그냥 짜고 사용한 후 버리는 것이라면 모르겠지만, 계속적으로 업데이트를 하고 재사용하는 코드라면 두 예의 차이는 엄청날 수도 있다.

우선 예 1은 인자로 받는 s 의 문자열 내용을 바꿀 수도 있다. 즉, 시간이 지나서 업데이트를 하는 도중에 s 의 문자열 자체를 변경하는 코드를 실수로 은연중에 넣을 경우 엄청난 재앙(?)이 발생할 수도 있다. 또한, 추가적인 코드에 의해 문자열의 길이를 음수로 반환하는 오류도 배제할 수 없다.

예 2는 그러한 우려를 원천적으로 차단한다. 프로그래머가 실수를 범했다 할 지라도 컴파일에서 error 를 알려준다. 이는 규모가 큰 프로그램일 경우 훨씬 더 막강한(?) 힘을 발휘한다.

결국 프로그래밍의 철학은 장기적으로 보았을 때 프로그램의 완성도에 직접적이고 중요한 영향을 미친다.

(하지만 이러한 factor들이 대부분의 경우 제대로 된 평가를 받지 못하고 있다는 것이 불편한 진실이라고나 할까.)

예 2에서 붙은 const 와 size_t 라는 키워드는, 곧바로 정량적으로 보이는 획기적인 향상은 측정할 수 없으나, 중요하고 필요한 것임에는 분명하니.... 이런건 아트라고 해도 되지 않을지...