Recommanded Free YOUTUBE Lecture: <% selectedImage[1] %>

함수에 대해서 다루기

4. 함수에 대해서 다루기

4.1. 복잡한 프로그램 다루기

2절에서 다루었던 프로그램은 어셈블리의 기본적인 설명을 위한 매우 단순한 코드들로 이루어져 있으며 단지 하나의 섹션만을 가지고 있다. 프로그램이 단순하다면 관계 없지만. 만약 좀 규모있는 프로그램을 만들어야 한다면 수천, 수만라인을 작성해야 될지도 모른다. 이런 프로그램을 하나의 섹션으로 유지하면 유지보수가 매우 힘들 것이다. 특히 여러명의 프로그램이 하나의 프로젝트에 참가할 경우 프로그램의 유지 보두는 더욱 힘들게 될것이다.

이렇게 프로그램의 크기가 커지거나 두명이상의 프로그래머가 참가할 때는 코드를 하는일에 따라서 몇개의 조각으로 나누어서 관리하면 될것이다. 물론 이경우 나중에 조각을 하나로 합치기 편하도록 잘 정의된 인터페이스를 만들고 여기에 따라서 자신이 맡은 코드를 작성하도록 해야 한다. 이렇게 하면 각 프로그래머는 자신이 맡은 영역만을 독립적으로 테스트 할 수 있으므로 프로그래밍의 집중도를 높일 수 있게 되고, 다른 프로그래머와 좀더 효율적으로 협업할 수 있게 된다.

이러한 독립적인 개발을 위해서 프로그래머는 함수라고 불리우는 여러개의 코드조각을 만들게 된다. 워드 프로세서를 만든다고 가정해보자. 그럼 "가" 개발자는 사용자의 키입력을 처리하는 handle_type_chracter 함수를 만들도록 한다. 이 함수는 유저의 키입력을 읽어 들여서 그 값을 화면에 출력하는 일을 한다. "나"라는 개발자는 check_chracter_spell 이라는 오타검사 함수를 만들도록 한다. 이 함수는 handle_type_chracter에 의해서 입력된 문자를 검사해서 오타가 발생하는지를 확인하는 일을 한다. 함수는 이렇게 주어지는 데이터와 이의 처리하는 루틴단위로 만들어 지게 된다.

좀 규모있는 프로그램들은 수백에서 수천의 이러한 조그마한 함수들로 이루어지게 된다. 이러한 함수들 중에는 개발자가 필요에 의해서 그때 그때 만들어 지는 것들도 있지만 시스템에 의해서 제공되는 함수들도 있다. 당연하지만 이미 만들어진 함수는 사용자가 다시 만들어서 사용할 수 없다. 이런 시스템에서 제공하는 함수들을 원시(primitive)함수라고 부른다. 예를 들어 그래픽 유저 인터페이스를 지원하는 프로그램을 작성한다고 가정해 보자. 여기에는 메뉴를 생성하기 위한 함수가 있을 것이다. 이 함수들은 글자를 쓰거나, 아이콘을 배치하거나, 배경의 색을 설정하거나 마우스의 위치를 계산하는 함수들이 있을 것이다. 그라나 이러한 함수들 외에도 기본적으로 선을 만들거나, 점을 찍거나 하는 등의 가장 기본적인 함수들이 있을 것이고 이것들은 운영체제 수준에서 지원하게 될것이다. 이러한 함수를 primitive함수라고 보면 된다. 최종적으로 여러분이 큰 규모의 프로그램을 작성한다면 이러한 primitive함수들을 가진 작은 (사용자 가 만든)함수들의 모임으로 이루어 질 것이다. 이상 일반 GUI 애플리케이션을 예로 들어서 설명했는데, GUI 에서의 primitive는 어셈블리에서는 시스템콜이다. 여기에서는 시스템콜에 대한 자세한 내용을 다루진 않을 것이다. 시스템 콜에 대한 내용은 리눅스 시스템 콜 퀵 레퍼런스리눅스 어셈블리 하우투를 참고해 보기 바란다.

4.2. 함수 만들기

함수는 다음과 같은 요소들에 의해서 만들어진다.

함수 이름

