Microsoft MVP성태의 닷넷 이야기
디버깅 기술: 216. WinDbg - 2가지 유형의 식 평가 방법(MASM, C++) [링크 복사], [링크+제목 복사],
조회: 3718
글쓴 사람
정성태 (seongtaejeong at gmail.com)
홈페이지
첨부 파일
 
(연관된 글이 1개 있습니다.)
(시리즈 글이 2개 있습니다.)
디버깅 기술: 216. WinDbg - 2가지 유형의 식 평가 방법(MASM, C++)
; https://www.sysnet.pe.kr/2/0/13860

디버깅 기술: 219. WinDbg - 명령어 내에서 환경 변수 사용법
; https://www.sysnet.pe.kr/2/0/13910




WinDbg - 2가지 유형의 식 평가 방법(MASM, C++)

이번 글은 아래의 내용을 그대로 참조한 것입니다. ^^

WinDBG expression evaluation tutorial
; https://codemachine.com/articles/windbg_expressions.html

Evaluating Expressions
; https://learn.microsoft.com/en-us/windows-hardware/drivers/debuggercmds/evaluating-expressions

WinDbg. From A to Z!
; http://windbg.info/download/doc/pdf/WinDbg_A_to_Z_color.pdf

별도 설정을 하지 않았다면, WinDbg는 기본적으로 MASM 방식으로 식을 평가합니다.

6: kd> .expr
Current expression evaluator: MASM - Microsoft Assembler expressions

물론 기본값을 /s 옵션을 통해 바꾸는 것도 가능합니다.

// C++ 방식으로 변경
6: kd> .expr /s c++
Current expression evaluator: C++ - C++ source expressions

// MASM 방식으로 변경
6: kd> .expr /s masm
Current expression evaluator: MASM - Microsoft Assembler expressions

또는, 명시적으로 방법을 지정할 수 있는데, "?" 물음표 1개 명령어는 기본 설정된 방식의 평가를 하는 반면 "??" 2개 명령어는 언제나 "C++" 방식으로 평가합니다. 이후의 모든 예제는 기본 방식이 MASM으로 설정된 것을 가정하기 때문에 "?" 물음표 한 개를 사용했을 시 MASM 방식의 평가가 됩니다.




두 가지 평가 방식의 주요 차이점을 살펴볼까요? ^^


1) 심벌의 주소 평가

우선, MASM에서는 모든 심벌에 대해 주솟값으로 평가를 하는 반면 C++은 그런 해석을 하지 않습니다.

// MASM 방식에서는 nt 모듈을 나타내는 심벌을 그것의 로딩 주소로 평가
6: kd> ? nt
Evaluate expression: -8786477580288 = fffff802`3d200000

// 반면 C++ 방식에서는 오류
6: kd> ?? nt
Couldn't resolve error at 'nt'

재미있는 건 표현식 내에서 특정 영역을 MASM 또는 C++ 방식으로 해석을 바꿀 수 있는 "@@" 기호가 제공되므로, 이것을 도입하면 혼용이 가능합니다.

// 괄호 안의 식을 MASM 방식으로 평가
@@masm( ...[식]...)

// 괄호 안의 식을 C++ 방식으로 평가
@@c++( ...[식]...)

가령, 위에서 nt 심벌을 C++ 평가식에서 주소 참조를 하고 싶다면 다음과 같이 할 수 있습니다.

6: kd> ?? @@masm(nt)
unsigned int64 0xfffff802`3d200000

참고로 masm, c++을 지정하지 않고 단독으로 "@@" 기호만을 사용하면 현재 표현식의 평가 방식에서 무조건 다른 평가 방식을 사용하는 것으로 간주합니다. 즉, MASM 방식이었다면 C++ 방식으로, C++ 방식이었다면 MASM 방식으로 평가를 합니다.

// C++ 평가 방식에서 "@@" 기호를 통해 MASM 방식으로 평가
6: kd> ?? @@(nt)
unsigned int64 0xfffff802`3d200000

// MASM 방식에서, "@@" 기호를 통해 C++ 방식으로, 다시 "@@" 기호를 통해 MASM 방식으로 평가
6: kd> ? @@( @@(nt) )
Evaluate expression: -8786477580288 = fffff802`3d200000

마지막으로, MASM 평가 방식에서만 사용할 수 있는 @!"...[심벌 문자열]..." 구문이 있는데요, 심벌 내에 "<", ">"이나 공백과 같은 특수 문자가 포함된 경우에 사용할 수 있습니다.

