Media Log

여러분은 이게 어떤 기분인지 알 것입니다. 이는 우리 모두가 한번쯤 겪었던 일입니다. 여러분은 코드를 수십 번 읽어 보았으나 여전히 무엇이 문제인지 발견하지 못하고 있습니다. 하지만 그 코드에는 여러분이 도저히 처리할 수 없는 버그 혹은 오류가 존재하고 있습니다. 여러분은 코딩하고 있는 컴퓨터에 문제가 있거나 프로그램을 실행하는 운영체제, 혹은 사용하는 도구나 라이브러리에 문제가 있다고 생각해버립니다.


“분명히 그럴꺼야!”


하지만 아무리 좌절스럽더라도 그런 접근을 해서는 안됩니다. 우연에 맡기는 프로그래밍은 이제 그만하십시오. 그건 정말 나쁜 행동입니다. 어렵고 모호한 버그와 맞서는 것이 좌절감을 줄 수도 있지만, 그런 좌절감 때문에 잘못된 길로 들어서서는 안됩니다.


좋은 프로그래머가 되기 위한 필수 조건은 여러분이 작성한 코드에 문제가 있을 때마다 그것이 항상 자신의 잘못이라고 생각하는 일입니다. 이 내용은 실용주의 프로그래머에서 ‘select 함수는 잘못되지 않았다’ 부분에 잘 요약되어 있습니다:


대부분의 프로젝트에서 당신이 디버깅하고 있는 코드는 당신의 프로젝트 팀, 써드파티 결과물(데이터베이스, 네트워킹, 그래픽 라이브러리, 특화된 통신 방법들이나 알고리즘 등), 플랫폼 환경 상에서(운영체제, 시스템 라이브러리, 컴파일러) 당신과 다른 이들에 의해 작성된 응용 프로그램 코드의 결합물일 것이다.

운영체제나 컴파일러 혹은 써드파티 제품에 버그가 있을 수도 있다. 하지만 그것이 첫 번째로 의심하는 사항이 되어서는 안된다. 당신의 코드에 버그가 존재할 가능성이 훨씬 더 크다. 라이브러리 자체에 버그가 있다고 의심하는 것보다는 라이브러리를 호출하는 응용 프로그램의 코드가 문제가 있다고 생각하는 것이 보통 더 이득이다. 설사 써드 파티 코드에 문제가 있었다 할지라도, 어짜피 버그리포트를 제출하기 전에 자신의 코드를 제거하는 일을 해야만 한다.

우리는 한 프로젝트에서 솔라리스 운영체제에 있는 select 시스템 함수에 버그가 있다고 확신하는 한 고참 개발자와 일한 적이 있었다. 어떤 설득이나 논리도 그의 생각을 바꿀 수는 없었다. (같은 컴퓨터에서 다른 네트워크 프로그램들은 모두 잘 작동했지만 그건 그에게 별 상관이 없었다.)

그는 몇 주동안 문제를 해결하려고 이리저리 코드를 변경해보았지만, 이상하게도 그 문제는 해결이 되지 않는 것처럼 보였다. 그가 우리의 강요에 의해 억지로 자리에 앉아 select함수의 문서를 읽었을 때, 그제서야 그는 어떤 것이 문제였는지 깨달았고 그 문제를 몇 분만에 해결했다. 그 이후 우리는 누군가가 자신의 실수일 수 있는 문제를 시스템의 버그라고 탓하는 것을 볼 때마다 “select 함수의 버그” 라는 말을 사용하게 되었다.


코드 주인의식의 다른 말은 코드 책임의식입니다. 여러분의 소프트웨어의 문제가 무엇이던지 간에 -그 문제가 자신의 코드에 없었다 할지라도- 항상 자신의 코드안에 문제가 있다고 가정해야 하고 그에 따라 행동해야 합니다. 만약 여러분의 소프트웨어가 최고가 되길 원한다면 제품의 버그에 대해 전적으로 책임을 져야만 합니다. 사실 엄밀히 말하면 그럴 필요까지는 없습니다. 다만 이것이 여러분이 존경과 신뢰를 얻게 되는 방법입니다. 만약 그 문제에 대해 다른 사람들, 회사나 다른 소스로 그 잘못을 떠넘긴다면 여러분은 분명 존경과 신뢰를 얻지 못하게 될 것입니다.