어셈블리어에서 함수의 이름은 함수의 코드가 시작하는 주소를 가리키는 심볼(symbol)로 선언되며, 심볼은 함수의 이름 다음에 콜론(":")을 붙이는 걸로 선언된다. 이 뒤에 있는 코드는 모두 함수의 코드가 된다. 점프를 위해서 사용되는 라벨과 매우 비슷하게 사용된다.

함수 인자

함수 인자(parameter)은 함수가 처리해야 하는 데이터 아이템이다. 예를 들어 사칙연산을 수행하는 함수가 있다고 가정해 보자. 사칙연산을 위해서는 2개의 피연산자가 필요하다. 그러므로 함수에 2개의 (숫자)데이터를 넘겨줄 수 있어야 한다. 이 함수는 2개의 데이터를 넘겨 받기 위해서 2개의 인자를 필요로 하게 된다.

지역 변수

지역변수는 함수에셔 연산을 위해 사용되는 데이터를 저장하기 위해서 함수안에서만 사용되는 변수다.

전역 변수

전역변수는 함수외부에서 관리되어 지는 데이터 저장영역이다. 예를 들어 간단한 텍스트 편집기를 만들경우 편집기에서 불러들인 파일의 내용은 저장, 편집, 찾기, 치환등과 관련된 다양한 함수에서 사용될 것이므로 모든 함수에 걸쳐서 (전역적)으로 관리되어져야 할 필요가 있다. 만약 전역 변수가 아니라면 각 함수마다 파일을 열어서 내용을 메모리에 저장하고 작업을 해야 하는 번거로운 코드가 추가 되어야 할 것이다.

리턴 주소

"리턴 값"과 혼동하지 않도록 한다. 리턴 주소는 보이지 않는 인자로 사용되며 함수에서 직접다루지 못하는 값이다. 리턴 어드레스는 함수가 종료된 완전히 종료된 후 실행되어야할 영역의 주소값을 알려준다. 대부분의 프로그래밍 언어에서 리턴 주소는 자동적으로 계산되어서 사용하므로 프로그래머가 특별히 신경쓸 필요가 없다. 반면 어셈블리어에서의 경우 call명령을 이용해서 제어를 해주어야 한다.

리턴 값

함수에서 일을 수행한 후의 결과를 메인 프로그램에 전달하기 위해서 사용한다. 대부분의 프로그래밍 언어는 하나의 함수에 대해서 단지 하나의 값만을 리턴할 수 있도록 허용한다.

이들 특징들은 어셈블리어뿐만 아니라 다른 모든 언어들도 공통적으로 가지고 있다.

변수를 만들어서 저장하고, 인자를 사용하고, 값을 리턴하는 표현하는 방법은 언어마다 다르긴 하지만 언어에 관계없이 공통적으로 사용되어 진다. 그러므로 언어에 관계없이 일반적으로 표현이 가능할 것이다. 이러한 표현의 방법을 calling convention라고 부른다.

calling convention은 매우 다양하며, 자신만의 calling convention을 만들어서 사용할 수 도 있다. 그렇긴 하지만 다른 만들어진 함수가 다른 언어로 포팅되는 것을 고려해야 되는 경우도 있으므로 가능하면 표준적인 calling convention을 따르는 것이 좋을 것이다. 여기에서는 C 언어의 calling convention을 따를 것이다. 왜냐하면 가장 널리 사용되어지는 언어이며 또한 가장 많은 예제 코드를 가지고 있기 때문이다.

참고

과거에는 파스칼, C의 calling convention을 주로 사용했지만 지금은 상황이 좀달라져서 java, c++, python등의 다양한 calling convention이 사용되어지고 있다.

4.3. C calling convention을 이용한 어셈블리어 함수설명

만약 당신이 컴퓨터의 스택이 어떻게 작동하는지에 대한 이해를 가지고 있지 않다면 어셈블리어에서 함수를 작성할 수 없을 것이다. 우리가 컴퓨터에서 프로그램을 실행시키면, 프로그램은 각각의 함수를 수행하기 위해서 스택영역을 확보한다. 스택은 말그대로 더미란 뜻인데, 당신의 작업책상위에 놓인 처리해야될 서류더미를 연상하면 된다. 당신은 처리해야할 서류를 서류더미에 올려놓고 처리해야될 경우 가장 위에 있는 서류부터 처리하게 될것이다. 처리된 서류는 다른 한쪽으로 치우거나.. 기분나쁜 서류의 경우 스레기통에 버릴 것이다.

