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

첫번째 프로그램 만들기

3. 첫번째 프로그램 만들기

이번 장에서는 리눅스 어셈블리 프로그램을 만드는 방법에 대해서 알아볼 것이다. 여기에 더불어 어셈블리 프로그램의 구조와 어셈블리 프로그래밍을 작성하는데 필요한 어셈블리 명령어들에 대해서도 알아볼 것이다.

여러분이 주로 중/고급 언어만을 다루어 왔다면, 아마도 이들과는 전혀다른 저수준의 어셈블리어에 당황할 것이다. 어셈블리어에 익숙해지는 방법은 다른 언어에 익숙해지기 위해서 사용했던 방법들과 동일하다. 즉 꾸준히 코드를 접하고 많은 시간에 걸쳐서 연습하고 생각하고 인터넷혹은 서적을 통해서 관련 정보를 수집하고 자신의 것으로 만드는 것이다. 많은 시간이 필요하며 시행착오 역시 겪게 되겠지만 이러한 모든 것들이 어셈블리어를 여러분의 것으로 만드는데 반드시 필요한 도움이 되어줄 것이다.

3.1. Entering in the Program

간단한 프로그램을 만드는 것부터 시작해 보도록 하자. 처음으로 만들 프로그램은 아무런 하는일이 없는 -실행 후 바로 종료해 버리는- 어이 없는 프로그램이지만 어셈블리어와 리눅스 프로그래밍에 대한 기본적인 방법을 보여준다. 여러분이 좋아하는 에디터로 아래의 예제를 편집한 다음 exit.s 이름을 가지는 파일로 저장하도록 한다. 지금 당장 아래의 예제 코드가 이해되지 않는다고 해서 걱정할 필요는 없다. 우선은 아래의 코드를 만들고 컴파일 해서 실행 시키는데에만 신경쓰도록 한다. 실제 코드에 대한 상세한 설명은 4절에서 다룰 것이다.

# 시작 후 바로 종료하는 간단한 프로그램으로 종료 할때
# 프로그램의 리턴값을 리눅스 커널에 전달한다.
#

# 입력 : 없음
#

# 출력 : 코드의 종료값을 리턴한다.
# 프로그램의 종료값은 쉘에서 다음과 같이 확인 가능하다.
# echo $?
#

# 변수 :
# %eax는 시스템 호출(system call)번호를 저장한다.
# %ebx는 종료값을 저장한다.

.section .data
.section .text
.global _start

_start:
movl $1, %eax    # 리눅스 커널에 exit(2)시스템 콜을 요청한다.
                 # exit(2)의 시스템 콜 번호는 1번이다.

movl $0, %ebx    # 프로그램이 종료하면서 운영체제에 넘겨줄
                 # 종료값이다.
                 # 프로그램 종료후 쉘상에서
                 # echo $? 로 확인 할 수 있다.
                 # 값을 바꾸어 가면서 테스트 해보도록 하자.

int $0x80        # exit 시스템콜을 실행한다.
			
위와 같은 문장들을 우리는 소스코드(source code)라고 부르며, 소스코드가 저장된 파일을 소스파일 이라고 부른다. 소스코드는 인간이 쉽게 읽을수 있도록 만들어진 프로그램의 양식이다. 그러나 소스코드는 프로그램의 양식일 뿐 그 자체로 실행가능한 프로그램은 아니다. 인간이 이해하기는 쉽지만 컴퓨터는 인간의 문장을 이해할 수 없기 때문이다. 그래서 소스코드를 컴퓨터가 이해해서 실행 가능하도록 기계의 언어로 번역(transform)하는 작업이 필요하게 된다. 이러한 작업을 위해서 assemblelink가 사용된다.

먼저 assemble를 이용해서 소스코드를 기계를 위한 명령으로 변경해야 한다. 이러한 변경(혹은 번역) 과정을 Assembling라고 한다. 이러한 어셈블링을 위해서 as가 제공된다.

# as exit.s -o exit.o
			
as는 소스파일인 exit.s를 번역해서 그 결과를 exit.o라는파일에 쓴다. exit.o와 같은 번역된 결과를 담고 있는 파일을 object file라고 부른다. 오브젝트 코드는 기계어로 이루어져 있다. 오브젝트 코드가 기계어로 이루어져 있으니 이 자체로 완전한 프로그램이라고 생각할 수 있겠지만 그렇지는 않다. 대부분의 규모가 있는 프로그램들은 여러개의 소스파일로 이루어 지고 이들 소스파일들은 각각 별개의 오브젝트 파일들을 만들어 낸다. 그러므로 이러한 여러개의 오브젝트 파일들을 합쳐서 하나의 실행파일로 만들어야 하는 작업이 필요하게 된다. linker라는 프로그램을 이용하면 여러개의 오브젝트 파일을 하나로 합쳐서 커널에서 실행가능한 프로그램을 만들어 낼 수 있다. 리눅스는 ld라는 linker프로그램을 제공한다. 다음과 같은 방법으로 오브젝트 파일을 링크 시켜서 완전한 프로그램을 만들어 낼 수 있다.
# ld exit.o -o exit
			