여러분도 알다시피, 오류가 여러분의 잘못이 아닌 소프트웨어의 오류일 확률은 통계적으로 극히 드뭅니다. 스티브 맥코넬은 그의 책 코드 컴플리트에서 이 사실을 증명하는 두 가지 연구사례를 인용했습니다:


1973년과 1984년에 수행된 연구는 보고된 총 버그의 약 95%가 프로그래머에 의해서 발생한 것들이라는 것을 발견했다. 2%는 시스템 소프트웨어(컴파일러나 운영체제) 이고 2%는 그 외의 다른 소프트웨어들이었으며 1%는 하드웨어였다. 오늘날 시스템 소프트웨어와 개발도구들은 1970~80년대에 비해서 훨씬 많은 사람들에 의해 사용되어지고 있기 때문에 지금은 프로그래머의 오류 비율이 예전보다 더 높아졌을 것이라 추측한다.


여러분의 소프트웨어가 가지고 있는 문제가 무엇이든지간에 주인의식을 가지시기 바랍니다. 먼저 여러분의 코드에서부터 출발해서, 그 문제 원인에 대한 결정적 증거를 손에 넣을 때까지 계속해서 바깥으로 범위를 넓혀가며 살펴보십시오. 만약 그 문제가 자신이 컨트롤 할 수 없는 다른 코드에 존재한다면 문제 진단 및 디버깅 기술을 배워야 할 뿐만 아니라 자신의 주장을 뒷받침 해줄 수 있는 증거 또한 확보해 두어야 할 것입니다.


이 과정은 어깨를 한번 으쓱하며 운영체제, 개발도구, 프레임워크의 잘못으로 돌리는 것 보다는 훨씬 더 번거로운 일입니다. 그러나 이는 여러분에게 남 탓으로 돌리기나 문제 회피 등으로는 절대로 얻지 못하는 신뢰와 존경을 얻도록 할 것입니다.


만일 진심으로 좋은 프로그래머가 되고 싶다면 “그건 내 실수야. 내가 그 원인이 뭔지 찾아내고 말거야.” 라고 말하는 것을 꺼리지 마십시오.


이 글은 스택오버플로우를 개발한 제프 앳우드가 2008년 3월에 쓴 The First Rule of Programming: It’s Always Your Fault 를 원저자의 허락을 받고 번역한 글입니다.


--

2년여 전 즈음에, 카카오의 동료들하고 심심풀이로 기술 관련 좋은 글들을 구글 독스에서 소셜 번역(?) 을 하곤 했었는데, 그 중 하나였던 이 글이 최근 자꾸 생각나서 다시 읽어보다가 블로그에 올려야지 생각하게 되었다.(Jenny, Clare, Probe 감사!)

이 글은 코딩 호러의 이펙티브 프로그래밍이라는 번역서 내에도 담겨있으며, 이 책에는 다른 좋은 글들도 많으니 관심이 있는 사람들은 읽어보는 것도 좋겠다. 제프 앳우드의 다른 책으로는 코딩 호러가 들려주는 진짜 소프트웨어 개발 이야기라는 책도 있다. 둘 다 너무 재미있고 좋은 이야기들이 많이 담겨있는 책이다.

저작자 표시 비영리 동일 조건 변경 허락
신고
  1. 재호님팬 at 2014.04.01 22:07 신고 [edit/del]

    정말 좋은 글이네요. 공유해주셔서 감사합니다..

    Reply

submit
이전에 윈도우 드라이버를 공부하면서 정리해놨던 내용들이다.
여기에 있는 내용들은 전부 이 책에서 배운 내용들이다.
아마 지금까지 나온 윈도우 드라이버 책 중에 가장 좋은 책일 것이며, 앞으로도 이런 책은 안나올 것 같다.