컴퓨터도 마찬가지로 이러한 스택을 가지고 있다. 컴퓨터에서 스택은 메모리의 처음위치에 존재한다. 당신은 pushl명령을 이용해서 스택의 가장 위에 값을 밀어 넣을 수 있다. 스택의 가장 위에는 레지스터와 다른 값들이 들어 갈 수 있다. 우리는 말할 수 있을 것이다. "왜 하필 가장 위에 저장해야하죠?" 그 이유는 간단하다. 접시를 씻기 위해서 쌓아 두었을 때 위에서 부터 꺼내서 씻는 이유를 생각해보면 된다. 혹은 책상에 쌓여있는 서류의 처리를 생각해 보라. 위에서 부터 꺼내어서 처리하는게 아래, 혹은 중간에서 꺼내어서 처리하는 것보다 훨씬 쉽다. 컴퓨터에서의 메모리 관리 역시 위에있는 것부터 꺼내어서 가져오는게 구조상 훨씬 쉽도록 되어 있다. pushl을 이용해서 스택의 가장 위에 데이터를 올렸다면, 이 데이터는 popl을 통해서 가져올 수 있다. popl명령을 이용하면 스택의 가장 위에 있는 데이터를 가져오게 된다.

스택에 값을 밀어 넣게되면 현재 스택의 제일 윗부분의 주소를 계산해서 그 위치에 값을 추가한다. 스택의 제일 위의 위치값은 %esp 레지스터를 통해서 알아올 수 있다. 참고로 스택은 언제나 연속적으로 위치하게 되므로 데이터의 크기만 알고 있다면 위치를 정확하게 계산할 수 있다.

pushl을 통해서 스텍에 어떤 값을 넣게 되면 스택의 제일 위를 가르키는 포인터에 4가 더해져야 한다. 고로 %esp에서 4를 빼면 가장 최근에 저장된 데이터를 읽어올 수 있게 된다. (저장되는 데이터는 워드(word)즉 4바이트임을 기억하라). 만약 스택으로 부터의 데이터 삭제는 popl명령을 수행하면 된다. pushl과 popl은 하나의 오퍼랜드만을 가진다.

만약 스택의 가장위에 접근하길 원한다면, %esp 레지스터를 indirect 어드레스 모드로 접근하면 된다. 다음은 스택의 가장 위에 있는 값을 %eax 레지스터로 옮기는 예이다.

	
movl (%esp), %eax
# 혹은
movl %esp, %eax
			
위의 예는 indirect 모드 이므로 %eax에는 스택의 위에 있는 값이 아닌 스택의 위의 위치주소가 저장된다. 만약 주소에 저장된 값을 가져오길 원한다면 아래와 같이 하면 된다.
movl 4(%esp), %eax
			
위의 예제를 보면 base pointer 주소모드를 이용해서 가져오고 있음을 알 수 있다. %esp에서 4만큼 이동한 위치에 저장된 값을 %eax에 복사한다.

C언어의 calling convention에서 스택은 함수의 로컬변수, 인자, 리턴주소들을 구현하는데 중요하게 사용된다.

subl  $8, %esp
			
위의 명령은 %esp로 부터 8을 뺀다(word는 4바이트 크기를 가짐을 기억하라). 이런식의 사용은 함수를 호출해서 스택에 값을 저장하는데, 이런 저런 거추장 스러운 것들(레지스터 주소등..)을 이용하지 않고도 간단히 사용할 수 있다는 장점을 가진다. 일단 함수가 호출되어서 스택이 할당되면 함수가 종료할때까지 계속 남아 있게 된다. 우리가 함수에서 리턴하면 스택 프래임과 여기에 있는 변수가 제거된다. 이렇게 특정한 함수의 내부에서만 사용되는 특징으로 이들을 로컬변수라고 부른다. 우리가 2 word의 로컬 저장소를 만들었고 스택은 다음과 같이 보일 것이다.
Parameter  #N    <--- N*4+4(%ebp)
...
Parameter  2     <--- 12(%ebp)
Parameter  1     <---  8(%ebp)
Return Address   <---  4(%ebp) 
Old %ebp         <---   (%ebp)
Local Variable 1 <--- -4(%ebp) 
Local Variable 2 <--- -8(%ebp)  and (%esp)
			
