그림을 잘 보면 스택의 bottom 이 위쪽에 있고, top 이 아래쪽에 있다!
stack은 메모리를 더 사용할수록 밑으로 자라난다.
(cf. heap 은 위로 자라남)
rsp 라는 특수한 레지스터가 스택의 맨 아래 주소를 가리키고 있음 (즉, 스택의 top 원소를 가리킨다)
pushq src
스택에 원소를 push 하면 rsp 가 가지고 있던 주소값을 내려준다 (즉, %rsp 레지스터가 가리키고 있던 주소를 8byte 만큼 뺴준다.)
자동으로 rsp 의 주소값을 8을 빼주거나 더한다.
(movq 와 같은 명령어로 데이터를 삽입하는 명령어들은 내가 직접 %rsp 주소값을 직접 뺴주거나 더해야함)
popq dst
- 명령어 : callq jump할주소 < 함수명 >
ex) callq 400544 < func1 > => 함수 func1 을 호출하고 400544 라는 주소로 jump 한다.
c언어에서 함수라고 불리는 것이 어셈블리에서는 Procedure 이다.
( 즉, function call == procedure call)
c언어에서는 함수 호출시 명시적으로 정의가 되어있어서 그냥 호출하면 되지만, 어셈에서는 함수가 아닌 그냥 특정 위치에 있는 코드(label) 을 호출하는 방식이다.
명령어 : ret
해당 procedure 가 종료후, 스택에 저장되어있던 다시 되돌아 가야 할 주소 (return address) 로 돌아간뒤에 스택에 저장된 return address 를 삭제한다.
=> function call (즉 procedure call) 이 일어날떄 함수의 내용을 수행 후 종료될때 다시 자기가 돌아갈 주소를 알고 있어야한다!
400550 : callq 400500 < mult2 >
=> mult2 라는 함수호출이 발생하면 400550 이라는 주소로 jump 한다 (400500 == mult2 함수의 시작주소)
rsp : 앞서 배웠듯이 stack 의 시작주소를 가리키는 포인터이다.
rip : 현재 수행중인 명령어(instruction) 코드를 가리키는 레지스터(포인터) 이다.
=> top 에 저장하기위해선, 기존에 스택의 top 에 저장되어있던 데이터를 한칸 미루고 생긴 빈공간에 push 하면 된다.
어셈블리에서는 함수의 인자를 register 와 stack 을 활용해서 제한없이 받을 수 있다.
- register 에는 최대 6개까지 인자를 할당 가능하고,
6개가 오바되면 스택에 인자들이 쌓인다.
함수의 주는 input 값은 rdi, rsi, rdx, rcx, r8, r9 순으로 들어간다.
위 그림에서 보이는 레지스터 6개만 함수 호출시 input argument 를 넣어줄 수 있다.
(즉, 함수의 인자(parameter) 값으로는 최대 6개만 선언 가능하다)
만일 함수의 인자가 6개가 넘어가면 스택에 쌓인다.
( 스택에 함수의 7,8,...,n번쨰 인자가 차례대로 쌓이는데, 거꾸로 쌓인다. 스택의 맨 아래가 top 이므로 거꾸로 쌓임 )
yoo() 함수가 who() 를 호출하고, who() 함수가 amI() 함수를 호출하고, amI() 함수는 재귀함수로써 자기자신을 호출한다.
이렇게 함수가 불리고 불리는 관계를 트리 형태로 그려놓은 것을 Call Chain 이라고 한다.
각 함수마다 자신만의 stack frame 을 가지고 있다.
각각의 함수(procedure)는 자신만의 스택 공간이 있어서,
자신의 스택 공간에만 각자 데이터를 할당 가능하다.
예를들어 함수안에서 int a; 를 선언하면 자신의 스택에 할당한다.
함수 호출시 스택에 지역변수(local variable) 들이 push 되다가 마지막에 return address 가 push 된다. 여기까지가 해당 함수의 stack frame 이다. 이 과정이 다 끝나면 또 다른 새로운 함수가 호출될 것이다!
각 함수에 대한 stack frame 마다 가지고 있는 데이터는 아래와 같다.
1) return information
2) local storage (
3) temporary space
스택 frame 의 할당은 function call 을 할때 일어나고,
(by "call" 명령어)
해제는 함수를 return 할때 실행된다. (by "ret" 명령어)
과정1)
위와 같이 call chain 이 생겼다. yoo - who - ami - ami(재귀호출) 구조이다.
위에서 계속 배웠듯이 rsp 가 스택의 top 을 가리킴
rbp 는 stack frame 의 시작점을 가리킨다.
과정2)
과정3)
과정4)
과정5)
설명 생략하겠삼~
rsp 에서 16을 뺀다. 즉 rsp 가 스택의 top 을 가리키고 있었는데 16을 빼고 스택에 새로운 2칸을 만든다.
새로운 함수를 호출하기 전에, 항상 현재 실행중인 함수에 대한 정보를 register 가 아닌 어딘가에 저장해놔야 한다.
call 입장에 데이터를 저장하고 싶은경우, 해당 데이터를 어딘가에 옮겨놓고 기억하면 될 것이다.
caller 입장에서는, 새로운 함수를 그냥 바로 호출하고, 새로운 함수가 호출되자마자 "아,직전의 함수가 내가 필요하다고 했었지?" 내가 저장해 놨다가 리턴할 떄 줘야겠다라는 것
- caller saved : 호출된 함수 자기자신이 데이터를 저장하는 방식
- callee saved : 새롭게 호출한 함수가 자신을 호출해준 함수에 대한 데이터를 임시적으로(temporary data) 저장하는 방식
caller (부르는 애) 가 자기껄 자기가 챙기면 caller saved 라고 부름
callee (불리는 애) 가 저장해줬다가 리턴할 떄 다시 복구해주면 callee saved 이다.
새로운 함수가 호출될 떄 마다, 아래의 15개의 레지스터들은 항상 모두 새롭게 비워줘야한다!
=> 새로운 함수가 호출되면, 그 함수만 무조건 독단적으로 아래 레지스터들을 사용해야하기 때문.
%rax
caller-saved 라고 불림 (자기자신이 데이터를 저장함)
=> 리턴할 값(return value) 을 자기자신이 포함하고 있음.
rax 에 어떤 데이터값을 저장해 놓았다면, 그 데이터는 반드시 스택에 따로 저장을 해줘야한다!
%rdi, ..., %r9
Callee saved register
- callee가 해당 레지스터 값을 변경하지 않는 것을 보장하는 레지스터.
- caller는 해당 레지스터 값이 변경되지 않음을 예상할 수 있다.
Caller saved register
- callee는 해당 레지스터를 변경할 수 있다.
- 그러므로, caller는 해당 레지스터 값을 저장해 두었다가 회복(restore)시켜야 할 것이다.
(뭐가 맞는 설명일까..)
callee saved 레지스터이다. call_incr2 을 호출한 직전 함수가 있을텐데, 그 직전함수의 데이터를 rbx 에 pushq 연산을 통해 전달받고 rbx에 저장하고 있다가 나중에 함수가 종료될때 해당 함수한테 다시 넘겨줘야한다.
terminal case == base condition
testq %rdi %rdi : %rdi 값이 0인가 아닌가를 묻는것
(testq a b : a 와 b 를 AND 하는것. 즉, a와 b 둘다 0이면 0이되고, 둘중 하나라도 0이면 0이고, 둘 다 1이여야지만 1이된다.
이에따라 testq %rdi %rdi란, 곧 rdi 값이 0인지 아닌지를 묻는것.)
rbx 에 대해 caller saved 가 되었다. 새로운 함수들이 재귀함수로써 계속 호출되어도, 현재 함수에 대한 rbx 에는 계속 데이터가 저장되어 있어야함
callee saved 로써 재귀구조에서 직전에 자신을 불러준(호출한) 함수의 데이터를 스택에 게속 종료되기 전까지 저장한다. 변하지 않는값!
(x & 1) 과 pcount_r(x >> 1) 을 더해서 리턴해줘야하는데, 아직 재귀함수가 돌지 않아서 pcount_r(x >> 1) 의 값을 모른다.
movq %rdi, %rbx : 따라서 rbi ( = x) 의 값을 rbx 에 저장해두고,
andl $1, %ebx : x에 AND 연산, 즉 x & 1 을 하고 다시 rbx (=ebx)에 저장해둔다.
shrq : 그러고나서 rdi 가 저장하고 있는 값에대해 쉬프트연산을 한 값을 pcount_r 의 인자로 넣어준다.
재귀가 끝나고 나면 rbx 쟁겨놨던 값과 함수의 리턴값을 rax 에 넣어서 리턴시킬 준비를 한다.
그리고 자신을 불러준 함수에 대한 데이터의 공간인 rbx 를 pop해준다.