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:
4: int 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
|
31: int 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
31: void* __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로 보면 진행이 어느정도 됐는지 확인 가능하다.