이제 우리는 base pointer addressing 방법을 이용해서 %ebp로 부터 원하는 모든 값에 접근할 수 있게 되었다. 물론 여러분은 base pointer addressing 모드를 위해서 다른 레지스터를 이용할 수도 있긴하지만, 일반적은 x86에서 %ebp레지스터가 가장 빠른 관계로 %ebp가 선호된다.

이번장을 통해서 우리는 전역변수와 static 변수를 메모리상에서 어떻게 접근할 수 있는지를 배웠다. 전역변수와 static의 차이점은 static가 오직 함수내에서만 사용되고 전역변수는 여러함수에서 사용될 수 있다는 점이다. 다른 모든 언어에서와 마찬가지로 어셈블리어에서도 이들 변수를 다루는 방법은 동일하다.

함수는 실행될때 기본적으로 3가지일을 해주어야 한다. 첫번째로 %eax에 리턴 값을 저장한다. 두번째로 호출되기 전에 스택을 초기화(reset)한다. 마지막으로 리턴의 제어는 ret 명령을 통해서 이루어진다. 이것은 스택의 제일 위의 값을 가져오는(pop)하는 일을 하고, 명령의 포인터인 %eip를 설정한다.

함수는 리턴할 때 그것을 호출한 코드에게 제어권을 되돌려 주어야만 하는데, 그러기 위해서는 반드시 이전의 스택프레임을 복구해 주어야만 한다. 그러나 ret 명령은 이러한 일을 하지 않는다. 왜냐하면 우리의 최근 스택프레임에 있는 리턴 주소는 스택의 제일 위를 가리키지 않기 때문이다. 그러므로 함수를 수행하기 전에 리턴할 스택포인터인 %esp와 베이스 포인터 %ebp를 리셋해주어야 한다. 그러므로 함수로 부터 리턴하기 위해서 다음과 과정이 필요하게 된다.

	
movl   %ebp,  %esp
popl   %ebp
ret 
			
함수가 리턴할 때 여러분이 사용했던 모든 로컬변수는 처분되어 버리게 됨을 주목하기 바란다. 왜냐하면 여러분이 스택 포인터를 뒤로 이동하게 되면, 그 앞의 스택을 다른 값들이 덮어써 버리기 때문이다. 결국 지역 변수들은 함수가 생성되어서 작동하고 있을 때만 유효하게 된다. 함수가 리턴하게 되면 이제 프로그램의 제어권은 함수를 호출한 코드로 넘어오고, 제어권을 가진 코드는 %eax를 이용해서 리턴값을 검사할 수 있게 된다.

page 47에 대한 자세한 해석이 필요.

4.4. 함수 예제

이제 실제 프로그램에서 어떻게 함수를 호출하는지 예를 들어서 알아보도록 하겠다. 우리는 power(거듭제곱)연산을 하는 이다. power 함수는 2개의 숫자를 필요로 한다. 예를 들어 2, 3이 들어 갔다면 2 * 2 * 2 연산을 하게 되고 8을 리턴하게 된다.

다음 코드는 완전한 프로그램이다. 파일의 이름은 power.s로 하겠다.

# 하는일 : 2^3 + 5^2의 값을 계산후 되돌려준다.
#

.section .data
.section .text

.globl _start
_start:
pushl $3                    # 두번째 인자에 3을 넣는다.
pushl $2                    # 첫번째 인자에 2를 넣는다.

call power                  # power 함수를 호출한다. 

addl $8, %esp               # 스택의 포인터를 이동한다. 

pushl %eax                  # 함수호출 결과값을 저장한다.

pushl $2                    # 두번째 인자에 2를 넣는다. 
pushl $5                    # 첫번째 인자에 5를 넣는다.
call power                  # power 함수를 호출한다.
addl $8, %esp               # 스택포인터를 이동한다. 

