다음을 통해 공유


수동으로 스택 걷기

경우에 따라 스택 추적 함수가 디버거에서 실패합니다. 이는 디버거가 반환 주소의 위치를 잃게 하는 잘못된 주소에 대한 호출로 인해 발생할 수 있습니다. 또는 스택 추적을 직접 가져올 수 없는 스택 포인터를 접했을 수 있습니다. 또는 다른 디버거 문제가 있을 수 있습니다. 어떤 경우든 스택을 수동으로 걸을 수 있는 것이 중요한 경우가 많습니다.

기본 개념은 매우 간단합니다. 스택 포인터를 덤프하고, 모듈이 로드되는 위치를 확인하고, 가능한 함수 주소를 찾고, 가능한 각 스택 항목이 다음 항목을 호출하는지 확인하여 확인합니다.

예제를 진행하기 전에 kb(Display Stack Backtrace) 명령에 Intel 시스템에 추가 기능이 있다는 점에 유의해야 합니다. kb=[ebp] [eip] [esp]를 수행하면 디버거는 기본 포인터, 명령 포인터 및 스택 포인터에 대해 지정된 값을 사용하여 프레임의 스택 추적을 각각 표시합니다.

예를 들어 실제로 스택 추적을 제공하는 오류가 사용되므로 결과를 마지막에 확인할 수 있습니다.

첫 번째 단계는 로드되는 모듈을 알아내는 것입니다. 이 작업은 x(기호 검사) 명령을 사용하여 수행됩니다(일부 기호는 길이를 이유로 편집됨).

kd> x *! 
start    end        module name
77f70000 77fb8000   ntdll     (C:\debug\ntdll.dll, \\ntstress\symbols\dll\ntdll.DBG)
80010000 80012320   Aha154x   (load from Aha154x.sys deferred)
80013000 8001aa60   SCSIPORT  (load from SCSIPORT.SYS deferred)
8001b000 8001fba0   Scsidisk  (load from Scsidisk.sys deferred)

80100000 801b7b40   NT        (ntoskrnl.exe, \\ntstress\symbols\exe\ntoskrnl.DBG)
802f0000 8033c000   Ntfs      (load from Ntfs.sys deferred)
80400000 8040c000   hal       (load from hal.dll deferred)
fe4c0000 fe4c38c0   vga       (load from vga.sys deferred)
fe4d0000 fe4d3e60   VIDEOPRT  (load from VIDEOPRT.SYS deferred)
fe4e0000 fe4f0e40   ati       (load from ati.SYS deferred)
fe500000 fe5057a0   Msfs      (load from Msfs.SYS deferred)
fe510000 fe519560   Npfs      (load from Npfs.SYS deferred)

fe520000 fe521f60   ndistapi  (load from ndistapi.sys deferred)
fe530000 fe54ed20   Fastfat   (load from Fastfat.SYS deferred)
fe5603e0 fe575360   NDIS      (NDIS.SYS, \\ntstress\symbols\SYS\NDIS.DBG)
fe580000 fe585920   elnkii    (elnkii.sys, \\ntstress\symbols\sys\elnkii.DBG)
fe590000 fe59b8a0   ndiswan   (load from ndiswan.sys deferred)
fe5a0000 fe5b7c40   nbf       (load from nbf.sys deferred)
fe5c0000 fe5c1b40   TDI       (load from TDI.SYS deferred)
fe5d0000 fe5dd580   nwlnkipx  (load from nwlnkipx.sys deferred)

fe5e0000 fe5ee220   nwlnknb   (load from nwlnknb.sys deferred)
fe5f0000 fe5fb320   afd       (load from afd.sys deferred)
fe610000 fe62bf00   tcpip     (load from tcpip.sys deferred)
fe630000 fe648600   netbt     (load from netbt.sys deferred)
fe650000 fe6572a0   netbios   (load from netbios.sys deferred)
fe660000 fe660000   Parport   (load from Parport.SYS deferred)
fe670000 fe670000   Parallel  (load from Parallel.SYS deferred)
fe680000 fe6bcf20   rdr       (rdr.sys, \\ntstress\symbols\sys\rdr.DBG)

