Major S-T-U-D-Y/System Programming

8. Procedure Call and Stack

rlo-lo 2024. 11. 12. 13:43

computer systems 3rd edition chap3를 인용했습니다. 

 

Program Counter 

- pc라고 불린다. 

- intel x86-64 환경에서 rip 레지스터가 pc의 역할을 수행한다. 

- 다음 명령어의 메모리 주소 정보를 저장한다. 

 

Stack Pointer

- sp라고 불린다. 

- intel x86-64 환경에서 rsp 레지스터가 sp 역할을 수행한다. 

- 스택의 가장 끝 (top)을 가리킨다.

- 스택은 위에서 아래로 쌓이는 구조 

 

 

 

call

caller 와 callee 로 나뉜다. caller 는 return address(돌아갈 주소) + argument 저장장소 정보를 가져야 한다. callee 는 return value 정보를 가져야 한다. 

caller와 callee는 같은 cpu에서 run 되기 때문에, 같은 레지스터를 사용한다. 

→ 레지스터 개수는 한정적이기 때문에 레지스터 재활용이 이뤄진다. 이 상황을 잘 다루는 것이 관건!

 

 

 

Calling Convention 

 

시스템 환경에 따라 다르게 적용된다. 

해당 글에서는 x86-64 환경을 기준으로 설명하도록 한다. 

스택 = 스택프레임의 집합이다. 런타임 때 프로시져가 프로시저 call 단위의 정보를 스택프레임에 할당한다. 

→ callee는 return address를 정리해줘야 한다. 

- 항상 복귀가 필요하기 때문에 리턴 주소를 stack에 저장해둔다. (jmp instruction 과의 차이) 

- caller argument register (rdi, rsi, rdx, rcx, r8, r9 6개) 스택은 아래에서 낮은주소로 쌓이기 때문에 , 해당 레지스터는 역순으로 저장된다. 역순으로 저장되어야 rdi, rsi, rdx,,, 순으로 pop될 수 있으니!!!  

- callee register (rbp, rpx, r12~r15)

- caller와 callee는 같은 register을 사용한다. 

 

Q. 왜 스택에 저장해야 할까?

→ 프로시저가 끝난 후, call은 저장된 return address로 다시 '되돌아' 가야 한다 . 하지만, 레지스터는 한정적이고 수많은 return addresses를 저장해야 할 수 있다. 

 

스택 : 프로시저의 context 에 대한 정보를 저장하는 메모리 지역 LIFO(Last In First Out 구조) 

 

 

x86-64 Stack and Stack Pointer

Stack : procedure을 관리하기 위한 메모리 공간 

- stack frame의 집합

- 런타임 시점에 스택 프레임에 저장하기 때문에 run-time stack 이라고도 불린다.  

- 낮은 주소방향으로 저장된다 !! 

 

%rsp : 스택의 top(가장 낮은 주소) 주소를 담고 있는 레지스터 

%rbp : 가장 최근 스택프레임의 첫번째 요소 주소 (거의 안 쓰임) 

 

 

Push / Pop Stack Operation

pushq src  

1. %rsp 를 8만큼 줄인다 (sub 8 byte)  

Allocation subq $8, %rsp → 규약상 pushq만 존재한다 (b,w, l X ) 

2. %rsp 공간에 데이터 를 삽입한다. //%rsp 메모리 접근

Copy movq %rcx, (%rsp)

주어진 주소에 src의 content를 담는다. 

 

popq dst 

1. dst에 %rsp 값을 저장한다. //%rsp 메모리 접근

Store content movq (%rsp), %rcx //stack top에 있는 data를 저장

2. %rsp를 8만큼 증가시킨다.

addq $8, %rsp

→ 기존 bits 공간은 여전히 존재한다. 다만, 사용하지 않을 뿐!!

 

 

Prodecure Control Flow 

프로시저의 call과 return을 위해 stack을 이용한다. 

