포스트

디스어셈블리 코드로 살펴보는 레퍼런스 변수와 포인터 변수

디스어셈블리 코드를 통해 C++의 전치 연산과 후치 연산의 동작 원리를 살펴 본다

디스어셈블리 코드로 살펴보는 레퍼런스 변수와 포인터 변수

1. 변수, 포인터 변수, 레퍼런스 변수는 내부적으로 어떻게 작동 되어질까?

정수형 변수와 이를 가리키는 정수형 포인터, 그리고 레퍼런스 변수가 내부적으로 어떻게 다루어지고 이들간에 어떤 차이가 있는지 궁굼하지 않은가?

그렇다면, 지금부터 disassembly 코드를 통해 이에 대해 살펴 보자.

디스어셈 포스팅 스크린샷1

다음은 위의 코드를 디스어셈블리 코드로 번역한 것이다. (via ms visual studio 2010)

디스어셈 포스팅 스크린샷2

위에서 우리가 살펴봐야 할 부분은 녹색 칸으로 표시된 부분이다.

1
2
3
4
5
6
7
8
9
10
int i = 10;  //integer variable
002A139E  mov         dword ptr [i],0Ah 

int *ptr = &i; //pointer to integer variable
002A13A5  lea         eax,[i] 
002A13A8  mov         dword ptr [ptr],eax

int &ref = i; //reference variable
002A13AB  lea         eax,[i] 
002A13AE  mov         dword ptr [ref],eax

1) 정수형 변수

int i = 10; → 정수형 변수 i에 10을 할당하라는 c/c++ 명령어이다.

다음은 디스어셈블리 코드로 번역된 코드이다.

002A139E mov dword ptr [i],0Ah → 10진수 10의 16진수 값인 0Ah 값을 변수 i라 이름 붙여진 메모리 공간에 32비트(4bytes) 크기인 double >word 형식으로 접근하여 이동(복사) 시킨다. 좌측의 002A139E는 명령어가 저장되어 있는 메모리의 주소이다.

2) 포인터 변수

int *ptr = &i; → 포인터 변수 ptr에 정수형 변수 i의 주소 값을 할당하라는 c/c++ 명령어.

002A13A5 lea eax,[i] → 정수형 변수 i의 유효 메모리 주소값을 범용레지스터인 eax로 전송. i의 주소값은 run-time 상에서 달라질 수 있기 때문에 그 유효 주소값(유효주소 = 세그먼트 주소+오프셋)을 >추출하는 lea 명령어가 쓰였다.

002A13A8 mov dword ptr [ptr],eax → eax에 들어있는 i의 주소값을 정수형 포인터 변수 ptr로 double word 형식으로 엑세스하여 전송.

3) 레퍼런스 변수

int &ref = i; → 정수형 변수 i를 대변하는 레퍼런스 변수 선언.

002A13AB lea eax,[i] → 정수형 변수 i의 주소 값을 추출하여 eax로 전송.

002A13AE mov dword ptr [ref],eax → eax에 들어 있는 변수 i의 주소 값을 ref 변수로 전송.

언뜻 보면 앞서 살펴본 포인터 변수와 레퍼런스 변수의 차이가 없는것 같으나 둘은 엄연히 다르다.

포인터 변수 ptr은 ptr 만의 메모리 공간을 따로 할당 받지만, 레퍼런스 변수 ref는 별도의 메모리 공간을 할당 받지 못한다는 결정적인 차이점이 존재한다.

좀 더 이해하기 쉽게 디버깅 테이블을 통해 살펴 보도록 하자.

아래와 같이 정수형 변수 i의 주소값이 레퍼런스 타입 변수 ref에 전송되기 전에 주소값이 없는 체로 존재하다가,

디스어셈 포스팅 스크린샷3

mov dword ptr [ref],eax 가 실행되어 정수형 변수 i의 주소값이 전송되어 지고 나면 레퍼런스 타입 변수 ref는 아래와 같이 i와 동일한 주소값을 공유하게 된다.

디스어셈 포스팅 스크린샷4

다시 말해,
정수형 변수 i와 같은 주소를 공유하게 되며 이를 대변하는 별칭의 개념으로써 존재하게 되는 것이다.

포인터 변수 ptr의 주소와 정수형 변수 i의 주소값을 비교해 보면 서로 다르지만,

참조형 변수(레퍼런스 변수) ref와 정수형 변수 i는 주소 값은 물론 데이터 값 모두 동일하다.

2. 포인터 변수에 대한 레퍼런스 타입 변수 선언도 가능할까?

