별빛의 낙하지 :: '분류 전체보기' 카테고리의 글 목록 (2 Page)

[C++0x] Move Constructor

C++ 2012. 11. 15. 23:38 Posted by byulbit
/*
	Move Constructor( 이동 생성자 )

	c++0x 에는 r-value reference의 도입으로 인해서 Move Constructor가 생겼다.
	이동 생성자의 특징은 복사 대상의 데이터를 지우는 소유권 이전 방식이다.

	기존에 있던 reference는 그래서 이름이 l-value reference 로 불린다.
	l-value 와 r-value 의 차이는 대입식에서 왼쪽에 오느냐, 오른쪽에 오느냐의 차이인데
	
	간단히 말해서 l-value 는 변수라고 보면 되고, r-value는 상수라고 보면 된다.
	기존에 l-value reference는 l-value들만, 즉 변수들만 가리킬 수 있는 반면에
	r-value reference는 r-value 들만, 즉 상수들을 가리킬 수 있다.
	
	int b = 3;
	int& a = b;
	int& c = 3; // error
	
	int&& d = a; // error
	int&& d = 3; // ok.

	r-value reference가 사실 그냥 보기에는 이동 생성자와 전혀 무관해 보이는데, 이것은 기존에
	복사 생성자로 표현하기 어색했던, 소유권 이전방식을 적절히 표현하기에 적합하다.

	무슨 말이냐면,  기존에 auto_ptr 과 같은 소유권 이전의 스마트 포인터들은 값을 이동시키고 싶어하지만,
	적절한 표현이 없어서, const를 제외시킨 아래와 같은 방식으로 구현을 했었다.

	auto_ptr(auto_ptr& a)
	
	문제는 이것이 복사하려는 것인지, 이동하려는 것인지에 대한 명확한 지표가 되기 힘들다는 것.
	이 문제를 해결한 것이 r-value reference(&&)의 등장이다.
r
*/

#include 

class MoveClass
{
public:
	// Default Constructor
	MoveClass() {
		std::cout << "Default Constructor Call" << std::endl;
	}
	
	// Copy Constructor
	MoveClass(const MoveClass& a)	// 반드시 const로 해야 된다. 그렇지 않으면 Move Constructor가 제대로 호출되지 않는다.  
	{
		std::cout << "Copy Constructor Call" << std::endl;				

	}

	// Move Constructor
	MoveClass(MoveClass&& a)
	{
		std::cout << "Move Constructor Call" << std::endl;

	}
};


// 값이 복사되는 곳에서는 일반적으로 복사 생성자가 호출된다.
MoveClass foo(MoveClass& a)
{
	MoveClass local;
	return local;		// 값이 복사 되는데, 우선순위가 존재한다.
						// MoveClass(const MoveClass& a) 형태의 복사 생성자가 존재하고,
						// MoveClass(MoveClass&& a) 형태의 이동 생성자가 존재하면
						// 이동 생성자를 호출한다.
}

int main()
{
	MoveClass a;	// Default Constructor Call

	foo(a);

	MoveClass b(a);					// Copy Consturctor call
	MoveClass b(std::move(a));		// Move Constructor call

}

[C++] C++ name decoration 혹은 name mangling

C++ 2012. 11. 13. 19:48 Posted by byulbit

C++은 함수 오버로딩을 지원하기 때문에 기존의 C스타일의 이름의 함수로는 함수오버로딩을 지원할 수 없다. 

따라서 함수 이름에 대한 데코레이션(decoration) 혹은 맹글링(mangling)이 필요하다.

(데코레이션과 맹글링은 이름을 꾸미는 것, 섞는 것이라는 그런 의미다.)

  

*네임 맹글링 자체가 함수를 구분해주기 위한 용도로써라고 이해하기 되면 약간 거시기 하다.

정확히 이야기하면 C에서도 함수를 부를때 네임 맹글링을 한다. 

"_"를 함수 이름에 붙이는데,  C++기준으로 보면 이건 네임맹글링 수준이 매우 미미하다.


여기서는 함수 오버로딩을 지원하기 위한 측면에서 살펴보겠다.

name mangling, 혹은 name decoration을 하기 위한 것은 컴파일러가 결정하게 되는데,

일반적으로 함수의 파라미터로 함수 지원한다고 생각했었으나 실제 테스트 해보니 그렇지 않았다.

VC++ Compiler에서 반환값도 네임 맹글링에 포함된다.

예를 들어서 아래는 반환값 test2(void) 의 함수 시그니처이다.

 

int __cdecl test2(void)" (?test2@@YAHXZ) -H(int return)

