Maystyle :
Admin : New post
Guestbook
Local
media
Catergories
Recent Articles
Recent Comments
Recent Trackbacks
Calendar
Tag
Archive
Link
Search
 
  Windows Internals 다시보기 14 
작성일시 : 2010. 2. 19. 15:08 | 분류 : Windows Server/Kernel

* 아직은 많이 부족하기 때문에 제가 자신이 생길 때 까지 본 글은 제 블로그에 대한 링크만 허용합니다.

쓰래드의 백미는 스택입니다.

가끔 우리는 어항과 물고기를 통해 이 프로세스와 쓰래드의 관계를 표현하게 됩니다.
즉 어항은 프로세스 실제 해엄을 치고 있는 물고기는 쓰래드가 되죠.

그 경우 이 물고기들은 자신이 해엄치는 자취를 남기게 되는데, 이를 콜 스택이라고 볼 수 있습니다. 다시 말해서 어떠한 일을 한다 즉 함수를 실행 할 때 그 함수 실행의 자취를 콜 스택으로 비유 할 수 있습니다.

우리가 만드는 함수는 아래와 같은 모습이 됩니다.

Int Procedure1( ) { --------- 4
Procedure2( )       --------- 5
}
Int Procedure2( ) { --------- 6
…                      --------- 7
}
Main( ) {              --------- 1
…                      --------- 2
Procedure1( );       --------- 3
…                      --------- 8
}

제가 함수 옆에 숫자를 붙여 놓았는데, 이는 실행 순서 입니다. 여러분도 아시는 것 처럼 이 함수들은 먼저 Main( )이 실행 된 다음 Procedure1( )이 호출 되고, 호출된 Procedure1( ) 이 실행 되고 다시 Procedure2( ) 가 실행되고 다시 Main( )이 실행 됩니다.

이를 위하여 컴퓨터에서는 복귀 주소를 이용하게 되는데, 아래 그림과 같이 먼저 Main( )이 실행 하면 먼저 Main( )의 복귀 주소가 저장이 됩니다. 그리고 Procedure1( )을 호출 하게 되면 스택에 역시 Procedure1 ( )의 복귀 주소가 저장이 되고, 다시 Procedure1 ( )에서 Procedure2 ( )가 호출되게 되면 역시 Procedure 2( )의 복귀 주소가 저장이 되고, 실행 코드가 실행 이 된 후 다시 차례로 자신을 호출했던 복귀 주소로 돌아가 실행 결과를 전달하게 됩니다.

image

이는 콜 스택이라는 형태로 Thread 상에 표현되게 됩니다.
즉 Main의 복귀 주소를 저장한 후 Procedure1 이 실행 되고, 이 Procedure1 실행 중 Procedure2 를 호출하는 경우 다시 Procedure1 의 복귀 주소를 스택에 저장한 후 Procedure2가 실행 되는 거죠. 역시 최종적으로 실행 되던 Procedure2가 실행이 끝나면 저장했던 복귀 주소를 통해 차례로 Procedure1 과 Main 이 실행 됩니다.
(참고로 콜 스택의 현재 실행 위치는 ESP 레지스터에 저장됩니다.)

아래는 사용자가 notepad의 윈도우의 형태를 변경하는 메시지를 입력하는 순간에 잡은 유저 쓰래드입니다.
자 쓰래드를 한번 들여다 보도록 하겠습니다.

아래에서 보시는 것 처럼 notepad.exe의 콜 스택에서 81bddcb0 이 실행 중이였습니다.
kd> !thread
THREAD 81bddcb0 Cid 094c.0950 Teb: 7ffdf000 Win32Thread: e19a7970 RUNNING on processor 0
Owning Process 81e5db78 Image: notepad.exe
ChildEBP RetAddr Args to Child
f7f8ad4c 808234cb 0007fefc 00000000 00000000 win32k!NtUserGetMessage (FPO: [SEH])
f7f8ad4c 7c8285ec 0007fefc 00000000 00000000 nt!KiFastCallEntry+0xf8 (FPO: [0,0] TrapFrame @ f7f8ad64)
WARNING: Stack unwind information not available. Following frames may be wrong.
0007fed8 01002a3b 0007fefc 00000000 00000000 ntdll!KiFastSystemCallRet
0007ff1c 01007527 01000000 00000000 000a24b2 notepad!WinMain+0xe5 (FPO: [4,8,0])
0007ffc0 77e6f23b 00000000 00000000 7ffda000 notepad!WinMainCRTStartup+0x182 (FPO: [SEH])
0007fff0 00000000 010073a5 00000000 78746341 kernel32!ProcessIdToSessionId+0x209

