Programming world: Pointer in C++



| 보안 이슈와 포인터의 남용 개요


  모든 어플리케이션에서 보안과 신뢰성은 주요한 관심 요소이다. 잦은 비도로 나타나는 보안 사고와 어플리케이션의 비정상 동작 떄문에 보안과 신뢰성에 대한 관심이 더욱 강화되고 있다. 애플리케이션의 보안에 대한 책임은 대부분 그 개발자에게 있다. 


  C로 안전한 어플리케이션을 작성하는 것은 언어에 고유한 몇 가지 속성 떄문에 쉽지 않다. 예를 들어, C는 프로그래머가 배열의 영역을 넘어선 영역에 데이터를 기록하는 것을 막지 않는다. 이러한 접근은 메모리가 손상되어 보안에 잠재적인 취약점이 될 수 있다. 또한, 포인터의 부적절한 사용은 종종 많은 보안 문제의 근본적인 원인이 되기도 한다.


  어플리케이션이 예상하지 못한 방식으로 동작한다고 해도 최소한 인가되지 않은(unauthorized) 접근이 발생하지 않는다는 점에서 보면, 보안 문제로 보이지 않을 수도 있다. 하지만 이런 비정상적인 동작을 이용해서 애플리케이션의 서비스를 거부하여 침해를 당할 수도 있다. 


  CERT(http://www.cert.org/)에서는 C와 다른 언어에서의 보안 이슈를 포괄적으로 다루고 있다. 이 조직은 인터넷 보안 취약점에 대해 연구한다. 포인터의 사용에 관한 보안 이슈에 대해 집중해 살펴보도록 하자. CERT 조직의 보안 이슈 중 많은 부분이 포인터의 부적절한 사용에 기인한다. 포인터와 그 적절한 사용 방법에 대한 이해는 안전하고 신뢰할 수 있는 애플리케이션을 개발하는 데 매우 중요한 역할을 한다. 


  운영체제의 보안은 많이 개선됐다. 그런 개선 사항 중의 일부는 메모리의 사용 방법에 반영되었다. 일반적으로 운영체제의 개선 사항은 개발자의 통제 범위를 벗어나 있긴 하지만 애플리케이션에도 영향을 준다. 이런 이슈를 이해해야만 애플리케이션의 동작에 대해 설명할 수 있다. 주소 영역 배치 랜덤화(Address Space Layout Randomization)와 데이터 실행 방지(Data Execution Prevention)에 대해 집중적으로 살펴보고자 한다.


  주소 영역 배치 랜덤화(ASLR) 절차는 메모리 내 애플리케이션의 데이터 영역을 랜덤하게 배치한다. 데이터 영역은 코드, 스택, 힙을 포함한다. 이 영역의 배치를 랜덤화하면 공격자가 메모리가 어디에 위치할지 예측하기 어려우므로 데이터 영역에 접근하기 힘들다. return-to-libc같은 특정한 종류의 공격은 스택 일부를 덮어쓴 후, 제어를 이 영역으로 넘긴다. 이 영역은 보통 공유 C 라이브러리인 libc이다. 스택과 libc의 위치가 알려지지 않는다면, 이런 공격은 성공하기 어려울 것이다.


  데이터 실행 방지(DEP) 기법은 코드가 메모리의 실행 불가능한 영역에 있을 때 실행을 차단한다. 몇몇 종류의 공격은 메모리의 영역을 악성 코드로 덮어쓴 후, 제어를 이 영역으로 넘긴다. 이 코드 영역이 스택이나 힙처럼 실행 불가능한 영역일 경우 실행되지 않는다. 이 기법은 하드웨어나 소프트웨어로 구현할 수 있다.


앞으로 다음과 같은 몇 가지 측면에서 보안 이슈에 대해 알아보도록 하겠다. 


* 포인터의 선언과 초기화

* 부적절한 포인터 사용

* 메모리 해제 문제



※ 'Understanding and Using C Pointers' 책에서 일부 내용을 따온 것입니다.




일반적으로 C 포인터로 큰 규모의 메모리를 다룰 때 메모리가 조각화되어 관리된다. 즉, 인접한 메모리를 할당하는 것이 아니라, 적당하게 힙 메모리에서 떨어진 구역의 메모리들을 가져와서 마치 하나로 연결되어 있고 인접되어 있듯이 사용하는 것이다. 


그렇다면 한 번에 인접한 메모리를 할당하려고 한다면 어떻게 해야 할까?

이 포스트에서, C언어로 다차원 배열을 할당하는 경우 메모리를 인접하게 할당하는 방법에 대해서 다룰 것이다.




| 일반적인 코딩 방법


우리는 일반적으로 다차원 배열을 동적으로 할당할 때, 다음과 같이 코딩을 한다.


 int rows = 2;
 int columns = 5;
 int i=0, j=0;

 int ** matrix = (int**) malloc(rows*sizeof(int*));

 for(i=0; i<rows; i++){
         matrix[i] = (int*)malloc(columns * sizeof(int));
 }


그런데, 이렇게 해버리면 다음과 같이 결과가 나타난다.




▲ [0][4] 원소와 [1][0] 원소 사이에 16바이트 갭이 생겼다.


첫 번째 행의 마지막 원소와 두 번째 행의 첫번쨰 원소의 주소가 int 변수의 크기인 4 byte만큼 차이가 나는 것이 아니라 16 byte가 차이난다. (위 숫자들은 전부 16진수임을 알기를 바란다. 40에서 50으로 갔음은 10바이트가 아니라 16바이트가 차이났음을 의미한다.)


이제, 해결책에 대해서 알아보자.


2차원 배열에 인접한 메모리를 할당하는 두 가지 접근 방법이 있다. 첫 번째 기법은 '바깥쪽' 배열을 먼저 할당한 후 전체 열에 대한 메모리를 할당하는 방법이고, 두 번째 기법은 모든 메모리를 한 번에 할당하는 방법이다. 






| 첫 번째 방법


첫 번째 기법을 다음 예제 코드에서 설명하고 있다. 첫 번째 malloc 함수 호출로 정수에 대한 포인터의 배열을 할당한다. 각 요소는 열에 대한 포인터를 가지게 된다. 두 번째 malloc 함수 호출은 우리가 선언할 다차원 배열의 제일 첫 번째 원소의 주소에 모든 요소에 대한 메모리를 할당하게 된다. (말이 어렵다면, 그냥 배열의 이름에다가 이후에 저장할 모든 메모리의 크기를 한 번에 할당한다고 생각하자.) 그리고 for 루프는 첫 번째 배열의 각 요소에 두 번째 malloc에서 할당한 메모리의 일부분을 지정한다.


int rows = 2;
int columns = 5;
int i=0, j=0;

int ** matrix = (int**)malloc(rows*sizeof(int*));
matrix[0] = (int*)malloc(rows*columns*sizeof(int));
for(i=1; i<rows; i++)
        matrix[i] = matrix[0] + i*columns;



엄밀히 따지면, 첫 번째 배열의 메모리는 배열의 '본체(body)' 부분과 떨어져 있을 수도 있다. 하지만 배열의 본체 부분에서는 메모리의 인접한 영역이 할당된다. 



▲ 배열의 모든 요소들이 인접하게 나열됬다.





| 두 번째 방법


다음 예제 코드에서는 두 번째 기법을 설명하고 있다. 


int * matrix = (int*)malloc(rows*columns*sizeof(int));


결과는 다음과 같다.



▲ 배열의 모든 요소들이 인접하게 나열됬다.


나중에 코드 안에서 이 배열을 참조할 때는 배열 첨자를 사용할 수 없다. 대신, 다음 코드에서 설명하는 것처럼 배열에 대한 인덱스를 수동으로 계산해야 한다. 각 배열 요소는 그 인덱스의 곱으로 초기화한다. (컴파일러가 배열 첨자를 허용할 때 필요한 배열의 형태에 대한 정보가 없으므로 배열 첨자를 사용할 수 없기 때문에 배열 첨자를 사용할 수 없는 것이다.)


int i=0, j=0;

for(i=0; i<rows;i++){
        for(j=0; j<columns; j++){
                *(matrix+(i*columns)+j) = i*j;
        }       
} 


2차원 배열에 대한 인접한 메모리를 할당하는 두 가지 일반적인 접근 방법에 대해 설명했다. 어떤 방법을 이용할 것인지는 어플리케이션에 따라 다르다. 하지만 두 번째 접근 방법이 '전체'배열에 대한 단일 메모리 블록을 생성한다.



C++ Pointers and Memory – Free Coding Tutorials



* 배열의 이름은 무엇을 의미하는가?

배열의 이름은 포인터이다. 단 그 값을 바꿀 수 없는 '상수 형태의 포인터'이다. 다음 예제에서는 이러한 사실을 증명하고 있다.

- 열혈 C 프로그래밍(윤성우 저) 에서 -



 우리나라 C프로그래밍 기초 기본서로 꽤 유명한 '열혈 C 프로그래밍'(윤성우 저)에서는 위와 같이 배열의 이름은 포인터라고 언급하고 있습니다. 물론, 저도 이 책을 처음으로 프로그래밍을 시작하고, 프로그래밍의 아주 기본적인 기법이라던지, C프로그래밍의 기본적인 문법을 익히고 기본 개념을 쌓는데 큰 도움을 받은 것이 사실입니다.


 하지만, C프로그래밍의 최고급 지식을 쌓기 위해 고군분투하는 요즘, 윤성우님의 책보다 조금 더 권위있는 책을 읽고 있습니다. 그리고 그 책에는 '배열의 이름은 포인터가 아니다'라고 언급하고 있습니다.


배열과 포인터에 대한 일반적인 오해는 서로 완벽하게 맞바꾸어 사용할 수 있다고 생각하는 것이다. 배열의 이름은 포인터가 아니다. 때로 배열의 이름을 포인터로 다루기도 하고, 배열의 표기가 포인터와 함께 사용되기도 하지만, 둘은 분명히 구분되어야 하고 언제나 서로 대치할 수 있는 것은 안다. 이 차이점을 이해하면 배열과 포인터의 표기법을 잘못 사용하지 않을 수 있다


- Understanding and Using C Pointers (리차드 리스 저) 에서 - 


  배열의 이름만 따로 사용한다면 배열의 주소를 반환하기는 하지만, 배열의 이름을 할당(assignment)의 대상으로 이용할 수 없으므로, 배열의 이름을 포인터와 동일한 개념, 혹은 포함되는 개념등으로 이해하면 안된다는 것입니다.




| 문제


주어진 그래프가 만약 DAG(Directed Acyclic Graph, 방향성 비사이클 그래프)라면, 그 그래프의 꼭짓점들을 위상 정렬 하는 프로그램을 구현하라. DFS(Depth-First Search, 깊이 우선 탐색) 과정을 진행하는 동안, 작은 숫자의 꼭짓점들을 먼저 방문하도록 설계해야 한다.


입력(표준 입력)

첫 번째 에는 꼭짓점의 개수를 입력한다.

다음 줄부터, 그래프 G의 인접 리스트가 서로 연결되어 있는 꼭짓점인 x와 y로 표현되어야 한다. 이 것은 꼭짓점 x에서 꼭짓점 y로 나아가는 간선이 존재함을 의미한다.


출력(표준 출력)

첫 번째 줄에는, 주어진 그래프 G가 DAG라면 1을 출력하고, 아니면 0을 출력하게 한다.

그리고 그래프가 DAG이면 그 다음 줄에, 위상 순서대로 꼭짓점들을 출력해야 한다.


example)

