OS

[ARM] 함수 호출 규약

string_ 2025. 3. 10. 03:00
함수 호출 규약 (Calling Convention) 이란?

 

함수 호출 규약은 함수 호출 시 인자를 전달하는 방식, 반환 값을 처리하는 방법, 호출자의 스택 정리 방식 등을 정의하는 규칙이다.

이는 운영체제와 컴파일러 언어에 따라 다양하게 호출 규약이 존재한다.

 

ARM 어셈블리 명령에서 레지스터로 연산의 결과를 임시로 저장하는 역할을 한다.

이때 각 레지스터의 역할, 순서에 대해 이해하는 것이 중요하다.

 

함수 호출 규약은 CPU 아키텍처(x86 , x86-64) 및 운영체제 (Windows , Linux)에 따라 다르게 적용된다.

 

32bit(x86)와 64비트(x86-64) 기준으로 차이점을 정리하면 다음과 같다.

  32비트 (x86) 64비트 (x86-64)
레지스터 EAX, EBX, ECX, EDX, ESI, EDI (6개) RAX, RBX, RCX, EDX, RSI, RDI, R8, R9, R10, R11, R12, R13, R14, R15 (16개)
스택 포인터 ESP RSP
프레임 포인터 EBP RBP
인자 전달 방식 스택을 사용 (cdecl, stdcall) 레지스터를 사용 (AMD64, Microsoft x64)
호출 규약 cdecl, stdcall, fastcall 등 Windows와 Linux에서 각각 1가지 규약 사용
스택 정렬 4Byte (DWORD) 8Byte (QWORD)

 

 

32bit에서 호출 규약은 다음 요소에 따라 차이가 난다.


인자 전달 방식 

레지스터를 이용한 전달 (fastcall)

스택을 이용한 전달 (stdcall, cdecl)


반환 값(Return Value) 방식

반환 값을 특정 레지스터에 저장하여 반환한다. (eax, rax, xmm0 등)

스택을 사용하여 반환하는 경우도 있다. (큰 구조체 반환 시)


스택 정리 (Caller, Callee)

Caller(호출자)가 정리 : 호출한 함수가 스택을 정리 (cdecl)

Callee(피호출자)가 정리 : 호출된 함수가 스택을 정리 (stdcall, fastcall)


 

이와 같이 인자 전달 방식, 반환 값 저장 방식, 스택 정리 방식에 따라 차이점이 있다는 것을 알 수 있다.

 

 

 

1. cdecl (c Declaration) 방식
2. stdcall (Standard Call) 방식
3. fastcall 방식
4. thiscall 방식
5. vectorcall 방식
6. x86-64 리눅스(64bit) 방식
7. 리눅스 syscall 방식 (x86) 32비트
8. 리눅스 syscall 방식 (x64) 64 비트
9. x86-64 Windows(64bit) 방식
10. 윈도우 syscall 방식 (x64) 64 비트

 

1. cdecl (c Declaration) 방식

주로 기본적인 C언어의 함수 호출 방식으로 사용된다.

인자 전달 : 오른쪽 -> 왼쪽 (Right to Left) 마지막 인자가 스택의 가장 위에 위치한다.

스택 정리 : Caller (호출자)가 수행

#include <iostream>

int sum(int a, int b) { return a + b; }

int main(void)
{
	sum(1, 2);
	return 0;
}

 

push 2       ; 2번째 인자 (b)
push 1       ; 1번째 인자 (a)
call sum     ; 함수 호출
add esp, 8   ; 스택 정리 (Caller가 수행)

 

 

 

2. stdcall (Standard Call) 방식

 

Windows API에 사용된다.

인자 전달 : 오른쪽 -> 왼쪽 (Right to Left) 마지막 인자가 스택의 가장 위에 위치

스택 정리 : Callee(피호출자)가 수행

#include <iostream>

int __stdcall sum(int a, int b) { return a + b; }

int main(void)
{
	sum(1, 2);
	return 0;
}

 

push 2       ; 2번째 인자 (b)
push 1       ; 1번째 인자 (a)
call sum     ; 함수 호출
; sum 내부에서 "ret 8" 수행 → 스택 자동 정리


int __stdcall sum(int a, int b) { return a + b; }
push        ebp  
mov         ebp,esp   
....
mov         esp,ebp  
pop         ebp  
ret         8   << Callee가 스택 정리 수행

 

 

 