아 보니깐 대충 “모듈명!함수명”으로 되어 있습니다.

그리고 이 콜 스택을 통해 해당 함수의 로컬 변수 뿐 아니라 매개 변수까지 모두 사용할 수 있습니다. 방법은 간단하죠. 하나의 함수가 선언되어 있다면 해당 함수의 복귀 주소가 저장이 될 것이고 차례로 지정해 놓은 변수들이 해당 스택에 쌓이게 될꺼니깐요. 만약 해당 변수를 매개 변수로 받는 다면 현재의 복귀 주소에서 함수 호출 시 사용한 매개 변수가 있는 위치만큼 위치를 더해서(4K 단위로) 값을 가져 올 수 있습니다.

그래서 이용하는 레지스터가 EBP입니다.

ESP는 현재 실행 하고 있는 콜 스택 상의 최 상위 실행 번지 입니다. 문제는 함수가 실행 함에 따라 ESP는 지속적으로 변경이 된다는 거죠. 그렇다면 우리가 사용하는 변수들을 단순위 ESP 값에 번지를 더하는 수준으로는 사용이 어려울 수 있습니다. 그래서 함수가 호출되게 되면 처음에 ESP의 값을 EBP 값으로 변경하고 이 EBP를 기준으로 변수를 접근하게 됩니다.

이렇게 함수들이 실행이 되게 되면 최종적으로는 쌓이다가 다시 꺼내게 되어 있습니다. 이때 사용하는 명령어가 바로 어셈블리어로 Ret 인데, 위의 예제에서는 RetAddr로 표현되고 있습니다. 이 Ret 명령어는 복귀 주소를 ‘Pop’ 즉 꺼내서 ‘Jmp’ 점프 즉 복귀하라는 의미입니다.

