
어셈블리에서 함수는 procedure 혹은 subroutine으로 부른다
코드가 점점 비대해지고 복잡해지면 함수로 자주 사용하는 기능을 분리해서 관리하기 쉽게 해야한다
원하는 procedure를 만들어주고 call [procedure이름]으로 호출이 가능하다
호출한 procedure에서 ret을 만나게 되면 call한 부분 이후로 이동하게 된다
call PRINT_MSG ;procedure 호출
xor rax, rax
ret
PRINT_MSG: ;procedure 구현
PRINT_STRING msg
NEWLINE
ret
section .data
msg db 'Hello World', 0x00
PRINT_MSG라는 procedure를 구현하고 call로 호출하여 output으로 Hello World가 출력되는 걸 확인할 수 있다
추가로 C++과 마찬가지로 procedure안에서 다른 procedure를 call할 수 있다
MAX:
call PRINT_MSG
cmp eax, ebx
jg L1
mov ecx, ebx
jmp L2
MAX procedure에서 PRINT_MSG procedure를 call한 상태임
그렇다면 input,output이 필요한 경우에는 어떻게 사용할까?
우선 첫번째 방법으로는 register를 이용하는 방법이 있다
mov eax, 10
mov ebx, 5
call MAX
PRINT_DEC 4, ecx
NEWLINE
xor rax, rax
ret
MAX:
cmp eax, ebx
jg L1
mov ecx, ebx
jmp L2
L1:
mov ecx, eax
L2:
ret
위 코드는 2개의 값을 비교하여 더 큰 값을 출력하는 코드이다, MAX라는 procedure에서 eax, ebx를 비교하여 더 크면 L1, 그렇지 않으면 ecx에 ebx를 넣어 ret하는 로직이다
결과값은 더 큰 숫자인 10으로 나오게 된다
이때 인자가 엄청 많으면 어떻게 할까?
레지스터에 값을 할당하는 방식으로는 부족하다, 그리고 eax, ebx등 레지스터에 중요한 값이 있으면 사용할 수 없다
.data영역에 변수를 여러개 만들어놓고 사용해도 가능은 하지만 인자가 엄청 많다면 변수를 엄청 많이 선언하고 관리해야 하기 때문에 좋은 방식은 아니다
이럴때는 다른 메모리 구조인 stack메모리 영역을 사용해야 한다
stack메모리는 함수가 사용하는 일종의 메모장이라 생각하면 된다
매개변수 전달, 함수 반환 주소값 관리, 로컬변수 선언 시 사용되는 메모리 영역이다

exe파일을 실행하게 되면 ram에 올라가게 되는데 이때 ram은 Code, Data, Bss, Heap, Stack 영역으로 구분이 되어 있다
Code영역은 프로그래머가 짠 코드가 들어있고 Data,Bss영역은 전역변수 데이터, 지역변수 데이터는 Stack, 동적할당 시는 Heap에 할당이 된다
이때 Code, Data, Bss영역은 Compile시 이미 크기가 결정되지만 Heap, Stack은 런타임에 크기가 결정된다
Stack Memory를 설명하기 위해서는 Stack Frame의 개념을 알아야 한다
다음은 스택 프레임을 시각화 한 그림이다

Stack은 높은 주소 -> 낮은 주소로 사용을 한다
함수를 호출하게 되면 Stack Memory에는 지역 변수, 반환 주소값, 매개변수 데이터가 쌓이게 되는데 이 데이터들을 Stack Frame이라고 부른다 이때 반환 주소값이 있기때문에 함수가 종료되고 다음 명령으로 돌아갈 수 있는것이다
레지스터는 굉장히 다양한 용도로 사용된다
이제까지 많이 사용한 a,b,c,d 범용 레지스터 뿐 아니라 포인터 레지스터도 있다
ip (Instruction Pointer)는 다음 수행 명령어의 위치, sp (Stack Pointer)는 현재 stack top위치, bp (Base Pointer)는 스택 상대주소 계산용으로 사용된다
가장 기본적인 스택 사용법은 push, pop이다
push 1
push 2
push 3
pop rax
pop rbx
pop rcx
push로 값을 스택에 밀어 넣을 수 있고 pop으로 값을 빼내올수도 있다
위 코드를 디버깅해서 register를 확인해보면 다음과 같다