void __cdecl test2(void)" (?test2@@YAXXZ) -X(void return)

float __cdecl test2(void)" (?test2@@YAMXZ) -M(float return)

char __cdecl test2(void)" (?test2@@YADXZ) -D(char return)


보다시피 반환값이 바뀌게 되면 함수의 시그니처 또한 바뀐다.

약간 재밌는 것은 VS에서 C++에서 함수를 호출하는데, 함수의 원형 선언이 없으면 아래와 같은 에러를 
ClCompile 단계에서 낸다. ( C에서는 함수의 선언과 관계가 없이  정의가 존재하면, 함수의 선언과 여부없이 에러를 내지 않는다.)

error C3861: 'test2': identifier not found

위의 에러는 test2 함수를 선언하지 않은 경우로, test2에 대한 identifier 가 없다는 에러인데, 컴파일 단계, 즉 번역 단계에서 에러가 나는 것이라고 추론할 수 있다. 

  또한 C++에서는 이름이 같은 함수끼리의 함수 오버로딩 집합이 존재하며, 오버로딩 집합을 통해서 함수를 부를때 호출하는 측의 시그니처가 동일하지 않아도 오버로딩 해석에 따라서 함수를 부를 수가 있다.

함수에 대한 다른 오버로드한 함수가 존재해서 부르는 함수의 파라미터 개수가 맞는 것이 없다면 아래와 같은 오률르 낸다.
error C2661: 'test2' : no overloaded function takes 3 arguments


함수가 선언이 존재하고, 해당 함수의 정의가 존재하지 않으면 외부 심볼을 찾을 수 없다고 링커 오류를 낸다.
error LNK2019: unresolved external symbol "int __cdecl test3(void) )

#include <stdio.h>


void test2();

int main()
{
//printf("%d", test2());
test2();

}


그렇다면 반환값만 다른 함수의 선언이 있다면 어떻게 될까?
이 경우 아래와 같은 에러를 낸다.
 error LNK2019: unresolved external symbol "void __cdecl test2(void)" (?test2@@YAXXZ) referenced in function _main

이것이 에러가 나는 지점은 함수의 선언이 존재하는데, 정확히 일치하는 해당하는 함수의 정의를 찾지 못했다는 의미다.

자 그러면 A라는 함수의 원형을 선언해놓고, 호출하는 측에서 B라는 함수를 호출, 다른 오브젝트 파일에 B 함수를 정의하면 어떻게 될까?

void test2의 정의를 넣어보았다.

// test2.cpp
int test2()
{
printf("dfdfdf");
return 0;
}

void test2()
{
printf("111111");
}


그러자 이번에는 아래와 같은 에러 메시지가 나온다. 
error C2556: 'void test2(void)' : overloaded function differs only by return type from 'int test2(void)'
error C2371: 'test2' : redefinition; different basic types

즉 오버로딩 함수들은 return type이 다른 것은 인정하지 않는 다는 것을 알 수 있다.
그래서 매개변수가 있는 오버로드 함수를 추가해보았다.

int test2(int a, int b)
{
return a + b;
}

부르는 쪽에서 보았을 때 함수의 선언이 존재하지 않아서 아래와 같은 에러를 낸다.
error C2660: 'test2' : function does not take 2 arguments 

이것은 함수의 오버로딩 집합이 선언에 의해서 판별된다는 것을 의미한다.
C++은 함수의 원형과 관련된 존재가 필수적이라는 것을 알 수 있다.
C의 경우 함수의 심볼이 함수의 이름으로 한정된다.  
즉 여러 오브젝트 파일을 링크 할때, 함수의 이름이 같다면, 해당하는 함수를 추가하기가 힘들다.
아래와 같이 심볼이 중복 정의 되어 있다고 에러를 내기 때문이다.
 fatal error LNK1169: one or more multiply defined symbols found

그렇다면 C++은 어떠할까?
C++의 경우 함수원형의 선언이 필수적이지만, 시그니처가 일치한다면 해당하는 함수를 부를 수 있으며,
오버로딩을 이용하여 함수의 이름이 같지만, 매개변수가 다른 것들을 추가할 수 있다.
물론 이 경우도 함수를 사용할시에 사용할 함수의 선언이 필수적이다.
 

C에서는 부르는 쪽에서 해당 함수가 존재하지 않으면 당당히 링커에서 오류를 낸다. 
 error LNK2019: unresolved external symbol _test1
위의 에러를 보면 알겠지만, "_함수이름" 으로 C에서는 네임 맹글링을 하며, 
즉 C는 오버로딩이 지원이 안된다.
하지만 해당하는 심볼이 존재한다면 호출이 가능한데 예를 들어서 함수의 정의 자체가

