WinDbg 디버깅2015. 3. 22. 22:48
반응형

이번 포스팅에서는 지난 번에 다뤘던 수동으로 64비트 콜스택 추적하기의 이론적인 배경을 알아본다.


지난번 포스팅에서 함수 하나가 사용하는 스택의 크기를 계산하기 위한 공식은 다음과 같았다.


stacksize = 리턴주소 push 크기 + 함수 프롤로그에서 사용한 push 크기 + 함수 프롤로그에서 rsp를 빼는 크기


이런 계산이 가능한 이유는 함수에서 사용하는 스택 크기가 항상 정해져 있기 때문이다.


이를 이해하기 위해서는 일단 64비트의 기본적인 스택 구조를 이해해야 한다.

그림으로 표현해 보면 다음과 같다.



 리턴주소

특수목적 임시저장

(레지스터, 보안쿠키 등)

... 

지역변수

지역변수

지역변수

 ...

... 

파라미터6

 파라미터5

 파라미터4

 파라미터3

 파라미터2

 파라미터1

리턴주소


<= 상위 함수의 rsp

상위 함수의 리턴 주소


함수 내부에서 사용하는 스택 영역







함수 내부에서 호출하는 Sub 함수 중 파라미터가 가장 많은 함수의 파라미터만큼 예약한 공간

(최소 4개 예약)




<= 이 함수의 rsp (파라미터 1의 스택 주소)

Sub 함수 호출시 저장되는 이 함수의 리턴 주소



어떤 함수가 실행 중일 때 스택을 사용하는 형태를 보면 그림과 같이 구성된다.

특별한 경우를 제외하고는 대부분 이와 같은 형태를 보인다.

따라서 이렇게 사용되는 원리를 이해하면 수동으로 콜스택 추적이 가능해 진다.


각 영역들에 대해 알아보면 다음과 같다.


1. 상위 함수 리턴 주소

설명이 필요없이 이 함수를 호출할 때 스택에 저장되는 호출자 주소다.



2. 함수 내부에서 사용하는 스택 영역

지역변수, 임시 저장 영역 등으로 컴파일러가 구성한 스택 영역이다.

컴파일 시에 컴파일러가 이 영역의 크기를 계산하기 때문에 컴파일 시에 크기가 정해진다.



3. Sub 함수 파라미터 예약 공간

이 함수 내부에서 여러 개의 Sub 함수를 호출할 수 있을 텐데 그 중에서 가장 많은 파라미터 수 만큼 예약해 놓는다. 컴파일 시에 컴파일러가 계산해 놓을 수 있다.


함수 내부에서 사용하는 스택 크기는 2번하고 3번을 더한 값이다.

컴파일러는 컴파일시 함수 프롤로그에서 push와 sub rsp로 2, 3번을 구성해 준다.


컴파일러가 함수에서 사용하는 스택의 크기를 미리 계산해서 함수 프롤로그에 코드를 만들어 주기 때문에...

거꾸로 함수 프롤로그를 보고 스택을 사용하는 크기를 계산해서 함수가 사용하는 스택 크기를 알 수 있는 것이다. 



4. 최소 4개 예약?

함수 내부에서 호출하는 Sub 함수들이 파라미터를 1개 밖에 받지 않는다고 하더라도 이 파라미터 공간은 최소 4개가 예약된다. 4개로 정해지는 이유는 64비트에서 파라미터 전달이 4개까지는 rcx, rdx, r8, r9 레지스터를 이용하기 때문인 것 같다. Sub 함수의 파라미터가 5개라면 5개의 파라미터를 저장할 공간을 예약해 놓는다.


재미있는 것은 Sub 함수를 호출할 때 현재 함수에서는 파라미터1~파라미터4 예약 공간을 채우지 않는다는 것이다. 파라미터는 이미 rcx, rdx, r8, r9에 넣어 놨으므로 스택에 다시 저장하지 않는다. 다만 파라미터가 5개 이상인 경우는 스택을 통해 파라미터를 전달하기로 되어 있으니 파라미터5 이후는 스택에 저장해 준다.


파라미터가 7개인 PsCreateSystemThread를 호출하는 코드를 보면서 확인해 본다.


fffff800`01795328 488364243000    and     qword ptr [rsp+30h],0

fffff800`0179532e 488d055bfeffff  lea     rax,[nt!InbvRotateGuiBootDisplay]

fffff800`01795335 488d4c2458      lea     rcx,[rsp+58h]

fffff800`0179533a 4889442428      mov     qword ptr [rsp+28h],rax

fffff800`0179533f 488364242000    and     qword ptr [rsp+20h],0

fffff800`01795345 4533c9          xor     r9d,r9d

fffff800`01795348 4533c0          xor     r8d,r8d

fffff800`0179534b 33d2            xor     edx,edx

fffff800`0179534d e802a21900      call    nt!PsCreateSystemThread 


첫번째 라인은 마지막 7번째 파라미터 스택 위치인 [rsp+30h]에 0을 저장하는 것이다. 네번째 라인에서 [rsp+28h]에 6번째 파라미터를 저장하고 다섯번째 라인에서 [rsp+20h]에 5번째 파라미터를 저장한다.

첫번째 파라미터부터 네번째 파라미터까지는 스택에 저장하는 모양은 볼 수 없고 rcx, edx, r8d, r9d에 저장하는 모양이 나타나 있다.


그러면 예약해 놓은 앞 네개의 파라미터 스택 공간은 언제 사용할까?

MSDN 상으로는 호출된 Sub 함수가 레지스터로 전달된 파라미터를 스택에 저장하고 싶으면 사용할 수 있다고 되어 있다. 디버깅을 편하게 하기 위해 Sub 함수 프롤로그에서 레지스터로 전달된 파라미터를 여기에 저장하게 할 수도 있다고 설명은 하고 있지만... 실제로 대부분 이 영역은 사용하지 않아서 쓰레기값만 저장되어 있다. 그래서 콜스택을 디버깅할 때 함수간에 전달된 파라미터를 찾기가 매우 힘든 것이다.



5. 이 함수의 rsp

64비트에서 함수가 실행될 때 사용하는 스택 베이스는 rsp다.

32비트에서는 함수에 진입하면 ebp를 스택 베이스로 잡고 함수 내부에서 사용하는 스택은 [ebp-xx]로 접근한다. 

하지만 64비트에서는 함수에 진입하면 이 함수에서 사용하는 스택 크기를 모두 계산해서 미리 빼놓고 rsp에 저장한 후에 [rsp+xx]로 접근한다.


그림에서 알 수 있듯이 rsp는 호출할 함수에 전달할 파라미터 1의 주소를 가리키게 된다. 

그래서 다음 위치는 항상 다음과 같은 의미를 가진다.


[rsp+00h] : 호출 함수에 전달할 파라미터 1을 저장

[rsp+08h] : 호출 함수에 전달할 파라미터 2를 저장

[rsp+10h] : 호출 함수에 전달할 파라미터 3을 저장

[rsp+18h] : 호출 함수에 전달할 파라미터 4를 저장


하지만 현재 함수에서 이 위치를 참조하는 일은 없다. ㅠㅠ

그냥 의미만 알아 놓자. 현재 함수가 호출하는 sub 함수에 파라미터 5가 있다면 [rsp+20h]에 저장한다는 것은 알았으니... 



이렇게 해서 64비트 콜스택 구성에 대해 모두 알아봤다.

일단 이정도만 알아도 64비트를 탐험하는 작업이 좀 더 편해질 것 같다.


반응형
Posted by GreeMate