위의 명령은 오브젝트 파일은 exit.o를 링크시켜서 실행파일인 exit를 만들어 낸다. 만약 위의 링크 과정중 에러가 발생했다면 프로그램의 소스코드를 잘못 작성했을 경우가 대부분이다. 이런 경우 여러분은 소스코드를 주의 깊게 살펴서 문제가 된 부분을 수정해야 한다. 소스코드에 대한 수정이 이루어 졌다면 다시 assemble 과 link과정을 거쳐야 한다. 아무런 문제가 없이 링크까지 성공했다면 다음과 같이 exit [1] 프로그램을 실행 시킬 수 있다.
# ./exit
			
./는 컴퓨터에게 실행시켜야 하는 프로그램이 일반적인 프로그램 실행 경로(PATH)가 아닌 현재 디렉토리에 있음을 알려주기 위해서 사용한다. 위의 프로그램을 실행 시켜보면 아무런 변화도 없이 다음 프롬프트가 떨어지는 걸 확인 할 수 있을 것이다. 프로그램은 하는일이 아무 것도 없는 것 같지만 내부적으로 종료하면서 종료값을 커널에 되돌려 주는데, 다음과 같은 방식으로 exit 프로그램의 종료값을 확인할 수 있다.
# echo $?
			
아마도 0이 출력될 것이다. 이것은 우리가 만든 exit프로그램 뿐만 아니라 다른 모든 프로그램에 공통적으로 적용된다. 모든 프로그램은 정상적으로 주어진 일을 해결하고 종료 했을 때는 0을 리턴하고 실패했을 경우 0이외의 다른 숫자를 리턴한다. 이 프로그램 종료값을 이용해서 프로그래머는 실행시켰던 프로그램이 일을 제대로 수행했는지 아니면 어떤 오류가 발생했는 지를 확인할 수 있게된다. 0은 정상종료, 1은 파일열기 실패, 2는 잘못된 계산 등으로 정의해서 각 상황에 맞게 종료값을 넘기고 종료하면 된다.

3.2. 어셈블리 프로그램의 개요

exit.s를 보면 많은 줄이 #처리 되어 있는걸 볼 수 있다. 이것은 주석이라고 불리운다. 주석은 어셈블러에 의해서 해석 되지 않는 부분으로 주로 프로그래머에게 프로그램 코드에 대한 설명, 힌트등을 명시해서 프로그래머가 좀더 쉽게 코드를 파악할 수 있도록 하기 위해서 사용된다. 잘 작성된 주석은 프로그램을 만들어낸 당사자 뿐만 아니라 프로그램을 읽어야 하는 다른 (팀동료등과 같은)프로그래머에게 많은 도움을 준다. 잘 작성된 주석은 다음과 같은 요소들을 가진다.

  • 코드가 하는일

  • 코드의 대략적인 흐름

  • 주의를 기울여야 할만한 여러가지 특이사항들 [2]

몇 개의 주석후에 다음과 같은 줄이 등장한다.

.section .data
			

.section .text
			
텍스트 섹션이 시작함을 알린다. 텍스트 섹션에는 프로그램 명령 (어셈블리 코드가 위치한다)

.globl _start
			

_start:

이제 실제적인 커퓨터 명령이 등장한다. 처음 등장한 명령은 다음과 같다.

movl $1, %eax
			
프로그램이 실행되었을 때 이명령은 숫자1을 %eax 레지스터에 넣어라고 해석한다. 어셈블리어에 사용되는 많은 명령은 오퍼랜드(operands)를 가진다. 위의 movl명령은 sourcedestination 두개의 오퍼랜드를 가진다. 이 경우 source는 숫자 1이 되고 destination은 %eax레지스터가 된다. 오퍼랜드는 숫자, 참조 메모리 위치 혹은 레지스터가 올 수 있다. 명령은 그 종류에 따라서 각각 다른 형식의 오퍼랜드를 가지게 된다. 각 명령이 가질 수 있는 오퍼랜드의 정보는 Appendix B를 참고하기 바란다.

명령에 따라서 오퍼랜드의 형식이 달라지긴 하지만, 대부분의 명령은 2개의 오퍼랜드를 가진다. 첫번째로 등장하는 오퍼랜드를 source 오퍼랜드, 두번째로 등장하는 오퍼랜드를 destination 오퍼랜드 라고 한다. 이런류의 명령으로는 addl, subl, imul등이 있다. 이들 명령은 각각 "더하기","빼기","곱하기" 연산을 하며 source 오퍼랜드로 부터 destination 오퍼랜드로 연산을 하고 결과는 destination 오퍼랜드에 저장된다.

x86프로세스는 movl명령에 이용할수 있는 여러개의 general-purpose 레지시트럴 가지있다.

  • %eax

  • %ebx

  • %ecx

  • %edi

  • %esi

이들 gerneal-purpose 레지스터와 함께 몇개의 special-purpose 레지스터도 가지고 있다.

  • %ebp

  • %esp

  • %eip

이들 레지스터중에는 %eip와 %eflags와 같은 특별한 명령에만 접근가능한 레지스터들이 있다. 우선은 이정도만 설명하도록 하겠다. 더 자세한 내용은 나중에 설명할 기회가 있을 거이다.