int test(int a)
{
printf("test(int a)");
return 0;
}

이렇게 정의가 되어 있더라도, 해당하는 함수를 다른 매개변수로써 호출할 수 있다.
int main()
{
test("Dfdfdfdf",2);

}

위의 코드는 test(int a)를 호출한다. 이것이 가능한 이유는 함수를 호출 할때는 함수의 심볼을 보고 찾기 때문이다.  즉 C에서는 test(int a)나 test(const char*, int) 로 추정되는 것들이나 함수의 심볼 자체는 _test로 같은 것으로 여겨지게 된다.

보통 C++에서 함수의 매개변수와 반환값 등으로 네임 맹글링을 한다고 생각할 때, 그럼 C++에서는 어떻게 가변인자 함수를 만들고 부를까? 라는 의구심이 생길 수 있다.  왜냐면 함수의 심볼이 같아야 함수의 정의를 찾을 수 있고, 따라서 함수 호출이 가능하기 때문이다.
테스트 해본 결과 재밌는 사실은 매개변수의 갯수 뿐만 아니라, 가변인자 함수인 경우의 네임맹글링도 존재하는 것이다.

void sprintfs(char *Result, const char *Format,  ...  ); 

이런 함수가 존재할 경우에

sprintfs("%d %d %d %d %d", &a, &b, &c, &d, &e); 이렇게 함수를 호출하는 경우와
sprintfs("%d %d %d %d", &a, &b, &c, &d);  이런 경우에 대해서 가변 인자 함수가 아니라면 심볼이 일치 하지 않아서 함수를 찾을 수 없어야 한다.

확인해본 결과 가변인자 함수의 경우 시그니처는 sprintfs@@YAXPADPBDZZ 로 동일하며 부르는 쪽의 매개변수가 달라져도,  심볼은 같으므로, 호출이 가능하다.


[PE View] 제작(1)

카테고리 없음 2012. 11. 7. 03:56 Posted by byulbit

PE View를 만드는 과정을 블로그에 업데이트 할 예정이다.



[C/C++] switch의 jump table

C++ 2012. 11. 7. 02:58 Posted by byulbit

switch 문의 경우 case 조건이 4개 이상일때 단순한 비교값 jump가 아니라 jump table을 만들고, 
jump table에 이동할 case값을 기록한다. 
그리고 그 값을 참조하여 이동한다. 

그럼 실제로 한번 살펴보자.

#include <stdio.h>

int main() { int a; scanf("%d", &a); switch(a) { case 0: printf("0000"); break; case 1: printf("1111"); break; case 2: printf("2222"); break; case 3: printf("3333"); break; case 4: printf("4444"); break; case 5: printf("5555"); break; } }

  

8: switch(a)

000A1039 8B 45 F8             mov         eax,dword ptr [a]  

000A103C 89 85 30 FF FF FF    mov         dword ptr [ebp-0D0h],eax  

000A1042 83 BD 30 FF FF FF 05 cmp         dword ptr [ebp-0D0h],5  

000A1049 0F 87 A1 00 00 00    ja          $LN1+17h (0A10F0h)  

000A104F 8B 8D 30 FF FF FF    mov         ecx,dword ptr [ebp-0D0h]  

000A1055 FF 24 8D 30 11 0A 00 jmp         dword ptr  (0A1130h)[ecx*4]  

     9: {

    10: case 0:

    11: printf("0000");

000A105C 8B F4                mov         esi,esp  

000A105E 68 54 31 0A 00       push        offset string "0000" (0A3154h)  

000A1063 FF 15 E0 30 0A 00    call        dword ptr [__imp__printf (0A30E0h)]  

000A1069 83 C4 04             add         esp,4  

000A106C 3B F4                cmp         esi,esp  

000A106E E8 ED 00 00 00       call        _RTC_CheckEsp (0A1160h)  

    12: break;

000A1073 EB 7B                jmp         $LN1+17h (0A10F0h)  

    13: case 1:

    14: printf("1111");

000A1075 8B F4                mov         esi,esp  

000A1077 68 4C 31 0A 00       push        offset string "1111" (0A314Ch)  

000A107C FF 15 E0 30 0A 00    call        dword ptr [__imp__printf (0A30E0h)]  

000A1082 83 C4 04             add         esp,4  

000A1085 3B F4                cmp         esi,esp  

000A1087 E8 D4 00 00 00       call        _RTC_CheckEsp (0A1160h)  

    15: break;

000A108C EB 62                jmp         $LN1+17h (0A10F0h)  

    16: case 2:

    17: printf("2222");

000A108E 8B F4                mov         esi,esp  

000A1090 68 44 31 0A 00       push        offset string "2222" (0A3144h)  

000A1095 FF 15 E0 30 0A 00    call        dword ptr [__imp__printf (0A30E0h)]  

000A109B 83 C4 04             add         esp,4  

000A109E 3B F4                cmp         esi,esp  

000A10A0 E8 BB 00 00 00       call        _RTC_CheckEsp (0A1160h)  

    18: break;

.........

    27: break;

    28: }

    29: 

    30: 

    31: }