입력

출력 

9

1 4

1 5

2 5

4 5

4 6

6 9

7 6

7 8

8 9

EOF 입력(윈도우에서는 Ctrl+Z)

1

7 8 3 2 1 4 6 9 5 





| 소스 코드


#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#define WHITE			0
#define GRAY			1
#define BLACK			2

#define TREE_EDGE		1
#define BACK_EDGE		2
#define FORWARD_EDGE	3
#define CROSS_EDGE	4

#define TRUE			1
#define FALSE			0

typedef struct _vertex{
	int color;
	int f;
	int d;
} Vertex;

typedef struct _VertexInfo{
	int out_degree;
	int * list;
} VertexInfo;

Vertex * vertex;
VertexInfo * vertexInfo;
int numV, time;
int * topoList;
int topo_num=0;
int isDAG = TRUE;

void addEdge(int fromV, int toV){
	int i=0;
	int cur=0;

	for(i=0; i < numV; i++)
	{
		if(vertexInfo[fromV].list[i] == 0)
			break;
		cur ++;
	}
	vertexInfo[fromV].list[cur] = toV;
}

void DFS_Visit(int fromV){
	int i=0, toV;

	/* DISCOVERED */
	time++;
	vertex[fromV].color = GRAY;
	vertex[fromV].d = time;

	for(i=0; i < vertexInfo[fromV].out_degree; i++){
		toV = vertexInfo[fromV].list[i];
		switch(vertex[toV].color)
		{
		case WHITE:
			DFS_Visit(toV);
			break;

		case GRAY:
			isDAG = FALSE;
			break;

		case BLACK:
			break;
		default:
			break;
		}
	}

	/* FINISHED */
	vertex[fromV].color = BLACK;
	topoList[numV-(topo_num++)] = fromV;
	time ++;
	vertex[fromV].f = time;
}