만일 드라이버를 처음 접해서 아래 내용이 전혀 이해가 안되더라도 그냥 외워서 따라할 수 있도록 정리해놨다. 경험이 쌓여가면서 하나씩 이해가 될 것이다.

  • IRP를 완료시킬 때는 STATUS_PENDING 이라는 상태 코드를 써서는 안된다.
    물론 디스패치 루틴에서 STATUS_PENDING을 리턴할 수는 있지만, IoStatus.Status에 STATUS_PENDING을 할당한 뒤 I/O를 완료시키면 안된다는 뜻이다.
  • IoCompleteRequest를 호출 하기 전에는 IRP에 대해 지정한 취소 루틴을 제거해야 한다.
    Driver Verifier는 이런 경우를 적절히 찾아내서 버그체크를 띄워준다. 단, Irp가 큐잉된다면(StartPacket을 한다던지 Cancel-Safe Queue에 들어간다던지) 디큐될 때에 자동으로 취소 루틴이 제거된다.
  • Irp를 종료할 때, IoStatus.Status와 리턴 값은 일관성이 있어야 한다.
    즉 IoStatus.Status는 성공코드를 넣고 실패코드로 리턴하는 경우가 있으면 안된다.
    Driver Verifier가 이 오류 또한 적절히 발견해줄 것이다.
  • 디스패치 루틴에서 STATUS_PENDING을 리턴하기 전에는 Irp를 Pending으로 Marking해준다. 반대로 Irp를 Peding으로 마킹해 주지 않으면 STATUS_PENDING으로 리턴하면 안된다.
    둘은 항상 짝이 맞춰져서 다녀야 한다.
  • StartPacket을 호출하고 나면 더이상 IRP를 건드리지 않는다. 함수가 리턴되자마자 Irp는 이미 완료되었을 수 있다.
  • STATUS_MORE_PROCESSING_REQUIRED를 리턴하지 않는 모든 완료루틴에 대해서 다음의 코드를 넣는다.
    if(Irp->PendingReturned)
    {
        IoMarkIrpPending(Irp);
    }

    완료루틴을 설정하지 않았을 경우에는 시스템이 Irp를 완료시켜가면서 상위 스택으로 Pending 비트를 복사해주지만,
    완료 루틴이 설정되어 있으면 시스템이 이 작업을 해주지 않기 때문에 해당 완료 루틴에서 직접 PendingReturned 값을 체크하여 Irp를 Pending으로 마크해주어야 하기 때문이다. -어떤 경우라도 시스템은 PendingReturned 변수만큼은 항상 잘 셋팅해준다.
    만약 잘 이해되지 않는다면 그냥 외우기만 해도 좋다. 이는 언제나 참이다. 단, 완료 루틴에서만이다. 다른 루틴(디스패치 루틴이라던지)과 착각하면 안된다.
  • Lower Driver로 Irp를 건네주는 과정에서 완료루틴을 설정한다면 반드시 IoCopyCurrentIrpStackLocationToNext를 호출해야 한다.(IoSkipCurrentIrpStackLocation은 안된다.)
  • Next Driver에 Irp를 전달한 이후에는 더 이상 Irp는 자신의 소유가 아니며 건드리지 말아야 한다. 그 Irp는 다른 드라이버나 쓰레드에서 free되거나 완료되었을 수 있다. 하지만 만일 드라이버가 스택 아래로 Irp를 건네준 이후에 그 Irp에 접근하고 싶다면 반드시 완료 루틴을 설정해야만 한다.
    Io 매니저가 해당 완료루틴을 호출해 줄 때 완료루틴 내에서 잠시 다시 Irp는 내 드라이버의 소유가 되고, Irp에 액세스 할 수 있다.
    만약 Next Driver가 Irp를 완료 시킨 후에, 내 드라이버의 디스패치 루틴이 그 Irp에 접근해야만 한다면 완료루틴은 반드시 STATUS_MORE_PROCESSING_REQUIRED를 리턴해야만 한다. 이는 Irp의 소유를 디스패치 루틴에게로 건네준다. IO 매니저는 Irp의 처리를 중지하고 디스패치 루틴에게 궁극적으로 해당 Irp의 Completion을 맡긴다.
    디스패치 루틴은 이후에 IoCompleteRequest를 호출해서 Irp를 완료시킬 수도 있고 처리를 더 하기 위해 Pending으로 마킹할 수 있다.
  • 드라이버는 다음과 같은 경우 반드시 STATUS_PENDING을 리턴해야만 한다.
    • Irp가 완료되기 전에 디스패치 루틴이 리턴하는 경우
    • 해당 Irp가 다른 쓰레드에서 완료되는 경우
  • Arbitrary 쓰레드에서는 비동기적 Irp만을 생성할 수 있다. NonArbitrary 쓰레드에서는 동기 Irp도 생성할 수 있다.
  • IRQL이 Dispatch Level 이상인 경우에는 쓰레드를 대기 시켜서는 안된다. 즉, KeWaitFor- 같은 함수 들을 사용할 수 없다는 이야기이다.
    단, 쓰레드를 재우지 않고 단순히 오브젝트가 시그널 되었는지 Peek 해보는 것은 상관없다. 대기 함수들에 타임아웃값을 0으로 넘겨주면 Peek만 시도할 수 있는데, 유저모드에서는 WaitFor함수 패밀리에 dwMilliseconds를 0으로 건네주면 된다.
    커널 모드(KeWaitFor 함수 패밀리)에서는 유저모드처럼 그냥 0을 넣어버려서는 안된다. KeWaitFor 함수에 NULL 포인터를 전달하게 되면 Peek하겠다는 것이 아니라 무제한 기다리겠다는 의미이다. 반드시 LARGE_INTEGER 값을 0으로 셋팅해서 함수에 넘겨주어야만 한다.
    쓰레드를 재우면 안되는 이유는, IRQL이 Dispatch Level 이상이 되면 다시 재스케줄링 될 수 없기 때문이다.
    이는 비단 WaitFor 함수 패밀리뿐 아니라, 쓰레드를 대기 상태로 만드는 모든 함수를 사용 할 수 없다는 의미임을 알아야 한다.
  • IRQL이 디스패치 레벨 이상이 된 경우에는 특정 데이터를 참조하기 위해 페이지 폴트를 일으켜서는 안된다.
    페이지 폴트는 소프트웨어 예외 중 하나인데 사실 이는 디스패치 레벨 상태에서는 페이지 폴트 뿐만이 아니라 모든 경우의 Exception이 발생해서는 안된다는 의미이다. 이는 위에서 설명한 대기 함수를 사용할 수 없는 이유와 같다.
  • #pragma alloc_text(PAGE, Function Name)
    위와 같은 디렉티브를 선언했다면 그 함수의 시작부에 PAGED_CODE 매크로를 사용한다.
    이는 반드시 한 쌍으로 있어야 한다.(둘다 있거나 둘 다 없어야 한다.)
    Pagable에 대한 의미는 따로 설명하지 않는다.
    PAGED_CODE에 대한 더 자세한 내용은 여기에 써두었다.
  • 커널 스택은 x86에서 12K만큼 할당된다. amd64에서는 24K이다.
    단지 자신의 드라이버에서만 커널 스택을 사용할 것이라 착각해서는 안된다. 자신의 드라이버의 상위에 다른 필터 드라이버들이 있을 경우, 해당 필터드라이버가 사용하고 남은 커널 스택만큼을 받아서 사용하게 된다.
    따라서 평소엔 잘 돌아가던 드라이버가 백신같은 필터 드라이버가 설치된 환경에서는 블루스크린이 발생하기도 한다.
    이것은 모든 드라이버에서 최대한 스택을 아껴써야 함을 의미한다.
    유저모드에서 하듯이 다음 줄처럼 코딩 해서는 안되며
    WCHAR sz[MAX_PATH]; // 지역변수 하나가 커널 스택의 약 5%를 써버렸다.

    이는 좀 귀찮더라도 아래와 같은 방법으로 수정되어야 한다.
    PWCHAR sz = ExAllocatePoolWithTag(PagedPool, sizeof(WCHAR) * MAX_PATH, POOL_TAG);
    if(sz == NULL)
    {
        // Process the error.
    }
     
    __try
    {
        // Do something
    }
    __finally
    {
        if(sz)
        {
            ExFreePoolWithTag(sz, POOL_TAG);
        }
    }

  • 커널 스택은 pagedpool에 할당됨을 알고 있어야 한다. -함수가 대기 상태로 되면 시스템에 의해서 언제라도 Page out 될 수 있다.
    Event 객체는 NonPagedPool에 할당되어야 하는데, 스택에 할당했을 경우에는 KeWaitFor 함수의 WaitMode인자로 KernelMode를 전달함으로써 이 스택 메모리가 Page out 되지 않도록 할 수 있다.
  • DriverEntry가 아닌 곳에서 디바이스를 생성한다면, 초기화가 끝난 이후에 DO_DEVICE_INITIALIZING 플래그를 지워줘야 한다. -이는 AddDevice 루틴도 예외가 아니다.
    deviceObject->Flags &= ~DO_DEVICE_INITIALIZING;
    이것을 지워주지 않는다면, CreateFile같은 Api로 장치를 열 수 없다.
    만약 DriverEntry에서 만드는 경우에는 리턴되자마자 IoManager가 알아서 플래그를 지워주므로 생략해도 상관없다.
  • 캔슬 큐에 집어 넣고 pend 상태로 처리한 Irp는 나중에 완료하기 직전에 꼭 Cancel Queue에서 제거해주어야 한다.
    이 때 Cancel Queue에서 빼면서 리턴값을 꼭 검사해서 실제로 Queue에 존재했었고 제거된 경우에만 이 Irp를 완료시켜야 한다.

    // pIrpContext는 Insert할 때 넣어주었던 그 변수를 사용해야 한다.
    PIRP pPendedIrp = IoCsqRemoveIrp(&g_pDevExt->cancelIo.CancelSafeQueue, pIrpContext);
    if(pPendedIrp)
    {
        DriverLog(SHOW, "pIrp = 0x%08X\n", pIrp);
        pIrp->IoStatus.Status = status;
        pIrp->IoStatus.Information = information;
        IoCompleteRequest(pIrp, IO_NO_INCREMENT);
    }
    이렇게 해야 여러 요청이 동시에 들어왔을 때의 미묘한 동기화 문제를 피할 수 있다.(그렇지 않으면 같은 Irp를 2번 이상 완료시킬 수 있다)