000A1039 8B 45 F8             mov         eax,dword ptr [a]  : 변수 a 값을 eax에 대입한다.  

000A103C 89 85 30 FF FF FF    mov         dword ptr [ebp-0D0h],eax     :      eax를 ebp-0D0h에 기록한다.

000A1042 83 BD 30 FF FF FF 05 cmp         dword ptr [ebp-0D0h],5      :      case 범위를 넘기는가 비교

000A1049 0F 87 A1 00 00 00    ja          $LN1+17h (0A10F0h)               : ja(jump above) 5 보다 크면 switch case를 통과한다.


mov         dword ptr [ebp-0D0h], eax : 조건변수 값(eax)을 ebp-0D0h 번지에 기록함을 알 수 있다.

// 여기서는 001AF934~001AF936번지를 쓰고 있음을 알 수 있다. 

001AF934 02 00                add         al,byte ptr [eax]  

001AF936 00 00                add         byte ptr [eax],al  


000A1055 FF 24 8D 30 11 0A 00 jmp      :   dword ptr  (0A1130h)[ecx*4]  명령을 통해서

000A1130 + 8h 에 있는 값으로 jmp를 한다.( dword ptr  (0A1130h)[ecx*4]  )

000A1138h에는 000A108E 주소값이 들어 있으며, 이 주소는 case 2의 시작하는 주소와 같다.


<case 2>

000A1138 8E                   db          8eh  

000A1139 10                   db          10h  

000A113A 0A                   db          0ah  

000A113B 00                   db          00h  


<switch table>

000A1130 5C                   db          5ch  

000A1131 10                   db          10h  

000A1132 0A                   db          0ah  

000A1133 00                   db          00h  

000A1134 75                   db          75h  

000A1135 10                   db          10h  

000A1136 0A                   db          0ah  

000A1137 00                   db          00h  

000A1138 8E                   db          8eh  

000A1139 10                   db          10h  

000A113A 0A                   db          0ah  

000A113B 00                   db          00h  

000A113C A7                   db          a7h  

000A113D 10                   db          10h  

000A113E 0A                   db          0ah  

000A113F 00                   db          00h  

000A1140 C0                   db          c0h  

000A1141 10                   db          10h  

000A1142 0A                   db          0ah  

000A1143 00                   db          00h  

000A1144 D9                   db          d9h  

000A1145 10                   db          10h  

000A1146 0A                   db          0ah  

000A1147 00                   db          00h


    16:  case 2:

    17:  printf("2222");

000A108E 8B F4                mov         esi,esp  

000A1090 68 44 31 0A 00       push        offset string "2222" (0A3144h)  

000A1095 FF 15 E0 30 0A 00    call        dword ptr [__imp__printf (0A30E0h)]  

000A109B 83 C4 04             add         esp,4  

000A109E 3B F4                cmp         esi,esp  




C++ 관련 책을 보면 아래와 같은 코드에서 C++ 컴파일러가 클래스 내에서 생성자가 정의되어 있지 않을 경우, 기본 생성자(default constructor)를 삽입한다고 알려져 있다.

하지만 실제로는 컴파일러 구현체에 따라서 달라지겠지만, 내가 현재 주로 쓰고 있는 VC++에서는 그러한 뻘짓(?)을 하지 않는다.


class Ref
{
public:
	int a;
	int b;

};

int main()
{
	Ref a;
	a.a = 12;
	a.b = 13;
	Ref b = a;

}

실제 어셈블리로 변환된 코드는 아래와 같다.

Ref a;

a.a = 12;

002A101E  mov         dword ptr [a],0Ch  

a.b = 13;

002A1025  mov         dword ptr [ebp-8],0Dh  

Ref b = a;

002A102C  mov         eax,dword ptr [a]  

002A102F  mov         dword ptr [b],eax  

002A1032  mov         ecx,dword ptr [ebp-8]  

002A1035  mov         dword ptr [ebp-18h],ecx  


