제가 이 글에 답변을 쓴 이유는 박지훈님이 쓴 글을 오래 전에 저도 읽어 보았고, SysUtils.pas의 Initialization 섹션에서 Exeception Handler가 설치 되기 전이기 때문이라는 이유 때문입니다. 델파이에서 기본적인 예외처리 핸들러의 셋업은 SysUtils 이전에 System.pas에서 명백히 초기화 되지요. 그리고 Runtime Error 217 은 시스템 주요파일이 손상 되어 있거나, 시스템 레지스트리 등이 깨져있을 때, 이유는 버그가 있는 프로그램의 설치로 인해서, 또는 서비스팩 업데이트 중 얘기치 않은 설치 오류로 인해 시스템 주요 파일이나 레지스트리등이 깨져 있을 때, 이를 운영체제에서 에러 상황으로 보고 런타임 에러를 발생하게 됩니다.
Runtime Error 217은 부팅과정에서 나타날 수도 있고, 탐색기나 웹브라우저 등 프로그램 실행할 때, 델파이와 상관없이 나타날 수 있는 운영체제 에러 상황입니다. SysUtils에서 파일처리 래퍼함수들을 구현하고 있으니까 오히려 델파이에서 편의대로 운영체제 에러인 Runtime Error 217을 오버라이딩 해서 처리하고 있는 거지요. ^^
박지훈.임프 님이 쓰신 글 :
: 빌더님의 주장과 제 주장은 논지의 차이가 많지만, 카라얀님의 논지는 제 논지의 일부를 조금 다른 방식으로 쓰셨을 뿐인데, 같은 얘기를 가지고 제 생각이 틀렸다고 하시는군요. 게다가 그 부분은 핵심도 아닌데요.
:
: 아래는 카라얀님이 쓰신 내용입니다.
: "빌더님이 말씀하신 대로 SysUtils.pas와 상관없이 이미 System.pas에서 예외처리를 위한 셋업이 완료되고, 그 이후에 추가로 설치되는 예외핸들러는 System.pas에서 핸들링되는 예외처리에 대한 세컨드, 서드 식의 Chained 예외 핸들러에 불과할 뿐이죠."
:
: 제가 쓴 내용을 제대로 읽어보신 거 맞는지. System에 기본 예외처리 루틴이 있고, 일반적인 상황에서는 거기서 SysUtils의 예외처리루틴이 호출된다고 썼지요. 그게 카라얀님이 쓰신 내용이랑 뭐가 다른 거죠? 체이닝된 세컨드 서드 따위는 예외처리 루틴이라고 불러줄 수 없다, 이런 얘기신지요?
:
: 제가 쓴 글의 핵심은, 근본적으로는 SysUtils의 ExceptProc이 nil인 상황에서 Runtime error 217이 발생한다는 겁니다. 그리고 ExceptProc이 nil인 상황이 가장 흔한 경우가 유닛의 initialization 섹션에서 예외가 일어날 때라는 얘깁니다. 이게 핵심인데 정작 이 부분은 언급도 하지 않으신 채로 제 주장이 틀렸다고 하니...
:
: 또 빌더님이나 카라얀님은 System 유닛의 레벨에서 예외 처리가 다 들어있다고 말씀하시는데, System 유닛만으로는 VCL 예외가 넘어오기만 하면 무조건 Runtime error 217 메시지만 뜨고 종료하게 되어 있습니다. 이걸 개발자나 사용자가 알아들을 수 있는 친절한 예외 메시지로 처리하는 기능은 SysUtils의 Exception 클래스와 그 자손들에게 있죠.
:
: 예를 들어보면요. 윈도우7에서 UAC가 걸려있고 권한상승을 안한 상태에서, 프로젝트의 아무 유닛에서나 다음과 같은 코드를 포함해서 컴파일 및 실행시키면 항상 Runtime error 217이 발생합니다.
:
: initialization
: TFileStream.Create('c:\111.txt', fmCreate);
:
: 반면, 똑같은 TFileStream.Create('c:\111.txt', fmCreate) 코드를 initialization 섹션이 아닌 초기화가 끝난 후의 일반적인 시점, 예를 들면 버튼 클릭 같은 데에 위치시키고 실행해보면 '액세스 거부'라고 친절한 메시지가 나오죠. 이 '친절한 메시지'가 Exception 클래스의 자손인 EFCreateError 클래스에서 나옵니다. 즉, 동일한 코드인데도 initialization 섹션에 있느냐 아니냐에 따라 예외가 처리되는 방법이 완전히 달라지게 되는 거죠.
:
: 그런데 빌더님은 제게 이렇게 반박하셨지요.
: "Runtime error 217"의 의미는 Exception과 관련이 있는 게 아니고... System Registry 와 같은 시스템 리소스가 깨져 있을 때...
: Critical Error 상황으로 보고 RTL에서 Runtime error 메세지를 출력하는 것일 뿐입니다... UAC와 관련해서 언급되고 있는 내용도... 문제의 본질과는 무관한 겁니다."
:
: 제가 보기엔 Runtime error 217은 분명히 Exception과 관련이 있습니다. SysUtils의 Exception 클래스를 타지 못해서 발생하거든요. UAC가 본질과 관계가 없다는 얘기는 제가 본문에서도 썼고, initialization 섹션과 관련이 있다고 썼지요. 오히려 레지스트리가 깨졌다든지 하는 특이한 상황이야말로 더 본질적이지 않은 것 같습니다.
:
:
:
:
: 제가 뭘 모르고 주장한다고 짐작하신 것 같으시니, 카라얀님이 새로 언급한 _InitExe 단계에서 시작해서, Runtime error 217 에러가 발생하는 전체 상황을 다 설명해보죠.
:
:
: 말씀하신대로 exe 파일의 가장 초기화 과정은 SysInit 유닛의 _InitExe에서 시작되어 System 유닛의 _StartExe을 호출하죠. 이 _InitExe에서 다시 System 유닛의 SetExceptionHandler가 호출됩니다. 이 SetExceptionHandler가 System 유닛 레벨의 최하위 예외처리 핸들러를 설정하는 루틴이죠. 여기서 실제로 설정되는 최하위 예외처리 핸들러가 _ExceptionHandler 루틴입니다.
:
: procedure SetExceptionHandler;
: asm
: XOR EDX,EDX { using [EDX] saves some space over [0] }
: LEA EAX,[EBP-12]
: MOV ECX,FS:[EDX] { ECX := head of chain }
: MOV FS:[EDX],EAX { head of chain := @exRegRec }
:
: MOV [EAX].TExcFrame.next,ECX
: {$IFDEF PIC}
: LEA EDX, [EBX]._ExceptionHandler
: MOV [EAX].TExcFrame.desc, EDX
: {$ELSE}
: MOV [EAX].TExcFrame.desc,offset _ExceptionHandler
: {$ENDIF}
: MOV [EAX].TExcFrame.hEBP,EBP
: {$IFDEF PIC}
: MOV [EBX].InitContext.ExcFrame,EAX
: {$ELSE}
: MOV InitContext.ExcFrame,EAX
: {$ENDIF}
: end;
:
: 따라서 exe 파일의 초기화가 성공해서 이 루틴이 실행된 후에는 예외가 발생할 때마다 가장 먼저 System의 _ExceptionHandler 루틴이 실행되게 되죠. 그럼 이제 이 _ExceptionHandler의 내용을 봅시다.
:
: procedure _ExceptionHandler;
: {$IF not defined(CPU386)}
: begin
: end;
: {$ELSE CPU386}
: asm
: MOV EAX,[ESP+4]
:
: TEST [EAX].TExceptionRecord.ExceptionFlags,cUnwindInProgress
: JNE @@exit
: {$IFDEF MSWINDOWS}
: CMP BYTE PTR DebugHook,0
: JA @@ExecuteHandler
: LEA EAX,[ESP+4]
: PUSH EAX
: CALL UnhandledExceptionFilter
: CMP EAX,EXCEPTION_CONTINUE_SEARCH
: //JNE @@ExecuteHandler
: //JMP @@exit
: JE @@exit
: {$ENDIF MSWINDOWS}
:
: @@ExecuteHandler:
: MOV EAX,[ESP+4]
: CLD
: CALL _FpuInit
: MOV EDX,[ESP+8]
:
: PUSH 0
: PUSH EAX
: PUSH offset @@returnAddress
: PUSH EDX
: CALL RtlUnwindProc
:
: @@returnAddress:
: MOV EBX,[ESP+4]
: CMP [EBX].TExceptionRecord.ExceptionCode,cDelphiException
: MOV EDX,[EBX].TExceptionRecord.ExceptAddr
: MOV EAX,[EBX].TExceptionRecord.ExceptObject
: JE @@DelphiException2
:
: MOV EDX,ExceptObjProc
: TEST EDX,EDX
: JE MapToRunError
: MOV EAX,EBX
: CALL EDX
: TEST EAX,EAX
: JE MapToRunError
: MOV EDX,[EBX].TExceptionRecord.ExceptionAddress
:
: @@DelphiException2:
:
: CALL NotifyUnhandled
: MOV ECX,ExceptProc
: TEST ECX,ECX
: JE @@noExceptProc
: CALL ECX { call ExceptProc(ExceptObject, ExceptAddr) }
:
: @@noExceptProc:
: MOV ECX,[ESP+4]
: MOV EAX,217
: MOV EDX,[ECX].TExceptionRecord.ExceptAddr
: MOV [ESP],EDX
: JMP _RunError
:
: @@exit:
: XOR EAX,EAX
: end;
:
: 위 코드에서 DelphiException2 레이블이 붙은 곳 아래를 보시면, ExceptProc에 값이 있는지 검사한 후, 값이 있으면 ExceptProc을 실행하고 없을 경우 noExceptProc으로 점프하게 되어 있죠.
:
: 여기서 ExceptProc은 System 유닛의 전역변수 함수포인터로서, System 유닛 레벨에서는 nil입니다. 따라서 nil인 상태 그대로 예외가 발생할 경우 noExceptProc 레이블의 코드가 실행되겠죠? 여기서는 EAX에 ExitCode 217을 넣고 _RunError를 실행합니다.
:
: _RunError를 따라서 더 추적해보시면, _Halt0에서 MakeErrorMessage를 호출하는데요. 이 MakeErrorMessage 루틴에서 "Runtime error ??? at ..." 라는 문자열이 들어있는 runErrMsg에다 넘어온 ExitCode를 조합해서 "Runtime error 217 at ..." 이라는 에러 메시지를 완성하고, WriteErrorMessage 루틴에서 MessageBoxA를 이용해서 메시지를 화면에 뿌립니다. 그런 후 finalize 작업을 하게 되죠.
:
: 그래서, System 유닛까지만 초기화된 상태에서는, 예외가 넘어오면 무조건 Runtime error 217을 뿌리게 되어있습니다.
:
: 그런데 아시다시피 VCL 애플리케이션에서는 SysUtils 유닛도 포함됩니다. 이 SysUtils 유닛에는 최상위 예외 클래스인 Exception을 비롯해서 기본적인 예외 클래스들 대부분이 선언되어 있죠. Exception 클래스의 생성자 Exception.Create을 보면 다음과 같이 되어있습니다.
: class constructor Exception.Create;
: begin
: InitExceptions;
: end;
:
: 다시 InitExceptions 루틴은 다음과 같죠.
: procedure InitExceptions;
: begin
: OutOfMemory := EOutOfMemory.CreateRes(@SOutOfMemory);
: InvalidPointer := EInvalidPointer.CreateRes(@SInvalidPointer);
: ErrorProc := ErrorHandler;
: ExceptProc := @ExceptHandler;
: ...
:
: 즉, System 유닛의 ExceptProc 함수 포인터에 SysUtils의 ExceptHandler 루틴을 지정합니다.
:
: 요약하자면, SysUtils가 로딩된 후 Exception 클래스의 자손인 예외 클래스가 생성될 때마다 예외 처리 루틴을 SysUtils의 ExceptHandler 루틴으로 지정합니다. 그래서 SysUtils가 로딩된 후에 Exception 예외가 발생하면 Runtime error 217이 아닌, ExceptHandler 핸들러 루틴이 실행되는 거죠. 이 ExceptHandler 루틴을 보면 Exception 객체를 받아 예외의 클래스에서 정해진 메시지를 뿌리는 등의 동작을 하도록 되어 있습니다.
:
: 참고로, ExceptProc 함수 포인터에 예외 핸들러가 지정되는 과정은 VCL의 버전에 따라 다릅니다. 델파이/C++빌더 2007 이전의 버전들에서는 SysUtils 유닛의 Initialization 섹션에서 지정되고, 그 이후의 버전들에서는 Exception 클래스의 생성자에서 지정됩니다.
:
: 즉, 일반적인 VCL 애플리케이션에서 보는 친절한 예외 메시지(원인이 포함된)는 SysUtils의 ExceptHandler 루틴을 통해 Exception 클래스(더 정확하게는 그 자손 클래스들)에서 나오는 것입니다. 그런데 일반적인 VCL 예외라도 이 Exception 클래스가 사용될 수 없는 상황에서는 ExceptProc이 nil이므로 Runtime error 217이 발생하는 겁니다. 그런 대표적인 경우가 initialization 섹션의 코드에서 예외가 발생하는 경우고요.
:
: System 유닛의 ExceptProc 함수 포인터가 nil인 상태에서 예외가 넘어올 경우는 두가지가 있을 수 있겠죠. SysUtils의 예외 클래스들에 해당하지 않는 저수준 예외가 발생했을 때이거나, 혹은 Exception의 자손 클래스라고 해도 각 유닛의 initialization 섹션에서 발생했을 때입니다. 이 두 케이스 중에서, 델파이나 C++빌더 개발자들이 더 흔히, 그리고 개발자의 실수로 인해 겪게되는 상황은 initialization 섹션의 문제겠죠?
:
:
:
:
: 저도 델파이 팁 게시판의 원문 글을 쓰면서 이런 정도는 분석해보고 쓴 거였습니다.
:
:
:
:
:
:
:
:
:
: 카라얀 님이 쓰신 글 :
: : 박지훈.임프 님이 쓰신 글 :
: : : 포럼에는 댓글 알림 기능이 없어서... (안만든 놈이 바로 저... --;;)
: : : 한달 넘게 전에 댓글 다신 것을 이제야 봤습니다. 좋은 고견 감사드리구요.
: : :
: : : 근데, 제 생각에는 빌더님이 잘못 알고 계신 것 같아서, 추가로 제 생각을 댓글로 썼습니다.
: : : 혹시 제 논리가 틀린 것 같으면 지적 부탁드립니다. ^^;;;;
: : :
: : :
http://delphi.borlandforum.com/impboard/impboard.dll?action=read&db=del_tip&no=290
: :
: :
: : 자세한 설명은 빌더님께서 답변해주시겠지만 VCL 초기화 과정을 잠간 살펴 본 바로는 오히려 박지훈님이 잘못 알고 계신 것 같습니다. ^^
: :
: : 실행파일의 경우 VCL의 초기화 과정은 SysInit.pas의 _InitExe 부터 시작하는데, 64비트 예외처리 방식은 저도 그런게 있다는 것만 알고 있고 자세한 내용은 몰라서 32비트 예외처리로 한정해서 살펴 보면, _InitExe에서 System.pas의 _StartExe를 호출하는데, 여기서 이미 예외처리를 위한 셋업이 이루어 집니다. 스텍프레임 방식으로 예외를 처리하기 위한 프레임을 구성한 후, System.pas의 InitUnits를 호출하는데, InitUnits에서 하는 일은 링크시 포함되는 유닛들의 Initialization 섹션들을 호출해서 초기화 하는게 전부 입니다.
: :
: :
: : _StartExe에서 이미 예외처리를 위한 셋업이 되어 있기 때문에
: :
: : 각기 유닛들의 Initialization 섹션을 호출해서 초기화 하는 InitUnits 는 다음과 같이 try... except 블럭을 사용할 수 있게 됩니다.
: :
: : try
: : while I < Count do
: : begin
: : P := Table^[I].Init;
: : Inc(I);
: : InitContext.InitCount := I;
: : if Assigned(P) and Assigned(Pointer(P^)) then
: : begin
: : {$IF defined(MSWINDOWS)}
: : TProc(P)();
: : {$ELSEIF (defined(POSIX) and defined(CPUX86))}
: : CallProc(P, InitContext.Module^.GOT);
: : {$ELSE}
: : TProc(P)();
: : {$ENDIF}
: : end;
: : end;
: : except
: : FinalizeUnits;
: : raise;
: : end;
: :
: : SysUtils.pas의 Initialization 섹션을 포함한 모든 유닛들의 Initailization 코드들은 이미 예외처리가 동작하는 상황에서 호출이 됩니다.
: : 그리고 SysUtils.pas의 Initialization 섹션에서는 예외처리와 관련한 코드가 전혀 없습니다.
: :
: : 빌더님이 말씀하신 대로 SysUtils.pas와 상관없이 이미 System.pas에서 예외처리를 위한 셋업이 완료되고, 그 이후에 추가로 설치되는 예외핸들러는 System.pas에서 핸들링되는 예외처리에 대한 세컨드, 서드 식의 Chained 예외 핸들러에 불과할 뿐이죠.
: :
: : 제 생각엔 오히려 박지훈님이 잘못 알고 계신 것 같은데요 ^^