티스토리 뷰

이번에도 지난번 글과 같이 간단한 함수의 디스어셈블을 통하여 함수호출시에 일어나는 일들을 살펴보고자 한다.

참고로 이와 같은 학문(?)의 범주는 다음과 같은 위치에 있다.

* C 언어 표준 스펙
* intel x86 CPU의 System V Application Binary Interface
* gcc의 구현

System V ABI 를 검색어로 찾아 보면 원하는 글을 찾을 수 있을 것이다.

void func6( char x1, char x2, char x3)
{
x1 = 1;
x2 = 2;
x3 = 3;
}

struct _p
{
int a1;
int a2;
int a3;
};

void func7( struct _p p1, int p2 )
{
p1.a1 = 1;
p1.a2 = 2;
p1.a3 = 3;
p2 = 4;
}

struct _p func8(void)
{
struct _p y1;
y1.a1 = 1;
y1.a2 = 2;
y1.a3 = 3;
return y1;
}

int main()
{
return 0;
}


위 코드를 다음과 같이 컴파일한다.

$ gcc -save-temps b.c

이렇게 하고 나면 b.s 라는 파일이 생성되며 그 파일을 분석한다.

void func6( char x1, char x2, char x3)
{
x1 = 1;
x2 = 2;
x3 = 3;
}
5 func6:
6 pushl %ebp
7 movl %esp, %ebp
8 subl $4, %esp
9 movl 8(%ebp), %eax
10 movl 12(%ebp), %edx
11 movl 16(%ebp), %ecx
12 movb %al, -1(%ebp)
13 movb %dl, -2(%ebp)
14 movb %cl, -3(%ebp)
15 movb $1, -1(%ebp)
16 movb $2, -2(%ebp)
17 movb $3, -3(%ebp)
18 leave
19 ret

인자가 char 라고해서 넘기는 값도 8 bit라고 생각하면 오산이되기 쉽다. gcc만 아니라 대개의 구현에서 char 를 char 크기만큼 넘기는 것 보다 CPU word 크기인 int로 변환된 크기를 넘기며, 위 코드에서는
8(%ebp), 12(%ebp) 와 같이 인자의 위치가 4 byte 씩 떨어져서 접근하는 것을 보아 알 수 있다.
그리고 신기한 것은

8 subl $4, %esp

에서 알 수 있듯이 만들지도 않은 자동변수가 있듯이 스택을 4 byte 확보를 하고, 그곳에 일단 인자들의 사본을 만들어 놓고 연산을 수행함을 알 수 있다.

만약 gcc를 쓴다면, 인자를 char로 넘기는 것보다 int로 넘기는 것이 오히려 연산 수가 줄어드는 것을 알 수 있다.

void func7( struct _p p1, int p2 )
{
p1.a1 = 1;
p1.a2 = 2;
p1.a3 = 3;
p2 = 4;
}
23 func7:
24 pushl %ebp
25 movl %esp, %ebp
26 movl $1, 8(%ebp)
27 movl $2, 12(%ebp)
28 movl $3, 16(%ebp)
29 movl $4, 20(%ebp)
30 popl %ebp
31 ret

struct 로 넘기는 것은 스택에 상당히 많은 양이 들어가게 된다. 단지 두개의 변수를 넘기지만, 스택접근하는 것을 보면 struct의 각 변수들을 모두 직접 접근하게 되며, 두번째 인자가 20(%ebp)임을 확인할 수 있다.

struct _p func8(void)
{
struct _p y1;
y1.a1 = 1;
y1.a2 = 2;
y1.a3 = 3;
return y1;
}
35 func8:
36 pushl %ebp
37 movl %esp, %ebp
38 subl $24, %esp
39 movl 8(%ebp), %edx
40 movl $1, -24(%ebp)
41 movl $2, -20(%ebp)
42 movl $3, -16(%ebp)
43 movl -24(%ebp), %eax
44 movl %eax, (%edx)
45 movl -20(%ebp), %eax
46 movl %eax, 4(%edx)
47 movl -16(%ebp), %eax
48 movl %eax, 8(%edx)
49 movl %edx, %eax
50 leave
51 ret $4