popl %ebx                   # 두번째 호출한 pow함수의 리턴값은 
                            # 이미 %eax에 저장되어 있다. 
                            # 우리는 첫번째 pow함수의 결과값을 스택에서 
                            # 가져온다음 %ebx에 저장한다.  

addl %eax, %ebx             # 각 결과값들을 더한다. 
                            # 더한 결과값은 %ebx에 저장된다.

movl $1, %eax               # exit (%ebx 값이 리턴된다)   
int  $0x80                  # exit 시스템 호출을 위해서 인터럽트를 건다.


# 함 수 명 : power
# 하 는 일 : 주어지는 인자를 이용해서 거듭제곱 연산을 한후
#            되돌려준다.   
#
# 입    력 : 첫번째 인자 - 기본숫자 
#            두번째 인자 - 거듭제곱 숫자 
#
# 출    력 : 연산의 결과 
#
# 주    의 : 인자는 반드시 1보다 커야 한다.
#
# 변 수 들 : 
#            %ebx - 기본 숫자
#            %ecx - 거듭제곱 숫자 
#
#            -4($ebp) - 최근 결과값 
#
#            %eax 는 임시저장소로 사용된다.
#

.type power, @function
power:
	pushl %ebp           # 이전의 베이스 포인터를 저장한다. 
	movl %esp, %ebp      # 스택포인터를 베이스 포인터로 만든다.
	subl $4, %esp        # 지역 저장소를 위한 공간을 만든다.
	
	movl 8(%ebp), %ebx   # 첫번째 인자를 %eax에 저장한다.
	movl 12(%ebp), %ecx  # 두번째 인자를 %ecx에 저장한다.

	movl %ebx, -4(%ebp)  # 최근값을 저장한다. 

power_loop_start:
	cmpl $1, %ecx        # 만약 power이 1이면 end_power로 점프한다. 
	je end_power
	movl -4(%ebp), %eax  # 최근 결과값을 %eax에 저장한다.
	imul %ebx, %eax      # 최근 결과값에 기본 숫자를 곱한다.
	
	movl %eax, -4(%ebp)  # 최근 결과값을 저장한다.
	decl %ecx            # power을 감소시킨다.

	jmp power_loop_start # 다음 거듭제곱(power)를 실행한다.

end_power:                 
	movl -4(%ebp), %eax  # 리턴값을 %eax에 저장한다.
	movl %ebp, %esp      # 스택포인터를 복구한다.
	popl %ebp            # 베이스 포인터를 복구한다.  
	ret
			

위의 코드를 작성하고 컴파일 시킨다음 실행시켜 보도록 하자. 그리고 power함수를 다른 인자를 주어서 호출해서 결과값이 제대로 나오는지 확인해 보도록 하자. 이때 결과값은 256을 넘기지 않도록 적당히 인자를 조절하도록 해야 한다. 테스트가 끝났다면 두개의 결과값을 뺀값을 리턴하도록 코드를 수정해 보도록 하자. power 함수를 3번 호출하고 그 값을 리턴하는 코드도 만들어 보자.

프로그램의 코드는 매우 간단하다. 당신이 인자를 스택에 집어 넣고 함수를 호출하면, 함수는 스택 포인터를 뒤로 되돌리고 스택에 있는 인자를 읽어와서 연산을 하고 결과값을 %eax에 저장한다. 위 프로그램에서는 pow함수를 두번 호출하는데, 우리는 첫번째 값을 스택에 저장했다. 이유는 레지스터에 단지 한번에 하나의 값만 저장될 수 있기 때문이다. 우리는 첫번째 pow함수 리턴값을 스택에 저장하고 나중에 두번째 함수의 결과가 나왔을때 스택의 값을 가져와서(pop) 두번째 결과와 더해서 원하는 결과를 얻어 올 수 있다.

이제 함수 코스드에 대해서 알아보자. 함수에서 주목해야될 부분은 함수가 어떻게 선언되는지, 그리고 인자가 어떻게 넘어가고 리턴값을 넘겨주는지 하는 부분이다. 이러한 입력부분과 출력부분은 프로그래머가 함수를 설계할 때 가장 일차적으로 고려하는 부분으로 흔히 함수의 인터페이스(Interface)라고 한다. 인터페이스를 제대로 구성하기 위해서 프로그래머는 스택과 %eax에 대해서 이해하고 있어야 한다. 스택은 입력, %eax는 출력(리턴)을 위해서 사용된다.