놀랍지 않은가?  보시다시피 기본 생성자를 부를꺼라고 예상했지만, 실제로는 그러한 코드는 없다.

또한 디폴트 복사 생성자도 존재하지 않음을 알 수 있다.


만약 생성자가 존재하는 상태에서 기본 복사 생성자도 존재할까?

아래의 코드를 디스어셈블 해보았다.

class Ref
{
public:
	int a;
	int b;
	Ref(int a, int b) { cout << "1 "; }
};

int main()
{
	Ref a(1, 2);
	Ref b = a;
}


그 결과 아래와 같은 코드로 변환이 되었음을 알 수 있었다.

Ref a(1, 2);

00EC101E  push        2  

00EC1020  push        1  

00EC1022  lea         ecx,[a]  

00EC1025  call        Ref::Ref (0EC1090h)                     // 생성자 호출 

Ref b = a;

00EC102A  mov         eax,dword ptr [a]  

00EC102D  mov         dword ptr [b],eax  

00EC1030  mov         ecx,dword ptr [ebp-8]  

00EC1033  mov         dword ptr [ebp-18h],ecx  

즉 사용자가 정의한 생성자는 호출되었지만, 복사 생성자는 역시 존재하지 않음을 알 수 있다.

객체의 동적 할당인 경우는 어떻게 될까?

아래의 코드를 보자

class Ref
{
public:
//	Ref(){ cout << "constructor call" << endl; }
//	~Ref() { cout << "destructor call" << endl; }

};

int main()
{
	Ref* p = new Ref;

	delete p;


}

Ref* p = new Ref;

00EA101E  push        1  

00EA1020  call        operator new (0EA106Ch)  

00EA1025  add         esp,4  

00EA1028  mov         dword ptr [ebp-0E0h],eax  

00EA102E  mov         eax,dword ptr [ebp-0E0h]  

00EA1034  mov         dword ptr [p],eax  


delete p;

00EA1037  mov         eax,dword ptr [p]  

00EA103A  mov         dword ptr [ebp-0D4h],eax  

00EA1040  mov         ecx,dword ptr [ebp-0D4h]  

00EA1046  push        ecx  

00EA1047  call        operator delete (0EA1066h)  

00EA104C  add         esp,4  

즉 여전히 기본 생성자는 찾아 볼 수가 없다. 메모리 할당, 해제와 관련된 함수 호출이 있을 뿐이다.  여기서 재미있는 것은 소멸자 또한 없다는 사실이다.

비교를 하기 위해서 아래는 명시적으로 생성자를 지정한 경우의 동적할당 어셈코드이다.


class Ref
{
public:
	Ref(){ cout << "constructor call" << endl; }
	~Ref() { cout << "destructor call" << endl; }

};

int main()
{
	Ref* p = new Ref;

	delete p;


}

Ref* p = new Ref;

0080103D  push        1  

0080103F  call        operator new (801B7Ah)  

00801044  add         esp,4  

00801047  mov         dword ptr [ebp-0F8h],eax  

0080104D  mov         dword ptr [ebp-4],0  

00801054  cmp         dword ptr [ebp-0F8h],0  

0080105B  je          main+70h (801070h)  

0080105D  mov         ecx,dword ptr [ebp-0F8h]  

00801063  call        Ref::Ref (801100h)  

00801068  mov         dword ptr [ebp-10Ch],eax  

0080106E  jmp         main+7Ah (80107Ah)  

00801070  mov         dword ptr [ebp-10Ch],0  

0080107A  mov         eax,dword ptr [ebp-10Ch]  

00801080  mov         dword ptr [ebp-104h],eax  

00801086  mov         dword ptr [ebp-4],0FFFFFFFFh  

0080108D  mov         ecx,dword ptr [ebp-104h]  

00801093  mov         dword ptr [ebp-14h],ecx  


delete p;

00801096  mov         eax,dword ptr [ebp-14h]  

00801099  mov         dword ptr [ebp-0E0h],eax  

0080109F  mov         ecx,dword ptr [ebp-0E0h]  

008010A5  mov         dword ptr [ebp-0ECh],ecx  

008010AB  cmp         dword ptr [ebp-0ECh],0  

008010B2  je          main+0C9h (8010C9h)  

008010B4  push        1  

008010B6  mov         ecx,dword ptr [ebp-0ECh]  

