ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 함수호출 과정
    @ 16. 1 ~ 17. 1/시스템 프로그래밍 2016. 11. 19. 12:42

    스택세그먼트 : 프로그램에서 사용하기 편하도록 메모리를 몇 가지 용도로 나눈 것중 하나를 의미한다.

    프로그램 코드가 들어있는 코드 세그먼트, 전역변수를 저장하는 데이터 세그먼트, 등의 일부임

    스택프레임 : 함수호출할때 복귀주소나 인자 같은걸 특정 절대 주소로 사용하면 중첩호출이 불가능

    그래서 호출할때마다 새로운 스택프레임을 구성한다.


    C에서의 함수라는 건 일반적으로 프로시저 및 함수라고 부르는 두가지 개념을 모두 포함한것.

    자주 쓰이는 기능을 프로시저라는 별도의 코드로 작성해 놓고 필요한 곳에서 프로시저를 부르는것..

    C에서는 별도로 두지 않고 함수의 리턴값이 없는 void를 허용함으로서 대신함...

    예를 들어 이런게 없으면 매번 printf 호출할때마다 계속 똑같이 써야한다..그 내용전부를..

    그런데 엄연히 함수와 프로시저는 닮았지만 함수는 호출의 결과값이 있다는 점에서 다르다.


    함수의 중요한 요소

    1. 함수 루틴으로 점프하여 수행이 끝난 후 다시 함수를 호출한 지점. 즉, 점프 인스트럭션이 호출된 지점으로 복귀되어야 한다.

    2. 함수를 호출할때 함수가 리턴할 때 함수와 호출된 지점 간에 데이터 교환이 있어야 한다.(호출하는 코드 : 콜러, 호출되는 함수 콜리)

    (즉, 함수를 호출할때는 인자값을 함수에게 넘겨 함수가 그 인자에 대해 적절한 동작을 할 수 있도록 해야하며, 이 동작의 결과 발생한 결과값을 다시 호출한 코드에서 활용할 수 있도록 어떤식으로 다시 넘겨받아야 한다)

    3. 함수의 호출은 중첩이 되어야 한다. 

    이런 3가지 기능을 지원하기 위해 스택 자료구조가 이용된다.


    jmp인스트럭션 등을 써서 함수의 코드가 있는 곳으로 점프 -> 하지만...함수가 끝나고 다시 호출된 코드가 있는 곳으로 돌아오는것 역시

    점프 인스트럭션등을 써서 구현하긴하는데....문제는 이렇다 함수의 코드 자체는 고정된 메모리 주소에 존재하기 때문에 컴파일러가 함수 호출이 발생할때마다 그 주소를 계산할 수 있지만...

    되돌아갈때는 주소가 고정되어 있지 않다....그래서 호출을 지원해주기 위해 특별한 레지스터를 마련하고 이 레지스터에 리턴 주소를 보관하는 방식이다. 

    call (jmp와 마찬가지로 주어진 주소로 점프, 단 이때 call 인스트럭션 바로 다음 주소를 특정한 레지스터에 저장하게 된다)

    ret이라는 복귀 인스트럭션을 제공..이 ret인스트럭션은 call때 저장한 특정한 레지스터 값으로 점프하는것..

    ** call -> call 다음주소를 저장해놓고 점프 -> 함수 수행 -> ret 수행(call 할때 저장된 값으로 점프)

    그런데 call 다음 주소를 저장해놓는다는게..계속 한곳에 쓰게 되면 안되잖아..그래서 고안된 방식이 바로 스택 프레임이다.

    복귀 주소를 앞에서 든 예처럼 특정 레지스터나 특정 메모리 주소에 저장하는 대신 스택 공간안에 쌓아간다고 생각하면..

    그리고 이렇게 복귀 주소를 쌓아둔 메모리 중 가장 윗 부분 주소를 특정 레지스터에 저장해두록 약속한다면..이걸 SP라고 부른다면


    main()

    {
        func1();

    }

    func1()

    {
         func2();

    }

    이런 구조로 되어있다면..

    func1을 호출한 바로 다음주소를 0x400, fun1에서 fun2를 호출한 다음주소가 0x100이라면?

    그리고 스택 세그먼트의 시작 주소를 0x88c번지 그리고 이 스택 주소는 항상 SP라는 스택 포인터 레지스터에 저자되어 있다고 가정


    1. func1()함수 호출전에는 0x88c를 sp가 가리킨다고 생각하겠지만 0x888이 맞다 왜냐면 main도 함수 이기 때문에 그렇다..

    2. func1()함수가 불리우면 스택 세그먼트에는 0x400주소가 들어가고 sp는 0x884를 가리킨다.

    2. func2()함수가 불리우면? 스택 세그먼트에는 0x100이 또 쌓이고 sp는 0x880을 가리킨다. 

    스택과 재귀호출의 문제점(해결방안)

    http://hanmomhanda.github.io/2015/07/27/%EC%9E%AC%EA%B7%80-%EB%B0%98%EB%B3%B5-Tail-Recursion/


    스택 프레임이란 함수를 호출 할때 그 함수 호출을 위해 필요한 데이터를 저장하는 메모리 덩어리


    어쨌든  함수를 호출할때 복귀 주소를 저장하고 그 주소 크기만큼 스택 포인터 SP를 조정하였다.  (32비트 주소체계를 가진 CPU라면 4바이트가 된다) 그런데 스택 프레임 사용해서 인자 전달까지 하는 경우 함수 호출 시(정확히는 인자는 함수가 호출되지 전에 스택에 저장해야 호출된 함수에서 스택 포인터 레지스터를 통해 엑세스 할 수 있다) 인자들을 복귀 주소 바로 다음에 차곡차곡 저장하고 복귀 주소와 저장된 인자 크기만큼 SP를 조정해주고 호출받은 함수 측에서는 다시 이 SP를 사용하여 해당 값을 엑세스해 인자 교환이 일어난다..

    근데 이때 인자 크기만큼이지만, 인자가 char이라고 1바이트라고해서 스택 세그먼트에서의 공간이 1바이트로 조정되는게 아니라, 일괄 4바이트(32비트 체계)로 된다 그 이유는 별도의 각각 자료형으로 cpu에서 처리가 안되고 설사 된다해도 딱딱 크기가 4바이트로 할당 해제되는게 성능에서 더 좋다

    스택 세그먼트에 저장된 인자는 SP를 이용해서 접근이 가능하다. SP + 4, SP + 8 이런식으로..

    즉, 스택 포인터 레지스터(SP)는 항상 스택의 제일 끝을 가리키고 있으며 이 끝 주소를 기준으로 앞 쪽에 차곡차곡 복귀 주소와 함수의 인자 값이 저장되어 있다는 것을 알고 있으므로 얼마든지 이들을 엑세스 할 수 있다.


    그럼 복귀주소, 인자값까지는 해결되었다 치자..반환값은? 반환값도 스택 프레임을 활용할까? 아니다..보통은 레지스터를 활용한다.

    호출할떄와 달리 반환값은 보통 하나로 고정되어 있기 때문이다. EAX라는 레지스터다.(정수가 아닌 실수라면 별도의 레지스터에 저장된다)

    스택 포인터로는 (x86 CPU에선 ESP라고 불린다)


    함수 호출과정이 끝나게 되면 호출하기 이전의 상태로 스택을 되돌려 놓아야 하는데..

    위의 과정에서 인자가 4바이트 2개 8바이트 + call 인스트럭션이 자동으로 저장하게 되는 복귀주소 4바이트 해서 = 12바이트여야 할것같은데

    실상은 그렇지 않다. 8바이트만 줄이면 된다.(스택은 큰 -> 작가니까 줄인다는건 큰 <- 작 방향으로 간다는것) 왜냐면

    자동으로 함수 호출 이후에 호출한 측에서는 복귀주소 4바이트를 미리 해제를 해주니까 4바이트는 신경안써도됨..그래서 8바이트만 신경쓰면됨

    add esp 8 이런거지요..


    함수를 호출할떄 사용한 스택을 해지하는 작업을 

    왜 일부는 호출된 함수 내부에서하고 일부는 호출 이후에 호출한 측에서 처리하는걸까? 

    이건 바로 호출규약의 문제이다. 


    특별히 명시 없는 기본은 __cdecl 이다 C의 기본 함수 호출규약을 따른다. 

    이 호출 방식에는 기본적으로 인자가 차지하는 스택을 해지하는 책임이 호출한 측에 있다.

    즉 위에서 살펴본 sum함수에 전달한 인자 값 8바이트를 스택에서 해지하는 것은 sum함수가 리턴한 다음에 호출한 측에서 add esp 8을 통해서 행하는 것이다.(인자값이 만약에 3개라면? 12바이트겠지..)


    다른 규약도 있는데 그건 __stdcall 이라고 한다. 이것은 인자에 대한 스택 해지까지 호출된 함수 내부에서 하며 따라서 호출한 측에서는

    함수 호출이 끝난 다음에 별도의 작업을 할 필요가 없어 독립성이 뛰어나다 (즉, 위의 add esp 8이라는 코드가 매번 함수 호출할때마다 생성될 필요가 없으므로 전체적인 프로그램의 코드 사이즈도 작아진다)


    추가로 속도를 더 높이기 위해 두 개의 파라미터까지는 메모리를 사용하지 않고 레지스터를 사용해서 전달하는 __fastcall이라는 호출규약도 있다. 이 경우 함수 인자 ecx와 edx라는 레지스터를 통해 전달(eax는 반환값에 대한 레지스터이다)하고 그 이상의 인자에 대해서는 __stdcall과 마찬가지로 함수의 내부에서 인자의 해지를 담당한다. 

    (__cdcel __stdcall에서 2개인자가 있을경우 메모리에 push 15H이런식인데 __fastcall은 move edx, 15h 이런식으로 레지를 사용한다)

    (ebp = esp를 대입해서 넣고 쓴다..temp변수같은 개념이네..그런데 왜 이런짓을 할까?

    그 이유는 esp의 원본값을 살려두기 위함..왜냐면 중간에 push나 pop이런걸 하게 되면 수시로 변하니까..그렇게 되면 상대벅인 오프셋값이

    틀어지니까..그냥 편하게 하기 위함이다..-_-결국 스택프레임을 구성하면서 ebp레지스터에 esp값을 옮겨와 사용하되 함수가 리턴하기 바로 직전에 이를 복구한다는..그래서 호출한 측에서는 여전히 같은 ebp값으로 작업이 가능하다능..)


    그런데 한가지 해결해야할게 남았다. 바로 지역변수다!

    어쨋든 함수의 독립성을 유지하는 가장 전형적인 방법이 매 함수 호출 때마다 함수 내부에서 사용하는 변수를 몽땅 메모리에 새로 할당받는 방법이다. 이렇게 되면 함수가 아무리 중복되어 호출되더라도 각 함수마다 서로 다른 메모리에서 작업하므로 함수간의 독립성을 유지할 수 있다. 

    또 어쨋든 지역변수가 있다면 스택포인터를 esp를 ebp에 대입하고 나서 지역변수의 해당크기만큼 공간을 늘리게 된다.

    왜냐면 esp 레지스터가 가리키고 있는 곳이 스택의 끝이니까.. 그리고esp로는 ebp - 4 ebp - 8 번지로 이렇게 엑세스 할 수가 있다.

    마지막에 move esp ebp가 있는데 왜 esp를 ebp로 하나면 아까 위에서 지역변수만큼 늘린공간에 대해서 복귀하기 위함이다.

    pop bsp란? 스택 세그먼트에서 pop하는건데..4바이트만큼;;


    아까 위의 내용을 다시한번보자 호출규약만 봤을땐 __stdcall이 더 좋아보이는건 사실이다 그런데 왜 모두다 __stdcall형태가 아닐까?

    그 이유는 가변인자 때문이다. 가변인자란 printf함수같이 얼마나 매개변수일지 모르는것이다. 

    두가지 방식에서의 차이는(stdcall의 문제라고 해야하나..) 함수 수행이 끝나고 인자 부분을 스택에서 해지할 때 생긴다.


    만일 cdcel방식이라면 인자 부분에 대한 해지를 호출한 곳에서 매번 해주므로 printf함수를 부를 때마다 인자 개수를 매번 다르게 주어도 아무 상관없다. add esp xx 이게 함수를 호출한 곳마다 생긴다는..xx만 알아서 컴파일러가 인자 개수만큼 정하면되는데..

    반면에 stdcall의 경우 이야기가 다르다. 인자 해지코드가 함수 내부로 들어가야 하는데..가변인자에서는 과연 몇개의 인자가 전달되었는지 함수 내부에선 모르기 때문이다..

    (아 ..이거다 이거..뭐냐면....호출한 함수에서는 push push를 해서 스택 세그먼트에서 그 최종 값을 알수 있는데.. 호출 된 함수에서는

    그 push과정이 없다는거다....그래서 모른다는건가>?? 아 시박..그런데 ret xx 이값은 어떻게 가변인자가 아닐때 알 수있냐고..

    (시벌..다른사람들도 다 이런다..함수는 인수 카운트를 얻고 인수들을 대상으로 하나씩 접근한다. 


    이런식의 가변 인자를 갖는 함수의 구현은 RET 명령어에서의 상수 인코딩시 스택 정리를 그에 맞게 변화 시키지 못하기 때문에 스택 정리의 책임을 호출하는 측에 맡기는 것이라고 한다.

    물론 이에 대해서 여러 의문들이 떠오를 것이다.
    그러나 부끄럽게도 이 글을 작성하고 있는 본인 또한 그저 컴파일러 설계상의 어려움 때문이 아닐까 하고 추정만 하고 있을 뿐이지 위 이상의 자세한 이유는 알지 못한다.

    이에 대해서는 나중에 더 공부를 하고 난 뒤에 추가적인 글을 다룰 생각이다.

    어찌 되었든, __cdecl 호출 규약은 가변적인 인수의 개수를 위한 스택 연산이 가능하다.

    반면 __stdcall 호출 규약은 가변적인 인수를 다룰 수 없는 대신에 서브루틴 호출을 위해서 생성하는 코드의 양을 줄여주며 호출하는 측이 스택을 정리하는 것을 잊지 않도록 해준다.

    클래스 상에서는 일반적으로 멤버 함수의 인자 개수가 정해져 있고 객체지향적인 관점에서의 프로시저의 독립성을 위해서 함수 내부에서 스택 프레임이 최종 반환되는 형태로 작업이 수행되는 것이다.)
    ㅡㅡ..일단 여기까지 아 



Designed by Tistory.