void Sort(int * list, int num)
{
	int i=0, j=0, tmp;

	for(i=0; i < num; i++)
	{
		for(j=i+1; j < num; j++)
		{
			if(list[i] > list[j])
			{
				tmp = list[i];
				list[i] = list[j];
				list[j] = tmp;
			}
		}
	}
}


void DFS(void)
{
	int i;
	for(i=1; i <=numV; i++)
	{
		vertex[i].d = 0;
		vertex[i].f = 0;
		vertex[i].color = WHITE;
		Sort(vertexInfo[i].list, vertexInfo[i].out_degree);
	}

	time = 0;
	for(i=1; i <=numV; i++)
	{
		if(vertex[i].color == WHITE)
		{
			DFS_Visit(i);
		}
	}
}


int main(void)
{
	int i=0, j=0;
	int fromV, toV;

	scanf("%d", &numV);

	/* INITIALIZATION START! */
	vertexInfo = (VertexInfo*)malloc(sizeof(VertexInfo)*(numV+1));
	for(i=1; i <=numV; i++){
		vertexInfo[i].list = (int*)malloc(sizeof(int)*numV);
		vertexInfo[i].out_degree = 0;
	}
	for(i=1; i <=numV; i++){
		for(j=0; j < numV; j++){
			vertexInfo[i].list[j] = 0;
		}
	}
	vertex = (Vertex*)malloc(sizeof(Vertex)*(numV+1));
	topoList = (int*)malloc(sizeof(int)*(numV+1));
	/* INITIALIZATION END! */
	

// INPUT
	while(scanf("%d %d", &fromV, &toV)!=EOF){
		addEdge(fromV, toV);
		vertexInfo[fromV].out_degree ++;
	}
	DFS();

// OUTPUT
	printf("%d\n", isDAG);
	if(isDAG)
	{
		for(i=1; i <=numV; i++)
			printf("%d ", topoList[i]);
	}

	return 0;
}