008010BC  call        Ref::`scalar deleting destructor' (801170h)  

008010C1  mov         dword ptr [ebp-10Ch],eax  

008010C7  jmp         main+0D3h (8010D3h)  

008010C9  mov         dword ptr [ebp-10Ch],0  

[C++] 어셈으로 보는 함수 호출(1)

C++ 2012. 11. 6. 23:56 Posted by byulbit

어셈블러 입장에서 C++의 파라미터를 값으로 넘기는 함수(call by value), 레퍼런스로 넘기는 함수(call by reference), 포인터로 넘기는 함수(call by pointer) 의 구현체들은 어떠한 차이가 있을까?


void callByValue(int a )
{

}

void callByReference(int& a)
{

}

void callByPointer(int* a)
{

}

int main()
{
	int number = 1;
	int value = number;
	int* pointer = &value;
	int& reference = value;
	
	int depointer = *pointer;
	int dereference = value;

	callByPointer(pointer);
	callByValue(value);
	callByReference(reference);


}

함수의 구현체는 파라미터의 종류에 관계 없이 동일한 방식이었다.

void callByPointer(int* a)

{

01091060  push        ebp  

01091061  mov         ebp,esp  

01091063  sub         esp,0C0h  

01091069  push        ebx  

0109106A  push        esi  

0109106B  push        edi  

0109106C  lea         edi,[ebp-0C0h]  

01091072  mov         ecx,30h  

01091077  mov         eax,0CCCCCCCCh  

0109107C  rep stos    dword ptr es:[edi]  


}


함수 호출전 변수에 값을 대입하는 코드는 변수의 경우 dword ptr[변수이름]을 통해서 해당하는 변수의 값을 가져와서 mov(복사) 하는 코드임을 알 수 있으며, pointer와 reference의 경우 lea 명령(load effective address)을 이용한다.

Load Effective Address란 의미로 다음의 인자를 주소값으로 인식합니다.


int number = 1;

010910AE  mov         dword ptr [number],1          // 1을 number가 가리키는 곳에 대입한다.

int value = number;

010910B5  mov         eax,dword ptr [number]      // number가 가리키는 곳의 값을 eax에 대입한다.

010910B8  mov         dword ptr [value],eax          // eax 값을 value가 가리키는 곳에 대입한다.

int* pointer = &value;

010910BB  lea         eax,[value]                          // value 값을 eax에 대입한다.

010910BE  mov         dword ptr [pointer],eax         // pointer가 가리키는 곳에 eax를 대입한다.

int& reference = value;

010910C1  lea         eax,[value]                          // value 값을 eax에 대입한다.

010910C4  mov         dword ptr [reference],eax     // reference가 가리키는 곳에 eax를 대입한다.

int depointer = *pointer;

010910C7  mov         eax,dword ptr [pointer]  // pointer가 가리키는 값을 eax에 대입한다.

010910CA  mov         ecx,dword ptr [eax]  // eax가 가리키는 값을 ecx에 대입한다.

010910CC  mov         dword ptr [depointer],ecx  // ecx 값을 depointer가 가리키는 값에 대입한다.

int dereference = value;

010910CF  mov         eax,dword ptr [value]      // value가 가리키는 값을 eax에 대입한다.

010910D2  mov         dword ptr [dereference],eax    // eax 값을 dereference에 대입한다.



  지난 3년간 안드로이드와 관련된 부분들은 주로 앱을 만드는 것부터 시작해서 안드로이드 프레임웍까지 관심을 가져왔다.  이 책은 '안드로이드의 모든 것 분석과 포팅' 이후 버전으로 나온 책으로 서문에 따르면, 원래 이전 책에서 다루고 싶었지만, 시간 관계상 미뤄졌던게 드디어 책으로 나왔다고 한다.

  국내에서 안드로이드의 구조를 분석한 책은 몇권 되지 않아서, 개인적으로 다 보았었는데(저자의 이전 책 포함), 편집이나 흐름 자체가 이전 책과 비슷한 맥락이다.

  책은 전반적으로 NDK를 다루고 있고, 현재 나와 있는 NDK로 지원안되는 부분들에 대한 방법(PDK)도 제시하고 있다.  (어차피 NDK나 SDK나 PDK에 포함되는 관계이므로 라는 설명으로)

  책 전반적인 구성은 JNI와 같은 C/C++와 자바를 이어주는 가교부터 시작해서, 빌드하는 방법, NDK 관련해서는 주로 지원되는 라이브러리(OpenGL, Neon 등)와 관련된 예제에 대한 설명이 주를 이룬다.  Makefile 관련해서는 필요한 설명만 되어 있어서,  실습할 때 발생하는 예외적인 경우는 구글링으로 찾아봐야 될 것이다. 

  책을 읽으면서 좋았던 부분중에 하나는 저자가 이전에 실제 삽질한 결과나 고생했던 것에 대한 케이스에 대한 설명이 있다는 것이고(ex) 라이브러리를 불러 오는 과정에 있어서 순서가 꼬인 경우), 전반적으로 학습하기에 정리가 잘 되어 있다는 점이다.(처음 부터 예제를 설명하지 않는다. 필요한 기본 선수 지식에 대한 설명이 되어 있다.)

  책을 읽으면서 인상깊었던 부분은 예전에 과제를 진행하면서 불편하지만, 있는지 조차 몰라서 쓰지 못했던 것들이다.  나같은 경우 JNI쪽의 SWIG을 비롯해서 디버깅쪽에 addr2line, ndk-gdb 등이다.

  또한 중반 이후로 순수(?) NDK를 지나서 PDK로 넘어가게 되면 직접적으로 binder 의 개념자체가 필수불가결하게 되는데, 이러한 설명이 구조와 연관된 부분이라 이 책에서는 많이 다루지 않아서 이 부분은 이책만 봐서는 이해하기 힘들것이다.

  그리고 Appendix 에서 안드로이드와 관련해서 대략적인 설명을 하고 있는데, NDK와 관련해서 큰 그림을 먼저 알고 싶다면, 먼저 봐도 괜찮을 것 같다.

  아쉬운점으로 IT 전공서적의 특징상 책의 소스코드가 사실 그렇게 보기 좋게 나열된 것은 아니다.  소스코드가 끝난 다음에서야 설명이 등장하는 식인데,  주석처럼은 아니지만 중간중간에 설명을 인라인화 시키는 것이 가독성 측면에서 어떨까 생각한다.


<덧붙여>

혹시나 NDK를 모르는 사람들을 위해서 한가지 이야기를 하자면, 기존의 안드로이드 앱 개발방식이 SDK를 통한 자바 개발환경이었다면, NDK는 C/C++ 환경을 제공한다는 것이다. 여기서 NDK에 대해서 자바 코드가 전혀 없이 순수하게 C/C++로 작성하는 방법이라고 잘못 알고 있는 경우도 있는데 그렇지 않다.  java 코드에서 jni 인터페이스를 통해서 C/C++ 라이브러리를 호출하는 것이 NDK이다. 심지어 액티비티를 네이티브 수준으로 끌어 내리는 NativeActivity 예제만 봐도 java 코드에서 C/C++로 작성된 so library를 로드하여 콜백 하는 형태로 되어 있다.


[C++] 헤더는 선언이지 정의가 아니다.

C++ 2012. 10. 10. 04:02 Posted by byulbit

  헤더를 설계할 때는 정의(오직 한번만 사용할 수 있다.)와 선언(여러번 사용할 수 있다.)의 차이를 반드시 기억해야 한다.  다음 문장은 정의이므로 헤더에서는 사용하지 말아야 한다.


extern int ival = 10; // 초기값이 있으므로 정의이다.

double fica_rate; // extern이 없으므로 정의이다.


같은 프로그램에 속한 파일들 가운데 두 개 이상에서 이 정의를 포함하면 여러 개의 정의를 사용했다는 불평과 함께 링커가 오류를 일으킨다.

<예외>

 헤더에 정의를 포함하지 말아야 한다는 규칙에는 예외가 3가지가 있다. 클래스, 컴파일 시점에 값을 알 수 있는 const 객체, inline 함수가 그것인데 이들은 모두 헤더에 정의를 포함한다. 이들 요소는 각 파일별 정의가 똑같을 경우 여러 소스파일에 정의할 수 있다.

왜 그럴까... 컴파일러가 코드를 만들려면 이 요소의 (선언이 아닌) 정의를 알아야 하므로 헤더에 정의해둔다.  예를 들어 클래스 타입 객체를 정의하거나 사용하는 코드를 만들려면 컴파일러는 그 타입에 어떤 멤버가 있는지 알아야 한다.  또한 이 객체에 어떤 연산을 해야 하는지도 알아야 하는데, 이런 정보는 클래스 정의에 들어 있다. const 객체를 헤더에 정의하는 이유는 좀더 설명이 필요하다.


const 객체는 기본적으로 파일에 지역적이다.

  const가 아닌 변수를 전역 사용 범위에서 정의하면 프로그램 전체에서 접근할 수 있다.  한 파일에서 const가 아닌 변수를 정의하고(적절히 선언했다고 가정하면)  다른 파일에서 그 변수를 사용할 수도 있다.



// file_1.cc

int counter; // 정의

// file_2.cc

extern int counter; // file_1의 counter을 사용한다.

++counter;     // file_1에 정의한 counter을 증가시킨다.


다른 변수와 달리(다른 방법을 사용하지 않고) const 변수를 전역 사용 범위에 선언하면 객체를 정의한 파일에 지역적이 된다.  그 변수는 해당 파일에만 존재하며, 다른 파일에서는 접근할 수 없다.


반면에 extern을 사용하면 프로그램 전체에서 const 객체에 접근할 수 있다.

// file_1.cc

// 다른 파일에서 접근할 const 변수를 정의하고 초기화한다.

extern const int bufSize = fcn();

// file_2.cc

extern const int bufSize;     // file_1의 bufSize를 사용한다.

// file_1에 정의한 bufSize를 사용한다.

for(int index = 0; index != bufSize; ++index)

.....



작년인가, 재작년에 행안부에서 자바 시큐어 코딩 가이드라인이라는 이름으로 문서를 배포한 적이 있다. 그 당시에 행안부 가이드라인을 읽었을 때는 구체적 사례 자체보다는 그냥 규정에 대한 모음집 같은 느낌이라서, 굳이 이렇게 코드를 작성해야 될 당위성이나 그 밑바탕에 깔려 있는 원리들에 대해서 알 수가 없어서 좀 아쉬웠다.
 
  이 책은 나름대로 위험성에 대해서 레벨을 정해서 기준을 세우고 있고, 규칙에 대해서 분류를 상당히 잘해놓은 편이다. 거기에 그 해당하는 규칙에 대한 필수 예제들을 도입하고 있고, 그 근본 방식에 대한 설명 자체도 나름 잘 되어 있는 편이다.
 
  나름 재미있게 기억나는 부분의 예를 들어보면, primitive type에서 객체 타입으로 자동으로 변환되는 오토 박싱(auto boxing)과 관련한 코드 지침이 있었는데,jvm마다 == 메서드에 대한 자동으로 기억하는 것이 다를 수 있어서 발생 될 수 있는 문제에 대해서 설명이 되어 있는 부분에 대해서 해당 규칙에 대한 당위성을 잘 설명했다고 생각한다. (이걸 설명하기 위해서 언어 명세뿐만 아니라 jvm의 명세에 대해서 이야기 하고 있다.)
 
 
 번역, 편집 상태도 괜찮은 편이다. 책이 꽤 두꺼움에도 현재 책을 절반이상 읽은 상태에서 평가했을 때, 눈에 띄는 오타가 잘 발견되지 않았고, 오타로 의심했던 단어들에 대해서는 검색결과 그것이 표준어라는 새로운 사실도 알게 되었다.(결괏값(O) 결과값(X))
 
 책의 구성은 비슷한 부류의 책인 C버전의 CERT 프로그래밍 책을잠깐 읽은 적이 있었는데, 분류방식이나 책의 구성 자체가 매우 비슷하다.  먼저 목차와 서문 부분을 읽고, 관심 있는 주제 쪽으로 펼쳐서 읽으면 된다.
각 챕터 별로 첫장에 규칙에 대한 목차가 있고, 목차의 제목, 그리고 해당 페이지가 있다.  그리고 바로 옆장에 위험 평가 요약이라고 우선수위에 따라 찾아볼 수 있게끔 하고 있다.  잠깐 예를 들자면 [ex) LCK10-J. 이중 검사 동기화의 잘못된 형태를 사용하지 않는다. - 415] 개인적으로 애매한 용어에 대한 원어 병어 표기와  원전 참조표기를 병기해 놓은것도 마음에 든다.

  책을 읽으면서 규칙과 사례, 지침, 그리고 설명을 읽으면서 이러한 규칙을 테크닉이라고 부를 수 있겠다고 생각을 했다. 그러한 측면에서 보자면 effective 시리즈나 exceptional 시리즈와 같은 느낌이다. (물론 effective 시리즈와는 다른 점이 있다. 내용 구성적인 측면에서책의 흐름 자체가 스캇 마이어스처럼 옆집 아저씨가 읽어주는 느낌은 아니라는 점이 다르다. )

 
  결론적으로 말하면 이 책에서 말하는 코딩 가이드 라인은 기존의 프로그램을 작성 시에 나중에 추후 다른 사용자가 오용하여 발생할 수 있는 문제들에 대해서 소스코드차원에서 예방하는 것을 말한다.  기본적으로 입력값 필터링과 같은 것에서부터 시작해서, injection 공격을 막기 위한 예방, 자바 언어, jvm의 특성에 관한 것이나, 평소에 좀더 간과하기 쉬운 부분들에서 공부를 하고자 한다면 좋은 지침서가 되리라 생각한다.