가장 먼저 해야 할일은 함수의 이름을 정하는 일이다.

.type power,@function
			
이것은 함수의 이름 power을 함수에 연결 시키는 일을 한다. 우리는 power을 이용해서 실제 함수를 실행 시킬수 있다. 이러한 선언은 현재로써는 그리 유용하지 않지만 만약 프로그램의 크기가 커져서 여러개의 파알로 나뉘어서 유지되어야 한다면 매우 유용하게 사용할 수 있다. 이겨에 대한 장보는 나중에 자세히 설명하도록 하겠다. 함수를 선언했으니 실제 함수 코드를 적어야 할것이다. 함수코드가 시작되는 위치는 다음과 같이power 라벨을 이용해서 명시할 수 있다.
power:
			
이렇게 해서 우리가 최초 선언한 power은 이 라벨이 있는 주소를 가리키게 된다. 이제부터 실제 함수의 실행부를 작성하면 된다.
pushl  %ebp
movl   %esp, %ebp
subl   $4, %esp
			
가장 먼저 스택을 제어하기 위한 명령이 실행되는데, 위의 명령을 수행한후 스택의 구조는 아래와 같다.
Base Number    <------- 12(%ebp)
Power          <-------  8(%ebp)
Return Address <-------  4(%ebp)
Old %ebp       <-------   (%ebp) 
Current result <-------  -4(%ebp) and (%esp)
			
이 프로그램은 base number와 연산에 사용할 피연산자를 저장하기 위한 %ebx, 현재 값을 저장하기 위한 레지스터(-4(%ebp))를 가지고 시작한다. 그리고 몇번의 거듭제곱을 해야하는지는 %ecx에 저장된다. %ecx는 한번의 거듭제곱이 일어나면 1씩 감소되고 1이되면 루프에서 벗어난다.

거듭제곱 함수를 보면 imuldecl가 등장한다. imul은 곱셈연산을 한다. 연산의 결과는 두번째 오퍼랜드에 저장된다. decl은 레지스터의 값을 1감소시킨다. 명령에 대한 자세한 내용은 Appendix B를 참고하기 바란다.

4.5. 재귀 함수

다음 프로그램은 머리를 좀더 굴려줘야 한다. 이 프로그램은 팩토리얼(factorial)을 구한다. 팩토리얼 연산은 자신과 1사이의 모든 숫자를 곱하는 연산을 한다. 예를 들어서 7의 팩토리얼은 7*6*5*4*3*2*1이고 4의 팩토리얼은 4*3*2*1이다. 팩토리얼 연산을 할때 여러분은 하나의 규칙을 발견할 수 있을 것이다. 4팩토리얼의 경우 3팩토리얼이 4번 발생하는 것이고, 3팩토리얼은 2팩토리얼이 3번 발생하고, 2팩토리얼은 1팩토리얼이 2번 발생한다. 1팩토리얼은 1번발생으로 끝난다. 이러한 형태의 정의를 재귀(recursive)정의 라고 한다. 팩토리얼 함수를 구현할때 이 재귀 정의를 사용하면 문제를 더 쉽게 풀 수 있다.

팩토리얼 함수는 함수자신을 포함하는 재귀적 구조를 가진다. 함수가 함수 자신을 포함한다면 무한히 자신을 호출할 수도 있으므로 base case 가 정의되어 있어야만 한다. base case는 재귀가 종료되는 시점을 결정한다. base case가 없다면 함수는 무한히 호출될 것이고 결국 stack 공간을 모두 소비하게 될 것이다. 팩토리얼의 경우 base case는 숫자 1이 된다. 팩토리얼 함수를 재귀호출하게 되면 번호는 1씩 줄어들고 base case와 같은 수가 된다면 함수를 빠져나오게 된다.

  • 숫자를 평가한다.

  • 숫자가 1인가?

  • 그렇다면 팩토리얼 연산을 종료하고 값을 리턴한다.

  • 그렇지 않다면 숫자를 1감소 시키고 팩토리얼 함수를 재 호출한다.

