DKU/시스템프로그래밍

[시프] 4-3 스택(stack)

ʕ민지ʔ 2022. 11. 14. 16:41

01 스택이란?

LIFO(Last In First Out) 특성을 가진 메모리 공간의 배열이다. stack에서 사용하는 operation에는 push와 pop이 있다. 그리고 stack은 bottom과 top으로 다루며, 이는 intel에서의 SS와 ESP이다.

 

02 intel 구조에서의 stack

stack을 다루기 위해 사용되는 push와 pop은 실제로 intel에서 어떻게 구현될까? 먼저 ESP에 대해 알아야한다. ESP란 Extended Stack Pointer의 약자로, stack에서 가장 마지막으로 push된 item의 위치, 즉 top을 가리키는 레지스터이다. stack은 바로 이 ESP 레지스터를 이용한 push와 pop 메소드 사용으로 관리된다. push는 ESP가 감소하고 stack의 top에 data를 write하고, pop은 top에서 data를 read하고 ESP가 증가한다. stack의 내부에는 arguments(parameters), 함수의 수행이 끝나고 돌아올 주소(return address), 지역 변수(local variable)가 존재한다.

 

더 자세한 내용은 https://www.intel.com/content/www/us/en/developer/articles/technical/intel-sdm.htm

 

Intel® 64 and IA-32 Architectures Software Developer Manuals

These manuals describe the architecture and programming environment of the Intel® 64 and IA-32 architectures.

www.intel.com

 

03 linux에서의 stack

프로그램이 실행되면 main() → func1() → func2()의 순으로 함수가 호출된다. 이렇게 함수가 호출될 때마다 메모리에는 각각의 함수를 위한 stack 공간인 stack frame이 쌓이게 된다. 각각의 stack frame에는 stack의 구성요소들이 1) argument, 2) return address, saved ebp, 3) local variable 의 순으로 쌓인다. 주의해야 할 점은 argument가 쌓일 때에는 뒤에 있는 인자가 먼저 push된다는 점이다.

 

int func2(int x, int y) {
	int f2_local1 = 21, f2_local2 = 22;
    int *pointer;
    ...
}

void func1() {
	int ret_val;
    int f1_local1 = 11, f1_local2 = 12;
    ...
    ret_val = func2(111, 112);
    f1_local++;
    ...
}
int main() {
	...
    func1();
}

 

04 stack 예제1

#include <stdio.h>

int func2(int x, int y) {
	int f2_local1 = 21, f2_local2 = 22;
	int *pointer;
	
	printf("func2 local : \t%p, \t%p, \t%p\n", &f2_local1, &f2_local2, &pointer);
	pointer = &f2_local1;
	
	printf("\t%p \t%d\n", (pointer), *(pointer));
	printf("\t%p \t%d\n", (pointer-1), *(pointer-1));
	printf("\t%p \t%d\n", (pointer+3), *(pointer+3));
	
	*(pointer+4) = 333;
	printf("\ty = %d\n", y);
	return 222;
}

void func1() {
	int ret_val, f1_local1 = 11, f1_local2 = 12;
	ret_val = func2(111, 112);
}
int main() {
	func1();
}

<결과>

gcc로 컴파일할 때, gcc -o stack1 stack_ex1.c 로 실행시켰더니 예상한 결과값이 나오지 않았다.
현재 환경이 64비트여서 32비트로 컴파일하는 과정이 필요했기 때문이었다.
이를 위해 컴파일 시 -m32 -mtune=pentium 옵션을 사용하였다.

 

05 stack 예제2

#include <stdio.h>

void f1() {
	int i;
	printf("In func1\n");
}
void f2() {
	int j, *ptr;
	printf("f2 local : \t%p, \t%p\n", &j, &ptr);
	printf("In func2\n");

	ptr = &j;
	//ret = *(ptr+2);
	*(ptr+2) = f1;
}
void f3() {
	printf("Before invoke f2()\n");
	f2();
	printf("After invoke f2()\n");
}
int main(){
	f3();
}

<결과>

위의 예제코드는 함수 f2가 종료하고 복귀할 주소가 담겨있는 return address에 포인터로 접근해 값을 f1으로 바꾼 것이다. 이에 대한 결과로 함수 f2가 종료하고 f3으로 돌아가는 것이 아니라, 함수 f1으로 가게 되면서 "In func1\n"이 출력된다. 이는 stack overflow로 보안 취약성을 띄게한다. 또한, 결과에서 출력되는 '세그멘테이션 오류'는 f2에서 return address를 억지로 f1으로 바뀌면서 함수 f1이 호출되긴 했으나, 함수 f1은 제대로 만들어진 함수가 아니기 때문에 f1이 끝나고 돌아올 주소도 찾지 못하고, 엉뚱한 주소로 접근하면서 발생하는 것이다.