우리는 위에서 정수형 변수의 레퍼런스 타입 변수의 선언을 살펴 보았다. 그렇다면 포인터 변수에 대해서도 레퍼런스 타입의 변수를 선언 할 수 있지 않을까?

1) 포인터 변수에 대한 레퍼런스 타입 변수

다음은 이를 확인하기 위한 C++ 코드와 이에 대한 디스어셈블리 코드이다.

디스어셈 포스팅 스크린샷5

1
2
3
4
5
6
7
8
 int i = 10;
00EC13BE  mov         dword ptr [i],0Ah
 int* ptr = &i;
00EC13C5  lea         eax,[i]
00EC13C8  mov         dword ptr [ptr],eax
 int*& rptr = ptr;
00EC13CB  lea         eax,[ptr]  
00EC13CE  mov         dword ptr [rptr],eax  

우리가 살펴봐야 할 부분은 파란색 텍스트 부분이다. 보시다시피, 위의 정수형 변수 i에 대한 레퍼런스 타입 변수 ref의 예와 다를 것이 없다.

레퍼런스 타입 변수 rptr은 아래 디버깅 테이블 처럼 포인터 변수 ptr의 별칭으로써 존재하며 ptr의 주소와, ptr의 데이터, 다시 말해 ptr이 가리키고 있는 곳의 주소를 갖는다.

디스어셈 포스팅 스크린샷6

이로써 우리는 포인터 타입 변수에 대한 레퍼런스 타입 변수의 선언 또한 가능하다는 사실을 알 수 있게 되었다.

2) 함수 반환 과정에서의 포인터 변수에 대한 레퍼런스 타입 변수 분석

다음은 포인터 변수에 대한 레퍼런스 타입 변수를 반환하는 함수의 소스 코드이다.

디스어셈 포스팅 스크린샷7

new 연산자를 통해 힙 영역에 정수형 변수의 공간을 1개 확보한 뒤 그 값을 숫자 100으로 하였다.

그 다음 포인터 타입 변수에 대한 레퍼런스 타입 변수 형식으로 반환을 하는데 이 코드를 보면 힙 영역을 가리키고 있는 포인터 변수 pTemp를 그대로 반환하는 것 처럼 보인다 .

하지만 이는 단순히 pTemp가 가지고 있는 힙 영역에 대한 주소 값을 반환하는 것이 아님에 유의 해야 한다.

반환 타입이 그저 int* 와 같은 포인터 타입이었다면

1
2
 return pTemp;
013B13E0  mov         eax,dword ptr [pTemp]

와 같이 작동 하였겠지만,

반환 타입이 int*&와 같은 포인터 타입에 대한 레퍼런스 타입이므로

1
2
 return pTemp;
00DF13E0  lea         eax,[pTemp]

와 같이 작동 한다는 이야기다.

둘의 차이를 알겠는가?

전자는 포인터 변수 pTemp가 가지고 있는 주소 데이터를 eax 레지스터를 통해 반환하는 것이고, 후자는 포인터 변수 pTemp의 주소를 eax 레지스터를 통해 반환하는 것이다.

이제 이를 반환 받는 쪽을 살펴 보도록 하자.

먼저 포인터 변수로써 int*& 타입으로 리턴값을 반환 받는 과정을 살펴 보자.

디스어셈 포스팅 스크린샷8

위의 코드를 디스어셈블리 코드를 통해 설명해 보도록 하겠다.

int* rptr = func(); 00B9357E call func (0B911DBh)
-> 반환 받은 eax 레지스터에 포인터 변수 pTemp의 주소가 들어 있다.

00B93583 mov eax,dword ptr [eax]
-> pTemp가 가지고 있는 힙 영역에 대한 주소 값을 얻는다.

00B93585 mov dword ptr [rptr],eax
-> eax에 들어 있는 힙 영역에 대한 주소를 포인터 변수 rptr로 복사 한다.

이로써 포인터 변수 rptr은 힙 영역에 대한 주소를 얻었다. 이는 다음 디버깅 테이블에 잘 나타나 있다.

디스어셈 포스팅 스크린샷9

이 디버깅 테이블을 살펴 보기 전에 먼저 주의해야 할 점이 한가지 있다.

이는 함수가 반환되는 “ret” 전에 실행 되어지는 “pop ebp”에 의해 포인터 변수 pTemp의 주소가 바뀐다는 사실이다.

int*나 int와 같은 일반적인 변수나 포인터 변수를 반환하는 형식에서는 로컬 영역을 의미하는 스택 프레임 공간을 실질적으로 초기화 하는 “add esp, 스택 프레임 (로컬 영역) 크기” 에서 변수가 유효하지 않게 되어 디버깅 테이블에서 회색 표시가 된다.

