[OS 개발 3] 준비 단계(2) - 레지스터와 어셈블리 명령 체계


1. 어셈블리 언어?


 

 

OS를 제작하는 과정에서 우리는 어셈블리 언어와 맞닥뜨릴 것입니다. 그런데 이 어셈블리 언어가 그리 쉽지만은 않습니다. 저도 그 동안 프로그래밍을 해오면서 거의 C++이나 자바와 같은 하이레벨(High Level) 언어를 위주로 주로 공부했을 뿐, 로우 레벨(Low Level) 언어에 대해 제대로 공부해보기는 사실상 처음인 것 같고 어려움을 느꼈습니다. 하지만 운영체제 구축이 주 목적인 만큼, 우리는 어셈블리 언어에 대해서 잘 알아야만 합니다.

 

뿐만 아니라, 어셈블리 언어에 대해 공부하면 추후에 디스어셈블링 기술을 배울때도 유용합니다. 디스어셈블링 기술은 이미 제작된 소프트웨어를 역으로 해부하여 소스코드를 분석해내는 기술인데, 이때 어셈블리 언어 해석 능력이 매우 유용하게 작용합니다.

 

그렇다면 어셈블리 언어가 정확히 무엇일까요

 

CPU가 데이터를 읽어들일 때, 기계어코드를 통해 인식하게 되죠. 하지만 기계어코드는 CPU가 읽기에 최적화된 언어이므로, 이를 인간이 해석하기에 쉽지 않습니다. 따라서 기계어 코드의 각 비트를 사람이 읽기 쉽도록 의미있는 언어 형태로 변형한 언어가 바로 어셈블리 언어입니다. 그렇기 때문에, 추후 포스팅에서 보면 아시겠지만, 기계어 코드와 어셈블리 코드는 각각 1:1매칭되는 모습을 볼 수 있을 것을 보실 수 있습니다. 사실 어떻게 보면 기계어 코드 자체가 어셈블리 코드라고 볼 수 있습니다.

 

또 어셈블리 언어는 플랫폼에 종속적입니다. 예를 들어 인텔 아키텍처와 AT&T 아키텍처에서의 언어 체계가 다르죠. 본 블로그에서는 인텔 IA-32 아키텍처를 중심으로 설명하도록 하겠습니다. 아쉽지만 AT&T 아키텍처 언어체계를 공부하고자 하는 분들은 별도로 학습하기를 바랍니다. 기회가 되면 AT&T 아키텍처 어셈블리어 문법에 대해서도 포스팅하도록 하겠습니다.

 

 

 


2. 데이터 처리


 

 

어셈블리 언어가 어떠한 체계로 이루어지는지 알아보기 전에 프로세서와 메모리간에 어떻게 데이터가 처리되는지 간략하게 알아보고자 합니다. 사실 데이터 처리 과정은 꽤 복잡한 절차를 통해 이루어지며, 이는 컴퓨터 구조 전반에 대해 제대로 공부해야 합니다. 컴퓨터 구조에 대한 전반적인 이해까지 다루게 되면 그 범위가 광범위해지므로, 본 포스팅의 주제를 벗어날 수 있습니다. 따라서 본 포스팅에서는 최대한 어셈블리 언어와 직접적인 관련이 있는 사항에 대해 중점적으로 살펴보고자 합니다.

 

인텔의 8085 CPU의 8비트 마이크로프로세서에서 데이터 기본 처리 단위는 한 바이트(1Byte)이지만, 8086은 16비트 프로세서이므로 16비트인 1워드(1 Word)가 됩니다. 이후 인텔의 80386(i386)에서는 데이터형이 byte, word, doubleword로 구분되었습니다. 인텔의 32비트 CPU의 기본 바탕이 되는 명령어 세트 및 컴퓨터 아키텍처(IA-32)는 이 80386을 기준으로 확립되었으며, 80386과 이 CPU의 호환 프로세서들을 집합적으로 x86 또는 i386 아키텍처라고 일컫게 되었습니다.

 