// 사용자 모드의 C++ 프로그램을 디버깅하는 상황으로 가정

0:000> bu @!"ExecutableName!std::pair<unsigned int,std::basic_string<unsigned short,std::char_traits<unsigned short>,std::allocator<unsigned short> > >::operator="


2) 기본 진수(radix)

MASM 방식의 경우 별다른 접두사가 없다면 숫자에 대해 16진수로 평가하지만, C++ 방식에서는 10진수로 평가합니다.

// MASM 방식에서는 16진수로 평가
6: kd> n
base is 16

6: kd> ? 10
Evaluate expression: 16 = 00000000`00000010

// 기본 진법을 바꾸고 싶다면 n 명령어 사용
6: kd> n 10
base is 10

6: kd> ? 10
Evaluate expression: 10 = 00000000`0000000a

// C++ 방식에서는 10진수로 평가
6: kd> ?? 10
int 0n10

// 따라서, C++ 방식의 경우 16진수로 평가하고 싶다면 명시적으로 0x 접두사를 붙여야 함
6: kd> ?? 0x10
int 0n16


3) 레지스터 접근

이름에서 이미 유추할 수 있듯이, MASM 방식은 어셈블리어의 문법을 기반으로 식 평가를 지원하기 때문에 레지스터의 경우 이름을 그대로 사용할 수 있습니다.

// MASM 방식에서 rbx 레지스터 값을 평가
6: kd> ? rbx
Evaluate expression: -87374944424720 = ffffb088`6f79acf0

// C++ 방식에서는 오류
6: kd> ?? rbx
Couldn't resolve error at 'rbx'

// 대신, pseudo 레지스터 문법으로 접근
6: kd> ?? @rbx
unsigned int64 0xffffb088`6f79acf0

// MASM 방식에서도 '@' 기호를 명시해 로컬/전역 변수와 이름이 같은 경우를 위해 구분 용도로 사용 가능
// 예를 들어 로컬 변수로 rbx가 있다면, 아래의 식은 명식적으로 레지스터 rbx를 평가
6: kd> ? @rbx
Evaluate expression: -87374944424720 = ffffb088`6f79acf0

기타 WinDbg가 지원하는 사용자 정의 가상 레지스터에 대해서도 MASM 방식이 더 편리합니다.

// 사용자 정의 레지스터인 $t0에 0x1064 값을 설정
6: kd> r $t0 = 0x1064

// MASM 방식에서는 바로 평가
6: kd> ? $t0
Evaluate expression: 4196 = 00000000`00001064

// C++ 방식에서는 "@" 기호를 붙여야 함
6: kd> ?? $t0
Unexpected character in '$t0'

6: kd> ?? @$t0
unsigned int64 0x1064


4) 연산자 및 고유 언어별 확장

기타 연산자의 경우에는 2가지 방식 모두 동일하게 사용할 수 있습니다.

  • arithmetic (+ - * / %)
  • shift (<< >>)
  • bitwise ( & | ^)
  • comparison (== < > <= >= !=)

6: kd> ? (efl & ( 1 << 6)) != 0
Evaluate expression: 0 = 00000000`00000000

6: kd> ?? (@efl & ( 1 << 6)) != 0
bool false

단지 각자의 이름이 의미하는 것처럼 언어 차원에서 지원하거나 별도로 확장한 연산자가 있는 정도인데요, 가령 signed right shift 연산자(>>>)는 MASM에서만 지원합니다.

6: kd> ? 0xfa >>> 4
Evaluate expression: 15 = 00000000`0000000f

6: kd> ?? 0xfa >>> 4
Syntax error at '> 4'

// 그 외에 $scmp(), $sicmp(), $spat(), $vvalid 등의 확장 연산자도 MASM 방식에서만 지원

반면, sizeof, 구조체 및 배열 접근 연산자(-> . [])와 형변환 연산자 등은 C++ 방식에서만 지원합니다.

6: kd> ? sizeof(nt!_EPROCESS)
Couldn't resolve error at 'sizeof(nt!_EPROCESS)'

6: kd> ?? sizeof(nt!_EPROCESS)
unsigned int64 0xb80

6: kd> !process 0 0 notepad.exe
PROCESS ffffb0888f7bd0c0
    SessionId: 2  Cid: c508    Peb: 7f16d76000  ParentCid: 1b38
    DirBase: a75e6000  ObjectTable: ffffa08bafb18540  HandleCount: 929.
    Image: Notepad.exe

