어셈블리어 x86-64 시스템의 Stack 구조

msung99·2022년 9월 28일
2
post-thumbnail

x86-64 Stack

  • 그림을 잘 보면 스택의 bottom 이 위쪽에 있고, top 이 아래쪽에 있다!

  • stack은 메모리를 더 사용할수록 밑으로 자라난다.
    (cf. heap 은 위로 자라남)

  • rsp 라는 특수한 레지스터가 스택의 맨 아래 주소를 가리키고 있음 (즉, 스택의 top 원소를 가리킨다)

스택의 push

pushq src

  • 스택에 원소를 push 하면 rsp 가 가지고 있던 주소값을 내려준다 (즉, %rsp 레지스터가 가리키고 있던 주소를 8byte 만큼 뺴준다.)

  • 자동으로 rsp 의 주소값을 8을 빼주거나 더한다.

(movq 와 같은 명령어로 데이터를 삽입하는 명령어들은 내가 직접 %rsp 주소값을 직접 뺴주거나 더해야함)

스택의 pop

popq dst

  • 8 byte 만큼 더한다.
  • 주의) dst(destination) 은 반드시 memory 이여야한다! (레지스터x)

Procedure Control Flow

Procedure call ( = c언어 function call)

  • 명령어 : callq jump할주소 < 함수명 >

ex) callq 400544 < func1 > => 함수 func1 을 호출하고 400544 라는 주소로 jump 한다.

  • c언어에서 함수라고 불리는 것이 어셈블리에서는 Procedure 이다.
    ( 즉, function call == procedure call)

  • c언어에서는 함수 호출시 명시적으로 정의가 되어있어서 그냥 호출하면 되지만, 어셈에서는 함수가 아닌 그냥 특정 위치에 있는 코드(label) 을 호출하는 방식이다.

  • Procedure return

    명령어 : ret

  • 해당 procedure 가 종료후, 스택에 저장되어있던 다시 되돌아 가야 할 주소 (return address) 로 돌아간뒤에 스택에 저장된 return address 를 삭제한다.

  • return address

    => function call (즉 procedure call) 이 일어날떄 함수의 내용을 수행 후 종료될때 다시 자기가 돌아갈 주소를 알고 있어야한다!


Procedure call 예제

과정1

  • 400550 : callq 400500 < mult2 >
    => mult2 라는 함수호출이 발생하면 400550 이라는 주소로 jump 한다 (400500 == mult2 함수의 시작주소)

  • rsp : 앞서 배웠듯이 stack 의 시작주소를 가리키는 포인터이다.

  • rip : 현재 수행중인 명령어(instruction) 코드를 가리키는 레지스터(포인터) 이다.

과정2

  • mult2 함수를 호출후 종료될떄(함수가 return 될때) 다시 돌아올 주소를 스택의 top에 저장한다(push).
    (스택에 저장되는 되돌아갈 주소는 400539 임)

=> top 에 저장하기위해선, 기존에 스택의 top 에 저장되어있던 데이터를 한칸 미루고 생긴 빈공간에 push 하면 된다.

과정3

  • rip 레지스터(포인터) 가 함수의 return 구문을 가리키고 있다. 함수가 종료되면서, 스택에 저장되어 있었던 주소로 되돌아가면 된다.

과정4

  • 400549 주소로 다시 되돌아가고, 스택에 저장되어 있었던 주소를 삭제시킨다.

Passing data

  • input argument 를 실제로 어떻게 가져오는가를 살펴보자.

Procedure Data Flow

어셈블리에서는 함수의 인자를 register 와 stack 을 활용해서 제한없이 받을 수 있다.

  • register 에는 최대 6개까지 인자를 할당 가능하고,
    6개가 오바되면 스택에 인자들이 쌓인다.
  • 함수의 주는 input 값은 rdi, rsi, rdx, rcx, r8, r9 순으로 들어간다.

  • 위 그림에서 보이는 레지스터 6개만 함수 호출시 input argument 를 넣어줄 수 있다.
    (즉, 함수의 인자(parameter) 값으로는 최대 6개만 선언 가능하다)

  • 만일 함수의 인자가 6개가 넘어가면 스택에 쌓인다.
    ( 스택에 함수의 7,8,...,n번쨰 인자가 차례대로 쌓이는데, 거꾸로 쌓인다. 스택의 맨 아래가 top 이므로 거꾸로 쌓임 )

예제


Managing Local data

Stack-Based Languages

  • 함수 호출이 가능한 언어들 (stack 을 활용함)
    => 현재 수행하고 있었던 프로그램들이 한줄한줄 내려가다가 다른 곳으로 jump 하고 다시 되돌아오는 것이 가능한 언어들
    ex) C안아, Java 등등
  • 새로운 함수를 호출할 때 return address 를 스택에 저장해 놓고 stack pointer (%rsp) 를 최신화 하는데,
    이전의 함수에 대한 데이터들을 보지않고 나만의 데이터를 새롭게 관리하겠다는 개념이다. 즉 현재 호출된 함수에 대해서만 신경쓰고 관리한다!

