명령어 집합

명령어 집합() 또는 명령어 집합 구조()는 마이크로프로세서가 인식해서 기능을 이해하고 실행할 수 있는 기계어 명령어를 말한다. 마이크로프로세서마다 기계어 코드의 길이와 숫자 코드가 다르다. 명령어의 각 비트는 기능적으로 분할하여 의미를 부여하고 숫자화한다. 프로그램 개발자가 숫자로 프로그램하기가 불편하므로 기계어와 일대일로 문자화한 것이 어셈블리어이다.

최하위 레벨의 프로그래밍 인터페이스로, 프로세서가 실행할 수 있는 모든 명령어들을 포함한다. 명령어 집합, 곧 명령어 집합 구조는 자료형, 명령어, 레지스터, 어드레싱 모드, 메모리 구조, 인터럽트, 예외 처리, 외부 입출력을 포함한 프로그래밍 관련 컴퓨터 아키텍처의 일부이다. ISA는 특정한 CPU 디자인으로 추가된 순수 명령어인 opcode의 집합 규격(기계어)을 포함한다.

RISC CPU는 명령어의 구조와 명령어의 숫자를 단순화 하여 빠른 실행이 가능하고 하드웨어를 줄일 수 있다. ARM의 경우 단순화를 통해 실행속도와 전력소모에 유리하므로 이동전화와 같은 이동장치에 많이 사용한다. ARM의 명령어 길이는 32비트(16비트 Thumb 제외)로 구성되고, 오퍼랜드(operand)를 32비트 내에 존재한다. CISC의 오퍼랜드가 op 코드 다음에 오는 구조와 대조적이다. 대신 32비트 모두를 지정할 수 없으므로 원거리 주소나 데이터 지정이 불가능해져 복수의 명령어를 사용할 필요가 생길 수 있다.

각 기계어 명령어를 실행하는 명령 주기 단계 별 처리를 위해 마이크로코드로 작성된 프로그램이 마이크로프로세서 내의 메모리(ROM)에 고정되므로 이미 결정되어 있어 변경할 수 없다. 개발자가 인식하는데 불편하므로 어셈블리어로 프로그램 코드를 작성하고 어셈블러에 의해 기계어 코드로 전환되어 메모리에 넣고 실행한다. 하이레벨 프로그램 언어도 컴파일러에 의해 기계어 명령어로 바뀐다. C 언어로 프로그래밍 할 경우, C 언어는 같은 코드라도 다른 CPU에서 실행하려면 다른 숫자의 기계어로 전환되어야 하므로 다른 컴파일러 도구가 필요하다.

명령어 집합 구조를 물리적으로 구현하는 방법을 마이크로아키텍처 혹은 컴퓨터 조직이라고 하며, 같은 명령어 집합 구조를 서로 다른 마이크로아키텍처로 구현하기도 한다. 예를 들어, 인텔의 펜티엄과 AMD의 애슬론은 거의 같은 명령어 집합 구조를 서로 다른 마이크로아키텍처로 구현한 것이다.

번지 지정 방식(Addressing mode) 

데이터를 전송하거나 특정 코드로 실행제어를 변경하려면 주소를 지정해야 한다. 기계어 속에서 여러 가지 방식으로 주소를 지정한다.

명령어에서 번지를 지정하는 방식은 다양하게 여러 가지가 존재한다. CPU마다 존재하는 것도 다르고, 어셈블리어에서 표현하는 방식도 다르다. 번지는 데이터의 위치를 지정할 때와 다음 실행할 명령어가 있는 위치를 지정하는 경우가 있다.

데이터 지정

즉시 모드 (immediate mode) 
명령어에 대상 데이터가 포함된다.

 68000: moveq.l #$10,d0  ; 0x10을 D0에 넣는다.
 x86:   mov  eax,10H     ; 0x10을 EAX에 넣는다.

   +------+-----+-----+----------------+
   | add  | reg1| reg2|    constant    |    reg1 := reg2 + constant;
   +------+-----+-----+----------------+