이러한 마이크로프로세서(IA-32)는 데이터 처리 과정에서 각종 연산작업을 수행합니다. 이 과정에서 데이터 저장을 위해 RAM을 활용하지만 프로세서 입장에서 RAM은 외부 장치이며, 연산때마다 RAM에 접근하게 되면 상대적으로 속도가 느려지게 됩니다. 따라서 프로세서 내부 메모리를 사용하여 시간을 단축시키는데, 사실 내부 메모리에는 여러 요소들이 있지만, 그 중에서도 아래 그림과 같이 레지스터를 활용한다는 점이 중요합니다. 아래 레지스터는 4개로 표현되었지만, 실제로 IA-32의 범용 레지스터는 8개 입니다.

(그림 그리느라 힘드네요) 

 

 

레지스터 CPU RAM ALU프로세서와 ALU 및 메모리

 


프로세서 내부 ALU(Arithmetic Logic Unit)는 프로세서 제어기, 메모리(RAM)와 입출력장치들에게 직접 입출력 접근이 가능합니다. CPU 연산은 하나 이상의 ALU에 의해 수행되는데, ALU에 의해 처리될 데이터는 레지스터 혹은 주기억장치로부터 ALU로 입력됩니다. 입력에는 연산 코드, 명령어 그리고 하나 이상의 피연산자가 포함됩니다. 연산 코드를 통해 ALU는 지정된 연산이 무엇인지 파악하고 이를 수행한 후 연산의 결과를 출력 레지스터 중 하나로 저장합니다. 이때 연산 명령을 위한 명령어가 필요하며, 이에 어셈블리 코드를 통해 이루어집니다.

 

 

 


3. 레지스터의 종류


 

 

레지스터는 컴퓨터의 프로세서내에서 자료를 보관하는 아주 빠른 기억 장소이며, 역할에 따라 종류도 다양합니다. 레지스터에 대한 기능에 대해 어느정도 알고 있어야 나중에 어셈블리 코드를 통해 써먹거나 해석할 때 어려움이 없을 것입니다.

 

 

1. 범용 레지스터 (General Register)

데이터와 주소를 모두 저장할 수 있는 레지스터로, 주로 연산에 활용됩니다.

 

각 32Bit

구성 

설명 

 EAX

16bit AX

(AH 8bit, AL 8bit)

 정수 연산, I/O 연산 등에 주로 사용된다.

 EBX

16bit BX

(BH 8bit, BL 8bit)

 포인터 연산에 주로 사용된다.

 ECX

16bit CX

(CH 8bit, CL 8bit)

 루프와 같은 반복 연산에 주로 사용된다.

 EDX

16bit DX

(DH 8bit, DL 8bit)

 데이터나 문자 연산에 주로 사용

ESI

16bit SI

 메모리 복사 명령에서 출발지 주소 포인터로 주로 사용된다.

EDI

16bit DI

 메모리 복사 명령에서 목적지 주소 포인터로 주로 사용된다.

EBP

 16bit BP

 스택 베이스 포인터로 가장 많이 사용된다

ESP

16bit SP

 CPU 스택 포인터로 사용된다.

 

 


2. 프로그램 카운터 레지스터

    (Program Counter Register = Instruction Pointer Register)

 

 

 설명 

 EIP

 다음에 실행될 명령어의 주소를 가지고 있어 실행할 기계어 코드의 위치를 지정한다. 때문에 명령어 포인터라고도 한다

 

 

3. 세그먼트 레지스터 (Segment Register)

인텔이 8086에서 처음 메모리 분할(Segment) 기법을 도입하였는데, 이는 메모리 상의 640KiB 이상을 사용할 수 있도록 하였습니다. 이후 80286에서 개량된 메모리 분할 기법을 도입하였는데, 이 시점에서 가상 메모리와 메모리 보호를 지원할 수 있게 되었습니다. 따라서 기존의 운영 모드는 리얼 모드로 이름이 바뀌었고, 새롭게 보호 모드가 추가되었습니다. 따라서 두 모드 둘다 실제 메모리 주소를 계산할 필요성이 생겼으며, 이를 위해 16비트의 세그먼트 레지스터를 사용하게 되었습니다.

 

 