// MASM 방식에서는 구조체 멤버 접근을 위한 문법이 없으므로 dt 명령어 자체에서 제공하는 기능 사용
6: kd> dt _EPROCESS ffffb0888f7bd0c0 UniqueProcessId
nt!_EPROCESS
   +0x440 UniqueProcessId : 0x00000000`0000c508 Void

// C++ 방식에서는 구조체로의 형변환이 가능하고, 그것의 멤버 접근 가능
6: kd> ?? ((nt!_EPROCESS *)0xffffb0888f7bd0c0)->UniqueProcessId
void * 0x00000000`0000c508

6: kd> ?? @$proc->UniqueProcessId
void * 0x00000000`0000c508

이 외에도 C++ 방식에선 그 나름대로 잘 알려진 매크로 함수에 준하는 #CONTAINING_RECORD(), #FIELD_OFFSET(), #RTL_CONTAINS_FIELD(), #RTL_FIELD_SIZE(), #RTL_NUMBER_OF(), #RTL_SIZEOF_THROUGH_FIELD() 등의 몇 가지 함수를 지원합니다.

// C++ 방식에서만 지원
6: kd> ?? #FIELD_OFFSET(nt!_EPROCESS, UniqueProcessId)
long 0n1088




자, 그럼 여기까지 잘 이해했다면 지난 글의 마지막에 "win32k 서비스 함수"를 WinDbg 수식 한 줄로 찾는 실습을 해보겠습니다. ^^

// 실습 환경: Windows 10 (버전 10.0.19045.5247)
// 
// 함수표에서 NtGdiInvertRgn의 함수 번호는 Windows 10 환경에서 0x1064

// 1) 0x1064 함수 번호에서 0x1000을 뺀 오프셋을 구하고,
2: kd> dd /c1 win32k!W32pServiceTable + 4*(0x1064 - 0x1000) L1
ffffe8d1`72e59190  ff934140

// 2) 8바이트로 부호 확장
ff934140 ==> ffffffff`ff934140

// 3) 4비트 signed 우측 시프트
ffffffff`ff934140 ==> ffffffff`fff93414

// 4) 최종 주소 평가
2: kd> u win32k!W32pServiceTable + ffffffff`fff93414 L1
win32k!NtGdiInvertRgn:
ffffe8d1`72dec414 4883ec28        sub     rsp,28h

우선, MASM 방식으로 평가를 해볼까요? ^^

1) 단계는 해당 포인터가 가리키는 값에서 4바이트를 취해야 하는데요, 이를 위해 dwo 명령을 사용해 다음과 같이 작성할 수 있습니다.

// W32pServiceTable 심벌 풀이를 위해 프로세스 문맥으로 변경

// 대상 주소에서 4바이트 값을 반환
6: kd> ? dwo(win32k!W32pServiceTable + 4*(0x1064 - 0x1000))
Evaluate expression: 4287840576 = 00000000`ff934140

// poi, qwo 명령어의 경우 8바이트를 읽어 반환
6: kd> ? poi(win32k!W32pServiceTable + 4*(0x1064 - 0x1000))
Evaluate expression: -30098576766713536 = ff951180`ff934140

6: kd> ? qwo(win32k!W32pServiceTable + 4*(0x1064 - 0x1000))
Evaluate expression: -30098576766713536 = ff951180`ff934140

위의 값을 이제 2) 단계로 8바이트 부호 확장을 해야 하는데, 이게 좀 어렵습니다. 왜냐하면, dwo는 4바이트를 값을 주소로부터 읽어내기는 하지만 값 자체는 8바이트로 반환하기 때문에 부호 확장을 하려고 해도 상위 4바이트가 0으로 채워져 있어 음수 값으로 인식되지 않습니다.

어쩔 수 없이, 이런 경우에는 좌측 shift 연산자 + signed 우측 shift 연산자를 혼합해 동일한 효과를 내야 합니다.

// 4바이트 값을 8바이트로 부호 확장하는 효과

6: kd> ? dwo(win32k!W32pServiceTable + 4*(0x1064 - 0x1000)) << 0n32 >>> 0n32
Evaluate expression: -7126720 = ffffffff`ff934140

그럼 자연스럽게 3) 단계도 shift 크기만 늘리는 것으로 해결됩니다.

6: kd> ? dwo(win32k!W32pServiceTable + 4*(0x1064 - 0x1000)) << 0n32 >>> 0n36
Evaluate expression: -445420 = ffffffff`fff93414

마지막으로 4) 단계에서는 위에서 구한 주솟값을 그대로 "u" 명령어에 적용해 완성할 수 있습니다.