ARM의 정수값 설정 

ARM은 명령어가 32비트로 구성되어, CISC에서와 같이 오퍼랜드(operand)가 op-code 다음에 오지 않는다. 전체 명령어가 32비트안에서 명령코드와 정수가 표현되어야 하는 구조이다. 따라서 32비트내 명령코드(op-code)와 상수값을 정의 해야 하므로 한개의 명령으로 32비트 정수값을 표현하기가 불가능하다. 이것 역시 RISC의 명령 단순화에 따른 결과이다.

이 문제는 어셈블리어에서 32비트 전체에 해당하지 않는 경우와 32비트 전부가 필요한 경우로 나누어 처리한다.
 어떤 숫자가 32비트 전체가 아니라 16비트로 표현가능하다면 한개의 명령어 안에 정수값을 넣는다.
 32비트가 필요하지만 1의 보수를 취했을 때, 한명령어 안에 들어간다면 역시 32비트 내에 정수값을 보수값으로 넣고 읽어 들일 때 다시 보수화 해서 원래값으로 복원한다.
 위의 경우가 아니면 다시 2가지 방법으로 해결한다.
 기계어 다음에 일정 거리안에 리터럴풀을 만들어 숫자를 넣고, 데이터 위치 주소를 PC+오프셋(OFFSET) 방식으로 지정하여 실행할 때 값을 읽어 처리한다.
 2개의 명령어 MOV, MOVT 조합으로 32비트를 완성한다.

의사명령

32비트 내에 들어가는지 개발자가 일일이 생각하기 귀찮은 경우 의사명령을 사용할 수 있다. CISC의 어셈블리어가 기계어 명령어와 어셈블리어 한 개가 일대일 대응되는 것과는 달리, 의사명령은 하나 또는 2개의 기계어 명령어로 어셈블리에서 치환하여 실제 기계어 명령어로 대치한다.

상수값이 32비트 범위의 범위내에 정의할 수 있다면 다음과 같이 어셈블리 된다.
  LDR   r0, =42          => MOV R0, #42
  LDR   r2, =0xFFFFFFFF  => MVN R2, #0

그러나 다음과 같이 범위를 벗어나는 경우도 있다.

 LDR R1,=0x12345678    ; 의사명령 - 의미 : R0에 0x12345678 상수값을 넣어라.

어셈블리어에서 위와 같이 프로그램하면 32비트 숫자를 R0에 저장할 수 있다. 그러나 32비트 숫자는 32비트 명령어 전체의 일부분으로 정의되어야 하므로 표현이 불가능하다. 따라서 다음과 같이 2가지 방식으로 개발 도구에 의해 어셈블리 될 수 있다.