return address :  call instruction 후 되돌아갈!! 주소 → 스택에 저장해뒀기에 기억할 수 있음! 

call 인스터럭션은 함수의 시작부분으로 제어를 이동하는 반면, ret인스트럭션은 call다음에 오는 인스트럭션으로 제어를 되돌린다 ! 

 

Procedure call : call label 

1. 스택에 return address 저장 (1) 8바이트 sub 2) return address 저장) 

2. label 로 점프!! %rip 는 call한 곳의 주소 

Procedure return : ret

1. 스택에서 return address 를 pop 한다!! ( 1) return address 기억 2) 8바이트 add)

2. 해당 return address로 점프!! %rip는 return address

retq는 단순히 IA32가 아니라 X86-64 버전의 콜과 리턴 인스트럭션이라는 것을 강조한다. 

 

 

x86-64/Linux Stack Frame 

 

스택 프레임에 저장되는 경우 

 

1. return address 가 저장되어야하는 경우 (함수 호출 jmp 발생 후 되돌아가야 할 때 ) 

- call 명령어에 의해 스택에 push 된다 

2. 지역변수의 특수한 경우 (필요하다면 !) 

- 연산자 &가 사용되었으며, 이 변수의 주소를 생성할 수 있어야 한다. (volatile) 

- 일부 지역변수들이 배열 또는 구조체여서 이들이 배열이나 구조체 참조로 접근되어야 한다. 

→   스택보다 레지스터가 빠름  → 고로 스택 사용을 최소화 하자 

gcc proc.c -c –Og -fcf-protection=none -mpreferred-stack-boundary=3
//gcc -Og 최적화 옵션

 

3. argument 가 6개를 초과할 경우 

- 매개변수들을 스택으로 전달할 때, 모든 데이터 길이는 8의 배수로 반올림된다. 

 

 

Low Level Debugging

gdb -tui

$make
$gdb -tui myincr
//-tui or ctrl+a in gdb

(gdb) l
(gdb) layout split #asm or src
(gdb) focus cmd #asm or src
b main 
r
display /x $rip // gdb에서는 레지스터 $로 표기한다 
display /x $rsp
display /x *(long *) ($rsp) // 해당 스택프레임 value 를 알고 싶다면 포인터로 접근

 

 

Passing data 

- 레지스터는 메모리에 있지 않은 반면, stack은 메모리 상에 저장되어 있다. 

- caller는 6개의 인자까지 레지스터를 사용한다. 

(%생략) rdi, rsi, rdx, rcx, r8, r9

- 남은 인자들이 스택에 저장되어야 할 경우, 스택에서 순서대로 pop 될 수 있도록 역순으로 저장된다.

- callee는 리턴값을 %rax 에 저장한다. 

 

void proc(long,long,long,long,long,long,long,long);
void call_proc() {
long x1 = 1;
long x2 = 2;
long x3 = 3;
long x4 = 4;
long x5 = 5;
long x6 = 6;
long x7 = 7;
long x8 = 8;
proc(x1, x2, x3, x4,x5, x6, x7, x8);
}


/*
pushq $0x8
pushq $0x7
mov $0x6,%r9d
mov $0x5,%r8d
mov $0x4,%ecx
mov $0x3,%edx
mov $0x2,%esi
mov $0x1,%edi
callq proc
add $0x10,%rsp //함수 호출 후 스택포인터를 조정하는 역할 스택 포인터를 원래 위치대로 !! 
//실제로 주소가 쓰이지는 않고 인자만 전달되기 때문에 
retq
*/
더보기

  함수 호출 시, 스택에 푸시되는 것들은 크게 2가지로 나눌 수 있다. 

  1. 리턴 주소 (Return Address): 함수 호출 시 call 명령어가 실행되면, 리턴 주소가 스택에 푸시됩니다. 이는 함수가 끝나고 돌아갈 주소입니다. 리턴 주소는 8바이트 크기입니다.
  2. 인자들 (Arguments): 호출되는 함수에 전달되는 인자들이 있습니다. proc 함수는 8개의 인자를 받습니다. 첫 6개는 레지스터 (%rdi, %rsi, %rdx, %rcx, %r8, %r9)를 통해 전달되고, 나머지 2개는 스택을 통해 전달됩니다. 이 인자들은 각각 8바이트 크기입니다. (x7, x8)