// Windows 10의 경우 win32k의 0x1064 함수는 NtGdiInvertRgn

0: kd> u win32k!W32pServiceTable + (dwo(win32k!W32pServiceTable + 4*(0x1064 - 0x1000)) << 0n32 >>> 0n36) L1
win32k!NtGdiInvertRgn:
fffffb93`0e2ac414 4883ec28        sub     rsp,28h

이제 동일한 과정을 C++ 방식으로 해볼까요? ^^

1) 단계는 심벌 해석을 MASM 방식으로 바꿔서 처리하는 것만 신경 쓰면 되는데요,

6: kd> ?? *(long *)(@@(win32k!W32pServiceTable) + 4*(0x1064 - 0x1000))
long 0n-7126720

6: kd> ? 0n-7126720
Evaluate expression: -7126720 = ffffffff`ff934140

재미있게도 MASM 방식과는 달리 4바이트 값을 읽기 위해 (long *) 형변환으로 처리했기 때문에 자연스럽게 8바이트로 부호 확장까지 이뤄져 2) 단계가 필요 없게 되었습니다. 따라서 곧바로 3) 단계 처리로 갈 수 있는데요, 문제는 C++ 방식의 경우 signed right shift 연산을 지원하지 않기 때문에 여기서 다시 MASM 방식과 혼용을 해야 합니다.

6: kd> ?? @@( @@( *(long *)(@@(win32k!W32pServiceTable) + 4*(0x1064 - 0x1000)) ) >>> 4 )
unsigned int64 0xffffffff`fff93414

==> ?? @@masm( @@c++( *(long *)(@@masm(win32k!W32pServiceTable) + 4*(0x1064 - 0x1000)) ) >>> 4 )

갑자기 복잡해졌죠? ^^ 어쨌든, 저렇게 평가한 주솟값을 4) 단계에서 u 명령어로 적용하면 되는데요, 이 과정에서 다시 식 평가 방식을 고려해야 합니다.

왜냐하면, "??" 명령어와 "u" 명령어는 각자 고유의 명령어이기 때문에 "?? u ..."와 같은 식으로 명령을 사용할 수는 없습니다. 즉, 독자적으로 "u" 명령어만 사용해야 하는데요, 그렇다면 이제 u 명령어 내부에서 식 평가를 하는 것이기 때문에 WinDbg의 기본 방식인 MASM 방식으로 평가가 됩니다.

따라서 최종 식은 다음과 같이 작성하면 됩니다.

6: kd> u win32k!W32pServiceTable + ( @@( *(long *)( @@(win32k!W32pServiceTable) + 4*(0x1064 - 0x1000) ) ) >>> 4 ) L1
win32k!NtGdiInvertRgn:
fffffb93`0e2ac414 4883ec28        sub     rsp,28h

==> u win32k!W32pServiceTable + ( @@c++( *(long *)( @@masm(win32k!W32pServiceTable) + 4*(0x1064 - 0x1000) ) ) >>> 4 ) L1

물론, 기본 평가 방식이 C++로 설정됐다면 이렇게 바꿔서 처리할 수 있고!

6: kd> .expr /s c++
Current expression evaluator: C++ - C++ source expressions

6: kd> u @@(win32k!W32pServiceTable) + @@( @@( *(long *)(@@(win32k!W32pServiceTable) + 4*(0x1064 - 0x1000)) ) >>> 4 ) L1
win32k!NtGdiInvertRgn:
fffffb93`0e2ac414 4883ec28        sub     rsp,28h

음... ^^; 의외로 복잡하죠? 왠지 저런 수식을 단 번에 만드는 것은 쉽지 않을 듯합니다. ^^ 어쨌든 저렇게 완성했으면, 마지막으로 사용자 정의 가상 레지스터를 곁들이면 그럴싸하게 공식처럼 사용하는 것도 가능합니다.

// 0x1064 함수 번호의 win32k 서비스 함수를 찾고 싶다면?
0: kd> r $t0 = 0x1059

6: kd> u win32k!W32pServiceTable + (dwo(win32k!W32pServiceTable + 4*($t0 - 0x1000)) << 0n32 >>> 0n36) L1
win32k!NtGdiPatBlt:
fffffb93`0e2ac67c 4883ec48        sub     rsp,48h

