[system] 호출 규약

x86의 호출 규약

x86은 레지스터의 수가 적기 때문에 함수의 인자를 모두 레지스터에 올리기?가 좀 어렵다. 따라서 x86은 함수에 진입하기 전 스택에 함수의 인자들을 push하는 방법을 선택했다. 스택에 함수의 인자들을 올릴 때는 거꾸로 의 순서로 push한다. (마지막거가 제일 첫번째로 온다.) 그리고 caller가 사용한 stack을 정리한다.

x86-64의 호출 규약

x86-64는 x86에 비해 레지스터의 수가 많이 때문에 무조건적으로 스택에 함수의 인자들을 올리지는 않는다. 일단 최대한 레지스터에 올리고, 그래도 아직 처리해야 할 인자들이 있다면 그 인자들을 스택에 push한다. 리눅스는 sysv 기반으로 만들어졌다고 한다! sysv가 정의한 함수 호출 규약을 보면, 첫 여섯 개의 인자는 rdi, rsi, rdx, rcx, r8, r9에 순서대로 저장한다. 남은 인자가 있다면 이제서야 스택에 저장된다. (아마도 순서대로 저장되는 듯 하다.) 마찬가지로 caller가 사용한 stack을 정리 한다. 알아둬야 할 점은, 함수의 return 값이 rax에 저장된다는 것이다.

함수의 도입부

1
2
3
call funcname
push rbp
mov rsp, rbp

1. call funcname

이 부분은 함수의 도입부는 아니지만 함수가 시작되기 바로 직전에 call funcname <0x000000>와 같은 코드가 나타난다. 이것은 push와 jump 두 개의 코드가 합쳐져 있는 형태라고 생각하면 된다. 가장 먼저 진입한 함수 실행이 끝난 후 돌아와야 할 주소(call 다음 주소)를 push하여 stack에 저장하고, call이 가리키는 주소로 jump한다. 이 과정이 끝나면 되면 함수에 진입했다고 보게 된다.

2. push rbp / mov rsp, rbp

함수가 막 시작될 때, 즉 도입부일때 push rbp가 주로 나타난다. 이것은 rbp의 값을 스택에 올리라는 말이다. 그럼 rbp가 의미하는 값이 무엇인가? rbp는 스택 프레임의 가장 낮은 주소를 가리키는 말로, sfp(stack frame pointer)를 말한다. 그냥 이전 함수가 할당 받은 스택 공간의 시작점을 가리키고 있다고 생각하면 편할 것 같다! 결국 함수 진입 전에 실행되던 함수로 돌아가서 또 뚱땅뚱땅 처리해야 하는데, rbp를 덮어써버리면 이전에 실행되던 함수의 스택의 시작점이 어딘지 모르니까 rbp를 잠깐 스택에 push해 주는 거다. 이후에는 rbp에 현재 스택 프레임의 시작점, 즉 sub rsp 8 이런거 하기 전의 rsp 값을 저장해주면서 (mov rbp, rsp)현재 스택 프레임의 시작점을 rbp에 저장한다.

함수의 복귀

1
2
pop rbp (or leave)
ret

1. pop rbp (or leave)

아까 말했듯이 rax에 함수의 반환값이 저장된다. 저장 이후에는, 이전 함수로 돌아갈 준비를 하기 시작한다. pop rbp는 rsp가 가리키는 걸 한 칸 내리고 그 한 칸에 들어있던 정보(이게 결국은 아까 말했던 이전 함수의 스택 시작 주소)를 rbp에 저장한다. 결국 rbp는 이전 함수의 스택 시작 부분을 가리키게 된다. 아무런 스택 프레임을 할당하지 않은 경우는 이렇게 rsp를 한 칸만 딱 올리고 rbp에 저장하면 되지만 이전에 스택프레임이 할당 된 경우에는 한 칸만 올리면 이전 함수의 스택 주소가 바로 나타나지 않기 때문에 pop rbp를 직접적으로 사용하지 않고 leave를 사용한다.

2. ret

ret은 이제 진짜 이전 함수로 돌아가는 거다. 우리가 아까 함수의 도입부 1. call funcname에서 돌아와야 할 주소를 stack에 push했기 때문에, 그리고 위의 leave 때문에 rsp는 딱 돌아와야 할 주소를 가리키고 있다. 여기서 pop rip를 해서 instrunction pointer를 stack에 있는 지금 돌아와야 할 주소의 값을 저장하도록 해주고, jump rip를 해서 방법 rip에 저장한 주소로 jump, 즉 돌아가야 할 주소로 jump하면 ret이 끝난다.