오프셋

 설명 

CS

(Code Segment)

IP

 현재 사용중인 프로그램의 코드가 저장된 세그먼트의 주소를 가리킨다.

 DS

(Data Segment)

 AX, BX, CX, DX, BX, DI, SI

 현재 프로그램이 사용중인 데이터가 저장된 세그먼트의 주소를 가리킨다.

 ES

(Extra Segment)

 DI

 사용 방법이 프로그래머에 의해 결정된다.

SS

(Stack Segment) 

 SP, BP

 현재 프로그램에서 사용중인 스택이 저장된 세그먼트의 주소를 가리킨다.

 GS

 특별히 지정된 용도가 없다.

 FS

 

 

4. 제어 레지스터 (Control Register)

운영모드를 변경하거나 현재 운영 중인 모드에 대한 특정 기능을 제어하는 레지스터입니다.

 

 

 설명 

CR0

 리얼모드에서 보호모드로 전환하는 역할을 하며, 캐시, 페이지 등의 기능이 있다.

CR1

 프로세서에 의해 예약되어 사용되는 레지스터이다.

CR2

 페이지 폴트 발생시 가상주소가 저장된다.

CR3

 페이지 디렉터리의 물리주소와 페이지 캐시에 관한 기능이 설정된다. 

CR4

 프로세서에서 지원하는 확장 기능을 제어, 페이지 크기 확장, 메모리 영역 확장 등의 기능을 활성화한다.

 

 

5. 플래그 레지스터 (Flag Register)

마이크로 프로세서에서 다양한 산술 연산 결과의 상태를 알려주는 플래그 비트들이 모인 레지스터입니다.

 

 

 

 설명 

상태 플래그

(Status Flags) 

 CF

(Carry Flag)

 연산 명령 실행 후 최상위 비트에 덧셈에 따른 자리 올림(Carry) 또는 뺄셈에 의한 빌림(Borrow)이 발생할 때 설정된다.

 PF

(Parity Flag)

 연산의 결과 하위 8비트 중 1로 된 비트의 개수가 짝수 개일 때 셋(1)이 되고 홀수 개일 때는 리셋(0)된다.

 AF

(Auxiliary Flag)

 연산 결과 하위 4비트에 자리올림 또는 빌림이 발생할 경우 설정된다. 10진 연산 처리시 사용된다.

 ZF

(Zero Flag)

 연산 결과가 0이 될 때 설정된다.

 SF

(Sing Flag)

 연산 결과 최상위 비트가 1일 때 설정되고 0일 때는 리셋된다. 부호가 있는 수치의 경우 최상위 비트가 1이면 음수를 표시한다.

 OF

(Overflow Flag)

 오버플로우 발생시 설정된다.

제어 플래그

(Control Flags) 

 DF

(Direction Flag)

 스트링 처리에서 연속적으로 처리되는 문자열에 대한 처리 방향을 표시한다. 즉 DF가 0일 때는 하위->상위 번지, 1일때는 상위->하위 번지로 처리된다.

 IF

(Interrupt Flag)

 8086의 인터럽트에서 하드웨어로부터의 인터럽트에 대한 제어를 담당한다. 1일 경우 인터럽트를 받아들이며, 0일 경우에는 받지 않는다.

 TF

(Trap Flag)

 이 플래그가 셋(1) 되어있으면 단일 스탭 인터럽트가 발생한다. 그러면 하드웨어 도움 없이 프로그램 명령을 하나씩 실행하며 동작을 확인할 수 있다.

 

 

6. GDT 레지스터 (Global Discriptor Table Register)

 

48Bit

 설명 

GDTR

 글로벌 디스크립터 테이블은 프로세서의 메모리 세그먼트를 정의하는 메모리에 저장된 테이블이며, 보호 모드를 확실하게 작동할 수 있도록 돕는다. 이러한 GDT정보는 GDT 레지스터라는 특정 레지스터에 의해 가리켜진다.

 

 

7. IDT 레지스터 (Interrupt Discriptor Table Register)

 

