Stack and Subprograms 2

2 minute read

Accessing Parameters with ESP

image

print_int : 
            push ebp
            mov ebp, esp
            ...
            ...
            pop ebp

함수가 call 되면 ebp 레지스터가 그 당시의 esp 값을 복사한다.
단, ebp 레지스터는 범용 레지스터이기 때문에 호출 이전에 쓰였을 경우도 있으므로
push ebp와, 마지막 pop ebp 를 통해 ebp 를 복사, 저장한다.

ebp 에 들어있는 값은 바뀌지 않는다.
ebp 는 스택의 기준 주소가 된다. 그래서 ebp 에 index 를 더해서
parameter 에 접근한다.

image

일반적인 함수 호출자와 피호출자의 형태와 매개변수 전달

호출자 :

push  dword 1     ; 1을 매개변수로 전달
call  subprogram  
add   esp, 4      ; 스택에서 매개변수를 제거

여기서 마지막 줄의 add esp, 4 는 stack 에서 매개변수를 pop 하는 동작이다.
pop 하면 되지만, 사실 함수가 종료된 후 매개변수는 필요 없기 때문에,
어디다 저장하지 않고 esp 만 증가시켜준다.

피호출자 :

subprogram : 
            push ebp          ; 원래의 ebp 값을 스택에 저장한다.
            mov ebp, esp      ; 새 ebp 에 esp 값을 저장한다.
            
            ; subprogram code
            
            pop ebp           ; 원래 ebp 값을 복구한다.
            ret

push ebp 동작을 통해
이전에 뭘 했든 복구가 된다.

스택에 올라가는 지역변수

스택은 지역 변수의 위치로도 사용될 수 있다.
스택은 C 프로그램이 일반 (automatic) 변수들을 저장하는 바로 그곳.
subprogram 을 reentrant 하게 만들기 위함.
지역 변수는 필요한 바이트만큼 ESP 에서 빼는 방식으로 저장.
EBP 의 값이 스택에 들어간 직후 저장이 된다.
EBP 레지스터를 베이스로 한 indirect addressing 으로 접근한다.

esp - n byte 으로 지역변수를 할당한다.
즉 할당 후 ebp - m byte 로 지역변수에 접근한다. (n>=m)

이러한 설계 때문에 같은 함수를 또 부르더라도
두번째, 세번째 함수가 쌓여도 첫번째 함수가 쓰던 매개변수는 남아있다.

따라서 함수의 재귀호출이 가능하다. 이를 reentrant 하다고 한다.

정리

main() {
  ...
  i = func(1, 2);
  ...
}
int func(int x, int y) {
  int a, b, c;
  ...
  return c;
}

위의 C 코드는 아래의 어셈블리어와 같은 일을 한다.

...
push  dword 2
push  dword 1
call  func
add esp, 8
func :
       push ebp             ; 원래의 ebp 값을 저장
       mov  ebp, esp        ; 새로운 ebp 에 esp 저장
       sub  esp, 12         ; 지역변수로 사용할 바이트 할당
            
       ; subprogram code
            
       mov  eax, [ebp-12]  ; 리턴값 전달
       mov  esp, ebp       ; 지역변수 해제
       pop  ebp            ; 원래 ebp 복구
       ret

위에서의 스택 프레임은

ESP + 24 EBP + 12 2
ESP + 20 EBP + 8 1
ESP + 16 EBP + 4 Return address
ESP + 12 EBP Saved EBP
ESP + 8 EBP - 4 Local var a
ESP + 4 EBP - 8 Local var b
ESP EBP - 12 Local var c

가 된다. 또한, 이런 frame 은 함수 호출될 때 생겼다가 함수가 끝나면 사라진다.
함수가 중첩되면, 이 프레임이 계속 생기는 것이다. 밑에 계속해서 생긴다.(reentrant)
따라서, 함수가 제대로 종료되지 않고 계속 쌓이면 Stack Segment 를 넘어가게 되는 일이 발생.
이렇게 되면 Stack Overflow 가 난다. 그래서 Call 과 return 의 균형이 맞아야 한다.