저작자 표시 비영리 동일 조건 변경 허락
신고
  1. 아저씨 at 2011.05.25 00:14 신고 [edit/del]

    좋은 글 감사합니다.

    Reply
  2. 오곡 at 2013.07.09 18:09 신고 [edit/del]

    WDM 의 핵심들 잘배우고 갑니다~!

    Reply

submit
typedef BOOL int;
typedef int BOOL;
둘 중 어느 것이 맞는지 단박에 알아차릴 수 있겠는가?
나는 typedef만 쓰려고 하면 지금도 둘 중에 뭐가 맞는지 헷갈리고는 한다.
답은 아래것이 맞다.
그럼 앞에 있는 타입으로부터 뒤에 따라오는 새로운 타입을 만들겠다는 말인가?

아래 정의들을 보자.
typedef int BOOL, *PBOOL;

typedef struct tagFILEINFO
{
  int i;
} FILEINFO, *PFILEINFO;
이제 또 어디부터가 앞이고 뒤인지 햇갈린다.

typedef BOOL (*fn_t)(int, int*);
함수의 경우에는 조금 더 머리가 아프다.
빌어먹을, 대체 어디가 앞이고 어디부터 뒤란 말인가?

typedef을 정의할 때는 이를 헷갈리지 않기 위해서 딱 한 가지만 기억하면 된다.
변수를 적어야 할 위치에 새로운 타입을 적어라.