32bit

 설명 

 IDTR

 CPU 286부터 인터럽트가 관리되었으며, 이는 인터럽트 디스크립터 테이블이라고 불리는 메모리에 적재된 테이블에 의해 관리된다. x86은 IDT 작업이 유지될 수 있도록 레지스터를 포함하고 있는데 이를 IDTR이라고 부른다..

 

 

8. LDT 레지스터 (Local Discriptor Table Register)

 

32bit

 설명 

 LDTR

 GDT에 존재하는 LDT 세그먼트 디스크립터를 통해 접근 가능한 세그먼트로, 반드시 GDT내에 존재해야 한다. LDTR은 LDT의 정보를 담고 있다.

 

 

9. 작업 레지스터 (Load Task Register)

 

32bit

 설명 

 LTR

 멀티태스킹을 지원하는 OS에서 사용되며, 보호 모드나 롱 모드에서만 사용된다. 즉, 리얼 모드나 가상 8086 모드에서는 지원되지 않는다. 

 

 

10. 모델 명세 레지스터 (Model Specification Register)

 

32bit

 설명 

 MSR

 MSR은 CPU 작동 확인, 컴퓨터 수행 모니터링, 프로그램 실행 추적, 디버깅 등을 위한 x86 명령 세트에서 사용되는 다양한 제어 레지스터들 중 특정 하나를 가리킨다.

 



 


4. 어셈블리 명령 체계


 

 

위와 같이 다양한 레지스터들에 대해 정리해 보았다. 이를 통해 알 수 있듯이 레지스터들은 각기 다른 목적을 위해 활용되며, 앞으로 보게 될 어셈블리 명령어와 함께 그 기능이 사용될 것입니다.

 

우선 인텔의 가장 기본적인 명령어 구조를 보겠습니다.

 

어셈블리 명령어 체계어셈블리 명령어 체계

 

 

이와 같이 어셈블리 언어는 주로 옵코드와 오퍼랜드, 그리고 주석으로 이루어져 있으며, 오퍼랜드는 옵코드에 따라 한 개만 필요한 경우도 있습니다. 특히 오퍼랜드는 레지스터를 사용하여 명령 작업이 이루어집니다. 주석은 개발자가 필요한 경우에 해당 라인에 대한 부연설명을 붙이는 경우에 사용하며, 필수는 아닙니다. 위의 명령코드를 해석해 보자면

 "CS를 AX에 삽입 혹은 적재한다" 정도가 될 것입니다.

 

여기서 주의해야할 것은 CS값을 AX에 옮겨서 CS의 값이 제거되거나 하는 것이 아니라는 점입니다. 오히려 복사의 의미가 더 알맞을 것 같습니다.

이 밖에도 다양한 어셈블리 명령어들이 존재합니다. 여러 명령어들을 자주 보고 눈에 익혀두는 것이 좋을 것 같네요. 다만 그 양이 꽤 많아서 우선은 중요한 몇 가지 명령어들을 우선 정리하고 추후에 가능하면 업데이트 하도록 하겠습니다.

 

 

1. 산술 연산, 논리 연산, 비교 등의 명령어

 

 

 

2. 이동, 입력, 출력, 불러오기, 저장 등의 데이터 전송 명령

 

 

 

3. 조건에 따른 데이터 전송 명령

 

* 표 출처: itseeyou. (2015). [정보]어셈블리 명령어 기초. 2015년 1월12일 http://securityfactory.tistory.com/153

 

 

일단 이 정도에서 정리를 마무리 해야겠습니다. 본 포스팅이 매우 길어져서 지치네요. 일단 이 정도만 알고 있어도 앞으로 OS 제작에 큰 어려움은 없을 것으로 예상 되는데, 앞서 언급한대로 어셈블리 명령어는 정말 많습니다. 추후에 진행하면서 나오는 명령어들은 그때마다 따로 정리를 하도록 하겠습니다. 참고로 이 포스팅은 일부 위키피디아의 내용을 참고하였음을 밝힙니다.

 

 


TAGS.

Comments