본문 바로가기

Programming

Visual Studio의 disassembly로 숨겨진 작업을 찾아보자.

  Visual Studio(VS)에는 Disassembly 라는 기능을 지원한다. 직접 짠 C++ 코드를 디버깅 중에 assembly 코드로 볼 수 있는 기능이다. assembly상에서는 읽기 힘들 뿐이지 왠만한 작업들이 어떻게 작동하는지 숨김 없이 볼 수 있다. VS에서 편의를 위해 제공하는 기능들 또한 볼 수 있다. 그 기능들 중 일부를 살펴보자.
1
2
3
4
5
6
7
8
9
10
11
12
13
#include <iostream>
 
int main(int argc, char* argv[])
{
    char c = sizeof(c);
    short s = sizeof(s);
    long l = sizeof(l);
    float f = sizeof(f);
    int i = sizeof(i);
    double d = sizeof(d);
    return 1;
}
 
cs
 설명이 필요 없는 코드다. 이 코드를 빌드해서 디버깅 해보자.
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
     1#include <iostream>
     2
     3
     4int main(int argc, char* argv[])
     5: {
00007FF60A1D17C0  mov         qword ptr [rsp+10h],rdx  
00007FF60A1D17C5  mov         dword ptr [rsp+8],ecx  
00007FF60A1D17C9  push        rbp  
00007FF60A1D17CA  push        rdi  
00007FF60A1D17CB  sub         rsp,1A8h  
00007FF60A1D17D2  lea         rbp,[rsp+20h]  
00007FF60A1D17D7  mov         rdi,rsp  
00007FF60A1D17DA  mov         ecx,6Ah  
00007FF60A1D17DF  mov         eax,0CCCCCCCCh  
00007FF60A1D17E4  rep stos    dword ptr [rdi]  
00007FF60A1D17E6  mov         ecx,dword ptr [rsp+1C8h]  
00007FF60A1D17ED  lea         rcx,[__81AF6E79_sample@cpp (07FF60A1E1027h)]  
00007FF60A1D17F4  call        __CheckForDebuggerJustMyCode (07FF60A1D1082h)  
     6:     char c = sizeof(c);
00007FF60A1D17F9  mov         byte ptr [c],1  
     7:     short s = sizeof(s);
00007FF60A1D17FD  mov         eax,2  
     7:     short s = sizeof(s);
00007FF60A1D1802  mov         word ptr [s],ax  
     8:     long l = sizeof(l);
00007FF60A1D1806  mov         dword ptr [l],4  
     9:     float f = sizeof(f);
00007FF60A1D180D  movss       xmm0,dword ptr [__real@40800000 (07FF60A1D9BBCh)]  
00007FF60A1D1815  movss       dword ptr [f],xmm0  
    10:     int i = sizeof(i);
00007FF60A1D181A  mov         dword ptr [i],4  
    11:     double d = sizeof(d);
00007FF60A1D1824  movsd       xmm0,mmword ptr [__real@4020000000000000 (07FF60A1D9BB0h)]  
00007FF60A1D182C  movsd       mmword ptr [d],xmm0  
    12
    13:     return 1;
00007FF60A1D1834  mov         eax,1  
    14: }
00007FF60A1D1839  lea         rsp,[rbp+188h]  
00007FF60A1D1840  pop         rdi  
00007FF60A1D1841  pop         rbp  
00007FF60A1D1842  ret
cs
 
 짧은 코드임에도 어셈블리를 안보던 사람들은 보기 힘들 것이다. 다른 부분은 보지 말고 변수 초기화 이전 부분을 살펴
보자. 0CCCCCCCCh라는 값이 눈에 띄지 않는가? VS로 디버깅을 해봤다면 어쩐지 익숙한 값일 것이다. 이 값은 우리가 디버깅하면서 본 쓰레기 값이다. 디버그 모드에서 디버깅할시 초기화 안된 변수들이 이 값을 가지고 있었다. 다음 줄을보면 rep stos    dword ptr [rdi] 라는 코드가 있다.
 rep은 ecx의 값만큼 반복한다는 뜻이다. 문자열 관련 처리이기 때문에 가리키고 있는 주소를 반복한 만큼 증가 시킨다. 그 옆의 stos는 eax값을 가리키는 값에 복사한다는 뜻이다. 현재 가리키는 곳은 edi이므로 edi가 가르키는 곳에다 값을 넣는다는 것을 알 수 있다. edi는 지역변수를 가리키고 있다.
 즉, VS에서는 우리가 변수들을 초기화하기 이전에 0CCCCCCCCh 값을 넣은다는 뜻이다. 이것은 Debug모드로 빌드할때 생기는 일이다. Release 모드로 실행할때는 Debug모드와 다른 코드를 볼 수 있다.
 