# 팩토리얼 연산을 한다.
# 예를 들어 3은 3 * 2 * 1 
#           4는 4 * 3 * 2 * 1
# 이 된다.
.section .data

.section .text


.global _start
.global factorial

_start:
pushl  $4            # 팩토리얼 함수에 넘길 인자를 복사한다.

call   factorial     # 팩토리얼 함수를 실행한다.
addl   $4, %esp
movl   %eax, %ebx    # 팩토리얼 함수의 리턴값은 %eax에 저장되지만
                     # 프로그램 리턴값으로 넘길 필요가 있으므로 %ebx에 복사한다. 

movl   $1, %eax      # exit 시스템 콜을 호출한다.
int    $0x80

# factorial 함수 정의
factorial:
pushl  %ebp          # 리턴하기 전에 이전의 스택 포인터를 저장해야 한다. 
                     # 그래서 이전의 스택포인터를 저장한다. 
                     # 함수를 위한 기본 요소 
movl   %esp, %ebp    

movl   8(%ebp), %eax # 첫번째 인자를 %eax에 복사한다. 
                     # 4(%ebp)는 리턴어드레스 이고 
                     # 8(%ebp)는 첫째 인자의 어드레스이다. 

cmpl   $1, %eax      # 재귀에서 벗어나기 위한 base case 값은 1이다. 
                     # 여기에서 인자의 값이 1인지를 검사한다.
                     # 만약 1 이라면 end_factorial로 점프한다.  
je     end_factorial
decl   %eax          # 그렇지 않다면 %eax를 1 감소 시킨다.
pushl  %eax          # 이 값은 다음 factorial함수를 호출하면서 전달되어야 하므로 
                     # 스택에 밀어 넣는다.
call   factorial     # factorial을 실행한다. 

movl   8(%ebp), %ebx 

imul   %ebx, %eax    # 최근 호출했던 factorial의 리턴값과 곱한다.(%eax에 저장되어 있음)
                     # 곱한결과는 %eax에 저장한다.


end_factorial:
movl  %ebp, %esp    # 함수리턴을 위한 기본요소
popl  %ebp          # 리턴하기 전에 이전 함수의 시작포인터를 복구한다. 

ret                 # 함수를 리턴한다.
			

어셈블, 링크 과정을 거쳐서 실행시키고 리턴결과를 확인한다.

# as factorial.s -o factorial.o
# ld factorial.o -o factorial
# ./factorial
# echo $? 
			
프로그램의 리턴값이 24임을 확인할 수 있을 것이다. 24는 4의 팩토리얼 연산 값으로 4*3*2*1의 결과다.

여러분이 C와 같은 언어를 통해서 재귀호출을 경험해 보지 않았다면 위의 코드를 이해하는 데에 약간의 어려움이 있을 수도 있다. 이제 어떻게 이러한 계산이 가능한지를 알아보도록 하자.

_start:
pushl   $4
call    factorial
			
이 프로그램은 4의 팩토리얼을 계산한다. 호출될 함수에 이 값을 인자로 넘기기 위해서 함수 호출전에 스택에 밀어 넣었다. 함수의 인자는 함수에서 작업에 사용될 데이터라는 점을 기억하기 바란다. 이경우 함수는 하나의 인자를 가진다.

pushl 명령은 값을 스택의 가장위에 밀어 넣는다. call명령은 함수를 호출하기 위해서 사용한다.

다음 줄을 분석해 보도록하자.

addl   $4,    %esp
movl   %eax,  %ebx
movl   $1,    %eax
int    $0x80
			
factorial함수가 끝났으니, 4의 팩토리열의 값을 가져와야 할것이다. 우선 스택을 청소해야 한다. addl 명령을 이용해서 스택포인터를 $4만큼 뒤로 옮긴다. 팩토리얼 함수를 호출했다면 당신은 호출한 함수가 리턴된후 반드시 스택인자를 청소해 주어야 한다.