이제 movl명령은 숫자 1을 %eax레지스터로 옮기는 작업을 수행함을 이해했을 것이다. 1앞에 보면 달러표시가 있는데, 이는 immediate mode addressing을 이용할 것을 명시하기 위해서 사용한다. 달러표시가 없다면 direct addressing을 이용하게 되고 주소 1에 있는 어떤 값을 읽어 들이게 된다. 우리는 실제 숫자 1을 읽어들이기를 원하므로 immeiate mode를 사용했다.

우리가 숫자 1을 %eax에 옮긴 이유는 특정작업을 위해서 Linux 커널을 준비시키기 위함이다. 숫자 1은 시스템 콜(system call) 번호 1번인 exit를 가리킨다. 시스템콜은 운영체제에 어떤 도움을 요청하기 위해서 사용되는 것이다. 시스템콜에 대해서는 조만간 자세히 다루게 될 것이다. 파일을 열거나 메모리 할당을 요청하거나 하기 위해서는 해당 시스템 콜에 매핑되는 번호를 %eax에 써주면 된다. 이들 시스템 콜 번호는 운영체제마다 약간씩 다르다.

운영체제에 어떤 일을 요청하기 위해서 시스템 콜을 사용한다고 했는데, 시스템콜 번호 만으로 할 수 있는 일은 존재하지 않는다. 어떤 요청을 수행하도록 하기 위해서는 커널에 좀더 많은 정보를 알려줘야 한다. 파일을 여는 것을 예로 들어보면 "파일을 열어라"라는 요청 외에도 파일이름이 무언지, 어떤 상태로 열건지 등을 커널에게 알려주어야만 한다. 이러한 부가적인 정보를 parameters(인자)라고 부르며 이들 값 역시 레지스터를 통해서 커널에 전달된다. exit 시스템콜의 경우 컨널은 인자로 종료값을 요구한다. 이 값은 %ebx에서 읽혀 진다. 이 종료 값은 시스템에 리턴되어질 것이고 여러분은 echo $?를 통해서 리턴값을 읽을 수 있게 된다. %ebx에 0을 올리기 위해서는 다음과 같은 코드가 필요하다.

movl %0, %ebx
			
리눅스는 시스템콜을 만들기 전에 인자의 값을 읽기 위해서 레지스터의 값을 요구한다. %eax는 언제나 시스템콜 번호를 올리기 위해서 사용되며 인자를 올리기 위해서 다른 레지스터들을 사용하게 된다. exit 시스템콜에서 보면 종료 상태를 저장하기 위해서 %ebx를 필요로 함을 알 수 있다. 각각의 시스템콜이 필요로 하는 인자의 갯수가 서로 다르다. 이들에 대한 정보는 Appendix C를 참고하기 바란다.

다음으로 아래와 같은 상당히 수상한 명령이 내려진다.

int $0x80
			
int 는 C에서 정수형을 나타내는 int가 아니다. interrupt의 줄임말이니 혼동하지 않도록 하자. 0x80은 사용할 인터럽트의 번호다. 프로그램의 수행중 interrupt가 걸리게 되면 프로그램의 제어가 커널로 넘어가게 되고 커널은 프로그램이 요청한 시스템콜을 수행하게 된다. 이것은 배트맨에게 구조요청을 하기 위해서 신호를 보내는 과정과 같다. 여러분은 필요에 의해서 배트맨에게 신호를 보내고 배트맨은 여러분을 구조하러 온다. 구조가 된후에는 ? 물론 일상생활로 되돌아 가게 될것이다. 마찬가지로 커널이 필요한 일을 마치게 되면 제어권은 프로그램으로 다시 넘어 가게 된다. 만약 interrupt 신호를 사용하지 않는다면 어떠한 시스템 콜도 수행되지 않을 것이다.

이제 남은 일은 코드를 assemble시키고, 링크 시키고 실행하는 일이다. 테스트를 위해서 어셈블리 코드의 %ebx를 다른 값으로 바꾼후 echo $?의 출력값이 변하는걸 확인해 보기 바란다. 당연하지만 %ebx값을 변경한다음에는 assemble->링크->실행 과정을 거쳐야 변경된 내용이 적용된다.

3.3. Planning the Program

이번 프로그램은 좀더 복잡한 일을 하게된다. 여러개의 숫자가 주어지고 이중 가장 큰 숫자를 찾는 일을 하는 프로그램이다. 컴퓨터는 프로그래머가 필요한 모든것을 세밀하게 지정해줘야 하는 기계다. 그러므로 원하는데로 작동하는 프로그램을 작성하기 원하다면 작성전에 프로그램에 대한 명세서를 만들어 주어야 한다. 우리가 만들고자 하는 프로그램은 다음과 같은 명세에 대한 정의가 있어야 할 것이다.

  1. 숫자의 목록을 어디에 저장할 것인가.

  2. 가장 큰 숫자를 찾기 위해서 어떠한 프로시져를 필요로 하는가.

  3. 프로시져를 수행하기 위해서 어느정도의 공간을 필요로 하는가

  4. 공간은 레지스터에서 확보할 것인가 아니면 메모리를 이용할 것인가.