struct 입력, 출력의 재미는 여기에 있다. 지난 강좌에서 int의 리턴값은 스택을 사용하지 않고 %eax를 이용함을 보았는데, 그렇다면 32bit 레지스터의 크기를 넘는 구조체는 어떻게 넘어갈까였다.

맨 마지막을 먼저 보자
49 movl %edx, %eax
50 leave
51 ret $4
%edx를 %eax에 넘기는 것으로 보아 이번에도 %eax에 뭔가가 담기고 있다. 그리고 특이한 것은 ret 뒤에 $4가 있다는 것이다. 이것은 ret는 현재의 서브루틴을 탈출할때 stack pointer를 그만큼 더하라는 얘기가 된다. 즉 stack pointer에 뭔가가 더해진다는 것은 그 영역을 해제한다는 뜻인데, 여기에서는 인자의 크기중 4 byte를 제거하는데 사용된다. 왜 이런 코드가 있을까.

38 번째 줄은 %esp에서 24 byte를 빼는 (stubstrct) 행위로, y1 struct 크기만큼을 스택에 잡는 코드이며,
39 번째 줄은 우리가 지난 강좌에서 외웠던 8(%ebp)라는 첫번째 인자에 해당하는 값을 %edx에 복사하고
40에서 42는 C 코드 대로 y1 struct 값을 채우는 일을 하고 있으며,
43 movl -24(%ebp), %eax
44 movl %eax, (%edx)

45 movl -20(%ebp), %eax
46 movl %eax, 4(%edx)

47 movl -16(%ebp), %eax
48 movl %eax, 8(%edx)

이 코드들은 방금 채워넣은 y1의 각각의 값을 %edx가 가리키는 곳에 하나씩 복사하는 것을 한다. 이것이 바로 리턴하기 직전에 이 함수를 부른 녀석에게 struct 의 내용을 넘기는 곳인데, %edx가 마치 임시로 생성되어 있는 리턴용 struct의 위치인듯한다. 함수 호출시에 이름없는 임시 값으로 알려져 있는 것이다. 그 값이 바로 8(%ebp), 즉 첫번째 인자가 위치해야할 곳에 있는 것이다.

49 movl %edx, %eax
처음 저장했던 %edx를 다시 돌려주기 위해 복사한다.

중요한 것은 return 값이 int 혹은 그 이하 크기를 돌리는 함수들은 인자의 순서가 그대로 넘어 오며, %eax에는 그 돌리는 값이 들어가지만, struct에 대해서는 인자들의 맨 앞에 가상의 포인터가 하나 넘어오게 되고 그 다음부터 인자의 순서가 그대로 넘어오고 %eax에는 첫번째 인자로 넘겨줬던 값이 다시 돌아간다는 것이다.
즉, return 값이 struct인경우 스택의 첫째 값은 특별 취급된다.

그러면 return 할 때 스택포인터에 4를 더함으로써, 뭔가 해제하는 것은 아마도 처음인자를 제거하는데 사용하는 것 같다. 이것은 실제 func8을 호출하는 코드를 보면 자세히 알 수 있을 것이다.



자, 여기까지 분석한 것을 가지고 재밌는 장난을 해보자.
#include <stdio.h>

void func_a( int x, int y, int z )
{
printf("%d %d %d\n", x, y, z );
}

struct _p
{
int a1;
int a2;
int a3;
};

typedef void (*func_p)( struct _p w );

int main()
{
struct _p x;
func_p func_b;
func_b = (func_p) & func_a;

x.a1 = 1;
x.a2 = 2;
x.a3 = 3;

func_b( x );
return 0;
}

$ ./a.out
1 2 3



위 코드는 struct 를 넘기는 방식에 대한 이해가 있는 상황에서 만들어진 코드인데, func_a의 프로토타잎을 강제로 struct로 만든뒤 넘겨도 예상한 결과가 나온다는 예제이다.
반응형
댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
«   2024/07   »
1 2 3 4 5 6
7 8 9 10 11 12 13
14 15 16 17 18 19 20
21 22 23 24 25 26 27
28 29 30 31
글 보관함