3. fastcall 방식

 

성능이 중요한 함수에서 사용된다.

인자 전달 : 1번쨰, 2번째 인자는 레지스터 (ECX, EDX)로 전달, 나머지는 스택

스택 정리 : Callee(피호출자)가 수행

 

#include <iostream>

int __fastcall sum(int a, int b) { return a + b; }

int main(void)
{
	sum(1, 2);
	return 0;
}

 

mov edx, 2   ; 2번째 인자 (b)
mov ecx, 1   ; 1번째 인자 (a)
call sum     ; 스택 전달 시 Callee가 스택 정리 수행

 

 

 

 

4. thiscall 방식

 

C++ 클래스의 멤버 함수 호출에서 사용된다.

인자 전달 : this 포인터는 ECX(32bit) 나머지 인자는 stdcall 방식과 동일하다

스택 정리 : Callee(피호출자)가 수행

 

#include <iostream>

class MyClass {
public:
    int sum(int a, int b); 
};

int MyClass::sum(int a, int b) {
    return a + b;
}

int main() {
    MyClass a;
    a.sum(1, 2); 
    return 0;
}

 

push 2           ; 2번째 인자
push 1           ; 1번째 인자
mov ecx, [this]  ; 객체 주소를 ECX에 저장
call MyClass::sum