리터럴풀을 만들고 PC와 옵셋의 주소에 의해 32비트 상수값을 메모리에서 읽어오는 방식
    LDR R1, [PC, #offset to literal pool]  ; [pc + offset] -> r1
    ...
    BX      lr
    LTORG                                  ; Literal Pool contains literal 0x12345678

    ; offset은 범위가 제한되므로 일정한 거리 안에 있어야 한다.

2개의 명령어 사용
    MOV  R1,#0x5678   ; 16비트 상수값을 R0의 하위 비트 [15:0]에 넣는다.
    MOVT R1,#0x1234   ; 16비트 상수값을 R0의 상위 비트 [31:16]에 넣는다.
실제로는 이렇게 2개의 명령어로 구성하여 32비트 정수값을 설정할 수 있다.

직접 지정(direct) 
유효주소가 기계어의 OP 코드 다음에 연속되어 있다. 이 값을 주소로 지정하여 데이터의 위치를 결정한다.
   +------+-----+--------------------------------------+
   | load | reg |         address                      | 
   +------+-----+--------------------------------------+
   Effective address = address
 x86:  mov  eax, [0x00040222h]

간접 지정(indirect) 
기계어의 OP 코드 다음에 연속되어 오는 주소는 데이터의 주소 포인터 값이 된다. 따라서 OP 코드 다음의 주소를 읽어 주소 포인터를 결정하고, 다시 그 값을 주소값으로 한번 더 데이터를 읽는다.
   +------+-----+--------------------------------------+
   | load | reg |         address1                     | 
   +------+-----+-----------------+--------------------+
   Effective address = [address]  |
                                  |
                +-----------------v--------------------+
       address1 |            address2                  | 
                +-----------------+--------------------+
                                  |
                +-----------------v--------------------+
       address2 |              data                    | 
                +--------------------------------------+
x86 등 많은 CPU에서 지원하지 않는다.

레지스터 직접 지정 (register direct) 
레지스터의 값이 데이터이다.
   +------+-----+-----+-----+
   | add  | reg1| reg2| reg3|      reg1 := reg2 + reg3;
   +------+-----+-----+-----+

레지스터 간접 지정 (register indirect) 
레지스터의 값이 데이터의 위치인 포인터 주소값이다. 따라서 레지스터값을 주소값으로 하여 읽으면 대상 데이터가 된다.
   +------+------+-----+
   | load | reg1 | base| 
   +------+------+-----+
   Effective address = 베이스 레지스터(base register)

기계어 코드 위치 지정

절대 주소 지정 (absolute/direct) 
점프명령의 OP 코드 다음에 점프할 주소값 절대 주소값이다. 따라서 OP코드 다음에 주소를 읽어 PC에 넣으면 흐름이 변경된다.
   +-------+--------------------------------------+
   |  JMP  |         address                      | 
   +-------+--------------------------------------+
   Effective address = address(instruction 안에 존재)

프로그램 카운터 상대 지정 (Program Counter relative with displacement) 
현재 PC에서 상태 옵셋을 더하여 주소를 결정한다. 따라서 현재 위치로부터 상대적 위치가 된다.
   +----+------------------------------+
   |jump|           offset             |    jump relative
   +----+------------------------------+
   Effective PC address = next instruction address + offset (offset는 상대적을 음수일수도 있다)

레지스터 간접 지정 (register indirect) 
   +-------+-----+
   |jumpVia| reg | 
   +-------+-----+
   Effective PC address = 레지스터 reg의 값

명령어의 기능 및 종류 

명령어는 다음을 포함한다:
 더하기, 빼기와 같은 산술 연산 명령
 논리곱(and), 논리합(or),  논리 부정(not)와 같은 논리연산 명령
 이동, 입력, 출력, 불러오기, 저장과 같은 데이터 전송 명령
 goto, if … goto, call, return과 같은 실행 제어 명령
 인터럽트 제어 명령, 실행권한 명령, NOP, 기타.

산술과 논리 연산은 ALU에 의해 계산되므로 많은 경우 정수형 계산만이 가능하다. FPU에 의한 부동소수점 연산은 별도의 모듈이 장착된 CPU(i486, ARM의 응용 용)가 존재하므로 ALU과는 별도의 명령어에 의해 구동된다. 고성능의 CPU는 대부분 FPU가 존재하지만 저사항의 CPU(대부분의 8비트, 마이크로컨트롤러, ARM의 많은 경우)는 거의 정수형 계산을 하는 ALU만 지원하다.

DSP용 CPU는 수학계산 전용이므로 일반 레지스터에 연결된 ALU(계산모듈) 자체가 정수형과 실수형(부동소수점) 계산을 모두 수행한다. 경우에 따라 고정소수점용 FPU도 존재한다.

ALU에 의한 연산 명령어 

거의 모든 CPU는 산술연산은 사칙연산의 4가지를 지원하며, ALU가 정수형 계산을 한다. 나누기와 곱하기의 경우 계산 결과가 한개의 레지스터에 들어가는 것이 불가능하므로 2개의 레지스터를 사용하는 것이 일반적이다. 나누기의 경우는 결과가 몫과 나머지의 정수값으로 2개의 레지스터에 저장된다.

ALU을 통해 연산이 끝나면 CPU내의 FLAG 레지스터에 FLAG(C,Z,N,O) 결과 값이 저장되고 다음 명령어에 적용 시킬수 있다. 조건 점프 명령은 FLAG에 이미 설정된 값에 따라 조건이 판단된다. ALU와 관련된 명령어는 FLAG에 영향을 미친다.
   ; i386 경우

   mov   eax,10   ; EAX 레지스터에 10을 저장
   add   eax,2    ; EAX+2->EAX - ALU을 통해 연산하고 결과와 FLAG 저장
                  ; EAX는 10+2=>12으로 변경되고, FLAG Z=0, C=0, N=0, O=0으로 설정
   mov   ebx,eax

   xor   eax,eax  ; EAX을 클리어. 12 XOR 12 -> 0, FLAG Z=1, C=0, N=0, O=0으로 설정

더하기와 나누기 

더하기와 나누기 명령어는 정수형 연산이다. 부호를 고려하여 signed와 unsigned 명령어가 존재한다.
연산을 할 때, 플래그 중 C(Cary)을 사용하는 경우도 있고 사용하지 않고 레지스터 값 만으로 연산하는 경우도 있다.

곱하기와 나누기 
   ; i386 경우
   imul  bl     ; ax <- al * bl;                 8비트 signed
   imul  bx     ; dx:ax <- ax * bx;              16비트 signed
   imul  ebx    ; edx:eax <- eax * ebx;          32비트 signed
   mul   ebx    ; edx:eax <- eax * ebx;          32비트 unsigned
   idiv  ebx    ; edx <- eax % ebx; eax <-  eax / ebx; 32비트 signed
   div   ebx    ; edx <- eax % ebx; eax <-  eax / ebx; 32비트 unsigned

비교 명령어 

연산 명령어 중 CMP(어셈블리어에 따라 표현은 다름) 명령어는 ALU 통해 계산을 하고, FLAG 결과 만을 FLAG레지스터에 저장하고 결과값을 버린다.

다음과 같은 개념으로 컴파일러 될 수도 있다(다음 코드는 컴파일러의 환경과 상황에 따라 다르므로 차이가 있을 수 있다):
 // C 언어
   int a;
   a = 10;

   if (a == 10)
       a++
   //...

이 경우 다음이 같은 개념으로 변환될 수 있다.
; x86 경우
   mov   eax,10   ; EAX 레지스터에 10을 저장
   cmp   eax,10   ; EAX-10, FLAG 저장 - ALU을 통해 비교를 위해 연산
                  ; EAX 가 10일 경우 10-10의 결과는 Z=1, C=0, N=0, O=0으로 FLAG 설정
                  ;   EAX  값 변경은 없음
   jne   JumpNext ; NE(Not Equal, Z=0일 경우 같지 않음으로 판단), CMP결과가 Z=1이므로 점프하지 않음.
   inc   eax      ; EAX  증가, EAX =11 -  a++ 실행 a = 11.
JumpNext:
   ...

; ARM의 경우
   CMP R1,#10
   BNE skipNext
   ADD R1, R1, #1  ; R1+1=>R1
skipNext:

또는
   CMP    R1,#10
   ADDEQ  R1, R1, #1

논리 연산 명령어 

논리연산은 각 비트별로 AND, OR, NOT, XOR 등 논리연산을 수행한다.

시프트 명령어 

레지스터의 값을 왼쪽 또는 오른쪽으로 비트 시프트한다. 플래그 중 C(Cary)을 사용하는 경우도 있고, 레지스터 값 만으로 연산하는 경우도 있다.

부호를 고려하여 왼쪽으로 시프트할 때, 부호 비트가 유지되는 경우도 있다.

데이터 전송 명령어 

 직접 데이터 설정 명령어
 직접주소 전송 명령어
 레지스터 인덱스 주소 전송 명령어

스택 
 스택 넣기 PUSH 명령
 스택 가져오기 POP 명령

실행 제어 명령어 

어

조건 점프 명령어 
ALU에 의한 연산결과에 따라 FLAG 레지스터 값에 따라 조건을 판단하고, 조건이 만족하면 점프한다. 따라서 조건명령어가 실행되기 전에 FLAG의 레지스터에 설정이 되어 있어야 한다.

x86의 조건명령어 

조건 명령어와 조건 판단:

Unsigned Conditional Transfers

Signed Conditional Transfers

ARM의 조건 판단 명령어 

ARM 명령어는 32비트 중 조건을 판단하는 필드가 [31:28]에 4비트가 정의되어 있다.

만약 조건 점프 명령
 BEQ
EQ는 조건 필드값이 0b0000이므로 이 값으로 명령어가 시작된다.

call-return 명령어 
 절대주소 CALL
 RET

 IRET 인터럽트 서비스 루틴 종료

기타 명령어 

 NOP : 아무 작업도 하지 않고, 패치 만 이루어지고 다음 스탭으로 진행한다.

인터럽트 명령 
 EI : 인터럽트를 처리하도록 설정한다. CPU의 특수 레지스터의 비트가 설정된다.
 DI : 인터럽트 신호를 무시한다. CPU의 특수 레지스터의 비트가 클리어된다.

 INT v : 소프트웨어 인터럽트 명령어
인터럽트는 보통 하드웨어에 의해 발생한다. 이에 비해 소프트웨어 명령어로도 발생시킬 수 있다.
 x86 : int 10  : 벡터 10 인터럽트 발생

인터럽트의 벡터는 인터럽트의 숫자이다. x86의 경우 메모리에 ISR(Interrupt Service Routine)의 주소값을 정해진 공간에 차례대로 저장해 놓고 각 주소에 대해 벡터로 지정한다. i386은 주소공간이 32비트 이므로 4바이트로 구성된다. 따라서 인터럽트 벡터로부터 읽어야할 메모리 주소를 계산할 수 있다. 'Vector*4+주소저장공간 시작주소'하면 인터럽트가 걸렸을 때, 읽을 주소가 된다. 이 주소를 읽어 자동 점프한다. 이와 대조적으로 ARM의 경우 특정주소가 인터럽트 ISR의 주소가 있는 방식이다. 해당주소에 주소와는 다른 B(branch)명령이 있어 점프한다. B명령이 32비트 이므로 정해진 공간 다른 요소들이 차례로 연결되어 있다.

입출력 전송 명령 

입출력 맵 입출력 방식에서 명령어가 구별되었다. x86계열에서 사용한다.

 IN : 하드웨어의 레지스터나 입출력 장치의 메모리에서 읽는다.
 OUT : 하드웨어의 레지스터나 입출력 장치의 메모리에 쓴다.

모토롤라 계열이나 ARM등의 RISC에서는 주로 메모리 맵 입출력을 사용하므로 입출력 장치의 액세스는 메모리 전송명령어와 구별되지 않는다.

특권 레벨 제어 
CPU의 실행 권한을 설정한다. 저사양의 CPU에는 없는 기능이지만 고사양의 경우 CPU마다 여러 수준의 실행 상태를 유지한다. 실행 권한에 따라 실행될 수 있는 기계어가 제한되는 경우도 있다. 운영 체제에서 커널과 응용 프로그램 실행 시 설정된다.

 x86 : 4레벨
 68K : 2레벌 : 유저 레벨,  슈퍼바이저(supervisor) 레벨

같이 보기 
 컴퓨터 아키텍처
 에뮬레이터
 가상 머신
 마이크로컨트롤러
 마이크로프로세서
 중앙 처리 장치

각주 

중앙 처리 장치
마이크로프로세서
명령어 집합 구조
명령어 처리