1
2
3
4
5
6
7
8
9
10
11
12
     6:     char c = sizeof(c);
     7:     short s = sizeof(s);
     8:     long l = sizeof(l);
     9:     float f = sizeof(f);
    10:     int i = sizeof(i);
    11:     double d = sizeof(d);
    12
    13:     return 1;
00007FF797891000  mov         eax,1  
    14: }
00007FF797891005  ret
 
cs
 이게 Release 모드로 빌드했을때 볼 수 있는 코드다. 앞부분이 사라진 것 뿐만 아니라 값을 넣는 코드마저 없어졌다. 실질적으로 남아있는 코드는 2줄 뿐이다. 앞부분이 사라진 것은 그렇다쳐도 나머지 부분이 왜 사라질까? 그 이유는 Release 모드에서는 최적화를 하기 때문이다. 최적화 할 여지가 있다면(쓸데 없는 코드가 있다면) 알아서 코드를 수정해서 빌드한다.
 이번에는 C++의 포인터 할당부분을 보도록 하자. 생성자와 해제자를 보기 위해 클래스 두 개를 만들었다. 코드를 보도록 하자.
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
class Parent
{
public:
    int i;
    Parent()
    {
 
    }
    ~Parent()
    {
    }
};
 
class Child : public Parent
{
public:
    int j;
    Child()
    {
    }
    ~Child()
    {
 
    }
};
 