총 3개의 항목이 스택에 푸시되었으므로, 스택에서 이들을 제거하려면 24바이트를 정리해야 하지만, add $0x10, %rsp로 스택 포인터를 16바이트만 증가시키는 이유는, 실제로 x7과 x8만 스택에 저장되고, 리턴 주소는 함수 호출 후에 이미 정리되었기 때문입니다.

 

→ call 끝나는 순간 이미 return 주소는 스택에서 pop되고, %rsp 또한 정리된다. 

 

Register Saving Convention 

 

함수 호출 시, caller가 callee에 의해 레지스터 값이 덮어씌워지는 문제를 방지하기 위한 레지스터 저장 규약이 존재한다. 

 

yoo:
    movq $15213, %rdx     ; 호출자에서 %rdx에 값 저장
    call who              ; who 함수 호출
    addq %rdx, %rax       ; %rdx에 저장된 원래 값을 %rax에 더함
    ret                   ; 반환
who:
    pushq %rdx            ; %rdx를 스택에 저장
    subq $18213, %rdx     ; %rdx 값을 수정
    popq %rdx             ; %rdx를 스택에서 복원
    ret                   ; 반환

 

 

1. caller가 레지스터를 저장한다 

- caller가 call 이전 자신이 사용하는 레지스터 값을 스택에 저장하고, call 후에 그 값을 복원한다. 

yoo:
    movq $15213, %rdx     ; 호출자에서 %rdx에 값 저장
    pushq %rdx            ; %rdx를 스택에 저장
    call who              ; who 함수 호출
    popq %rdx             ; %rdx를 스택에서 복원
    addq %rdx, %rax       ; %rdx에 저장된 원래 값을 %rax에 더함
    ret                   ; 반환
    
who:
    subq $18213, %rdx     ; %rdx 값을 수정
    ret                   ; 반환

 

2. callee가 레지스터를 저장한다. 

- callee가 해당 레지스터값 사용 전 자신이 사용하는 레지스터를 스택에 저장하고, 반환 전에 복원한다. 

yoo:
    movq $15213, %rdx     ; 호출자에서 %rdx에 값 저장
    call who              ; who 함수 호출
    addq %rdx, %rax       ; %rdx에 저장된 원래 값을 %rax에 더함
    ret                   ; 반환

who:
    pushq %rdx            ; %rdx를 스택에 저장
    subq $18213, %rdx     ; %rdx 값을 수정
    popq %rdx             ; %rdx를 스택에서 복원
    ret                   ; 반환

 

 

Each register is designated as the responsibility of either the caller or the callee to preserve its value.

 

Callee saved registers
:  Callee saves values in its stack frame before using, then restores them before returning to
caller
- %rbx, %rbp, and %r12, %r13, %r14, %r15


Caller saved registers
: Caller saves values in its stack frame before calling Callee, then restores values after the call
- All other registers, except for the stack pointer %rsp

 

void multstore(long x, long y, long *dest) {
    long t = mult2(x, y);
    *dest = t;
}

long mult2(long a, long b) {
    long s = a * b;
    return s;
}

0000000000400540 <multstore>:
400540: push %rbx         # %rbx 값 저장
400541: movq %rdx,%rbx    # dest 주소(%rdx)를 %rbx에 저장
400544: call 400550 <mult2>  # 주소 0x400550에 있는 mult2 함수 호출
400549: movq %rax,(%rbx)  # %rax에 있는 결과값을 %rbx가 가리키는 주소(dest)에 저장
40054c: pop %rbx          # %rbx 복원
40054d: ret               # 함수 종료