여러분은 몇개의 숫자 중에서 가장 큰 숫자를 찾는 일을 하기 위해서 어떤 계획을 세운다거나 하지는 않을 것이다. 이런 일은 그냥 닥치면 한번에 쓰윽 보고 가장 큰 숫자를 골라 낼 것이다. 물론 숫자의 목록이 많다면 약간의 계획을 세워야 하겠지만 큰 문제가 되지는 않을 것이다 - 시간이 좀 걸리긴 하겠지만 -. 우리의 두뇌가 경험과 학습에 의해서 거의 반사적으로 그러한 일을 처리하기 때문이다. 여러개의 숫자 목록중에 가장 큰 숫자를 찾아야 한다면, 분명 여러분은 앞에서 숫자를 읽어 가면서 앞의 숫자 보다 더 크다면 그것을 머리에 새겨 두고, 머리에 새겨둔 숫자 보다 더 큰 숫자가 나온다면 숫자를 바꿔치기 할 것이다. 결국 마지막 숫자까지 모두 확인한다면 머리에 남아 있는 수가 가장 큰 수가 된다. 이러한 과정은 거의 자동으로 이루어진다.

컴퓨터의 경우 이러한 과정을 단계별로 알려주어야 한다. 이러한 단계를 명확히 정의 하기 위해서 약간의 계획이 필요하게 된다. 우선 가장 큰 숫자를 고르기 위해서 사용될 숫자의 목록이 있을 것이며 이들 숫자의 목록은 메모리 공간에 저장되어야 할 것이다. 이들은 data_items가 가리키는 메모리에 저장하도록 계획하겠다. 우리는 또한 숫자목록에서 현재 위치를 읽어 올 수 있어야 한다. 그래야 검토해야할 숫자를 읽어 올 수 있기 때문이다. 그리고 가장큰 숫자를 저장하기 위한 공간도 마련되어야 한다. 이들 숫자 정보를 저장하기 위해서 다음과 같은 레지스터를 사용하도록 계획했다.

  • %edi 는 목록에서 현재의 위치를 저장한다.

  • %ebx 는 현재 목록에서 가장 큰 숫자를 저장한다.

  • %eax 는 검토할 숫자를 저장하기 위해서 사용한다.

한 가지 예외적으로 처리해야할 숫자가 있는데, 바로 목록의 가장 처음 가져온 숫자이다. 이 숫자는 다른 어떤 숫자와도 비교할 수 없으므로 자동적으로 가장 큰 숫자가 되어야 할것이다. 이러한 손쉬운 수행을 위해서 현재 위치가 가리키는 숫자를 0으로 하면 된다. 그러면 목록의 처음 숫자는 0과 비교되고 현재 시점에서 가장 큰 숫자로 %ebx에 저장될 것이다. 그 다음에는 목록의 다음 숫자와 배교하면 된다. 이러한 과정을 순서대로 기술해 보도록 하자.

  1. 목록의 처음 숫자가 0인지 확인한다.

  2. 만약 0이라면 종료(exit)한다.

  3. 현재 위치를 1증가 시킨다(%edi)

  4. 다음 값을 읽어 와서 %eax 레지스터에 값을 저장한다.

  5. 현재 값 %eax와 가장 큰 값 %ebx를 비교한다.

  6. 만약 현재 값이 최근의 가장 큰 값 보다 크다면 현재 값을 %ebx에 저장한다.

  7. 반복한다.

이것을 프로시져(procedure, 의사진행)이라고 한다. 프로시져를 작성하다 보면 매우 자주 만약(if)라는 단어가 등장함을 볼 수 있다. 이것은 진행(흐름)을 분기 시키기 위해서 사용한다. if 다음에 주어진 조건을 만족하느냐 그렇지 않느냐에 따라서 수행하는 명령이 달라지게 된다. 2 번을 보면 %eax의 값이 0인지 아닌지에 따라 흐름을 분기 시키고 있음을 알 수 있다. %eax가 0이라는 것은 우리가 처음 설정했던 데로 목록을 끝을 나타내는 것이므로 더이상 흐름을 진행 시킬 필요가 없기 때문이다. 0이 아니라면 목록의 다음 값을 가리키도록 하고 4, 5, 6을 진행하면서 이전의 가장 큰값과 비교해서 더 큰 값을 %ebx에 저장한다. 그리고 프로그램은 아직 목록의 마지막까지 숫자들을 조사하지 않았 으므로 다시 2 번으로 가서 지금까지의 비교작업을 반복한다.

if 의 흐름을 분기한다는 특성 때문에 흐름제어(flow control)명령이라고 부른다. 이전에 다루었던 첫번째 어셈블리 프로그램은 어떠한 흐름제어 명령도 포함하고 있지 않았다. 단지 하나의 흐름만이 존재했었기 때문이다. 그러나 이 프로그램은 좀더 다양한 방법으로 데이터를 다루어야 하기 때문에 흐름제어 명령이 필요해 지게 된다.