스택 트레이싱의 예
(처음에는 그냥 작성 하다 정말 좋은 글을 발견 했습니다. 출처: http://byung.egloos.com/4750412)

제가 간단한 프로그램을 만들어서 해당 프로그램을 통해서 확인 하도록 하겠습니다.
(먼저 스택은 포인터의 집합이고 실제 데이터는 힙에 산계하여 있음을 알려드립니다.)
프로그램은 아래와 같습니다.

#include "stdio.h"
int sum(int a, int b);
int min(int a, int b);
int _tmain(int argc, _TCHAR* argv[])
{

int a, b, c;
a=7;
b=3;
scanf("%d", &c);
sum(a,b);
min(a,b);
return 0;

}

nt sum(int a, int b)
{

int s;
s = a + b;
return s;

}

int min(int a, int b)
{

int m;
m = a - b;
return m;

}

Windbg를 통해서 해당 실행 파일을 실행 합니다.
먼저 int Sum() 함수에 BP 즉 브레이크 포인터를 걸겠습니다.
Kd> Bp DebugTest!sum
Kd> g

해당 함수가 실행 되는 상황에서 콜 스택을 확인 했습니다.
0:000:x86> k
ChildEBP RetAddr
002efdcc 010e1414 DebugTest!sum [d:\000. documents\visual studio 2008\projects\debugtest\debugtest\debugtest.cpp @ 22]
002efecc 010e1a88 DebugTest!wmain+0x54 [d:\000. documents\visual studio 2008\projects\debugtest\debugtest\debugtest.cpp @ 16]
002eff1c 010e18cf DebugTest!__tmainCRTStartup+0x1a8 [f:\dd\vctools\crt_bld\self_x86\crt\src\crtexe.c @ 579]
002eff24 76fe3677 DebugTest!wmainCRTStartup+0xf [f:\dd\vctools\crt_bld\self_x86\crt\src\crtexe.c @ 399]
002eff30 77569d72 kernel32!BaseThreadInitThunk+0xe
002eff70 77569d45 ntdll32!__RtlUserThreadStart+0x70
002eff88 00000000 ntdll32!_RtlUserThreadStart+0x1b

자 그렇다면 이때 무슨일이 일어 나고 있는지 CPU 레지스터 정보를 보겠습니다.

0:000:x86> r
eax=00000003 ebx=7efde000 ecx=00000007 edx=700c13e8 esi=002efddc edi=002efecc
eip=010e1490 esp=002efdd0 ebp=002efecc iopl=0 nv up ei pl zr na pe nc
cs=0023 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00000246

DebugTest!sum:
10e1490 55 push ebp

위에서 저는 ESP의 한계로 인해 EBP를 이용한다고 말씀 드렸습니다. 막 스택에 EBP를 넣었군요. 그렇다면 다음에는 ESP의 값을 EBP에 넣어주겠죠? 하지만 지금은 그 단계는 아닙니다.

위에서 현재 실행되고 있는 스택의 주소는 002efdd0 입니다.
직접 스택을 보도록 하겠습니다.
로컬 변수 7과 3 역시 스택에 저장되어 있으며, 스택의 꼭대기에는 현재 복귀를 위한 주소가 저장되어 있습니다.

0:000:x86> dds esp
002efdd0 010e1414 DebugTest!wmain+0x54 [d:\000. documents\visual studio 2008\projects\debugtest\debugtest\debugtest.cpp @ 16]
002efdd4 00000007
002efdd8 00000003
002efddc 00000000
002efde0 00000000
002efde4 7efde000

자 스택의 ESP 에 있는 010e1414 함수를 확인 하기 위하여 디스어셈블리 해보도록 하겠습니다.
16 010e140f e84bfcffff call DebugTest!ILT+90(?sumYAHHHZ) (010e105f)
16 010e1414 83c408 add esp,8

그리고 이전에 설명 드린것과 같이 EBP를 기준으로 하여 로컬 변수 7과 3이 저장하는 것을 볼 수 있습니다.
13 010e13de c745f807000000 mov dword ptr [ebp-8],7
14 010e13e5 c745ec03000000 mov dword ptr [ebp-14h],3

보시는 것과 같이 자신의 EBP를 기준으로 포인터를 4 8 C… 즉 4 단위로 이동하면서 지정하는 것을 확인 할 수 있습니다.
위와 같이 DebugTest!ILT 를 호출합니다.
보시는 것과 같이 해당 함수의 다음 라인 즉 함수가 처리된 이후에 리턴 될 주소를 가르키고 있습니다.
ChildEBP RetAddr
002efdcc 010e1414

우리는 레지스터를 통해 현재 실행 중인 명령어 라인을 알 수 있습니다.
0:000:x86> r
eax=00000003 ebx=7efde000 ecx=00000007 edx=700c13e8 esi=002efddc edi=002efecc
eip=010e1490 esp=002efdd0 ebp=002efecc iopl=0 nv up ei pl zr na pe nc
cs=0023 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00000246

DebugTest!sum:
010e1490 55 push ebp

마지막으로 이 콜 스택은 위에서부터 프래임 0 번부터 프레임 1 2 3 으로 표현이 됩니다.
우리는 프래임을 확인하여 실제 프레임에서 사용하는 매게 변수를 확인 할 수 있습니다.
0:000:x86> .frame 0
00 002efdcc 010e1414 DebugTest!sum [d:\000. documents\visual studio 2008\projects\debugtest\debugtest\debugtest.cpp @ 22]
0:000:x86> dv /i /V
prv param 002efdd4 @ebp+0x08 a = 7
prv param 002efdd8 @ebp+0x0c b = 3
prv local 002efdc4 @ebp-0x08 s = 3079640
0:000:x86> .frame 1
01 002efecc 010e1a88 DebugTest!wmain+0x54 [d:\000. documents\visual studio 2008\projects\debugtest\debugtest\debugtest.cpp @ 16]
0:000:x86> dv /i /V
prv param 002efed4 @ebp+0x08 argc = 1
prv param 002efed8 @ebp+0x0c argv = 0x001a1350
prv local 002efeac @ebp-0x20 c = –858993460
prv local 002efeb8 @ebp-0x14 b = 3
prv local 002efec4 @ebp-0x08 a = 7

|