| 문제


비방향성 그래프 G의 커넥티드 컴포넌트를 의미하는 DFS를 구현하라. DFS 과정을 진행하는 동안, 작은 숫자를 가진 꼭짓점들을 먼저 방문하도록 설계하고, 컴포넌트 숫자는 1로 시작한다.


입력(표준 입력)

첫 번째 줄에는, 꼭짓점의 숫자 N을 입력한다.

다음 줄 부터, 그래프 G의 인접 리스트를 두 정수 x,y로 표현한다. 이것은 x로 시작해서 y로 끝나는 간선이 있음을 의미한다.


출력(표준 출력)

첫 번째 줄에는, 커넥티드 컴포넌트의 숫자를 출력한다.

다음 줄부터 N개의 줄에는, 각각의 꼭짓점의 커넥티드 숫자를 출력한다.



example)

입력 

출력 

6

1 2

1 4

2 5

3 6

2

1

1

2

1

1

2





| 소스 코드

#include <stdio.h>
#include <stdlib.h>

#define TRUE		1
#define FALSE		0

int numV;
int * list;
int * Qlist;
int * rep;

typedef struct _vertex;

typedef struct _vertex
{
	int groupname;
	int value;
	int rank;
	struct _vertex * parent;
} Vertex;

Vertex * vertex;