흐름제어 명령 외에도 이프로그램이 완성되기 위해 2개의 다른 새로운 명령들이 사용될 것이다. conditional jump와 unconditional jump가 그것인데, conditional jump는 조건 점프로 조건을 만족하는지를 판단해서 특정 루틴으로 이동하기 위해서 사용한다. uncoditional jump는 무조건 점프로 조건의 만족과 관계없이 지정한 루틴으로 이동시킨다. 이들 jump에 대한 자세한 내용은 다음 장에서 다루게 될 것이다.

흐름제어를 위해 사용되는 또다른 장치는 루프(loop)이다. 루프는 반복적으로 실행되는 코드의 조각을 말한다. 예를 들어 우리가 작성할 프로그램은 처음에 0과 목록의 첫번째 값을 비교해서 가장 큰값으로 목록의 첫번째 값을 최고값으로 등록시키고 그 다음에는 목록의 두번째 값과 현재 최고값을 비교하는 식으로 동일한 일을 계속(목록의 마지막 숫자인 0에 도달할 때까지) 반복할 것이다. 이러한 반복작업의 처리를 위해서 루프를 사용하게 된다. 루프의 반복을 위해서 여기에서는 unconditional jump를 사용하고 있다. 루프 코드 영역의 마지막에 도달하면 무조건 루프 코드의 처음으로 돌아간다. 루프코드의 처음에서는 목록에서 숫자를 가져오고 0인지 아닌지를 판단한다음 0이라면 루프를 빠져나가기 위해서 jump를 시도할 것이다. 이때의 jump는 조건의 판단후에 이루어 지므로 conditional jump가 필요하게 된다.

다음장에서는 지금까지의 계획을 실제 프로그램의 작성에 적용할 것이다. 이 프로그램은 간단하지만 프로그램으로써 가져야할 대부분의 기본요건을 가지고 있으므로 제대로 이해한다면 앞으로 이 문서를 읽는데 큰 도움이 될것이다.

3.4. 최대 숫자 찾기

코드의 이름은 maximum.s로 한다.

# data_items에 있는 숫자의 목록중 가장 큰
# 데이터를 얻어와서 리턴한다.
#

# 변수들 : 프로그램의 작동을 위해서 사용되는 레지스터들
# %edi - 데이타 목록에서 조사할 숫자의 인덱스 저장용
# %ebx - 가장 큰 숫자 저장용
# %eax - 현재 비교할 숫자 저장용
#

# The following memory locations are used:
#
# data_items - 비교할 숫자데이터들 0은 마지막을 나타낸다.
#

.section .data

data_items:                         # 숫자 데이터들
    .long 3,67,34,222,45,75,54,34,44,33,22,11,66,0 
    .section .text
    .globl _start

_start:
    movl $0, %edi                   # move 0 into the index register
    movl data_items(,%edi,4), %eax  # load the first byte of data
    movl %eax, %ebx                 # since this is the first item, %eax is
                                    # the biggest

start_loop:                         # start loop
    cmpl $0, %eax                   # check to see if we ve hit the end
    je loop_exit
    incl %edi                       # load next value
    movl data_items(,%edi,4), %eax
    cmpl %ebx, %eax                 # compare values
    jle start_loop                  # jump to loop beginning if the new
                                    # one isn t bigger
    movl %eax, %ebx                 # move the value as the largest
    jmp start_loop                  # jump to loop beginning
loop_exit:                          # %ebx is the return value,
                                    # and it already has the number

    movl $1, %eax                   #1 is the exit() syscall
    int $0x80
			

이제 어셈블과 링크를 이용해서 실행파일을 생성하자.

# as maximum.s -o maximum.o
# ld maximum.o -o maximum
			
실행 후 결과를 확인해보도록 하자.
# ./maximum
# echo $?
			
222가 출력된것으로 위의 프로그램이 최대 숫자를 제대로 골라낸 걸 확인할 수 있을 것이다. 그럼 위의 코드를 한줄씩 분석해 보도록 하자. 데이터 섹션에서 가장 먼저 만나는 코드는 아래와 같다.
data_items:                         # 숫자 데이터들
    .long 3,67,34,222,45,75,54,34,44,33,22,11,66,0 
			
data_items는 숫자 데이터들이 저장된 위치에 대한 참조라벨이다. 실제 데이터가 정의된 코드에서는 .long 를 이용해서 저장될 데이터의 형(type)를 명시하고 있다. 그래야 제대로된 메모리 할당이 이루어질 수 있기 때문이다. 이제부터 data_items는 데이터의 첫번째 영역을 가리킨다. data_items가 데이터의 첫번째 영역을 가리키고 있기 때문에 우리는 data_items에 대한 간단한 연산으로 숫자 데이터들을 이용할 수 있다. 예를 들어서 movl data_items, %eax 명령을 내리면 3이 %eax 레지스터에 저장된다. 어셈블리어는 .long 외에도 다음과 같은 여러가지 데이터형을 제공한다.

.byte

1byte의 데이터값을 명시한다. 0-255범위의 값을 사용할 수 있다.

.int

2byte의 데이터값을 명시한다. 0-65535범위의 값을 사용할 수 있다.

.long

4byte의 데이터값을 명시한다. 0-4294967295범위의 값을 사용할 수 있다.

.ascii

