함수를 호출할 때 일어나는 일 ( cdecl )

시루·2021년 7월 8일
0

Reversing

목록 보기
1/1
post-thumbnail
int b(int arg1, int arg2) {
  return arg1 + arg2;
}

void a(void) {
  b(1,2);
} 

프로그래밍을 하다보면 위와 같이 함수 내에서 서브루틴을 자주 호출합니다.

어떻게 처리되는 걸까요?

Calling Convention

위 코드에서 b 함수를 호출하는 a를 caller라고 하고 호출 되는 b는 callee라고 합니다.
함수 호출을 처리할 때 caller와 callee 사이 상호작용을 함수 호출규약 ( Calling Convetion ) 이라고 합니다.

함수 호출 규약은 여러가지가 존재하는데 이 포스팅에선 cdecl에 대해 다룹니다.

cdecl

C Declaration

x86 architecture는 cdecl이라는 calling convention을 사용합니다.
따로 calling convention을 지정하지 않으면 기본으로 cdecl이 사용됩니다.

인자 전달 방법

caller가 stack을 통해 전달하며 리턴 값은 eax를 통해 전달받습니다.

stack 관리

caller가 stack의 argument를 정리합니다.

cdecl 함수 호출 처리 순서

  1. caller가 전달할 argument를 stack에 push
  2. callee prolog
  3. callee epilog
  4. caller가 call과정에서 사용한 stack 정리

어셈블리 분석

위 코드를 컴파일 후 핵심이 되는 부분만 남겨주면 아래과 같습니다.

b:
	push	ebp
	mov	ebp, esp
	mov	edx, DWORD PTR 8[ebp]
	mov	eax, DWORD PTR 12[ebp]
	add	eax, edx
	pop	ebp
	ret
a:
	push	ebp
	mov	ebp, esp
	push	2
	push	1
	call	b
	add	esp, 8
	nop
	leave
	ret

1. argument 전달

argument는 right-to-left로 전달됩니다.
이는 LIFO한 Stack의 특징을 생각해보시면 쉽게 이해할 수 있습니다.

push	2
push	1
after push
1
2

2. call

이 후 call instruction을 실행하면
내부적으로 아래와 같이 진행됩니다.

// call offset
push eip + sizeof(call instruction)
add eip, offset

💡 offset = callee instruction - call instruction - sizeof(call instruction)

우선 함수 호출 이 후 caller로 돌아올 수 있게 return address를 stack에 저장합니다.
이 후 instruction pointer를 callee의 instruction을 가르키게 합니다.

after call instruction
return addr
1
2

함수를 호출 시 jmp instruction을 쓰는 경우도 있습니다.
optimized tail-call이 그 예인데요

void a() {
  return b()
}

아래와 같이 서브 루틴의 리턴 값을 리턴하는 구조로 되어있을 때 b의 에필로그 이후 a로 돌아올 이유가 없습니다.

바로 a의 caller의 return address로 돌아가면 되기 때문에 a의 ret을 저장할 필요가 없어집니다.

3. callee prolog

함수가 호출되면 prolog라고 불리는 아래의 과정이 진행됩니다.

push ebp
mov ebp, esp

함수가 호출되었을 때 그 함수가 가지는 논리적인 공간을 stack frame이라고 합니다.
또한 stack frame내 ebp (base pointer)를 그 stack frame pointer라고 부릅니다.

caller인 a의 stack frame으로 돌아올 수 있게 a의 sfp를 저장 후 b의 stack frame을 생성합니다.
이 과정이 끝나면 아래와 같은 구조가 됩니다.

Stack
B's Stack Frame
A's Stack Frame
Main's Stack Frame

A와 B사이 stack은 다음과 같습니다.

after prolog
caller(a) sfp
return addr
1
2

4. callee 수행

stack은 커널영역을 보호하기 위해 높은 주소에서 낮은 주소로 진행됩니다.
ebp보다 먼저 저장된 데이터에는 ebp + offset 을 통해 전달받은 인자에 접근합니다.

mov	edx, DWORD PTR 8[ebp] ; 8[ebp] == ebp+8
mov	eax, DWORD PTR 12[ebp]

add 연산 결과 eax에 return value가 저장됩니다.

5. callee eplilog

이제 b함수는 eplilog라고 불리는 아래의 과정을 진행합니다.

pop ebp
ret
after pop ebp
return addr
1
2

caller(a)의 stack frame으로 되돌리기 위해 ebp를 a의 sfp로 바꾸어주고 ret 명령을 수행합니다.

pop eip
jmp eip
after ret
return addr
1
2

6. caller가 call 과정에서 사용한 stack 정리

이렇게 callee가 종료되면 caller는 call과정에서 사용한 스택을 정리해줍니다.

add	esp, 8

이렇게 cdecl에 대해 간단히 알아보았습니다.
다음 포스트에선 x86-64 architecture의 기본 호출 규약인 fast-call에 대해 알아보겠습니다.

--- 0918 추가 ---
https://stackoverflow.com/questions/56676840/is-pop-eip-legal-instruction

내부적으로 pop eip, jmp eip가 수행된다고 작성했었는데 오늘 피드백해주다가 새로운 사실을 알게되어 작성합니다.
내부적으로 처리되는게 저런 방식이란 거지 실제로 변환되서 하는 건 아닙니다.
eip가 general purpose register가 아니고 아예 ret의 opcode는 따로 있습니다.

profile
안녕하세요. 보안에 관심이 많은 학생입니다 : )

0개의 댓글