함수 프롤로그와 에필로그
개요
프로그램에서 함수의 호출은 어떤 과정으로 일어날까? 메모리의 스택 영역에는 함수가 호출될 때, 호출된 함수와 관계된 지역변수와 매개변수가 저장된다. 만약 프로그램이 함수를 여러 번 호출하거나 함수 내부에서 또 다른 함수를 호출한다면, 여러 함수들의 정보가 스택 영역에 저장될 것이다.
간단한 C 언어 소스코드와 컴파일된 main 함수 어셈블리 코드를 살펴보자.
1 | void function(int x){ |
1 | push %ebp |
1 | push %ebp |
위 코드에서는 main 함수 내부에서 정수형 인자 하나를 전달받는 function 함수를 호출하고, 프로그램을 종료한다. 어셈블리 코드를 보면 main 함수는 push $0x0
명령을 수행하고, function 함수를 호출한다. function 함수는 C 코드에선 아무런 내용이 없지만 어셈블리 코드에서는 어떤 과정을 거치고, ret
명령을 수행해서 main 함수로 복귀한다. 여기서 알 수 있는 것은 main 함수의 어셈블리 코드에서는 컴파일 타임에 function 함수 호출 코드를 포함하지만, function 함수에서는 돌아가야할 main 함수의 주소를 찾을 수 없다. 그럼 function 함수는 복귀주소를 어떻게 알 수 있을까?
함수 프롤로그
ebp 레지스터는 스택프레임의 제일 하단을 가리키는 베이스 포인터 역할을 한다. esp 레지스터는 스택프레임의 상단을 가리키는 포인터 역할을 한다. 즉, 함수가 호출되어 스택 메모리에 저장될 때는 매겨변수와 지역변수를 아무렇게나 저장하는 것이 아니라 스택프레임을 형성하고 그 영역 내부에 변수들을 저장하는 것이다.
앞서 언급했듯, function 함수의 C 소스코드에는 아무런 내용이 없었지만 어셈블리 코드에서는 어떤 과정을 거치는 것을 볼 수 있다. 이 과정은 함수의 스택 프레임을 형성하는 과정이다. function 함수가 호출되기 이전 ebp 에는 main 함수 스택프레임의 하단을 가리키고 있었을 것이다. 즉, function 함수가 호출되고 ebp 레지스터의 값을 스택에 push 하는 과정은 main 함수의 베이스 포인터를 저장하는 과정이다. 이후 mov %esp, %ebp
명령을 수행해서 esp 에 ebp 값을 넣는다. 위의 과정을 끝내고 push
명령어를 수행하면 esp 는 다음 주소를 가리키며, 스택의 상단을 가리킨다. 이전 함수의 ebp 값을 스택에 저장하고, ebp 값을 esp 값에 복사하는 과정을 함수 프롤로그라고 한다.
함수 에필로그
함수 프롤로그 과정이 끝나고, 함수 본체의 작업을 수행하고 종료할 것이다. 이제 function 함수를 호출했던 main 함수의 스택프레임으로 복귀하는 과정을 알아보자. 위 코드에서는 nop
하나만 존재한다. 이후 pop %ebp
를 수행하고 ret
명령어로 함수가 반환된다. pop %ebp
를 수행하면 스택 가장 상단에 있는 값이 ebp 레지스터에 저장된다. 여기서는 스택에 저장된 값이 main 함수의 ebp 값밖에 없기 때문에, ebp는 다시 main 함수의 베이스 포인터로 복귀된다. ret
명령어는 esp 가 가리키는 스택의 값을 pop
해서 eip에 저장하고, 해당 eip로 jmp
하는 동작을 수행한다. 앞서 main 함수의 어셈블리 코드를 보면 call 80483db
명령으로 main 함수를 호출하는데, call
명령어에는 현재 수행하고 있는 명령의 다음 명령의 주소를 스택에 저장하는 역할을 포함한다. 즉, pop %ebp
를 마치고 난 후의 스택에는 function 함수가 끝나고 수행해야할 명령이 있는 주소가 저장되어 있고, ret
명령이 수행되면 eip에 그 주소가 저장되며 함수 호출이 종됴뢴다. 이전 함수의 베이스 포인터 값과 스택에 저장된 다음 명령 주소를 복구하는 이 과정을 함수 에필로그라고 한다.
정리
지금까지 과정을 정리해보자
- 함수가 호출되면 다음 명령어의 주소를 스택에 저장하고, 호출한 함수의 명령을 실행한다.
- 호출된 함수는 이전 함수의 ebp 값을 저장하고, ebp 값을 갱신하는 함수 프롤로그 과정을 거친다.
- 함수가 종료될 땐, 스택에 저장된 ebp 값으로 이전 함수 스택프레임의 베이스 포인터를 복구하고, 함수 종료 후 실행할 명령을 eip에 저장하는 함수 에필로그 과정을 거친다.
일반적으로 위의 과정을 거쳐 함수의 스택프레임을 관리하지만, 실제로는 좀 더 다양한 형태의 프롤로그, 에필로그 과정이 있다. 본 문서에서 설명하지 않은 함수의 매개변수를 저장하는 방식도 함수 호출 규약에 따라 달라지고, 전형적인 모습과 다른 형태를 띄는 에필로그/프롤로그도 있다. 다음에 시간적으로 여유가 있을 때, 함수 호출규약, 스택프레임의 개념과 함께 더 구체적인 내용을 업로드하겠다.