메모리에 문자열을 직접 저장하기 위해서 사용한다. 각각의 문자는 1byte의 공간을 차지한다. 만약 .ascii "Hello there\0" 로 정의 했다면 어셈블러는 12byte의 공간을 확보하고 확보된 공간에 위의 문자열을 저장하게 된다. 첫번째 위치에는 H가 두번째 위치에는 e가 저장된다. 제일 마지막에는 '\0'이 저장되는데, 이 값은 화면에 출력되지 않고 단지 문자열의 마지막 이라는 것을 알려주기 위해서 사용한다. 이외에도 탭과 개행문자를 표시하기 위해서 '\t'와 '\n'같은 문자들도 사용된다.

위의 예제의 경우 어셈블러는 숫자의 목록을 저장하기 위해서 4byte * 14 만큼의 메모리 공간을 확보게 된다.

숫자의 목록을 저장하는데 있어서 마지막에 0을 사용했다는 것을 주목하기 바란다. 이 프로그램에서는 숫자 목록의 마지막이라는 것을 알려주기 위해서 0을 사용하고 있다. 숫자 목록의 갯수를 명확히 하기 위해서 이밖에도 숫자 목록의 처음에 전체 숫자의 갯수를 적어준다거나 프로그램내에 전체 숫자의 갯수를 직접 넣는 방법이 있을 수 있다. 이 외에도 여러가지 방법을 통해서 숫자의 갯수를 명시할 수 있을 것이다. 이 처럼 목록의 끝을 나타내기 위해서 여러가지 방법들이 동원되는 이유는 컴퓨터의 경우 우리가 명확하게 끝을 지정해 주기 전에는 목록의 끝이 어딘지 측정 할 수 없기 때문이다.

이제.globl에 대해 알아보도록 하자. 이것은 어떠한 데이터도 가지지 않으며 단지 프로그램이 시작되는 실행 위치를 알려주기 위한 목적으로 사용된다. Linux는 프로그램을 시작하기 위해서 어디부터가 실행영역인지를 알고 있어야 하며 _start 부터 실행한다. .globl은 _start를 참조시킨다. .globl을 생략할 경우 링크시 다음과 같은 에러가 발생할 것이다.

# ld maximum.o -o maximum
ld: warning: cannot find entry symbol _start; defaulting to 08048074
			

이 걸로 해서 우리가 사용할 모든 데이터의 준비를 마쳤으니, 이제 본격적으로 데이터를 이용해서 필요한 작업(가장 큰 숫자를 가려내는)을 해야 한다. 위 코드를 보면 # 변수들라고 되어 있는 주석을 볼 수 있을 것이다. 이들 변수는 작업을 위해서 사용되는 데이터들을 저장하기 위한 용기로 사용한다. 이 프로그램은 다음과 같은 변수들이 사용된다.

  1. 최근의 최대값을 저장하기 위한 변수

  2. 숫자 목록에서 가져와야할 숫자의 위치를 저장하기 위한 변수

  3. 숫자 목록에서 가져온 숫자를 저장하기 위한 변수

우리가 만든 프로그램은 매우 간단하므로 단지 몇개의 변수만 하용하면 되며, 레지스터리만으로 이들 변수를 담아낼 수 있다. 그러나 프로그램이 커지고 거기에 따라서 다루어야하는 데이터양이 커질 경우 레지스터만으로 모든 작업을 할 수 없게 된다. 여기에 대해서는 나중에 다루게 될것이다.

이 프로그램에서 가장 큰 숫자를 저장하기 위해서 %ebx를 사용했다. %edi는 현재 가져와야할 숫자의 위치(index)를 저장하기 위해서 사용한다. %edi를 이용해서 우리는 data_items로 부터 몇 번째 숫자를 읽어와야 할지를 결정할 수 있게 된다. 프로그램이 막 시작했을 때는 data_items의 첫번째 숫자를 가져와야 되므로 %edi는 0이 입력된다. 첫번째 숫자를 가져왔다면 %edi에는 1이 들어가고, 다음 숫자를 가져올 때는 data_items의 두번째 숫자를 가져오게 된다.

movl  $0, $edi
			
%edi는 데이터 목록의 몇번째 데이터를 가져와야 하는지만을 알려주는 인덱스 이다. 우리는 이 인덱스 값을 이용해서 data_items로 부터 값을 가져와야 하는데 다음과 같은 같단한 연산이 사용된다.
movl data_items(,%edi,4), %eax
			
다음은 목록으로 부터 숫자를 가져오고 최고값을 저장하는 루틴이다.

  1. data_items는 우리가 계산에 사용할 숫자 목록의 첫번째 숫자를 가르킨다.

  2. 숫자목록의 숫자들은 .long 형이므로 4byte의 크기를 차지한다.

  3. 각 숫자가 차지하는 공간이 4byte인 것을 이용해서 다음 숫자를 가져올 수 있다.

  4. 우리는 지금 숫자와 다음 숫자를 비교해서 최고 큰 숫자를 알아낼 수 있다.

  5. 가장 큰 숫자는 우리가 준비한 레지스터에 복사한다.

  6. 다시 처음으로 되돌아 간다.

