시스템 프로그래밍

본 토픽은 현재 준비중입니다. 공동공부에 참여하시면 완성 되었을 때 알려드립니다.

Call & Return : 호출하고 돌아오기

함수A 에서 함수B를 호출하고, 다시 함수A로 돌아오는 방법을 배웁니다
  1. main 함수에서 어떻게 swap 함수의 코드가 저장되어 있는 메모리 공간으로 이동할 수 있을까?
  2. swap 함수에서 어떻게  main 함수의 코드가 저장되어 있는 공간으로 돌아올 수 있을까?
  3. main 함수에서 swap을 호출할 때, 어떻게 파라미터(i의 주소와 j의 주소)를 넘겨줄 수 있을까?
  4. swap 함수 내에서 지역변수를 어디에 저장할까?
  5. main 함수와 swap 함수가 레지스터를 같이 사용한다면 서로의 값을 변경해버리는 불상사가 발생하지 않을까?
  6.  swap 함수는 어떻게 리턴 값(return value)을 main으로 전달할까?

 

1번 문제를 해결하기 위해서는 jump를 사용할 수 있을 듯 하다.

그러나 swap에서 main으로 이동하기 위해 jump를 사용할 수 있을까?

swap 의 마지막에 jmp main 을 한다면 main이 아니라 다른 함수, 이를 테면 max에서 swap을 호출한후 max 대신 main으로 돌아가버리는 대참사가 발생한다. 

그렇다면 고정된 라벨의 주소로 돌아가는 것이 아니라, 호출한 함수의 코드가 저장된 주소를 레지스터에 저장한 후 swap이 끝나면 그 주소로 돌아가게 하면 어떨까?

main:
    ## something
    movl $main_return_point, %eax
    jmp swap ## call swap

main_return_point:
    ## something after calling swap 

max:
    ## something
    movl $max_return_point, %eax
    jmp swap ## call swap

max_return_point:
    ## something after calling swap

swap:
    ## something
    jmp *%eax ## return to main_return_point or max_return_point

그러나 이 경우에도 역시 문제가 있다.

swap이 print_swap이라는 다른 함수를 호출한다고 생각해보자.

똑같이 %eax에 돌아올 주소를 저장한다면, main 함수의 주소를 덮어쓰게 되므로 main으로 돌아오지 못한다.

 

따라서 평범한 레지스터를 활용하여 문제를 해결하는 것은 적절하지 않다.

이를 해결하기 위해, 새로운 물리적 저장공간인 스택(stack)을 이용한다.

가끔 들어보았을 스택 넘침(stack overflow)이 여기서 유래하는데, 함수 안에서 함수를 또 부르고, 그 함수 안에서 다른 함수를 또 부르는 일을 너무 많이하는 바람에 (보통 재귀함수 등의 종료 조건이 잘못된 경우일 때)  돌아갈 주소를 저장해놓는 공간 - 스택 - 이 넘쳐버린 것이다.

 

오로지 스택을 관리하기 위해, ESP (stack pointer register)라는 새로운 레지스터가 등장한다. 이 친구는 스택의 맨 위에 있는 값을 가리킨다. 즉, 스택의 맨 윗 값의 주소를 저장한다.

 EIP는 바로 다음에 수행할 명령의 주소를 저장하므로,

  1. 함수를 호출할 때 마다 그 명령의 주소(즉 EIP)를 스택에 넣는다.
  2. 다시 돌아올 때마다 가장 최근에 밀어넣어진(Last in) 명렁의 주소를 불러온다. 즉, ESP를 불러온다.
  3. 돌아왔으면 스택에서 사라져야하므로, ESP를 이동시킨다. C언어 명령 하나가 4바이트만큼의 공간을 차지한다고 가정하면, 4바이트만큼 ESP를 이동시키면 두 번째로 최근에 호출한 함수를 불러올 수 있다.

위 단계를 어셈블리 명령어로 구현한 것이 바로 call, ret, push, pop이다.

이들은 ESP와, 직접 접근할 수는 없는 EIP를 컨트롤해준다.

늘 그렇듯이, 32비트 운영체제라는 걸 알려주는 'l'이 명령어 뒤에 붙는다.

 

차례차례 명령어의 의미를 알아보자.

  1.  calll은 리턴 값을 스택에 밀어넣는다(push)
  2. ret은 스택의 맨 위에 있는 주소로 돌아간다(return)
  3. push는 스택에 주소를 밀어넣는다(push)
  4. pop은 스택의 맨 위에 있는 주소를 잡아뺀다(pop)

 

C언어 명령 하나가 4바이트만큼의 공간을 차지한다고 가정하고 push, pop, call, ret를 보다 기초적인 어셈블리어로 번역해보자.

명령어 의미

pushl src

subl $4, %esp

movl src, (%esp)

popl dest

 

movl (%esp)

dest

addl $4, esp

call addr

pushl $eip

jmp addr

ret

popl %eip

*eip에는 접근할 수 없다! 굳이 어셈블리어로 써보자면 이런 개념이라는 뜻이다.

 

그럼 이제 main에서 swap을 부르고, swap에서 print_swap을 부르는 프로그램을 어셈블리 언어로 짜보자.

main:
    ## something
    movl $main_return_point, %eax
    call swap ## call swap

main_return_point:
    ## something after calling swap

swap:
    ## something
    call print_swap ## return to main_return_point or max_return_point

swap_return_point:
    ## something
    ret main_return_point

print_swap:
    ## something
    ret swap_return_point

뭐 구체적인 내용은 하나도 없고 실용적이지도 않아서 이해가 힘들 수 있다. 그냥 ret이랑 call이 어떻게 쓰였는지만 확인하자.

 

 

 
  • 봤어요 (0명)

댓글

댓글 본문
버전 관리
레트로
현재 버전
선택 버전
graphittie 자세히 보기