이야기는 바뀌어, 함수가 불렸을 때 일어나는 일.
도대체 함수가 불렸을 때, 무슨 일이 벌어 졌는지 구체적으로 살펴 보신적이 있는지요? Stack 얘기를 하다가 갑자기 왠 함수가 불렸을 때, 무슨 일이 벌어지는지 아냐고 물어보는 건 무슨 경우일까 라고 궁금하실 수 있습니다만, stack 데이터 구조에서 history 기능, 즉, 선입후출에 의하여, 가장 최근의 일을 알아 낼 수 있다는 특성을 이용하여, 함수를 계속 불러 제껴도, 자기 자리로 돌아 올 수 있다는 허무한 이야기를 대단한 듯 얘기 해 볼까 합니다. 어쨌거나, 함수를 전체 System 관점에서 다룰 때는 3가지 Question을 해결 해야 스토리 전개가 확실 할 듯 하네요.
" 함수를 호출하게 되면 실행위치의 이동은 어떻게 되는가요?"
" 함수를 호출한 후, 전달하는 인자들은 어떻게 호출된 함수로 전달되는가요?"
" 호출된 함수가 실행을 끝내고 나면 어떻게 이전에 실행하던 위치로 복귀할 수 있는가요?"
오, 참으로 신비스럽고, 성스러운 얘기가 아닐 수 없습니다.. 요.
예를 들어, 다음과 같은 함수 call들이 있다고 치고요,
A()
{
............
B();
............ ●
}
B()
{
............
C();
............
}
C()
{
............
D();
............
}
D()
{
............
}
A()는 B()를 부르고, B()는 C()를 부르고, C()는 D()를 부르고, 그렇게 점점 깊숙이 들어가다 보면, 결국 D()를 실행하고 나서는 A() 입장에서는 ● 지점으로 다시 돌아 와야 합니다. 어떤 식으로 돌아 올까요? 그렇습니다, A()에서 B()를 부를 때, stack에다가, A()로 돌아갈 수 있는 표식을 넣어 둡니다. 또한 B()에서 C()를 부를 때, B()로 돌아갈 수 있는 표식을 stack에 쌓아 두고요, 마찬가지로 C()에서 D()를 부를 때는 C()로 돌아갈 표식을 stack에 쌓아 둡니다.
보시다시피, stack에 1, 2, 3 순서로 쌓아 둔 다음 다시 거꾸로 갈 때는 3, 2, 1 순서로 꺼내 가면서 자기가 돌아갈 자리로 돌아가게 되는 것입니다. 머 간단하지요? 이것만 있다면 아무리 많이 함수를 부르더라도 간단하게 최근 것부터 꺼내서 가장 위, 가장 예전 것까지 돌아갈 수 있습니다.
그러면, 조금 더 유식한 말로 함수가 불렸을 때 일어나는 일을 정리해 보면, 다음과 같이 정리할 수 있겠습니다. - stack에는 돌아갈 주소만 들어가느냐, 그 외에 전달인자들을 넣기도 합니다. -
서브 루틴 호출시 수행되는 일
1. 전달 인자와 돌아갈 주소를 스택에 push하는일
2. 함수 호출 (즉, pc를 불리워진 함수의 주소로 jump시킴)
3. 지역변수에 대하여 스택에 저장공간을 할당하는 일
4. 호출된 함수를 수행하는일
5. stack에서 부터 할당된 지역변수 저장공간의 해제
6. 돌아갈 주소를 stack으로 부터 꺼내와 함수로부터의 복귀
7. 전달인자에 의해 사용되던 공간을 해제
. . . .
정도로 아름답게 정리 할 수도 있겠습니다. 그 예를 들어, 메모리 상에서 함수의 호출을 그림으로 보면, main()이 sum()을 부르고, sum()이 display()를 호출한다면, 처음 main()영역 0xm1번지에서부터 opcode를 실행하여, sum()을 호출하는 0xm6번지를 만나게 되면, 0xs1번지로 jump를 하면서, stack에 돌아올 주소와 전달인자 등의 여러 가지 나중에 돌아오면 복구해야 할 것들을 집어 넣습니다. 또한 sum()을 실행하기 위하여, sum()영역의 0xs1번지로 pc가 jump한 후, 계속 실행이 되다가, display()를 호출하는 0xs4번지를 만나게 되면 또한 stack에 이것저것 집어 넣고, jump하게 되지요. 반대로 main()으로 돌아올 때는 반대의 경우로 생각하시면 됩니다. - 각각의 함수들은 symbol이니까, 자기 고유의 물리적 주소를 갖습니다. -
stack이라는 것을 이용하여, 이런 식으로 함수를 호출하는데도 사용하게 되며, 참으로 편리한 데이터 구조입니다. 한가지 ARM의 R14 - LR의 용법 중 중요한 것이 있어 짚고 넘어가려 합니다. ARM은 원래 Stack 관련한 CALL이나 RET 명령어를 지원하지 않고, Linked Register (R14)를 이용하여, Branch하기 전에 돌아올 주소를 R14에 넣어두고 복귀할 때 R14를 PC에 넣고 돌아오는 Mechanism을 이용합니다. 잘 기억해 주세요! 바로 다음 section에서 진짜로 그렇게 하는지 두고 볼 거에요. 그러면 실제 함수가 어떻게 생겨먹었는지 한번 들여다 보기나 하시죠. 함수가 무엇을 하는지는 자세히 보지 마시고 뭐 그러려니 하시고요. Stack을 이용하기 위하여 어떻게 함수가 구성되는 지만 잘 살펴 보세요.
word a_fuct (word arg, word param)
{
int localone, localtwo;
word ret;
localone = (int)(arg<<1);
localtwo = (int)(param>>1);
ret = b_funct ((word)localone, (word)localtwo);
if (ret>100)
message ("too big");
else
message ("appropriate");
return ret;
}
이렇게 생긴 함수가 Assembly로는 어떻게 생겼는지 한번 보세요.
addr/line__|code_____|label____|mnemonic________________|comment_________________ |word a_funct(word arg, word param)
3985|{
ST:1E6C1A10|B510 a_funct: push {r4,r14}
| int localone, localtwo;
| word ret;
|
3989| localone = (int)(arg<<1);
ST:1E6C1A12|0040 lsl r0,r0,#0x1 ; arg,arg,#1
3990| localtwo = (int)(param>>1);
ST:1E6C1A14|0849 lsr r1,r1,#0x1 ; param,param,#1
|
3992| ret = b_funct ((word)localone, (word)localtwo);
ST:1E6C1A16|B280 uxth r0,r0 ; localone,localone
ST:1E6C1A18|FFCEF7FF bl 0x1E6C19B8 ; b
ST:1E6C1A1C|4604 cpy r4,r0
|
3994| if (ret>100)
ST:1E6C1A1E|2C64 cmp r4,#0x64 ; ret,#100
ST:1E6C1A20|D903 bls 0x1E6C1A2A
3995| message ("too big");
ST:1E6C1A22|A078 add r0,pc,#0x1E0
ST:1E6C1A24|ED62F0AE blx 0x1E7704EC
ST:1E6C1A28|E002 b 0x1E6C1A30
| else
3997| message ("appropriate");
ST:1E6C1A2A|A078 add r0,pc,#0x1E0
ST:1E6C1A2C|ED5EF0AE blx 0x1E7704EC
|
3999| return ret;
ST:1E6C1A30|4620 cpy r0,r4 ; r0,ret
4000|}
ST:1E6C1A32|BD10 pop {r4,pc}
|
자, Assembly로 보면 이렇게 생겼네요. 처음에 a_funct()함수에 진입할 때 r4만 backup하면 되나 봐요. 일단 지가 판단하기에 r4만 backup 하면 된다고 생각했나 보죠! 뭐 이런 경우에 컴파일러는 register를 backup 할 수 있도록 Assembly를 만듭니다.
` 호출 받은 함수가 스크래치 레지스터 이외의 레지스터를 훼손하는 경우
` 가령 함수 내에서 R4, R5를 훼손시키게 된다면, Stack을 이용해 이전 값을 저장하고 복구하여야 한다.
여하튼, 이런 Policy에 입각하여, r4와 돌아갈 주소 r14를 backup하고서, 마지막에 보면 pop으로 r4를 복원해주고 돌아갈 주소를 pc에 넣어 줍니다. 이렇게 함으로서 호출한 함수로 되돌아 갈 수 있는 토대를 마련하는 것이죠!
한가지 더 미련을 두고 얘기한다면 a()라는 함수가 호출 될 때, bl이라는 명령어로 호출이 될 텐데, bl이라는 명령어를 만나면 Hardware적으로 자동으로 돌아갈 주소 값을 r14 (LR)에 넣어주니 호출된 함수에서는 r14값을 저장만 잘하면 되는 거죠.
여기에서, 한가지 질문이 생길 수가 있어요!
함수를 부르고 돌아오는 과정에서,
BL function
...
...
function
...
...
MOV PC, LR
이렇게 할 경우에..
Return하면 branch 다음다음 instruction으로 return하는 거 아닙니까? PC는 항상 현재 instruction의 address보다 두 개 앞의 값을 갖고 있기 때문에 call할 때 LR에는 branch 한 곳의 다음다음 instruction의 address가 들어갈 거 같은데..이게 branch 바로 다음 instruction으로 return되는 이유 좀 알려주세여~ 라고 말이죠.
뭐, 간단합니다. LR에 돌아갈 주소를 적어 넣을 때, BL 다음의 값을 LR에 자동으로~ 넣어준답니다. ㅋ 그러면 branch 바로 다음으로 돌아갈 수 있겠죠? PC-4를 넣어주는 operation으로 한방에 캭! 그러니까 기억해 둘 것은 돌아올 때는 branch 명령 바로 다음으로 돌아와야 한다는 사실이에요. 근데 그것이 branch 명령어의 경우 branch 명령 바로 다음 값이 lr에 자동 저장 된다는 점이죠.
완전 간단간단입니다.
자, 이제 대답할 수 있나요?
" 함수를 호출하게 되면 실행위치의 이동은 어떻게 되는가요?"
→ PC를 set해서 갈 수 있겠죠. 함수 호출이라는 것도 CPU 입장에서는 순차적으로 명령어를 실행하는 것뿐이고요, 다만 우리가 임의로 pc에 실행을 원하는 주소를 넣어주는 것 뿐인 겝니다.
" 함수를 호출하여 전달하고자 하는 인자들은 어떻게 호출된 함수로 전달되는가요?"
→ 요놈은 AAPCS라는 걸 이용해서 정해진 Register에 값을 전달하게 되는데, 앞에서 본 적 있지요? .
" 호출된 함수가 실행을 끝내고 나면 어떻게 이전에 실행하던 위치로 복귀할 수 있는가요?" → LR이란 걸 이용해서 하지요!
댓글