위의 루틴을 보면 처음으로 되돌아 간다고 되어 있다. 처음이라 함은 루프의 처음인 start_loop이 된다. 루프의 처음으로 되돌아 간다. 루프의 처음에는 다음과 같은 명령이 들어 간다.
cmpl  $0, %eax 
je    end_loop
			
cmpl은 2개의 값을 비교하기 위한 명령이다. 여기에서 우리는 0과 %eax에 저장된 값을 비교하고 있다. 비교를 했다면 비교에 대한 결과가 있을 것이다. 이 결과는 %eax레지스터가 아닌 %eflags레지스터에 들어간다. 이 레지스터는 상태(status) 레지스터라고도 불리우며 종종 사용된다. 자세한 내용은 나중에 다루도록 하겠다. 어쨋든 비교된 결과는 %eflags에 저장되고 저장된 값은 다음 코드에서 점프를 할지 안할지를 판단하기 위해서 사용된다. 만약 비교결과가 참이라면(같다면) end_loop로 점프를 하게 되고, 그렇지 않다면 그냥 다음 라인으로 넘어가게 된다. 우리는 점프를 위해서 je를 사용하고 있는데, 이외에도 다양한 점프 명령들이 존재한다.

je

두개의 값이 같으면 점프하라.

jg

두번째 값이 첫번째 값보다 크면 점프하라.

jge

두번째 값이 첫번째 값보다 크거나 같으면 점프하라.

jl

두번째 값이 첫번째 값보다 작으면 점프하라.

jle

두번째 값이 첫번째 값보다 작거다 같다면 점프하라.

jmp

무조건 점프한다.

이들 점프와 관련된 명령들의 목록은 Appendix B를 참고하기 바란다. 우리가 만든 프로그램에서는 0과 %eax의 값이 같을 경우 점프하도록 코딩되어 있다. %eax가 0과 같다면 즉 목록의 마지막 이라면 loop_exit로 점프한다.

만약 숫자목록에서 가져온 숫자가 0이 아니라면 다음 명령이 실행된다.

incl  %edi 
movl  data_item(,%edi,4),   %eax
			
이전의 내용들을 주의 깊게 읽어왔다면 %edi가 data_items의 목록의 인덱스를 저장하고 있다는 것을 기억할 것이다. 이제 다음 데이터 값을 인덱스 해야 하므로 %edi를 1만큼 증가 시켰다. 이쯤 되었다면 movl로 하는 일을 짐작했을 것이다. data_item에서 %edi 위치만큼 이동해서 데이터를 가져오고 이것을 %eax에 복사하는 일을 한다. data_item에 있는 각 데이터의 크기는 4바이트 이므로 4바이트만큼을 %eax에 복사하고 있다.

cmpl %ebx, %eax
jle  start_loop
			
이제 %ebx와 %eax를 비교한다. %ebx는 현재까지 찾아낸 가장큰 숫자가 들어 있다. 만약 현재 값이 이전 최고값보다 작거나 같다면 start_loop로 점프한다.
movl %eax, %ebx
jmp  start_loop
			
만약 현재 값이 이전 최고값보다 크다면 %ebx에 복사하고 start_loop로 점프한다. 이제 0을 만날때까지 위의 루틴을 반복한다. 만약 0을 만나면 loop_exit로 점프한다. 마지막 부분에서 이 프로그램은 리눅스 커널의 exit를 호출하게 된다. 시스템콜의 호출은 %eax에 저장되어 있는 번호를 통해서 호출된다. exit시스템콜의 번호는 1번이므로 다음과 같은 코드가 필요하다.
movl $1, %eax
int  0x80 
			

이것으로 우리가 작성한 프로그램에 대한 모든 설명을 마쳤다. 매우 간단한 프로그램이였지만 상당히 많은 것을 배울수 있었다. 간단한 프로그램 이므로 이해에는 큰 어려움이 없었을 것이다. 이해가 되지 않는 부분이 있다면 주석과 함께 프로그램을 주의 깊게 읽어 보기 바란다. 좀더 완벽하게 이해하고 싶다면 프로그램의 각 스텝별로 레지스터의 값의 변화를 기록해가면서 코드를 확인해 보기 바란다.

3.5. Addressing mode

우리는 2장에서 어셈블리어가 2가지 종류의 데이터 접근 방법 (Data Accessing Methods)를 가진다는 것을 배웠다. 이번 장에서는 이러한 어셈블리어에서 제공하는 어드레스 기반의 데이터 접근 방법에 대해서 알아보도록 하겠다.

메모리 주소를 참조하는 일반적인 형식은 다음과 같다.

ADDRESS_OR_OFFSET(%BASE_OR_OFFSET, %INDEX, MULTIPLIER)
			
위의 모든 필드들은 필수사항이 아닌 옵션사항들이다. 다음은 위의 필드들의 값을 이용해서 주소를 계산해내는 방법이다.
FINAL ADDRESS = ADDRESS_OR_OFFSET + %BASE_OR_OFFSET + MULTIPLIER * %INDEX
			
ADDRESS_OR_OFFSETMULTIPLIER 모두 상수로 레지스터다. 만약 어떤 값도 없다면, 0이 사용된다.

Addressing mode는 2장의 Data Accessing Method에서 이미 언급한적이 있지만 복습차원에서 간단히 설명하도록 하겠다.