fe6c0000 fe6f0920   srv       (load from srv.sys deferred) 

출력은 이전 버전의 Windows에서 온 것이며 모듈 이름은 현재 날짜 버전에서 다릅니다.

두 번째 단계는 스택 포인터를 덤프하여 x *! 명령에서 제공하는 모듈에서 주소를 찾는 것입니다.

kd> dd esp 
fe4cc97c  80136039 00000270 00000000 00000000
fe4cc98c  fe682ae4 801036fe 00000000 fe68f57a
fe4cc99c  fe682a78 ffb5b030 00000000 00000000
fe4cc9ac  ff680e08 801036fe 00000000 00000000
fe4cc9bc  fe6a1198 00000001 fe4cca78 ffae9d98

fe4cc9cc  02000901 fe4cca68 ffb50030 ff680e08
fe4cc9dc  ffa449a8 8011c901 fe4cca78 00000000
fe4cc9ec  80127797 80110008 00000246 fe6a1430

kd> dd 
fe4cc9fc  00000270 fe6a10ae 00000270 ffa44abc
fe4cca0c  ffa449a8 ff680e08 fe6b2c04 ff680e08
fe4cca1c  ffa449a8 e12820c8 e1235308 ffa449a8
fe4cca2c  fe685968 ff680e08 e1235308 ffa449a8
fe4cca3c  ffb0ad48 ffb0ad38 00100000 ffb0ad38
fe4cca4c  00000000 ffa44a84 e1235308 0000000a
fe4cca5c  c00000d6 00000000 004ccb28 fe4ccbc4

fe4cca6c  fe680ba4 fe682050 00000000 fe4ccbd4 