void MAKE_SET(Vertex * x, int value)
{
	x->value = value;
	x->parent = x;
	x->rank = 0;
}

Vertex * FIND_SET(Vertex * x)
{
	if(x != x->parent)
		x->parent = FIND_SET(x->parent);
	return x->parent;
}

void LINK(Vertex * x, Vertex * y)
{
	if(x->rank > y->rank){
		y->parent = x;			// 랭크가 큰 놈이 부모가 되도록!
	}
	else{
		x->parent = y;
		if(x->rank == y->rank){
			y->rank = y->rank + 1;
		}
	}
}

void UNION(Vertex * x, Vertex * y)
{
	LINK(FIND_SET(x), FIND_SET(y));
}

int SAME_COMPONENT(Vertex * x, Vertex * y)
{
	if(FIND_SET(x) == FIND_SET(y))
		return TRUE;
	else
		return FALSE;
}

int main(void)
{
	int * tmp_list;
	int v1, v2;
	int i=0, j=0, tmp=0;
	int order = 0;
	int compNUM = 0;

	scanf("%d", &numV);
	vertex = (Vertex*)malloc(sizeof(Vertex)*(numV+1));
	list = (int*)malloc(sizeof(int)*(numV+1));
	tmp_list = (int*)malloc(sizeof(int)*(numV+1));
	
	for(i=1; i<=numV; i++)
		MAKE_SET(&vertex[i], i);

	while(scanf("%d %d", &v1, &v2) != EOF){	
		if(FIND_SET(&vertex[v1]) != FIND_SET(&vertex[v2])){
			UNION(&vertex[v1], &vertex[v2]);
		}
	}
	for(i=1; i<=numV; i++){
		list[i] = FIND_SET(&vertex[i])->value;
		tmp_list[i] = FIND_SET(&vertex[i])->value;
	}

	for(i=1; i<=numV; i++)
	{
		tmp = list[i];
		if(tmp != 0)
		{
			compNUM ++;
			for(j=i+1; j<=numV; j++){
				if(tmp == list[j])
					list[j] = 0;
			}
		}
	}
	Qlist = (int*)malloc(sizeof(int)*(compNUM+1));
	order = 1;
	for(i=1; i<=numV; i++){
		if(list[i] != 0){
			Qlist[order++] = list[i];
		}
	}
	for(i=1; i<=numV; i++){
		for(j=1; j<=numV; j++){
			if(tmp_list[i] == Qlist[j]){
				list[i] = j;
			}
		}
	}
	
	printf("%d\n", compNUM);
	for(i=1; i<=numV; i++)
		printf("%d\n", list[i]);

	return 0;
}


| 문제


입력(표준 입력)

각각의 줄에는 최대 3개의 정수들을 포함하고 있어야 한다.

3개의 입력 정수들 중,

첫 번쨰 숫자가 0이면 결과를 출력하고 프로그램을 종료한다. (terminate)