다음 %eax를 %ebx로 복사한다. %eax는 factorial함수의 리턴값이다. 이경우 리턴값은 팩토리얼 연산결과이다. 팩토리얼 함수를 호출할 때 인자를 4를 줬으므로 리턴값은 24가 될것이다. 리턴값은 언제나 %eax에 저장된다는 것을 기억하기 바란다. 우리는 프로그램이 종료할 때 운영체제에 24를 되돌려 주어야한다. 리눅스의 경우 프로그램의 종료값은 %eax가 아닌 %ebx에 저장한다. 이제 exit() 시스템 호출을 실행하면 된다.

함수를 사용하면 우리는 크고 복잡한 프로그램을 만들 때 상대적으로 간단하고 이해하기 쉬운 여러개의 조각으로 코드를 만들 수 있는 잇점을 얻을 수 있다. 실질적으로 아주 간단한 경우를 제외하고, 대부분의 프로그램은 여러개의 함수로 이루어진다.

위의 에제를 통해서 우리는 함수를 어떻게 사용하는지 알게 되었다. C에서와 마찬가지로 어셈블리어에서도 함수는 선언과 정의 부로 나뉘게 된다.

.type  factorial, @function
factorial:
			
함수는 보통 위와 같은 방법으로 만들 수 있다. .type는 factorial 함수를 가리키기 위한 링커의 용도로 사용되는데, 생략할 수 있다. 우리가 만든 프로그램에는 .type를 사용하고 있다. facorial: 은 factorial이 가리키는 실제 함수코드가 이곳에 위치한다는 것을 정의 한다. 다음 줄부터 우리는 factorial의 실제적인 코드 내용을 기술하면 된다. 기술된 factorial 함수는 call 명령을 통해서 실행 시킬 수 있다.

factorial함수의 첫라인에는 다음과 같은 코드가 들어가 있다.

pushl   %ebp
movl    %esp, %ebp
			
이 두줄은 함수에서 사용할 스택영역을 생성한다. 이 두줄은 모든 함수의 시작부분에 포함된다.
movl    8(%ebp), %eax
			
베이스 포인터 어드레스를 함수의 첫번째 인자로 이동한다음 이곳의 값을 %eax에 보가한다. 4(%ebp)는 리턴어드레스를 가지고 있으며, 8(%ebp)는 함수의 첫번째 인자를 가리키고 있음을 기억하기 바란다.

다음 라인에서 함수의 인자가 base case에 도달했는지 검사한다. 만약 base case 즉 1이라면 팩토리얼 연산을 끝내고 end_factorial로 점프 한다.

cmpl    %1,  %eax
je      end_factorial
			
만약 base case에 도달하지 않았다면, 우리는 factorial 함수를 다시 호출 해야 한다. 호출할 때는 인자값(%eax)를 1만큼 감소 시켜야 한다.
decl    %eax
			
decl는 메모리 영역이나 레지스터의 값을 1감소시키기 위해서 사용한다. 인자를 1 감소한 다음에는 이값을 스택에 밀어 넣고 다시 factorial함수를 호출한다.
pushl   %eax
call    factorial
			
이제 factorial함수를 재귀호출 했다. 일단 함수를 호출하고 나면 우리는 %esp와 %ebp를 제외하고는 다른 값들을 알 수 없게된다. 그래서 우리는 주어진 인자 값을 %ebx에 복사하도록 한다.
movl    8(%ebp),  %ebx
			
이제 인자와 팩토리얼 함수를 곱하면 된다. 앞에서 우리는 함수의 리턴값은 %eax 를 통해서 가져올 수 있다는 것을 배웠었다. 그러므로 %ebx와 %eax를 서로 곱하면 된다.
imul    %ebx, %eax
			
곱한 값은 %eax에 저장된다. 이값은 함수가 종료할 때 그대로 리턴된다. 앞서 우리가 함수를 시작할 때 %ebp를 밀어 넣고, %esp를 %ebp로 복사해서 지금의 스택포인터를 만들었다. 그렇다면 함수가 종료할때 이것을 원래 상태로 만드는 작업이 필요할 것이다. 우리는 아래와 같은 코드를 이용해서 현재 스택프레임을 되돌릴 수 있다.
end_factorial:
movl   %ebp, %esp
popl   %ebp
			
이제 ret명령을 이용해서 리턴하면 된다.
ret