int main(int argc, char* argv[])
{
    Parent* Test1 = new Parent;
    Child* Test2 = new Child;
    Parent* Test3 = new Child;
 
    delete Test1;
    delete Test2;
    delete Test3;
 
    return 1;
}
cs
 Child 클래스가 Parent 클래스를 상속 받고 있다. 그리고 테스트를 위해 각각 자신의 자료형에 맞는 포인터에 할당하고 마지막에는 부모 포인터에 자식을 할당하고 있다. 그 뒤로는 해제만 한다. 어셈블리로 어떻게 되는지 보도록 하자.
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
    31int main(int argc, char* argv[])
    32: {
00007FF722E65B10  mov         qword ptr [rsp+10h],rdx  
00007FF722E65B15  mov         dword ptr [rsp+8],ecx  
00007FF722E65B19  push        rbp  
00007FF722E65B1A  push        rdi  
00007FF722E65B1B  sub         rsp,3A8h  
00007FF722E65B22  lea         rbp,[rsp+20h]  
00007FF722E65B27  mov         rdi,rsp  
00007FF722E65B2A  mov         ecx,0EAh  
00007FF722E65B2F  mov         eax,0CCCCCCCCh  
00007FF722E65B34  rep stos    dword ptr [rdi]  
00007FF722E65B36  mov         ecx,dword ptr [rsp+3C8h]  
00007FF722E65B3D  mov         qword ptr [rbp+368h],0FFFFFFFFFFFFFFFEh  
00007FF722E65B48  lea         rcx,[__81AF6E79_sample@cpp (07FF722E71027h)]  
00007FF722E65B4F  call        __CheckForDebuggerJustMyCode (07FF722E610AAh)  
    33
    34:     Parent* Test1 = new Parent;
00007FF722E65B54  mov         ecx,4  
00007FF722E65B59  call        operator new (07FF722E6119Ah)  
00007FF722E65B5E  mov         qword ptr [rbp+208h],rax  
00007FF722E65B65  cmp         qword ptr [rbp+208h],0  
00007FF722E65B6D  je          main+74h (07FF722E65B84h)  
00007FF722E65B6F  mov         rcx,qword ptr [rbp+208h]  
00007FF722E65B76  call        Parent::Parent (07FF722E61424h)  
00007FF722E65B7B  mov         qword ptr [rbp+378h],rax  
00007FF722E65B82  jmp         main+7Fh (07FF722E65B8Fh)  
00007FF722E65B84  mov         qword ptr [rbp+378h],0  
00007FF722E65B8F  mov         rax,qword ptr [rbp+378h]  
00007FF722E65B96  mov         qword ptr [rbp+1E8h],rax  
00007FF722E65B9D  mov         rax,qword ptr [rbp+1E8h]  
00007FF722E65BA4  mov         qword ptr [Test1],rax  
    35:     Child* Test2 = new Child;
00007FF722E65BA8  mov         ecx,8  
00007FF722E65BAD  call        operator new (07FF722E6119Ah)  
00007FF722E65BB2  mov         qword ptr [rbp+248h],rax  
00007FF722E65BB9  cmp         qword ptr [rbp+248h],0  
00007FF722E65BC1  je          main+0C8h (07FF722E65BD8h)  
00007FF722E65BC3  mov         rcx,qword ptr [rbp+248h]  
00007FF722E65BCA  call        Child::Child (07FF722E6141Ah)  
00007FF722E65BCF  mov         qword ptr [rbp+378h],rax  
00007FF722E65BD6  jmp         main+0D3h (07FF722E65BE3h)  
00007FF722E65BD8  mov         qword ptr [rbp+378h],0  
00007FF722E65BE3  mov         rax,qword ptr [rbp+378h]  
00007FF722E65BEA  mov         qword ptr [rbp+228h],rax  
00007FF722E65BF1  mov         rax,qword ptr [rbp+228h]  
00007FF722E65BF8  mov         qword ptr [Test2],rax  
    36:     Parent* Test3 = new Child;
00007FF722E65BFC  mov         ecx,8  
00007FF722E65C01  call        operator new (07FF722E6119Ah)  
00007FF722E65C06  mov         qword ptr [rbp+288h],rax  
00007FF722E65C0D  cmp         qword ptr [rbp+288h],0  
00007FF722E65C15  je          main+11Ch (07FF722E65C2Ch)  
00007FF722E65C17  mov         rcx,qword ptr [rbp+288h]  
00007FF722E65C1E  call        Child::Child (07FF722E6141Ah)  
00007FF722E65C23  mov         qword ptr [rbp+378h],rax  
00007FF722E65C2A  jmp         main+127h (07FF722E65C37h)  
00007FF722E65C2C  mov         qword ptr [rbp+378h],0  
00007FF722E65C37  mov         rax,qword ptr [rbp+378h]  
00007FF722E65C3E  mov         qword ptr [rbp+268h],rax  
00007FF722E65C45  mov         rax,qword ptr [rbp+268h]  
00007FF722E65C4C  mov         qword ptr [Test3],rax  
    37
    38:     delete Test1;
00007FF722E65C50  mov         rax,qword ptr [Test1]  
00007FF722E65C54  mov         qword ptr [rbp+2C8h],rax  
00007FF722E65C5B  mov         rax,qword ptr [rbp+2C8h]  
00007FF722E65C62  mov         qword ptr [rbp+2A8h],rax  
00007FF722E65C69  cmp         qword ptr [rbp+2A8h],0  
00007FF722E65C71  je          main+17Dh (07FF722E65C8Dh)  
    37
    38:     delete Test1;
00007FF722E65C50  mov         rax,qword ptr [Test1]  
00007FF722E65C54  mov         qword ptr [rbp+2C8h],rax  
00007FF722E65C5B  mov         rax,qword ptr [rbp+2C8h]  
00007FF722E65C62  mov         qword ptr [rbp+2A8h],rax  
00007FF722E65C69  cmp         qword ptr [rbp+2A8h],0  
00007FF722E65C71  je          main+17Dh (07FF722E65C8Dh)  
    37
    38:     delete Test1;
00007FF722E65C73  mov         edx,1  
00007FF722E65C78  mov         rcx,qword ptr [rbp+2A8h]  
00007FF722E65C7F  call        Parent::`scalar deleting destructor' (07FF722E6142Eh)  
00007FF722E65C84  mov         qword ptr [rbp+378h],rax  
00007FF722E65C8B  jmp         main+188h (07FF722E65C98h)  /div>
00007FF722E65C8D  mov         qword ptr [rbp+378h],0  /div>
nbsp;   39:     delete Test2;/div>
00007FF722E65C98  mov         rax,qword ptr [Test2]  
00007FF722E65C9C  mov         qword ptr [rbp+308h],rax  
00007FF722E65CA3  mov         rax,qword ptr [rbp+308h]  
00007FF722E65CAA  mov         qword ptr [rbp+2E8h],rax  
00007FF722E65CB1  cmp         qword ptr [rbp+2E8h],0  
00007FF722E65CB9  je          main+1C5h (07FF722E65CD5h)  
00007FF722E65CBB  mov         edx,1  
00007FF722E65CC0  mov         rcx,qword ptr [rbp+2E8h]  
00007FF722E65CC7  call        Child::`scalar deleting destructor' (07FF722E61433h)  
00007FF722E65CCC  mov         qword ptr [rbp+378h],rax  
00007FF722E65CD3  jmp         main+1D0h (07FF722E65CE0h)  
00007FF722E65CD5  mov         qword ptr [rbp+378h],0  
    40:     delete Test3;
00007FF722E65CE0  mov         rax,qword ptr [Test3]  
00007FF722E65CE4  mov         qword ptr [rbp+348h],rax  
00007FF722E65CEB  mov         rax,qword ptr [rbp+348h]  
00007FF722E65CF2  mov         qword ptr [rbp+328h],rax  
00007FF722E65CF9  cmp         qword ptr [rbp+328h],0  
00007FF722E65D01  je        &nb
 
 다른 것은 보지 말고 call 부분만 보도록 하자. call 부분에서 눈에 띄는 것은 operater new와 scalar deleting destructor다.
operater를 사용하지 않았지만, 기본적으로 메모리를 할당할 때 operater new를 호출한다. delete 부분은 특이한데 delete는 scalar deleting destructor안에서 operater delete를 호출한다.
 

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
    1//
     2// new_scalar.cpp
     3//
     4//      Copyright (c) Microsoft Corporation. All rights reserved.
     5//
     6// Defines the scalar operator new.
     7//
     8#include <stdlib.h>
     9#include <vcruntime_new.h>
    10#include <vcstartup_internal.h>
    11
    12// Enable the compiler to elide null checks during LTCG
    13#pragma comment(linker, "/ThrowingNew")
    14
    15////////////////////////////////////
    16// new() Fallback Ordering
    17//
    18// +----------+
    19// |new_scalar<---------------+
    20// +----^-----+               |
    21//      |                     |
    22// +----+-------------+  +----+----+
    23// |new_scalar_nothrow|  |new_array|
    24// +------------------+  +----^----+
    25//                            |
    26//               +------------+----+
    27//               |new_array_nothrow|
    28//               +-----------------+
    29
    30: _CRT_SECURITYCRITICAL_ATTRIBUTE
    31void* __CRTDECL operator new(size_t const size)
    32: {
00007FF6C36923E0  mov         qword ptr [rsp+8],rcx  
00007FF6C36923E5  sub         rsp,38h  
    33:     for (;;)
    34:     {
    35:         if (void* const block = malloc(size))
00007FF6C36923E9  mov         rcx,qword ptr [size]  
    33:     for (;;)
    34:     {
    35:         if (void* const block = malloc(size))
00007FF6C36923EE  call        malloc (07FF6C3691334h)  
00007FF6C36923F3  mov         qword ptr [rsp+20h],rax  
00007FF6C36923F8  cmp         qword ptr [rsp+20h],0  
00007FF6C36923FE  je          operator new+27h (07FF6C3692407h)  
    36:         {
    37:             return block;
00007FF6C3692400  mov         rax,qword ptr [rsp+20h]  
00007FF6C3692405  jmp         operator new+4Bh (07FF6C369242Bh)  
    38:         }
    39
    40:         if (_callnewh(size== 0)
00007FF6C3692407  mov         rcx,qword ptr [size]  
00007FF6C369240C  call        _callnewh (07FF6C3691348h)  
00007FF6C3692411  test        eax,eax  
00007FF6C3692413  jne         operator new+49h (07FF6C3692429h)  
    41:         {
    42:             if (size == SIZE_MAX)
00007FF6C3692415  cmp         qword ptr [size],0FFFFFFFFFFFFFFFFh  
00007FF6C369241B  jne         operator new+44h (07FF6C3692424h)  
    43:             {
    44:                 __scrt_throw_std_bad_array_new_length();
00007FF6C369241D  call        __scrt_throw_std_bad_array_new_length (07FF6C369126Ch)  
    45:             }
    46:             else
00007FF6C3692422  jmp         operator new+49h (07FF6C3692429h)  
    47:             {
    48:                 __scrt_throw_std_bad_alloc();
00007FF6C3692424  call        __scrt_throw_std_bad_alloc (07FF6C36913D9h)  
    49:             }
    50:         }
    51
    52:         // The new handler was successful; try to allocate again...
    53:     }
00007FF6C3692429  jmp         operator new+9h (07FF6C36923E9h)  
    54: }
00007FF6C369242B  add         rsp,38h  
00007FF6C369242F  ret  
cs
 
  operater new의 안을 들어가보면 결국엔 malloc을 호출한다는 점을 알 수 있다. 기본적으로 메모리를 할당하는 것에는 동작에 큰 차이가 없다. delete도 따라가보면 free를 호출한다. delete는 설명이 길어질 것 같아서 생략한다.
  Release 이야기를 더 하자면, Release 모드에서 최적화를 한 코드가 나온다고 이야기 했었다. 문제는 인라인으로 들어간 함수들은 디버깅시에 내부로 진입하지 않는다. call 부분이 없어지기 떄문이다. 이럴때 disassembly로 보면 진행이 어느정도 됐는지 확인 가능하다.