Call Chain

  • yoo() 함수가 who() 를 호출하고, who() 함수가 amI() 함수를 호출하고, amI() 함수는 재귀함수로써 자기자신을 호출한다.

  • 이렇게 함수가 불리고 불리는 관계를 트리 형태로 그려놓은 것을 Call Chain 이라고 한다.


Stack Frame

각 함수마다 자신만의 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" 명령어)


stack frame 예제

과정1)

  • 위와 같이 call chain 이 생겼다. yoo - who - ami - ami(재귀호출) 구조이다.

  • 위에서 계속 배웠듯이 rsp 가 스택의 top 을 가리킴
    rbp 는 stack frame 의 시작점을 가리킨다.

과정2)

  • 새로운 함수(who) 의 stack frame 이 생겼다.

과정3)

  • 재귀함수 amI 에 대해서 각각 따로 새로운 stack frame 이 생성된다.

과정4)

  • amI 가 리턴됨

과정5)

설명 생략하겠삼~


x86-64 리눅스 stack frame

  • 실제 리눅스의 stack frame 은 아래와 같이 생겼다.


incr() - increment 해주는 함수

  • rsp 에서 16을 뺀다. 즉 rsp 가 스택의 top 을 가리키고 있었는데 16을 빼고 스택에 새로운 2칸을 만든다.

    • rsp 현재 주소값에다 8을 더한 주소에 15213 을 push 한다.
    • 나머지 한 메모리 공간은 빈 상태(unused)로 남겨둔다. (컴파일러가 그렇게 만듦)

  • v1에 저장된 값인 15213 에 3000을 더한 값인 18213을 v2에 할당한다.




Register Saving Convention

  • 새로운 함수를 호출하기 전에, 항상 현재 실행중인 함수에 대한 정보를 register 가 아닌 어딘가에 저장해놔야 한다.

    • call 입장에 데이터를 저장하고 싶은경우, 해당 데이터를 어딘가에 옮겨놓고 기억하면 될 것이다.

    • caller 입장에서는, 새로운 함수를 그냥 바로 호출하고, 새로운 함수가 호출되자마자 "아,직전의 함수가 내가 필요하다고 했었지?" 내가 저장해 놨다가 리턴할 떄 줘야겠다라는 것

레지스터가 temporary data 를 저장하는 방법

  • caller saved : 호출된 함수 자기자신이 데이터를 저장하는 방식

  • callee saved : 새롭게 호출한 함수가 자신을 호출해준 함수에 대한 데이터를 임시적으로(temporary data) 저장하는 방식
  • caller (부르는 애) 가 자기껄 자기가 챙기면 caller saved 라고 부름

    • 함수 호출 이전에 caller 가 temporary data 를 저장한다.
  • callee (불리는 애) 가 저장해줬다가 리턴할 떄 다시 복구해주면 callee saved 이다.

    • temporary data : 현재 호출된 함수 본인의 데이터는 아닌데, 잠깐 보유하고 있다가 함수가 종료될 때 직전의 함수한테 다시 돌려줘야 하는 데이터

x86-64 Linux Register Usage

  • 새로운 함수가 호출될 떄 마다, 아래의 15개의 레지스터들은 항상 모두 새롭게 비워줘야한다!

    => 새로운 함수가 호출되면, 그 함수만 무조건 독단적으로 아래 레지스터들을 사용해야하기 때문.

  • %rax

    • caller-saved 라고 불림 (자기자신이 데이터를 저장함)
      => 리턴할 값(return value) 을 자기자신이 포함하고 있음.

    • rax 에 어떤 데이터값을 저장해 놓았다면, 그 데이터는 반드시 스택에 따로 저장을 해줘야한다!

  • %rdi, ..., %r9

    • caller saved 라고 불림
    • input arguments 를 저장 (함수의 input 파라미터 데이터들을 저장)
  • %r10, %r11
    • caller saved 라고 불림
    • 평소에 그냥 빈공간이고, %rdi, ..., %r9 대체용으로 안전빵 느낌으로 존재
  • %rbx, %r12, %r13, %r14
    • callee saved 라고 불림
      => 새로 호출된 함수가 직전에 호출된 함수의 데이터도 저장한다. 따라서 스택에 직전에 호출된 함수의 데이터를 넣어놓았다가, 새로 호출된 함수가 종료되고 리턴될떄 현 함수에 대해 데이터를 레지스터에 다시 채워줘야한다.
  • %rbp
    • callee saved
    • stack frame 의 포인터로써 역할
  • %rsp
    • pointer
Callee saved register

- callee가 해당 레지스터 값을 변경하지 않는 것을 보장하는 레지스터.
- caller는 해당 레지스터 값이 변경되지 않음을 예상할 수 있다.

Caller saved register

- callee는 해당 레지스터를 변경할 수 있다.
- 그러므로, caller는 해당 레지스터 값을 저장해 두었다가 회복(restore)시켜야 할 것이다.

Callee Saved 예제

  • rbx : callee saved 이다. incr 과 같은 새로운 함수가 호출되더라도, 자기자신이 계속 값을 보유하고 있는 형태가된다. 계속 값을 보유하기 위해 스택에 rbx 공간을 할당하고 값을 저장한다.
(뭐가 맞는 설명일까..)
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해준다.

profile
블로그 이전했습니다 🙂 : https://haon.blog

0개의 댓글