함수 주소일 가능성이 높은 값과 매개 변수 또는 저장된 레지스터를 결정하기 위해 가장 먼저 고려해야 할 사항은 스택에서 다양한 유형의 정보가 어떻게 표시되는지입니다. 대부분의 정수는 더 작은 값이 될 것입니다. 즉, DWORD(예: 0x00000270)로 표시될 때 대부분 0이 됩니다. 로컬 주소에 대한 대부분의 포인터는 스택 포인터(예: fe4cca78)에 가깝습니다. 상태 코드는 일반적으로 c(c00000d6)로 시작합니다. 유니코드 및 ASCII 문자열은 각 문자가 20-7f 범위에 있다는 사실로 식별할 수 있습니다. (KD에서 dc(메모리 표시) 명령은 오른쪽에 문자를 표시합니다. 가장 중요한 것은 함수 주소가 x *!로 나열된 범위에 있는 것입니다.

나열된 모든 모듈은 77f70000에서 8040c000 및 fe4c0000~fe6f0920 범위에 있습니다. 이러한 범위에 따라 이전 목록에서 가능한 함수 주소는 다음과 같습니다. 80136039, 801036fe(두 번 나열됨, 매개 변수일 가능성이 높음), fe682ae4, fe682a7a, fe682a78, fe6a1198, 8011c901, 80127797, 80110008, fe6a1430, fe6a10ae, fe6b2c04, fe685968, fe680ba4 및 fe682050. 각 주소에 대해 ln(가장 가까운 기호 나열) 명령을 사용하여 이러한 위치를 조사합니다.

kd> ln 80136039 
(80136039)   NT!_KiServiceExit+0x1e  |  (80136039)   NT!_KiServiceExit2-0x177
kd> ln fe682ae4 
(fe682ae4)   rdr!_RdrSectionInfo+0x2c | (fe682ae4)   rdr!_RdrFcbReferenceLock-0xb4
kd> ln 801036fe 
(801036fe)   NT!_KeWaitForSingleObject | (801036fe)   NT!_MmProbeAndLockPages-0x2f8
kd> ln fe68f57a 
(fe68f57a)   rdr!_RdrDereferenceDiscardableCode+0xb4  
                         (fe68f57a)   rdr!_RdrUninitializeDiscardableCode-0xa
kd> ln fe682a78 
(fe682a78)   rdr!_RdrDiscardableCodeLock | (fe682a78) rdr!_RdrDiscardableCodeTimeout-0x38

kd> ln fe6a1198 
(fe6a1198)   rdr!_SubmitTdiRequest+0xae | (fe6a1198)   rdr!_RdrTdiAssociateAddress-0xc
kd> ln 8011c901 
(8011c901)   NT!_KeSuspendThread+0x13 | (8011c901)   NT!_FsRtlCheckLockForReadAccess-0x55
kd> ln 80127797 
(80127797)   NT!_ZwCloseObjectAuditAlarm+0x7 | (80127797)   NT!_ZwCompleteConnectPort-0x9
kd> ln 80110008 
(80110008)   NT!_KeWaitForMultipleObjects+0x27c | (80110008) NT!_FsRtlLookupMcbEntry-0x164
kd> ln fe6a1430 
(fe6a1430)   rdr!_RdrTdiCloseConnection+0xa | (fe6a1430)   rdr!_RdrDoTdiConnect-0x4

kd> ln fe6a10ae 
(fe6a10ae)   rdr!_RdrTdiDisconnect+0x56 | (fe6a10ae)   rdr!_SubmitTdiRequest-0x3c
kd> ln fe6b2c04 
(fe6b2c04)   rdr!_CleanupTransportConnection+0x64 | (fe6b2c04)rdr!_RdrReferenceServer-0x20
kd> ln fe685968 
(fe685968)   rdr!_RdrReconnectConnection+0x1b6
                        (fe685968)   rdr!_RdrInvalidateServerConnections-0x32
kd> ln fe682050 
(fe682050)   rdr!__strnicmp+0xaa  |  (fe682050)   rdr!_BackPackSpinLock-0xa10 

앞에서 설명한 것처럼 801036fe는 두 번 나열되므로 스택 추적의 일부가 될 가능성이 없습니다. 반환 주소의 오프셋이 0이면 무시될 수 있습니다(함수의 시작 부분으로 돌아갈 수 없음). 이 정보에 따라 스택 추적은 다음과 같이 표시됩니다.

NT!_KiServiceExit+0x1e
rdr!_RdrSectionInfo+0x2c
rdr!_RdrDereferenceDiscardableCode+0xb4  
rdr!_SubmitTdiRequest+0xae
NT!_KeSuspendThread+0x13
NT!_ZwCloseObjectAuditAlarm+0x7
NT!_KeWaitForMultipleObjects+0x27c
rdr!_RdrTdiCloseConnection+0xa
rdr!_RdrTdiDisconnect+0x56
rdr!_CleanupTransportConnection+0x64
rdr!_RdrReconnectConnection+0x1b6
rdr!__strnicmp+0xaa 

각 기호를 확인하려면 지정된 반환 주소 바로 앞에 언어셈블하여 위의 함수에 대한 호출을 수행하는지 확인합니다. 길이를 줄이기 위해 다음이 편집됩니다(사용된 오프셋은 시행 착오로 발견됨).

kd> u 80136039-2 l1      //  looks ok, its a call
NT!_KiServiceExit+0x1c:
80136037 ffd3             call    ebx
kd> u fe682ae4-2 l1      //  paged out (all zeroes) unknown
rdr!_RdrSectionInfo+0x2a:
fe682ae2 0000             add     [eax],al
kd> u fe68f57a-6 l1      //  looks ok, its a call, but not anything above
rdr!_RdrDereferenceDiscardableCode+0xae:
fe68f574 ff15203568fe     call dword ptr [rdr!__imp__ExReleaseResourceForThreadLite]
kd> u fe682a78-6 l1      //  paged out (all zeroes) unknown

rdr!_DiscCodeInitialized+0x2:
fe682a72 0000             add     [eax],al
kd> u  fe6a1198-5 l1      //  looks good, call to something above
rdr!_SubmitTdiRequest+0xa9:
fe6a1193 e82ee3feff       call  rdr!_RdrDereferenceDiscardableCode (fe68f4c6)
kd> u 8011c901-2 l1      //  not good, its a jump in the function
NT!_KeSuspendThread+0x11:
8011c8ff 7424             jz      NT!_KeSuspendThread+0x37 (8011c925)
kd> u 80127797-2 l1      //  looks good, an int 2e -> KiServiceExit

NT!_ZwCloseObjectAuditAlarm+0x5:
80127795 cd2e             int     2e
kd> u 80110008-2 l1      //  not good, its a test instruction not a call
NT!_KeWaitForMultipleObjects+0x27a:
80110006 85c9             test    ecx,ecx
kd> u 80110008-5 l1      //  paged out (all zeroes) unknown
NT!_KeWaitForMultipleObjects+0x277:
80110003 0000             add     [eax],al
kd> u fe6a1430-6 l1      //  looks good its a call to ZwClose...
rdr!_RdrTdiCloseConnection+0x4:
fe6a142a ff15f83468fe     call    dword ptr [rdr!__imp__ZwClose (fe6834f8)]

kd> u fe6a10ae-2 l1      //  paged out (all zeroes) unknown
rdr!_RdrTdiDisconnect+0x54:
fe6a10ac 0000             add     [eax],al
kd> u  fe6b2c04-5 l1      //  looks good, call to something above
rdr!_CleanupTransportConnection+0x5f:
fe6b2bff e854e4feff       call    rdr!_RdrTdiDisconnect (fe6a1058)
kd> u fe685968-5 l1      //  looks good, call to immediately above
rdr!_RdrReconnectConnection+0x1b1:
fe685963 e838d20200       call    rdr!_CleanupTransportConnection (fe6b2ba0)

kd> u fe682050-2 l1      //  paged out (all zeroes) unknown
rdr!__strnicmp+0xa8:
fe68204e 0000             add     [eax],al 

이에 따라 RdrReconnectConnectionCleanupTransportConnection, RdrTdiDisconnect, ZwCloseObjectAuditAlarm, KiServiceExit로 호출된 것으로 나타납니다. 스택의 다른 함수는 아마도 이전에 활성 스택의 남은 부분일 수 있습니다.

이 경우 스택 추적이 제대로 작동했습니다. 다음은 답변을 검사 실제 스택 추적입니다.

kd> k 
ChildEBP RetAddr
fe4cc978 80136039 NT!_NtClose+0xd
fe4cc978 80127797 NT!_KiServiceExit+0x1e

fe4cc9f4 fe6a1430 NT!_ZwCloseObjectAuditAlarm+0x7
fe4cca10 fe6b2c04 rdr!_RdrTdiCloseConnection+0xa
fe4cca28 fe685968 rdr!_CleanupTransportConnection+0x64
fe4cca78 fe688157 rdr!_RdrReconnectConnection+0x1b6
fe4ccbd4 80106b1e rdr!_RdrFsdCreate+0x45b
fe4ccbe8 8014b289 NT!IofCallDriver+0x38
fe4ccc98 8014decd NT!_IopParseDevice+0x693
fe4ccd08 8014d6d2 NT!_ObpLookupObjectName+0x487
fe4ccde4 8014d3ad NT!_ObOpenObjectByName+0xa2
fe4cce90 8016660d NT!_IoCreateFile+0x433
fe4cced0 80136039 NT!_NtCreateFile+0x2d 

첫 번째 항목은 스택 추적을 기반으로 하는 현재 위치였지만, 그렇지 않으면 스택이 RdrReconnectConnection 이 호출된 지점까지 정확했습니다. 전체 스택을 추적하는 데 동일한 프로세스를 사용할 수 있습니다. 수동 스택 보행의 보다 정확한 방법을 사용하려면 각 잠재적 함수의 어셈블을 해제하고 각 푸시 을 따라 스택의 각 DWORD를 식별해야 합니다.