예를 들어 다음과 같은 코드는 문제가 발생할 수 있습니다.
procedure TForm1.FormCreate(Sender: TObject);
var
I, iCount: Integer;
XMLDocument: TXMLDocument;
begin
XMLDocument := TXMLDocument.Create(nil);
XMLDocument.LoadFromFile('mytest.xml');
iCount := XMLDocument.DocumentElement.ChildNodes.Count;
for I := 0 to iCount - 1 do
begin
ShowMessage(XMLDocument.DocumentElement.ChildNodes[I].NodeName);
end;
end;
변수 선언 타입이 IXMLDocument로 되어 있지 않고 TXMLDocument로 되어 있는데 간혹 인터넷을 찾아보면 저런 코드가 많은 것 같네요..
해당 코드는 XMLDocument.DocumentElement.ChildNodes.Count; 부분이 실행되거나 또는 해당 위치에 브레이크 포인트를 걸고 Count 값을 확인할 경우 XMLDocument 변수는 잘못된 값을 가르키게 됩니다. ( 보통 저렇게 코드를 작성해도 운이 좋게 실행되는 경우가 있기 때문에 발견하기 힘듭니다. )
문제가 발생하는 원리를 찾아보면...
우선 XMLDocument := TXMLDocument.Create(nil); 부분은 다음과 같은 코드가 실행됩니다.
function _ClassCreate(InstanceOrVMT: Pointer; Alloc: ShortInt): TObject;
{$IFNDEF CPUX86}
begin
if Alloc >= 0 then
InstanceOrVMT := Pointer(TClass(InstanceOrVMT).NewInstance);
Result := TObject(InstanceOrVMT);
end;
{$ELSE CPUX86}
asm
{ -> EAX = pointer to VMT }
{ <- EAX = pointer to instance }
PUSH EDX
PUSH ECX
PUSH EBX
TEST DL,DL
JL @@noAlloc
CALL DWORD PTR [EAX] + VMTOFFSET TObject.NewInstance
@@noAlloc:
{$IFDEF STACK_BASED_EXCEPTIONS}
XOR EDX,EDX
LEA ECX,[ESP+16]
MOV EBX,FS:[EDX]
MOV [ECX].TExcFrame.next,EBX
MOV [ECX].TExcFrame.hEBP,EBP
MOV [ECX].TExcFrame.desc,offset @desc
MOV [ECX].TexcFrame.ConstructedObject,EAX { trick: remember copy to instance }
MOV FS:[EDX],ECX
{$ENDIF STACK_BASED_EXCEPTIONS}
POP EBX
POP ECX
POP EDX
RET
{$IFDEF STACK_BASED_EXCEPTIONS}
@desc:
JMP _HandleAnyException
{ destroy the object }
MOV EAX,[ESP+8+9*4]
MOV EAX,[EAX].TExcFrame.ConstructedObject
TEST EAX,EAX
JE @@skip
MOV ECX,[EAX]
MOV DL,$81
PUSH EAX
CALL DWORD PTR [ECX] + VMTOFFSET TObject.Destroy
POP EAX
CALL _ClassDestroy
@@skip:
{ reraise the exception }
CALL _RaiseAgain
{$ENDIF STACK_BASED_EXCEPTIONS}
end;
{$ENDIF CPUX86}
class function TXMLDocument.NewInstance: TObject;
begin
Result := inherited NewInstance;
TXMLDocument(Result).FRefCount := 1;
end;
그리고 생성자 호출 이후에 바로 다음 함수가 호출됩니다.
function _AfterConstruction(Instance: TObject): TObject;
begin
try
Instance.AfterConstruction;
Result := Instance;
except
_BeforeDestruction(Instance, 1);
raise;
end;
end;
procedure TXMLDocument.AfterConstruction;
begin
inherited;
if (csDesigning in ComponentState) and not (csLoading in ComponentState) then
DOMVendor := GetDOMVendor(DefaultDOMVendor);
FOptions := [doNodeAutoCreate, doAttrNull, doAutoPrefix, doNamespaceDecl];
NSPrefixBase := 'NS';
NodeIndentStr := DefaultNodeIndent;
FOwnerIsComponent := Assigned(Owner) and (Owner is TComponent);
FXMLStrings := TStringList.Create;
FXMLStrings.OnChanging := XMLStringsChanging;
if FFileName <> '' then
SetActive(True);
TInterlocked.Decrement(FRefCount);
end;
위에 보시면 TInterlocked.Decrement(FRefCount); 하는 부분이 있는데 이미 여기서부터 문제의 조짐이 발생됨을 볼 수 있습니다. FRefCount가 생성자가 호출 될 때 1이었는데 여기서 값을 감소시키므로 0이 되어버립니다.
사실 여기까지는 괞찮은데 대입되는 변수가 인터페이스가 아니라 클래스 타입이므로 레퍼런스 카운트는 클래스에 대입되므로 참조 카운트가 증가하지 않고 그대로 0이 되고, 소멸자가 호출되지 않았기 때문에 객체가 해제가 되지 않을 뿐 결국엔 XMLDocument.DocumentElement.ChildNodes.Count 루틴이 호출되면서 레퍼런스 카운트가 0이 되면서 소멸자가 호출되고 해제 된 메모리 객체를 가르키게 되는 것입니다.
만약 IXMLDocument로 선언할 경우 위와 똑같이 실행되지만 추가로 AfterConstruction 함수가 호출 된 이후에 아래와 같이 내부적으로 _IntfCopy 함수인 인터페이스를 복사하는 함수가 호출되어 다시 레퍼런스 카운트를 1로 만들게 됩니다.
procedure _IntfCopy(var Dest: IInterface; const Source: IInterface);
{$IFDEF PUREPASCAL}
var
P: Pointer;
begin
P := Pointer(Dest);
if Source <> nil then
Source._AddRef;
Pointer(Dest) := Pointer(Source);
if P <> nil then
IInterface(P)._Release;
end;
{$ELSE}
asm
{
The most common case is the single assignment of a non-nil interface
to a nil interface. So we streamline that case here. After this,
we give essentially equal weight to other outcomes.
The semantics are: The source intf must be addrefed *before* it
is assigned to the destination. The old intf must be released
after the new intf is addrefed to support self assignment (I := I).
Either intf can be nil. The first requirement is really to make an
error case function a little better, and to improve the behaviour
of multithreaded applications - if the addref throws an exception,
you don't want the interface to have been assigned here, and if the
assignment is made to a global and another thread references it,
again you don't want the intf to be available until the reference
count is bumped.
}
TEST EDX,EDX // is source nil?
JE @@NilSource
PUSH EDX // save source
PUSH EAX // save dest
MOV EAX,[EDX] // get source vmt
PUSH EDX // source as arg
CALL DWORD PTR [EAX] + VMTOFFSET IInterface._AddRef
POP EAX // retrieve dest
MOV ECX, [EAX] // get current value
POP [EAX] // set dest in place
TEST ECX, ECX // is current value nil?
JNE @@ReleaseDest // no, release it
RET // most common case, we return here
@@ReleaseDest:
{$IFDEF ALIGN_STACK}
SUB ESP, 8
{$ENDIF ALIGN_STACK}
MOV EAX,[ECX] // get current value vmt
PUSH ECX // current value as arg
CALL DWORD PTR [EAX] + VMTOFFSET IInterface._Release
{$IFDEF ALIGN_STACK}
ADD ESP, 8
{$ENDIF ALIGN_STACK}
RET
{ Now we're into the less common cases. }
@@NilSource:
MOV ECX, [EAX] // get current value
TEST ECX, ECX // is it nil?
MOV [EAX], EDX // store in dest (which is nil)
JE @@Done
MOV EAX, [ECX] // get current vmt
{$IFDEF ALIGN_STACK}
SUB ESP, 8
{$ENDIF ALIGN_STACK}
PUSH ECX // current value as arg
CALL DWORD PTR [EAX] + VMTOFFSET IInterface._Release
{$IFDEF ALIGN_STACK}
ADD ESP, 8
{$ENDIF ALIGN_STACK}
@@Done:
end;
{$ENDIF !PUREPASCAL}
따라서 문제가 발생하지 않겠죠.
정말 간단한 부분인데 놓치기 쉬운 부분이기도 합니다.
'인터넷/IT > 프로그래밍' 카테고리의 다른 글
svn 에서 새 프로젝트 저장소 생성하기 (0) | 2014.04.08 |
---|---|
cannot be shown because the specified help collection 'ms-help://ms.WDK.v10.7600.091201' is invalid. (0) | 2014.03.18 |
DwmSetWindowAttribute 을 이용하여 창 애니메이션 없애기 (0) | 2014.03.11 |
GetNativeSystemInfo 함수를 이용 시 Windows 8.1 버전을 제대로 가져오지 못하는 경우 (0) | 2014.03.10 |
GetNativeSystemInfo 함수와 레지스트리키 이용한 윈도우 버전 및 CPU 정보 읽어오기 (0) | 2014.03.10 |
IE9 이상으로 브라우저를 업그레이드하거나, 크롬, 파이어폭스 등 최신 브라우저를 이용해주세요.