위에 나왔던 typedef 들을 하나씩 살펴 보겠다.
빨강색은 타입이요, 파랑색은 변수이다.

int형 변수를 선언할 때는 다음처럼 한다.
int
i;
아래처럼 한 줄에 포인터 변수와 같이 선언 할 수도 있다.
int j, *p;

이제 typedef를 다시 보면

typedef int BOOL;
typedef int BOOL, *PBOOL;


구조체를 정의함과 동시에 변수를 만들 수 있다는 것도 알고 있을 것이다.
struct FILEINFO
{
  int i;
} fileInfo; // 구조체를 선언함과 동시에 전역 공간에 fileInfo 라는 인스턴스를 생성하였다.

물론 아래처럼 여러 변수를 만들 수도 있다.
struct FILEINFO
{
  int i;
} fileInfo, *pFileInfo, ***pppFileInfo;

이제 typedef를 다시 보면

typedef struct tagFILEINFO
{
  int i;
} FILEINFO, *PFILEINFO, ***PPPFILEINFO;


아래 함수를 나타내는 타입은 무엇일까?
BOOL foo(int i, int* p);

타입은 다음과 같다.
BOOL (*)(int, int*)

타입이 있으므로 변수도 만들 수 있다.
그런데 함수의 경우에는 변수가 뒤쪽에 붙는 것이 아니라 가운데에 들어가는 것을 이해하는 것이 중요하다.
BOOL(*)(int, int*) 이라는 타입의 변수 pfn을 선언 하려면 다음과 같이 한다.

