함수 호출 규약 (Calling Convention)
- 호출자와 피호출자 간에 데이터(파라미터)를 전달할 때의 규칙
- 함수 호출 전후에 레지스터나 스택을 다룰 방법을 정해 놓은 약속
함수 호출 규약의 종류
CPU 아키텍처와 컴파일러 종류에 따라 호출 규약 역시 바뀜
EX) x64(32bit) / x64-86 (64bit)x64(32bit)
레지스터를 통해 피호출자의 인자를 전달하기에는 레지스터의 수가 적어 스택을 이용하는 함수 호출 규약 사용x86 호출 규약
Cdecl
- 인자를 오른쪽에서 왼쪽 순서로 스택에 push
- 함수 호출 이후, Caller(호출자)가 스택 정리
- 스택은 낮은 주소에서 높은 주소 방향으로 push
- 함수 호출 규약을 지정하지 않으면 cdecl을 사용
Cdecl Stack Frame
요점
메모리 구조상 스택은 높은 주소에서 낮은 주소로 올라가며, cdecl은 caller가 스택을 정리한다. 리버싱에서 함수 인자나 지역 변수를 찾을 때 스택 프레임 구조와 오프셋이 핵심이라고 한다. 스택에서는 RET adress가 push되므로, BOF같은 공격이 가능하다.
Stdcall
- 인자 전달은 오른쪽에서 왼쪽으로 전달하고 피호출자
- WinAPI에서 사용하고 함수가 끝나면 스택을 정리
Cdecl과 Stdcall 비교
cdecl stdcall 인자 정리 호출자(caller) 피호출자(callee) 인자 전달 순서 오른쪽 > 왼쪽 오른쪽 > 왼쪽 가변 인자 지원 가능 불가능 함수명 맹글링 그대로(func) _func@8(인자 크기 포함) 스택 안정성 낮음(호출자 실수 가능) 높음(callee가 항상 정리) 사용 예 일반 C 함수, GCC 환경 Windows 환경Fastcall
- 성능 향상을 위해 일부 인자를 레지스터를 통해 전달
- Microsoft 컴파일러 / Windows 성능이 민감한 코드에 사용
- 앞의 2~3개 인자는 레지스터에 전달하고 나머지는 스택에 전달
- 피호출자이며 ECX, EDX를 사용, 성능 향상이 목적
Fastcall 특징
인자 전달 순서 오른쪽 > 왼쪽 첫 번째, 두 번째 인자 ECX, EDX 레지스터 나머지 인자 스택에 저장 스택 정리 callee (피호출자) 함수명 맹글링 _@함수명@인자크기 (MSVC기준) 반환값 EAXx86-64-SYSV
- Linux, MacOs 등에서 사용하는 호출 규약
x86-6 System V
리눅스 및 유닉스 계열 OS에서 널리 사용되는 함수 호출 규약
함수 반환값 RAX Register 저장 스택 정렬 16 Byte 단위로 정렬 !! 함수 호출 전에 스택 포인터 RSP는 항상 16의 배수 !!x84-64 Prologue
- 함수 시작 직후 실행, 스택 프레임을 설정
함수가 실행되기 전
- Srack Frame 설정
- Register 백업
- 지역 변수 공간 확보 -> 함수 내 메모리 공간을 준비
push ebp/rbp 이전 함수의 베이스 포인터 값을 Stack에 저장 mov ebp,esp 현재 stack 포인터(esp)를 base 포인터(ebp)로 복사 새로운 스택 프레임 기준 설정 sub esp,XXX 지역 변수 공간 확보를 위해 stack 포인터를 감소x84-64 Epilogue
- 함수 종료 직전 실행, 스택 상태 복구 및 호출자에게 제어 반환
함수가 종료 후
- Srack Frame 해제 -> 함수 호출 전 상태 복구
- Regist 및 stack 상태 복원 후, Return Address로 복구
mov esp/ebp 스택 포인터를 Base Pointer 위치로 복구 pop ebp 이전 함수의 Base Pointer 값 복원 ret 호출한 함수로 복귀아키텍처 (Architecture)
아키텍처란?
- CPU가 명령어를 처리하는 방식을 나타냄
- 하드웨어 시스템의 전반적인 구조와 동작을 나타냄
x64 Register
범용 레지스터
데이터 연산을 위해 사용되는 레지스터
EAX 산술 연산 및 논리 연산 수행 + 함수의 반환값 저장 EBX 메모리 주소 저장 ECX 반복문 사용 시 카운터로 사용 EDX EAX와 같이 사용 + 큰 수의 곱셈과 나눗셈 연산 EDI 복사할 때 목적지 주소 저장 ESI 데이터를 조작하거나 복사할 때 데이터의 주소 저장 ESP 메모리 스택의 끝 지점 주소 포인터 EBP 메모리 스택의 첫 지점 주소 포인터 EIP 다음에 실행해야 할 명령어의 주소 포인터세그먼트 레지스터
아키텍처 메모리를 세그먼트 단위로 접근할 때 사용되는 특수한 레지스터
CS 기계 명령 포함 코드 세그먼트의 시작 주소를 가리킴 DS 프로그램에 정의된 데이터 영역의 시작 주소를 가리킴 SS 연산 결과 등을 임시로 저장 / 삭제할 때 사용 스택 영역의 시작부분을 가리킴 ES 추가로 사용된 데이터 세그먼트의 주소를 가리킴 FS 여분 레지스터 GS 여분 레지스터플래그 레지스터
CPU가 연산을 수행한 후 결과의 상태를 저장하는 특수한 레지스터
-> 조건문 등에 사용되어짐
ZF 연산결과가 0일 경우 참 CF 부호 없는 숫자의 연산 결과가 비트 범위를 넘으면 참 AF 연산 결과 하위 4 bit에서 비트 범위를 넘으면 참 OF 부호 있는 숫자의 연산 결과가 비트 범위를 넘으면 참 SF 연산 결과가 음수면 참 PF 연산 결과에서 1로 된 비트의 수가 짝수면 참 DF 문자열 조작에서 참이면 레지스터 값 감소, 거짓이면 증가 TF 디버깅에 사용명령어 포인터 레지스터
rip CPU가 실행시킬 코드를 가리키며 8byte의 크기를 지님
Caller / Callee
- Caller : 호출자. 함수를 호출
- Callee : 피호출자. 호출을 당하는 함수
EX) C언어 프로그래밍 도중 함수를 호출해야할 때
#include <stdio.h> int (함수명)(매개변수) { <- 피호출자 (Callee) - - - return 0; } int main() { - - - (함수명)(매개변수); <- 호출자 (Caller) - - - return 0; }
과제 1
1 - 2557
"Hello World!"를 출력하세요.
C
#include <stdio.h> int main(){ printf("Hello World!"); return 0; }asm
section .data str db "Hello World!" section .text global _start _start: mov rax, 1 <- 시스템 콜 번호 1 (sys_write) mov rdi, 1 mov rsi, str mov rdx, 13 syscall <- syscall 실행 mov rax, 60 mov rdi, 0 syscallsection .data
- db (define byte) : 바이트 단위로 데이터를 정의
- str에 "Hello World!" 문자열 삽입
_start
- rdi = 1: stdout
- rsi = num: 출력할 문자열
- rdx = 13: 출력할 문자열 길이
- write 시스템 콜을 사용해서 "Hello World!"을 출력
시스템 콜 번호 1은 sys_write
2 - 10171
고양이를 출력하세요.
C
#include <stdio.h> int main(void) { printf("\\ /\\\n"); printf(" ) ( ')\n"); printf("( / )\n"); printf(" \\(__)|\n"); return 0; }asm
section .data cat db "\ /\", 10, \ " ) ( ')", 10, \ "( / )", 10, \ " \(__)|", 10 catlen equ $ - catmsg section .text global _start _start: mov rax, 1 mov rdi, 1 mov rsi, cat mov rdx, catlen syscall ; exit(0) mov rax, 60 xor rdi, rdi syscallsection .data
- db (define byte) : 바이트 단위로 데이터를 정의
- cat에 출력하고자하는 모양 삽입
_start
- rdi = 1: stdout
- rdx = catlen : catlen에 저장된 값을 출력할 크기로 저장
- write 시스템 콜을 사용해서 cat에 담긴 모양 출력
3 - 1000
두 수를 입력받고 더한 값을 출력하세요.
C
#include <stdio.h> int main(){ int a,b; scanf("%d %d",&a,&b); printf("%d",a+b); return 0; }asm
section .data in db "%d %d", 0 out db "%d", 10, 0 section .bss x resd 1 y resd 1 section .text extern input extern print global start start: lea rsi, [x] lea rdx, [y] mov rdi, in xor eax, eax call input mov eax, [x] add eax, [y] mov esi, eax mov rdi, out xor eax, eax call print mov eax, 0 retsection .data
- in = scanf에 전달할 포맷 문자열
- out = printf에 전달할 포맷 문자열
section .bss
- 각각 int(4바이트) 공간을 확보
_start
- x+y 결과가 eax에 저장
- input는 scanf를 대신 호출하는 외부 함수
- print는 printf를 대신 호출하는 외부 함수
- main() 함수가 return 0; 과 같은 구조
4 - 1001
두 수를 입력받고 뺀 값을 출력하세요.
C
#include <stdio.h> int main(){ int a,b; scanf("%d %d",&a,&b); printf("%d",a-b); return 0; }asm
section .data in db "%d %d", 0 out db "%d", 10, 0 section .bss x resd 1 y resd 1 section .text extern input extern print global start start: lea rsi, [x] lea rdx, [y] mov rdi, in xor eax, eax call input mov eax, [x] sub eax, [y] mov esi, eax mov rdi, out xor eax, eax call print mov eax, 0 retsection .data
- in = scanf에 전달할 포맷 문자열
- out = printf에 전달할 포맷 문자열
section .bss
- 각각 int(4바이트) 공간을 확보
_start
- x-y 결과가 eax에 저장
- input는 scanf를 대신 호출하는 외부 함수
- print는 printf를 대신 호출하는 외부 함수
- main() 함수가 return 0; 과 같은 구조
5 - 10998
두 수를 입력받고 곱한 값을 출력하세요.
C
#include <stdio.h> int main(){ int a,b; scanf("%d %d",&a,&b); printf("%d",a-b); return 0; }asm
section .data in db "%d %d", 0 out db "%d", 10, 0 section .bss x resd 1 y resd 1 section .text extern input extern print global start start: lea rsi, [x] lea rdx, [y] mov rdi, in xor eax, eax call input mov eax, [x] imul eax, [y] mov esi, eax mov rdi, out xor eax, eax call print mov eax, 0 retsection .data
- in = scanf에 전달할 포맷 문자열
- out = printf에 전달할 포맷 문자열
section .bss
- 각각 int(4바이트) 공간을 확보
_start
- x*y 결과가 eax에 저장
- input는 scanf를 대신 호출하는 외부 함수
- print는 printf를 대신 호출하는 외부 함수
- main() 함수가 return 0; 과 같은 구조
과제 2