6: kd> u win32k!W32pServiceTable + ( @@( *(long *)(@@(win32k!W32pServiceTable) + 4*(@@($t0) - 0x1000)) ) >>> 4 ) L1
win32k!NtGdiPatBlt:
fffffb93`0e2ac67c 4883ec48        sub     rsp,48h




[이 글에 대해서 여러분들과 의견을 공유하고 싶습니다. 틀리거나 미흡한 부분 또는 의문 사항이 있으시면 언제든 댓글 남겨주십시오.]

[연관 글]






[최초 등록일: ]
[최종 수정일: 1/11/2025]

Creative Commons License
이 저작물은 크리에이티브 커먼즈 코리아 저작자표시-비영리-변경금지 2.0 대한민국 라이센스에 따라 이용하실 수 있습니다.
by SeongTae Jeong, mailto:techsharer at outlook.com

비밀번호

댓글 작성자
 




... 136  137  138  139  140  141  142  143  [144]  145  146  147  148  149  150  ...
NoWriterDateCnt.TitleFile(s)
1454정성태5/31/201326273Java: 15. Java 7 Control Panel 실행시키는 방법
1453정성태5/22/201325315기타: 32. Microsoft FTP 사이트에 접속하는 방법
1452정성태5/21/201333018Windows: 73. TabProcGrowth 값 삭제 후 IE를 실행시키면 다시 복원되는 경우 [3]
1451정성태5/17/201331929Windows: 72. 윈도우 서버 2012 기초 사용법
1450정성태5/16/201322733오류 유형: 176. SQL10007N Message "0" could not be retrieved. Reason code: "3"
1449정성태5/15/201329842오류 유형: 175. SpeechRecognitionEngine 사용 시 오류 유형 2가지
1448정성태5/14/201324835VC++: 68. #pragma warning(disable: ...)로 오류 제어가 안된다면?
1447정성태5/3/201326517개발 환경 구성: 191. Debugging Tools for Windows 독립 설치 버전 [1]
1446정성태4/30/201327314.NET Framework: 368. Encoding 타입의 대체(fallback) 메카니즘 [1]
1445정성태4/26/201325523디버깅 기술: 54. NT 서비스의 Main 메서드 안에서 Process.GetProcessesByName 호출 시 멈춤 현상 [1]
1444정성태4/26/201329515기타: 31. Internet Explorer: 자바스크립트로 숨겨진 파일 다운로드 경로를 알아내는 방법 [1]
1443정성태4/24/201325220개발 환경 구성: 190. Azure PaaS 웹 응용 프로그램 배포 후 SMTP 서버 구성 [2]
1442정성태4/21/201328791기타: 30. 마이크로소프트 워드의 CPU 점유 현상으로 글자 입력이 느려졌다면? [1]
1441정성태4/21/201335383.NET Framework: 367. LargeAddressAware 옵션이 적용된 닷넷 32비트 프로세스의 가용 메모리 [14]
1440정성태4/19/201324116오류 유형: 174. dumpbin.exe 실행시 mspdb110.dll 로드 오류
1439정성태4/18/201327972VS.NET IDE: 76. Visual Studio 2012와 Itanium 빌드 옵션 [2]
1438정성태4/17/201327390.NET Framework: 366. 다른 프로세스에 환경 변수 설정하는 방법 - 두 번째 이야기 [1]파일 다운로드1
1437정성태4/17/201327607VC++: 67. CRT(C Runtime DLL: msvcr...dll)에 대한 의존성 제거
1436정성태4/17/201332998.NET Framework: 365. Local SYSTEM 권한으로 코드를 실행하는 방법파일 다운로드1
1435정성태4/15/201341887Windows: 71. ad-hoc 보다 더 편리한 "가상 Wifi" 를 이용한 인터넷 공유 [2]
1434정성태4/9/201323160오류 유형: 173. TFS 서버의 이벤트 로그 오류 - WebHost failed to process a request. Parameter name: certificate
1433정성태4/9/201323452개발 환경 구성: 189. TFS에 설치된 SharePoint 의 PowerShell 콘솔 띄우는 방법
1432정성태4/5/201324468오류 유형: 172. System.Web.PipelineModuleStepContainer.GetEventCount 에서 NullReferenceException 이 발생한다면?
1431정성태4/5/201325086기타: 29. 부팅 가능한 (외장) HDD를 기존 부팅 메뉴에 추가하는 방법
1430정성태4/4/201326979제니퍼 .NET: 23. 모바일용 웹 사이트에서 발생하는 응답 시간 지연 현상 [5]파일 다운로드1
1429정성태3/29/201323343개발 환경 구성: 188. SCOM 2012 - ASP.NET 모니터링 방법
... 136  137  138  139  140  141  142  143  [144]  145  146  147  148  149  150  ...