BOOL (*
pfn)(int, int*);


아래는 함수 포인터를 사용하는 예제이다.

void foo(int x)
{
  printf("%d", x);
}

int main()
{
  void (*pfn)(int) = foo; // void (*)(int) 타입의 변수 pfn을 정의하면서 동시에 foo를 대입한다.
  pfn(10); // 물론 호출도 가능하다.
}

이제 typedef를 다시 보면,

typedef BOOL (*fn_t)(int, int*); // fn_t라는 새로운 타입을 정의하였다.


함수에 호출 규약까지 넣는 경우에는 아래처럼 꼭 괄호 안에 호출규약을 넣어 주어야 한다.
typedef BOOL (__stdcall *fn_t)(int, int*);

멤버 함수의 경우에는 타입을 다음처럼 쓴다.
void (MyClass::*)(int, int*);

위에서 설명한 규칙을 잘 기억했다면 이제 typedef을 쉽게 만들어 낼 수 있다.

typedef void (MyClass::* memberfn_t)(int, int*);

징그럽지만 어쩌겠는가.


신고
  1. Favicon of http://duckii81.wordpress.com BlogIcon 장현덕 at 2011.01.05 00:01 신고 [edit/del]

    요즘 meta programming 공부 하면서 typedef 오질라게 쓰고 있는데, 아직도 헷갈린다.

    Reply
  2. Favicon of http://eslife.tistory.com BlogIcon esstory at 2011.01.05 08:06 신고 [edit/del]

    자주 헷갈리는 부분인데 깔끔한 정리 감사합니다. ~

    Reply
  3. 무실 at 2011.04.07 11:23 신고 [edit/del]

    간만에 다시 c++ 하려니 헷갈렸는데 좋은글 보고 갑니다.

    Reply
  4. 초보 at 2011.12.05 16:19 신고 [edit/del]

    저는 typedef 와 #define 이 반대라서 헷갈리더군요.
    #define BOOL int
    typedef int BOOL ;
    왜 이렇게 만들었는지 ...

    Reply
  5. 겨울악령 at 2013.07.18 03:17 신고 [edit/del]

    구조체 typedef랑 typedef int INT
    typedef int INT, *PINT에 함수 포인터 typedef 까지보고

    도대체 어디까지가 정의인가 몰라서 이리저리 찾아보다가 여기까지 왔는데

    "변수를 적어야 할 위치에 새로운 타입을 적어라."

    이거 보니 딱 이해가 되네요

    좋은 내용 감사합니다.

    Reply
  6. 냐앙 at 2016.11.15 15:34 신고 [edit/del]

    마지막 말씀처럼 처음 볼때는 징그러운데 찬찬히 보니 읽을 수 있겠네요 고맙습니다~

    Reply

submit

Programming.

2008.03.09 17:43 | 에세이

사용자 삽입 이미지
프로그래밍 입문 3년째.

처음엔 하기 싫던 것을 꾹 참고 했다.

지금은 공부할 때가 가장 행복하다.

There are 10 kinds of people in this world.
Those who know binary, and those who don't.

신고

'에세이' 카테고리의 다른 글

처음 마음가짐을 잊지마  (0) 2008.03.16
아주 조금씩  (0) 2008.03.16
인내  (0) 2008.03.16
Practice makes perfect.  (6) 2008.03.16
  (0) 2008.03.16
Programming.  (0) 2008.03.09

submit