direct addressing mode

이것은 단지 ADDRESS_OR_OFFSET 만 사용한다. 다음은 사용예이다.

movl ADDRESS, %eax
					
%eax에는 메모리 주소 ADDRESS의 값이 직접 복사된다.

index addressing mode

ADDRESS_OR_OFFSET과 %INDEX가 모두 사용된다. 인덱스 레지스터에는 어떤 종류의 일반목적(general-purpose) 레지스터라도 사용할 수 있다. 인덱스 레지스터에는 1, 2, 4의 배수가 올 수 있는데, 이렇게 되므로써 byte, double-byte, words의 어떠한 인덱스라도 쉽게 계산할 수 있다. 예를 들어서 문자열이 들어 있는 string_start 변수가 있는데 여기에서 3번째 문자를 가져오고 싶다면 다음과 같이 코드를 만들면 된다.

movl string_start(,%ecx,1), %eax
					
물론 이경우 %ecx에는 인덱스값인 3이 들어있어야 할것이다. ADDRESS_OR_OFFSET 필드가 비어 있으므로 여기에는 0이 들어간다. 즉 string_start의 처음주소를 가리킨다. 인덱스인 %ecx가 3이므로 string_start의 3번째 주소를 가리키고, MULTIPLIER가 1이므로 1byte만큼 %eax로 복사된다.

(간접)indirect addressing mode

레지스터의 주소로 부터 값을 직접 읽어들인다. 예를들어 %eax의 값을 %ebx로 복사하고 싶다면 다음과 같이 하면 된다.

movl (%eax), %ebx
						

base-pointer addressing mode

레지스터주소 값에 상수값을 더한다는 것을 제외하고는 간접주소 지정방식과 매우 비슷한 방식이다. 예를 들어 당신이 어떤 레코드로 부터 4바이트의

immediate mode

Immediate mode는 매우 간다하다. 원하는 값을 레지스터나 메모리 영역으로 직접 저장하는 방식이다. 예를 들어 %eax에 12라는 값을 저장하기 원한다면 다음과 같이 Immediate mode를 이용해서 간단하게 처리할 수 있다.

movl $12,  %eax
						
Immediate mode를 이용할 때 숫자 앞에 달러($)표시가 사용되고 있음을 주목하도록 하자. 만약 달러표시를 생략하게 될경우 direct addressing mode로 연산을 시도하게 된다. 이렇게 되면 12가 %eax에 저장되는게 아니고 12의 메모리 위치에 있는 값이 저장되어서 전혀 엉뚱한 결과를 가져오게 된다.

register addressing mode

간단하게 하나의 레지스터를 다른 레지스터로 복사하기 위해서 사용한다.

메모리의 데이터를 제어하는데 있어서 위의 모드들 중 하나가 사용되므로 어드레싱 모드에 대해서 이해하는 것은 매우 중요하다. Immediate mode를 제외한 모든 모드들은 소스(source)나 목적지 오퍼랜드를 모두 이용할 수 있다. 반면 Immediate mode는 단지 소스 오퍼랜드만 이용한다.

각각의 모드들을 이용해서 메모리 복사와 같은 일을 하다 보면.. 다양한 크기의 데이터를 복사해야 하는 경우가 생길 것이다. 어셈블리는 이러한 경우를 대비하여서 자주 사용하는 데이터 크기에 대해서 사용가능한 명령들을 준비하고 잇다. 워드 단위의 데이터 복사를 원한다면 movl를 사용하고, 바이트 단위로의 복사를 원하다면 movb를 사용할 수 있다. 그러나 앞에서 다루었다 시피 데이터를 저장하기 위한 최소단위로 레지스터를 사용하는데 크기가 워드단위이다. 그러므로 바이트단위로 다루고자 할경우 레지스터의 일정영역만을 사용할 수 있게된다(어떻게 보면 나머지 공간을 낭비하는 결과를 가져온다고 볼 수 있다).

%eax를 예로 들어보도록 하자. 만약 2바이트의 크기를 복사하고자 할때 %ax를 사용한다. %ax는 least-significant halt 간단히 말해서 4바이트 크기의 워드(즉 %eax으)에서의 하위 2바이트의 영역을 가리킨다. %ax는 다시 1바이트씩 2개로 나뉠 수 있다. %al과 %ah가 그것으로 %al은 %ax의 마지막 바이트 %ah는 %ax의 처음 바이트를 가리킨다.

그림 3. %eax 레지스터의 모습

주석

[1]

유닉스와 리눅스는 윈도우와 달리 확장자(extensions)를 가질 필요가 없다. 윈도우즈라면 실행파일의 경우 .exe등의 확장자를 명시해야 하지만 유닉스에서는 어떠한 확장자도 가질 필요가 없다.

[2]

같은 일을 하는 프로그램이라도 매우 다양한 방법으로 작성될 수 있다. 일반적인 방법이 사용될 수 있지만 여러분만의 노하우를 이용해서 작성될 수 있으며, 이경우 다른 프로그래머가 코드를 분석하는데 문제가 될 수 있다. 이러한 특이 사항들을 명시해 두면 다른 프로그래머에게 많은 도움을 줄 수 있다.