위에서 설명한 ip가 존재하는걸 확인할 수 있다 이때 rip인 이유는 64bit로 연산하기 때문이다
이 0x4014e4이 다음 실행 될 명령어의 주소를 의미한다 (Code영역을 가리킴)
bp와 sp도 있는걸 확인할 수 있다

이 sp의 주소가 현재 사용중인 stack의 주소를 의미한다
rbp rsp 가 같은 이유는 코드 최상단에서 mov rbp, rsp가 기본적으로 들어가 있기 때문이다
다음은 pop의 결과이다

pop rax, pop rbx, pop rcx순으로 되어있고 위에서부터 pop되기 때문에 역순으로 3, 2, 1이 들어가 있는걸 확인할 수 있다
실제 함수를 호출하는 코드로 조금 더 확인해보자
push 1
push 2
call MAX
MAX:
xor rax, rax
ret
1,2값을 push하고 MAX procedure를 호출하는 코드이다
C++에서 함수를 사용하는 곳에서 어셈블리어로 확인해보게 되면 다음과 같은 코드 패턴을 확인할 수 있다
push rbp
mov rbp, rsp
mov rax, [rbp+16]
mov rbx, [rbp+24]
pop rbp
ret
C++에서 함수 사용 시 디버깅을 하면 이러한 코드 패턴은 왜 나오는 걸까?
우선 위의 코드로 Stack메모리에 데이터가 쌓이는 과정은 다음과 같다
1 -> 2-> Ret주소값이 쌓인 상태고 push rbp로 bp가 쌓이게 된다
이 bp는 왜 쓰는걸까?
함수에서 push된 1, 2값을 사용하기 위해서는 해당 주소를 알아야 한다 이때 sp를 이용해서 크기 만큼 위로 이동해서 사용하면 되지 않나? 라는 의문이 생기지만 sp는 유동적이기 때문에 상대 주소로 사용하기 부적합하다 따라서 고정되어 있는 주소값인 bp를 사용하는 것이다 (push rbp, move rbp, rsp로 rsp주소값을 rbp에 임시로 저장하여 사용한다)
함수 내부에 다른 함수들도 호출이 가능하기때문에 자기 자신의 함수 영역을 나타내는 느낌으로 사용한다고 생각하면 된다 (함수1 bp, 함수 2bp, 함수 3bp...)
그리고 mov rax, [rbp + 16], mov rbx, [rbp + 24]로 rbp 상대주소에서 두칸, 한칸 올라간 스택메모리 영역의 값을 rax, rbx에 넣는 코드이다 rbp기준으로 2칸, 3칸이기 때문에 2, 1이 나오게 된다
이때 아주 중요한 점은 스택영역은 사용했으면 반드시 깔끔하게 정리해야 한다는 것이다
(크래시 발생 가능)
push 1
push 2
call MAX
PRINT_DEC 8, rax
NEWLINE
add rsp, 16
MAX procedure가 호출되고나서 ret으로 돌아가야 하는데 push 1, push 2로 스택메모리에 1, 2값을 push했기때문에 다른 주소를 참조하게 되어 크래시가 발생한다 이를 방지하기 위해 스택메모리를 정리해야 한다
byte 2개를 push했기 때문에 rsp값을 16 add해주거나 pop을 두 번 해야 한다
push rbp
mov rbp, rsp
mov rax, [rbp+16]
mov rbx, [rbp+24]
pop rbp
ret
이 코드를 사용하다고 가정했고 rax, rbx 레지스터에 중요한 데이터가 있다면 이 procedure가 호출되기 전에 push rax, push rbx로 스택에 rax, rbx값을 올려두고 나중에 pop rbx, pop rax로 다시 복원 시켜주는 방식을 사용해야 한다