Microsoft MVP성태의 닷넷 이야기
디버깅 기술: 216. WinDbg - 2가지 유형의 식 평가 방법(MASM, C++) [링크 복사], [링크+제목 복사],
조회: 3597
글쓴 사람
정성태 (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

비밀번호

댓글 작성자
 




1  2  3  4  5  6  7  8  9  10  11  12  13  [14]  15  ...
NoWriterDateCnt.TitleFile(s)
13591정성태4/2/20249428닷넷: 2234. C# - WPF 응용 프로그램에 Blazor App 통합파일 다운로드1
13590정성태3/31/20249221Linux: 70. Python - uwsgi 응용 프로그램이 k8s 환경에서 OOM 발생하는 문제
13589정성태3/29/20249605닷넷: 2233. C# - 프로세스 CPU 사용량을 나타내는 성능 카운터와 Win32 API파일 다운로드1
13588정성태3/28/202410660닷넷: 2232. C# - Unity + 닷넷 App(WinForms/WPF) 간의 Named Pipe 통신 [2]파일 다운로드1
13587정성태3/27/20249538오류 유형: 900. Windows Update 오류 - 8024402C, 80070643
13586정성태3/27/202412384Windows: 263. Windows - 복구 파티션(Recovery Partition) 용량을 늘리는 방법
13585정성태3/26/202410100Windows: 262. PerformanceCounter의 InstanceName에 pid를 추가한 "Process V2"
13584정성태3/26/202411669개발 환경 구성: 708. Unity3D - C# Windows Forms / WPF Application에 통합하는 방법 [9]파일 다운로드1
13583정성태3/25/20249699Windows: 261. CPU Utilization이 100% 넘는 경우를 성능 카운터로 확인하는 방법
13582정성태3/19/202410815Windows: 260. CPU 사용률을 나타내는 2가지 수치 - 사용량(Usage)과 활용률(Utilization)파일 다운로드1
13581정성태3/18/202410477개발 환경 구성: 707. 빌드한 Unity3D 프로그램을 C++ Windows Application에 통합하는 방법
13580정성태3/15/20249461닷넷: 2231. C# - ReceiveTimeout, SendTimeout이 적용되지 않는 Socket await 비동기 호출파일 다운로드1
13579정성태3/13/202410410오류 유형: 899. HTTP Error 500.32 - ANCM Failed to Load dll
13578정성태3/11/202411145닷넷: 2230. C# - 덮어쓰기 가능한 환형 큐 (Circular queue)파일 다운로드1
13577정성태3/9/202411077닷넷: 2229. C# - 닷넷을 위한 난독화 도구 소개 (예: ConfuserEx)
13576정성태3/8/202410344닷넷: 2228. .NET Profiler - IMetaDataEmit2::DefineMethodSpec 사용법
13575정성태3/7/202410802닷넷: 2227. 최신 C# 문법을 .NET Framework 프로젝트에 쓸 수 있을까요?
13574정성태3/6/202410282닷넷: 2226. C# - "Docker Desktop for Windows" Container 환경에서의 IPv6 DualMode 소켓
13573정성태3/5/20249467닷넷: 2225. Windbg - dumasync로 분석하는 async/await 호출
13572정성태3/4/20249470닷넷: 2224. C# - WPF의 Dispatcher Queue로 알아보는 await 호출의 hang 현상파일 다운로드1
13571정성태3/1/20249434닷넷: 2223. C# - await 호출과 WPF의 Dispatcher Queue 동작 확인파일 다운로드1
13570정성태2/29/202410077닷넷: 2222. C# - WPF의 Dispatcher Queue 동작 확인파일 다운로드1
13569정성태2/28/202410180닷넷: 2221. C# - LoadContext, LoadFromContext 그리고 GAC파일 다운로드1
13568정성태2/27/20249719닷넷: 2220. C# - .NET Framework 프로세스의 LoaderOptimization 설정을 확인하는 방법파일 다운로드1
13567정성태2/27/20249630오류 유형: 898. .NET Framework 3.5 이하에서 mscoree.tlb 참조 시 System.BadImageFormatException파일 다운로드1
13566정성태2/27/20249543오류 유형: 897. Windows 7 SDK 설치 시 ".NET Development" 옵션이 비활성으로 선택이 안 되는 경우
1  2  3  4  5  6  7  8  9  10  11  12  13  [14]  15  ...