int MyClass::sum(int a, int b) {
push        ebp  
mov         ebp,esp  
....
mov         esp,ebp  
pop         ebp  
ret         8    << Callee가 스택 정리 수행

 

 

 

5. vectorcall 방식

 

부동소수점 연산이 많은 함수에서 최적화 목적으로 사용된다.

인자 전달 : xmm 레지스터를 활용하여 부동소수점 인자 전달

 

#include <iostream>

float __vectorcall sum(float a, float b) { return a + b; }

int main() {
    sum(1.3, 2.5); 
    return 0;
}

 

movss xmm1, [b]      ; 2번째 인자
movss xmm0, [a]      ; 1번째 인자
call sum             ; Caller가 스택 정리 수행

 

 

 

 

6. x86-64 리눅스(64bit) 방식

 

리눅스 x86-64 ABI에서 사용된다.

리눅스는 64비트 모드에서 파라미터를 전달할 때 Windows보다 레지스터를 더 많이 사용한다.

정수형의 파라미터를 전달할때는전달할 때는 6개의 레지스터로 6개 이상의 파라미터를 전달할 때는 스택을 사용한다. 순서는 다음과 같다.

1 RDI
2 RSI
3 RDX
4 RCX
5 R8
6 R9
.. 스택 사용

 

 

부동소수점 형의 파라미터는 XMM0~XMM7 레지스터로 처리한다.

1 XMM0
2 XMM1
3 XMM2
4 XMM3
5 XMM4
6 XMM5
7 XMM6
8 XMM7
.. 스택 사용

 

반환 값(Return Value) 같은 경우는 RAX, RDX 레지스터를 통해 반환 된다.

반환 값 (정수) 반환 값 (실수)
RAX xmm0
RDX xmm1

 

스택 정리 : Caller(호출자)가 수행

 

mov r9, 6     ; 6번째 인자
mov r8, 5     ; 5번째 인자
mov rcx, 4    ; 4번째 인자
mov rdx, 3    ; 3번째 인자
mov rsi, 2    ; 2번째 인자
mov rdi, 1    ; 1번째 인자
call sum      ; Caller가 스택 정리 수행

 

 

 

 

7. 리눅스 syscall 방식 (x86) 32비트

 

최대  6개의 파라미터를 받고 추가 파라미터는 스택으로 받는다.

일반 함수 호출 시와 syscall 호출 시 각 레지스터의 사용이 다르다. 파라미터 순서에 따라 사용되는 레지스터는 다음과 같다.

1 EBX
2 ECX
3 EDX
4 ESI
5 EDI
6 EBP

 

반환 값
EAX
mov eax, 4        ; syscall 번호 (sys_write = 4)
mov ebx, 1        ; 1번째 인자 (stdout = 1)
mov ecx, msg      ; 2번째 인자 (버퍼 주소)
mov edx, len      ; 3번째 인자 (버퍼 길이)
int 0x80          ; 시스템 콜 호출

 

 

 

 

8. 리눅스 syscall 방식 (x64) 64 비트

 

리눅스 64비트 에서 system call로 함수를 호출할 때 레지스터들의 상태이다.

1 RDI
2 RSI
3 RDX
4 R10
5 R8
6 R9

 

반환 값
RAX
mov rax, 1        ; syscall 번호 (sys_write = 1)
mov rdi, 1        ; 1번째 인자 (stdout = 1)
mov rsi, msg      ; 2번째 인자 (버퍼 주소)
mov rdx, len      ; 3번째 인자 (버퍼 길이)
syscall           ; 시스템 콜 호출

 

 

 

 

9. x86-64 Windows(64bit) 방식

 

Windows 운영체제 64비트에서 사용되는 호출 규약이다.

윈도우는 64비트 모드에서 파라미터를 전달할 때 레지스터 4개만 사용한다.

이에 따라 호출규약은 윈도우와 리눅스 레지스터 종류와 개수에 차이가 있는 것을 알 수 있다.

 

정수형의 파라미터 순서는 RCX, RDX, R8, R9를 사용하고 나머지는 스택으로 전달한다.

1 RCX
2 RDX
3 R8
4 R9
.. 스택 사용

 

부동소수점 형의 파라미터는 XMM0~XMM3까지 4개의 레지스터로 처리한다.

1 XMM0
2 XMM1
3 XMM2
4 XMM3
.. 스택 사용

 

반환 값(Return Value) 같은 경우는 RAX 레지스터를 통해 반환된다.

반환 값 (정수) 반환 값 (실수)
RAX xmm0

 

스택 정리 : Caller(호출자)가 수행

 

mov [rsp+28], 6    ; 6번쨰 인자
mov [rsp+20], 5    ; 5번쨰 인자
mov r9, 4          ; 4번쨰 인자
mov r8, 3          ; 3번쨰 인자
mov rdx, 2         ; 2번쨰 인자
mov rcx, 1         ; 1번쨰 인자
call sum           ; Caller가 스택 정리 수행

 

 

 

10. 윈도우 syscall 방식 (x64) 64 비트

1 RCX
2 RDX
3 R8
4 R9
.. 스택 사용

 

mov r10, rcx    ; 1번째 인자 (Windows는 r10을 사용)
mov eax, 0x25   ; NtWriteFile 시스템 콜 번호
syscall         ; 시스템 콜 호출

 

반환 값
RAX

 

 

 

호출규약을 표로 정리하면 다음과 같다.

호출 규약 정수 인자 전달 부동 소수점 인자 전달 스택 정리 책임 특징
__cdecl 스택(Right-to-Left) 스택 사용 호출자(Caller) C 기본 규약, 가변 인자 지원 (printf 등)
__stdcall 스택(Right-to-Left) 스택 사용 피호출자(Callee) Windows API 기본 호출 규약
__fastcall RCX, RDX, 나머지 스택 XMM0,
나머지 스택
피호출자(Callee) Windows 최적화된 호출 규약
thiscall ECX(this) + 스택 스택 사용 피호출자(Callee) C++ 클래스 멤버 함수 호출
__vectorcall RCX, RDX, R8, R9 XMM0 ~ XMM5 호출자(Caller) 벡터 연산 최적화, Windows 전용
x86-64 리눅스 64bit RDI, RSI, RDX, RCX, R8, R9 ,나머지는 스택 XMM0~XMM7,
나머지 스택
호출자(Caller) 64비트 Linux/macOS 기본 호출 규약
Linux Syscall (x86, 32bit) EAX(번호), EBX, ECX, EDX, ESI, EDI ST(0)
(FPU 레지스터)
호출자(Caller) int 0x80 인터럽트 사용
Linux Syscall (x86-64, 64bit) RAX(번호), RDI, RSI, RDX, R10, R8, R9 XMM0 호출자(Caller) syscall 명령어 사용
Windows x64 (64bit) RCX, RDX, R8, R9 → 나머지는 스택 XMM0~XMM3,
나머지 스택
호출자(Caller) Windows 64비트 기본 호출 규약
Windows Syscall (x86-64, 64bit) RAX(번호), RCX, RDX, R8, R9, R10 XMM0 호출자(Caller) syscall 명령어 사용, ntdll.dll 통해 호출