※ 질문/내용오류/공유할 내용이 있다면 jinkilee73@gmail.com으로 메일 주세요 :-)
앞서 굉장히 많은 Fact들을 나중에 배울 것이라며 그냥 무책임하게 넘겨버린 적이 있었다. 그 모든 것은 지금 이 순간에 Procedure Call을 통해서 설명을 한꺼번에 하기 위함이었다. 그리고 내 생각에 그 때 이 내용까지 전부 다 공부해버리면 머리 많이 아플 것이다. 아무튼 Procedure Call 공부를 시작해보자.
프로시저가 무엇일까? 위키피디아에서 말하기를
In computer programming, a subroutine is a sequence of program instructions that perform a specific task, packaged as a unit. This unit can then be used in programs wherever that particular task should be performed. Subprograms may be defined within programs, or separately in libraries that can be used by multiple programs.
In different programming languages a subroutine may be called a procedure, a function, a routine, a method, or a subprogram.
이라고 한다. 즉, 서브루틴(sub routine)이 우리가 말하는 프로시저이다. 서브루틴, 함수, 프로시저 등등이 모두 같은 의미로 쓰인다고 한다. 굉장히 이해하기 쉽게 설명해 놨다고 생각한다. 어떤 특정 작업을 수행하기 위한 instruction들의 연속이라. 내 입장에서는 완벽한 정의라고 생각한다.
우리가 프로그램을 작성하다보면 여러가지 함수를 호출하곤 한다. 그 호출을 진행할 때 우리의 가상 메모리 상에서는 굉장히 많은 일들이 일어난다. 이 블로그의 어느 순간에도 말한 적이 있을 것이다. 스택을 사용하여 프로시저 콜이 구현된다고. 우선, 프로시저 콜을 이해하기 위해 자주 논의될 레지스터 세 개를 소개하겠다. %ebp, %esp, %eip 이다.
%ebp : 스택 프레임 포인터.
%esp : 스택 포인터. 제일 Top을 가리킨다.
%eip : 프로그램 카운터, 즉 다음에 실행할 명령어의 주소를 가지고 있다.
스택 포인터는 어느 정도 이해할 수 있을 것이라고 믿는다. 그냥 스택의 Top을 가리키는 것이니까. 그렇다면 스택 프레임 포인터가 무엇인가? 스택 프레임이 무엇인지 먼저 설명을 해야한다. 그림으로 이해하면 편하다.
여러 칸의 집합이 하나의 스택 프레임이 된다. 저 스택 프레임 안에는 무엇이 있을까? 아직 궁금한게 많다. 차근 차근 설명을 따라오면 이해 가능할 것이라고 믿는다. 아무튼 지금은 이것만 알아두자. 위의 그림에서 색깔 별로 구분해둔 것 각각을 우리는 스택 프레임이라고 부를 것이고, 저것들을 하나하나가 함수들이다.
가령, F1이라는 함수 내에서 F2라는 함수를 호출을 하면, 프로그램 흐름이 젤 처음에는 F1을 실행하다가 F2를 실행하고 다시 F1으로 돌아와서 F1의 실행 역시 끝나게 되는 것지? 그것이 결국 스택이다. F1이라는 스택 프레임이 쌓였다가. F2라는 스택 프레임이 Push되고 거기에서 모든 작업이 수행되면 F2라는 스택 프레임이 Pop되면서 다시 F1 스택 프레임이 스택의 Top을 가리키고 있는 것이다. 이제 조금 이해가 가리라고 믿는다.
이제 스택 프레임이 무엇인지 알았으니, %esp %ebp 등이 스택에서 어떻게 존재하는 것인지 볼 것이다. 아래와 같다.
즉 %ebp와 %esp가 가지는 값은 이렇다.
%ebp : 가장 Top에 있는 스택 프레임의 시작 주소
%esp : 그 스택 프레임 내에서의 가장 Top에 해당하는 주소
위와 같은 스택 구조에서 프로시저 콜은 다음과 같이 일어난다. 가령, Stack frame1이 메인함수에 대한 스택 프레임이라고 하고 Stack frame2는 sum함수에 대한 스택 프레임이라고 하자. 그리고 메인함수에서 sum함수를 호출한다고 하자. sum함수를 호출하기 전에는 다음과 같은 모습을 보일 것이다.
여기에서 sum함수를 호출하게 되면 다음과 같아진다.
sum함수가 모두 실행되고 main함수로 리턴되면 다시 이전의 스택 프레임 모습으로 돌아가게 된다.
이렇게 된다. 즉, 함수 호출을 끝내면 이전의 모습으로 돌아오게 되는 것이다.
이제 기본적인 것은 전부 설명했으니 본격적으로 Procedure Call을 공부해볼까나?
다음의 C코드를 보자.
이 코드에 대한 어셈블리어는 다음과 같다. 다음의 어셈블리어는 Fedora 16에서 실제로 컴파일
push? pop? leave? ret? 처음 보는 명령어들도 많이 있는데 차근 차근 공부해보도록 하자.
우선, 젤 처음에는 push 명령어를 수행하는데, 자료구조의 스택을 공부해본 사람은 알겠지만 push는 스택을 한 칸 늘리는 명령어이다. 물론 이 스택은 가상 메모리 상에 있다는 것은 다들 알고 있겠지? 모르겠다면 앞의 포스팅을 복습복습 해보자.
아무튼 젤 처음에 push1 %ebp명령어를 수행하면 다음과 같은 결과를 갖게 된다.
(얼마전에 42만원이라는 거금을 주고 iPad-mini를 구매했는데, 대만족이다. 필기 어플 유패드와 같이 사용하니까 내가 원하는 딱 그 용도로 너무 만족하며 사용하고 있다.)
※ 명령어를 수행하여 변하는 부분은 빨간 글씨로 해놨으니까 참고하세용!!
위의 그림에서 보이듯이 push %ebp 명령은 다음의 두 명령어와 동등한 효과를 낸다.
subl $4, %esp
movl %ebp, (%esp)
esp 값을 한칸 내려서 새로운 칸을 만들고 그 칸에 %ebp값을 집어 넣는것이다. 이것을 왜 할까? 저 부분이 바로 나중에 main함수의 이전의 스택 프레임으로 돌아가기 위한 방법이다. 애초에 %esp에 저장하는 그 값이 %ebp값인데, 이 값은 main 함수 이전 프로시저의 스택 프레임의 가장 밑을 가리키고 있다. 따라서 이 %ebp값을 main 함수에 대한 스택의 가장 밑에 집어넣어두면 나중에 언제든지 돌아가고 싶을 때 돌아갈 수 있다는 뜻이다.
다음 명령어를 보자.
이 명령어는 왜 했는지 조금 추측할 수 있어야 한다. ebp가 새로운 스택 프레임(main 함수)의 밑을 가리키네? 즉, 메인함수 스택 프레임의 bottom을 가리키면서 자연스럽게 스택 프레임을 이동한 것이다.
위의 두 명령어는 도대체 왜 실행했는지 이유를 모르겠다;; 왜 했을까? 첫번째 andl 명령어 같은 경우 %esp의 값의 마지막 한 비트를 무조건 0으로 세팅해주는 효과를 낸다. 즉 이전의 %esp값보다 몇 칸 더 스택을 쌓는 꼴이 된다. 두번째는 몇 칸 더 쌓인 그 스텍 프레임에서 또 다시 스택 8칸을 더 할당시킨다. 왜 그럴까? 모르겠다;;
(엇;; 위의 그림 보니까 subl $32, %esp인데 오타가 났나보네;;;;ㅠㅠ)
아무튼 모르는 것은 일단 제껴두고 그 다음을 보자.
이 부분도 그림이 조금 잘못 됐다. %esp + 20을 하면 0x120C이다. 따라서 0x120C부터 0과 1이 쌓이기 시작해야한다. 그런데 이 부분이 조금 넌센스이다. 원래 일반적으로 책에서 배우기로는 old %ebp(0x1240)값 그 다음에 바로 0과 1같은 변수들이 쌓이기 시작해야 하는데 왜 이거는 이렇게 되는거지? 우선 이 부분도 넘어가자..ㅠㅠ
위의 과정은 쉽게 이해할 수 있어야 한다. 이 과정은 왜 하는가? 그 다음에 바로 sum 함수를 호출할 예정이기 때문이다. sum 함수는 0과 1을 파라미터 값으로 갖는다. 즉 sum함수에서 0과 1을 사용할 수 있어야 하겠지? 그러면 sum함수가 다가가기 쉬운 어떤 곳에 0과 1이 저장되어 있어야 하겠지? 바로 그것 때문에 여기에 저장해두는 것이다. 여기에 저장해두면 sum 함수 스택에서 여기를 찾기가 쉽나? 쉽다! 다음 명령어 그 다음 명령어를 보면 이해가 갈 것이다.
자... call sum 명령어이다. 이 명령어는 다음의 세 명령어 효과를 나타낸다.
subl $4, %esp
movl %eip, (%esp)
jmp sum
%esp값에 4를 빼서 한 칸을 더 확보한 후, %eip값, 즉 pc값을 %esp값이 가리키는 곳으로 집어 넣는다. 이유인 즉 이러하다.
pc값을 스택에 집어넣어놔야 나중에 sum 함수 부분에서 다시 main 함수 부분으로 돌아올 수 있다는 것이다. 위 그림에서 옆의 숫자는... 지금은 보기 쉽게 line 수로 되어있지만, 실제로 저 부분은 0x08384330과 같은 pc값이 들어간다. 즉, 각각의 명령어는 그에 대한 pc값이 있다. call 명령어를 통해 sum함수의 push 명령어 쪽으로 점프를 한 것뿐이다. 아까 %ebp 저장하고 한 것은 뭐냐고? 그거랑은 조금 다른 얘기다. 이것은 코드 영역 이야기이고 아까 노란 종이에 계속 그려가면서 설명했던 것은 스택 이야기이다. 두 개가 다른 이야기이니 혼동하지 말도록 하자.
아무튼 그 다음 스택의 모습은 이렇게 된다.
이제 sum함수에 대한 스택 프레임이 생겼다. 여기에서 수행하는 명령어는 이전에 수행했던 그 명령어들과 같으니까 추가적인 설명은 생략하겠다.
그 다음이 또 중요하다 12(%ebp)값이 무엇인지 보자. 1이다. 저 부분은 사실 main 함수의 스택 프레임 영역인데 연산을 통해서 그 영역을 침범하여 파라미터 값을 가지고 오고 있다. sum 함수가 0과 1이 필요하잖아. 그 값을 어디서 가져와야 한다 그렇지? 그걸 main 스택 프레임의 pc값 위에서 가져오는 것이다. 첫번째 파라미터부터 차례대로 쌓여있으니까 %ebp값을 기준으로 +8은 첫번째 파라미터 +12는 두번째 파라미터 +16은 세번째 파라미터 이런식으로 가게 된다.
위의 그림에서 수행한 movl명령어와 addl 명령은 간단한 명령이므로 따로 설명은 하지 않고 넘어가자.
pop명령어는 다음의 두 명령어와 같은 뜻이다.
movl (%esp), %ebp
addl $4, %esp
이는 스택의 Top에 있는 값을 %ebp에 저장한 후 스택 Top을 가리키는 레지스터인 %esp값에 4를 더하여 스택을 감소시키라는 뜻이다.
ret명령어는 위의 두 명령어와 같다. pop명령어를 통해 %eip(pc값)이 복원되었고, 그 값으로 jump하는 것이다.
그러면 이제 다시 main 함수로 돌아가서 나머지 명령어 들을 수행하면 된다.
간단한 예제인데도 그것에 대한 프로시저 콜을 공부하려고 하니까 아우... 골치가 아프다.
그래도 난 이게 재밌다. 재밌다. 재밌다.
다음에는 프로그램의 효율성을 높여보는 공부를 해보자. 지금보다는 훨씬 재밌어 질 것이다. 어떻게 효율성을 높이냐고? 이러한 어셈블리어를 이해하면 효율성을 높일 수 있는 방법이 있다. 그 방법을 공부해보자. 실제로 그러한 방법을 통해서 30초 정도 걸리는 프로그램을 1초 이내로 성능을 향상시킬 수 있다. 다음 포스팅에 뵐께요!!
p.s. 혹시라도 윗부분에 제가 설명을 말끔하게 못 해둔 부분.. 그 부분에 대해서 정답을 아시는 분이 있다면 뎃글 부탁드립니다. 뎃글 주시면 아주아주 감사하겠씁니다ㅋㅋ
'System Programming' 카테고리의 다른 글
[SP] Conditional Jump (1) | 2013.05.14 |
---|---|
[SP] Conditional Jump (Basic) (0) | 2013.05.14 |
[SP] Assembly Language Example (0) | 2013.04.30 |
[SP] Assembly Language (1) | 2013.04.30 |
[SP] Assembly Language Basic (0) | 2013.04.29 |