첫 번째 숫자가 1이면 두 번째 숫자를 삽입한다. (insert)

첫 번째 숫자가 2이면 가장 큰 숫자를 추출한다. (extract)

첫 번째 숫자가 3이면, 두 번째 숫자 인덱스의 원소를 세 번째 숫자로 바꾼다.

우선순위 큐의 인덱스는 1로 시작한다고 가정하자.

우선순위 큐의 최대 인덱스 값은 100000이다.


출력(표준 출력)

첫 번째 줄에, 추출된 원소들을 출력한다.

두 번째 줄에, 현재 힙을 출력한다.


example)


입력 

출력 

1 16

1 15

1 10

1 14

1 7

1 9

1 3

1 2

1 8

1 1

3 2 4

2

2

0

16 14

10 8 9 4 7 1 3 2 


| 소스 코드

#include <stdio.h>
#include <stdlib.h>

#define minus_inf -32767

int PARENT(int index){ return index/2; }
int LCHILD(int index){ return index*2; }
int RCHILD(int index){ return index*2+1; }

void array_swap(int arr[], int a, int b)
{
	int temp;
	temp = arr[a];
	arr[a] = arr[b];
	arr[b] = temp;
}

void Heapify(int arr[], int current, int length)
{
	int left, right, largest;
	left=LCHILD(current);
	right=RCHILD(current);

	if((right<=length) && (arr[right]>arr[current]))
		largest = right;
	else
		largest = current;

	if((left<=length) && (arr[left]>arr[largest]))
		largest = left;

	if(largest != current)
	{
		array_swap(arr, current, largest);
		Heapify(arr, largest, length);
	}
}

void PRINT(int arr[], int length)
{
	int i=0;
	for(i=1; i<=length; i++)
		printf("%d ", arr[i]);
}

int EXTRACT_MAX(int arr[], int length)
{
	if(length < 1)		// "heap underflow"
		return minus_inf;
	int max = arr[1];
	arr[1] = arr[length];
	length --;
	Heapify(arr, 1, length);
	return max;
}

void INCREASE_KEY(int arr[], int index, int key)
{
	int tmp=0;
	if(key < arr[index])		// not increasing, then fail.
		return;

	arr[index] = key;
	while(index>1 && arr[PARENT(index)]<arr[index])
	{
		/* EXCHANGE CURRENT WITH PARENT */
		tmp = arr[index];
		arr[index] = arr[index/2];
		arr[index/2] = tmp;

		index = PARENT(index);
	}
}

void INSERT(int arr[], int length, int key)
{
	arr[length] = minus_inf;
	INCREASE_KEY(arr, length, key);
}

int main(void)
{
	int arr[100000];
	int ext[100000];
	int length=0, extlength=0;

	int op, tmp, i;
	int index, num;

	int quit=0;

	//INSERT(arr, length, 1); length++;
	//INSERT(arr, length, 2); length++;

	while(quit==0)
	{
		scanf("%d", &op);
		switch(op)
		{
		case 0:// print
			quit=1;		// terminate this program.
			break;

		case 1:// insert
			scanf("%d", &num);
			length++;
			arr[length] = num;
			INCREASE_KEY(arr, length, num);
			break;

		case 2:// extract
			extlength++;
			ext[extlength] = arr[1];

			tmp = arr[1];
			arr[1] = arr[length];
			arr[length] = tmp;

			length--;
			Heapify(arr, 1, length);
			break;

		case 3:// replace
			scanf("%d %d", &index, &num);
			arr[index] = num;
			if(index != 1 && arr[index] > arr[PARENT(index)])
				INCREASE_KEY(arr, index, num);
			else
				Heapify(arr, index, length);
			break;
	
		default:
			break;
		}
	}

	for(i=1; i<=extlength; i++)
		printf("%d ", ext[i]);

	printf("\n");
	for(i=1; i<=length; i++)
		printf("%d ", arr[i]);

	return 0;
}


+ Recent posts