물론 다른 함수 호출을 통해 다시 한 번 스택 프레임을 할당 받는 것과 같이 다시 건들이지 만 않는 다면 해당 로컬 영역의 변수 데이터에는 변화가 없다. (이에 접근 하는 것은 옳지 않으니 지양 하자.)

하지만 반환 타입이 레퍼런스 타입인 경우에는 해당 함수의 지역 변수이든 반환 되는 레퍼런스 타입의 변수이든 모두 다 조금전 설명한 것과 같이 “pop ebp”에서 유효하지 않는 데이터로 식별 되어 회색 처리가 된다.

그리고 이때 이들 모두의 주소가 바뀌는데 이들의 offset의 크기(차이)는 그대로 유지 된다.

그런데 또 이를 디스어셈블리 모드가 아닌 일반 모드로 디버깅을 하면 주소 변동이 없다.

한마디로 들쭉날쭉 하다는 것이다.

왜 이러한 현상이 일어나는 것인지, 이것이 컴파일러에 의한 것인지 아니면 C++ 규약에 의한 것인지는 정확히 모르겠다.

다행히 이러한 현상은 우리가 공부를 하는데 큰 영향을 주지는 않는다.

그럼 이제 반환 받는 변수의 타입을 포인터 변수에 대한 레퍼런스 타입으로 바꿔 보자.

코드는 다음과 같다.

디스어셈 포스팅 스크린샷10

이를 디스어셈블리 코드를 통해서 설명해 보겠다.

int*& rptr = func(); 00D7357E call func (0D711DBh) -> 반환 받은 eax 레지스터에 포인터 변수 pTemp의 주소가 들어 있다.

00D73583 mov dword ptr [rptr],eax -> 이를 그대로 레퍼런스 변수 rptr을 생성하여 할당 한다.

이것은 main()의 레퍼런스 변수 rptr이 func() 함수내의 포인터 변수 pTemp의 별칭이므로 주소와 보유한 데이터 값이 모두 동일 하다는 것을 의미한다.

디스어셈 포스팅 스크린샷11

하지만 여기에는 치명적인 결점이 하나 있다.

바로 안전성 문제이다.

보시다시피 func() 호출 시 포인터 변수 pTemp를 위해 스택 프레임이 확보 되어진다.

그러나 이후 func() 호출이 종료 되면서 이 스택 프레임 또한 Clean up 되어지고 이때 당연히 스택 프레임내 지역 변수 pTemp를 위한 공간 또한 Clean up 되어 지는 것이다.

하지만 이에 대한 레퍼런스를 반환 함으로써 포인터에 대한 레퍼런스 타입 변수 rptr이 Clean up 된 포인터 변수 pTemp를 참조하게 되어 다른 함수에 의해 그 공간의 데이터가 바뀌거나 또 다른 스택 프레임 할당으로 인해 해당 데이터가 손상이라도 된다면 유효하지 않은 데이터를 참조하게 되어 크나큰 오류를 불러 일으키게 되는 것이다.

쉽게 말하자면,

포인터에 대한 레퍼런스 변수 rptr이 0x03fch 주소의 포인터 변수 pTemp의 별칭으로써 참조 되는데 이 레퍼런스 변수는 포인터 변수 pTemp가 있었던 0x03fch 주소의 데이터를 계속 해서 참조 한다.

이로 인해 차후 스택 프레임 할당 및 해제로 인해 0x03fch의 데이터가 수정되거나 삭제되어 잘못되어져도 이를 계속 해서 참조 하므로 심각한 오류를 일으킬 소지가 다분하다는 것이다.

마지막으로 아래의 그림은 설명을 돕기 위한 것이다.

디스어셈 포스팅 스크린샷12

위와 같이 기존의 포인터에 대한 레퍼런스 변수 rPtr은 메모리 주소 (0x..03fch)의 또다른 별칭이 되어 결과적으로 포인터 변수 pTemp를 참조한다.

디스어셈 포스팅 스크린샷13

스택 프레임 할당 및 해제로 인해 현재 참조중인 (0x..03fch)의 데이터가 바뀌었어도 이를 계속해서 참조하므로 데이터의 무결성 조건에 위배되는 오류가 발생하게 된다.

위와 같은 오류는 비단 포인터 변수에 대한 레퍼런스 타입 변수 뿐만 아니라 모든 레퍼런스 타입 변수를 다룰때에도 동일하게 벌어질 수 있는 일이기 때문에 취급시 주의해야 한다.

이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.