|
어셈블리어(Assembly language)
어셈블리어(영어: Assembly language)는 기계어와 일대일 대응이 되는 컴퓨터 프로그래밍의 저급 언어이다.
컴퓨터 구조에 따라 사용하는 기계어가 달라지며, 따라서 기계어에 대응되어 만들어지는 어셈블리어도 각각 다르게 된다. 컴퓨터 CPU마다 지원하는 오퍼레이션의 타입과 개수는 제각각이며, 레지스터의 크기와 개수, 저장된 데이터 형의 표현도 각기 다르다. 모든 범용 컴퓨터는 기본적으로 동일한 기능을 수행하지만, 기능을 어떤 과정을 거쳐 수행할지는 다를 수 있으며, 이런 차이는 어셈블리어에 반영되게 된다.
게다가 단일 명령 집합에 대해 여러 니모닉과 통사론이 대응될 수 있다. 그런 경우에는 제조사가 만든 문서에서 쓰이는 것이 가장 자주 쓰이게 된다.
어셈블러[편집]
어셈블러(assembler)는 니모닉 기호(mnemonics)를 opcode로 변환하고 메모리 위치와 기타 존재물에 따라 식별자를 다시 분석함으로써 목적 코드를 만들어낸다. 거꾸로 기계어를 어셈블리어로 바꾸는 것은 “역(逆)어셈블러”(disassembler)이다. 고급 언어와는 달리 어셈블리어는 간단한 문장에 대해 기계어와 일대일 대응 관계가 있지만, 자주 쓰이는 몇 명령은 둘 이상의 기계어 명령을 묶어 하나의 어셈블리 명령어에 대응시키기도 한다.
패스의 수
어셈블러에는 두 가지 종류가 있는데, 실행 프로그램을 만들기 위해 얼마나 많은 패스가 소스를 거치는지에 따라 다르다.
1패스(one-pass) 어셈블러는 소스 코드를 한 번만 거친다.
다중 패스(multi-pass) 어셈블러는 처음 패스들에서 모든 기호와 관련 값들이 포함된 테이블 하나를 만들고 나중 패스들에서 테이블을 이용하여 코드를 만들어낸다.
1패스 어셈블러들을 이용하는 본래 이유는 어셈블리의 속도 때문이다. 2차 패스가 되돌아가기를 요구할 수도 있기 때문이다. 그러나 현대의 컴퓨터는 납득하기 어려운 지연 없이 다중 패스 어셈블리를 수행한다. 다중 패스 어셈블러는 링크 프로세스를 더 빠르게 한다는 장점이 있다.
고급 어셈블러
더 복잡한 고급 어셈블러는 다음과 같은 언어 추상물을 제공한다.
진보화된 제어 구조
높은 수준의 프로시저/함수 선언 및 호출
구조/레코드, 유니언(union), 클래스, 집합을 포함한 높은 수준의 추상 자료형
복잡한 매크로 처리
클래스, 오브젝트, 추상화, 다형성, 상속과 같은 객체 지향 프로그래밍 기능
어셈블리어의 강좌 / 김성완(부산게임아카데미 교수)
0. 시작하며..
요즘은 과거처럼 어셈블리 언어를 잘 알지 못해도 프로그래머로 먹고 사는데 큰 지 장이 없는 세상이다. 하지만 이 바닥에서 '뛰어난' 프로가 되고자 한다면 여전히 어셈블리에 대한 지식은 필수이다.
물론 과거처럼 아주 상세하게 알 필요는 없지만 적어도 간단한 인라인 어셈블리 코 드는 작성할 수 있어야 하고, 릴리즈 버전의 버그를 잡기 위해서는 디버거가 보여 주는 어셈블리 코드 정도는 읽고 어떤 일이 일어나고 있는 지 파악할 수 있어야 한다.
그러니까, 과거처럼 풀 어셈블리로 프로그램을 작성할 필요는 없다해도 번역된 C/C++ 프로그램의 어셈블리 코드정도는 보고 이해할 수 있는 정도의 지식은 갖추고 있어야 한다.
정교하고 빠르게 동작하는 어셈블리 프로그램을 작성하고자 한다면 어셈블리 언어 로 프로그래밍하는 데 필요한 지식이 매우 방대하고 어려울 수 있지만, 간단한 인라인 어셈블리 코드를 작성하고 번역된 어셈블리 코드를 보고 읽을 수 있는 정도의 지식을 얻는 것은 매우 손 쉬운 일이다.
모든 일에는 결국 익숙해지는 과정이 필요하지만, 기본적인 어셈블리 프로그래밍 지식을 얻는데는 1주일이면 충분하다. 사실 2-3일만에도 가능한 일이기도 하다.
1. 레지스터 란?
어셈블리 언어로 프로그래밍을 하려면 C/C++ 같은 고급 언어와는 달리 CPU 내부에 있는 특별한 메모리에 대해서 잘 알고 있어야 한다. 우리가 일반적으로 알고 있는 메인메모리 혹은 시스템 메모리외에 CPU의 내부에는 레지스터(Register)라고 불리는 특별한 메모리가 있다.
레지스터는 CPU가 접근할 수 있는 메모리 중에서 가장 빠르게 동작하는 메모리로 CPU가 여러 가지 연산등의 처리를 하는 동안 필요한 임시적인 데이터들을 보관하는 데 사용된다.
메인 메모리는 바이트 단위로 매겨진 번지 혹은 주소를 이용해서 접근할 위치를 구분하는데, 레지스터의 경우는 번지의 개념이 없고 모두 고유한 이름이 부여되어 있다.
인텔 80x86 계열의 CPU에는 여러 개의 레지스터가 있어서 다양한 역할들을 맡고 있다. 여러 레지스터 중에서 운영체제가 특별한 용도로 사용하는 레지스터를 제외하면 일반적인 응용프로그램이 직접 이용할 수 있는 레지스터는 불과 10개 정도에 지나지 않는다.
2. 레지스터의 종류
2.1 범용 레지스터
EAX EBX ECX EDX
이들 레지스터는 말그대로 범용적인 목적으로 사용되는 레지스터로 크기는 각각 32비트이고 다른 레지스터에 비해서 비교적 다양한 역할을 한다. 이름에서도 짐작이 가겠지만 단순히 A B C D 라는 이름을 가진 레지스터이다. A B C D 는 Accumulator, Base, Counter, Data 라는 단어의 첫 글자이기도 하다.
16비트 시절에는 AX, BX, CX, DX 라는 이름을 가지고 있다가, 386 CPU부터 레지스 터의 크기가 32비트로 확장되면서 모두 이름앞에 E(Extended) 가 붙여진 것이다.
16비트 시절의 AX, BX, CX, DX 는 모두 16비트 크기였지만 8비트 크기로 나누어서 접근이 가능했다. 그래서 AX의 경우는 상위 8비트가 AH, 하위 8비트가 AL 이라는 이름을 가진다(H는 high, L은 Low라는 뜻). BX, CX, DX 의 경우도 마찬가지로 BH, BL, CH, CL, DH, DL 이라는 이름으로 8비트 단위의 접근도 가능하다.
EAX
----------------------------------
!..............EAX...............! 32비트
!---------------------------------
!...............!.......AX.......! 16비트
!---------------------------------
!...............!...AH...!...AL..! 8비트
----------------------------------
EBX
----------------------------------
!...............EBX..............! 32비트
----------------------------------
!...............!.......BX.......! 16비트
----------------------------------
!...............!...BH...!...BL..! 8비트
----------------------------------
ECX
----------------------------------
!..............ECX...............! 32비트
----------------------------------
!...............!.......CX.......! 16비트
----------------------------------
!...............!...CH...!...CL..! 8비트
----------------------------------
EDX
----------------------------------
!..............EDX...............! 32비트
----------------------------------
!...............!.......DX.......! 16비트
----------------------------------
!...............!...DH...!..DL...! 8비트
----------------------------------
그림처럼 네개의 범용 레지스터는 동일한 32비트 공간을 이름에 따라 다양한 크기로 접근할 수가 있다. 이는 범용 레지스터의 독특한 특징이기도 하다.
2.2 포인터 레지스터
ESI EDI ESP EBP (EIP)
이들 레지스터는 메인메모리의 번지값을 저장해서 포인터의 역할을 하는 레지스터로 16비트 시절에는 SI, DI, SP, BP, IP 라고 불리던 레지스트이다. 범용 레지스터와 마찬가지로 32비트로 확장 되면서 앞에 E가 붙게 된 것이다. 각각의 이름은 Source Index, Destination Index, Stack Pointer, Base Pointer, Instruction Pointer 에서 유래한다.
이중에서 EIP는 현재 실행되고 있는 프로그램의 실행코드가 저장된 메모리의 주소를 가리키는 레지스터로 프로그램의 실행이 진행됨에 따라 자동으로 증가하고 프로그램의 실행 순서가 변경되는 제어문이 실행될때 자동으로 변경된다. 그래서 직접 접근해서 값을 저장하거나 읽거나 하는 일이 없기 때문에 응용 프로그램에서는 손 댈 일이 없는 레지스터이다.
ESI와 EDI는 이름이 암시하듯이 주로 메모리의 한 영역(Source)에서 다른 역영(Destination)으로 데이터를 연속적으로 복사해서 옮길 때 사용한다. 이렇게 메모리의 번지를 저장하는 포인터의 역할 외에도 단순히 32비트 데이터를 저장하는 데도 사용할 수 있다.
ESP와 EBP 는 STACK 으로 동작하는 특별한 메모리 영역을 가리키는 데 사용되는 포인터 레지스터로 특히 ESP 의 경우는 절대적으로 이 용도로만 사용해야 한다.
스택은 보통 함수를 호출해서 프로그램의 제어가 서브루틴으로 넘어 간후 다시 원래 실행되던 루틴으로 돌아오기 위한 복귀 주소를 저장해 두는 곳으로 사용되고. 특히 고급언어로 작성된 프로그램의 경우 함수의 파라메터나 함수 내부에서 선언 된 지역변수의 저장 공간으로도 사용된다. ESP 는 바로 이런 스택공간의 꼭대기를 가리키는 포인터 레지스터이다. ESP의 값은 직접 변경할 수도 있지만 보통은 스택에 데이터를 넣고 빼는 PUSH, POP 명령어의 실행시 자동으로 변경된다.
EBP 는 ESP 에 보조적으로 사용되는 포인터 레지스터로 ESP를 대신하여 스택에 저장된 함수의 파라메터나 지역변수의 주소를 가리키는 용도로 주로 사용된다. 스택을 가리키는 데는 ESP 하나로도 충분하지만 ESP 대신 EBP를 보조적으로 사용하는 이유는 프로그램의 오류로 중요한 스택 공간이 망가지는 일을 어느 정도 방지할 수 있기 때문이다. EBP를 이런 용도로 사용하지 않을 경우에는 단순히 데이터를 저장 하는 용도로도 사용할 수 있다.(보통 속도 최적화 옵션으로 컴파일된 프로그램들의 경우 인텔의 모자라는 레지스터를 보충하기 위해 EBP를 데이터 저장용으로 사용하고, 스택은 ESP 하나로 감당하기도 한다.)
2.3 플래그 레지스터
EFLAGS
이 레지스터는 16비트 시절 FLAGS 였고, 32비트로 확장 되면서 앞에 E가 붙은 것이다. 이 레지스터는 이름이 암시하듯이 비트 단위의 플래그 들을 저장하는 레지스터로 아주 특별한 용도로 사용된다. 이 레지스터는 비트 단위로 의미를 가지는데 32 비트 모두가 사용되는 것은 아니고, 일부만 의미를 가진다. 각각의 플래그 마다 SF(Sign Flag), ZF(Zero Flag), CF(Carry Flag), DF(Direction Flag) 등의 이름이 붙어 있다.
보통 이 레지스터의 값을 직접 읽거나 쓰는 일은 거의 필요하지 않다. 각종 연산을 수행한 결과가 어떠한 지를 기록해 놓는 용도나 특정 명령어의 동작을 조절하는 용도로 활용된다. 연산 결과가 0인지 음수 인지 자리올림이 발생했는지 따라 각각 ZF, SF, CF등이 설정된다. 이렇게 설정된 플래그들은 조건 제어문들이 자동으로 참 조해서 자신의 동작을 선택하기 때문에 우리가 직접 그 값을 챙길 필요는 없다.
DF의 경우는 MOVS 같은 연속적으로 메모리 복사를 하는 명령어가 동작할 때 번지가 증가하면서 동작할지 감소하면서 동작할 지를 지정해주는 용도로 사용된다. 이 때 에도 이 플래그의 값을 켜고 끄는 명령어가 별도로 있기 때문에 직접 EFLAGS 의 값 을 손 댈 일은 없다.
일반적인 응용 프로그램에서 DF의 경우를 제외하면 나머지 언급하지 않은 플래그들은 물론 이거니와 ZF, SF, CF등도 관련되는 명령어들이 자동적으로 참조하기 때문에 이들 플래그의 값을 직접 신경쓰지 않아도 무방하고 그냥 없는 듯이 여기면 된다.
2.4 세그먼트 레지스터
CS SS DS ES (FS GS)
이들 레지스터는 모두 16비트 크기로 메모리의 특정 영역을 가리키는 용도로 사용 되는 레지스터이다. 인텔의 CPU는 메모리의 영역을 용도에 따라 세그먼트로 나누어서 사용한다. 과거 16비트 프로그래밍 시절에는 한 세그먼트가 가리키는 메모리 영역이 64KB에 불과 했기 때문에 중요한 의미를 가졌지만 요즘의 32비트 프로그래밍 시절에는 32비트 주소만 해도 4GB 라는 방대한 메모리 영역을 나타낼 수 있다. 그래서 운영체제가 한번 설정을 해주고 나면 응용 프로그램에서는 신경 쓸일이 거의 없는 레지스터들이다. 일반적인 프로그램에서는 없듯이 생각해도 무방하다는 것이다.
각각의 레지스터의 이름은 Code Segment, Stack Segment, Data Segment, Extra Segment이고 이름이 암시하듯이 코드 영역, 스택 영역, 데이터 영역 을 가리키는 용도로 사용된다.
ES, FS, GS 는 모두 Extra Segment로 DS 처럼 데이터 영역을 가리키는 용도로 사용 되고 DS를 보조 해주는 세그먼트 레지스터이다. 두곳 이상의 데이터 저장영역을 가리켜야 할 때 DS와 함께 사용된다. 하지만 32비트 프로그램에서는 DS와 ES가 같은 영역을 가리키고 있기 때문에 굳이 세그먼트 레지스터에 대해서 신경을 쓸 필요가 없다. 특히 FS, GS는 286 이후에 추가된 것으로 운영체제를 작성하는 게 아니라면 없듯이 여겨도 된다.
3. 꼭 알아야 하는 레지스터
위에서 언급한 레지스터 외에도 다른 레지스터가 더 있지만 나머지는 모두 운영체제 같은 시스템 프로그램들에서나 신경 쓸 레지스터이고 일반적인 응용 프로그램을 작성한다면 아래 8개의 레지스터만 잘 알고 있으면 충분하다.
EAX, EBX, ECX, EDX
ESI, EDI, ESP, EBP
4. 명령어
인텔 80x86 CPU 가 지원하는 기본 명령어는 FPU의 명령어도 포함해서 대략 180개 정도이다. 이외에도 MMX 명령어, SSE 명령어들이 추가로 있다. 명령어의 갯수만 해도 질리도록 많다. 만약 이렇게 많은 명령어들을 모두 알아야 어셈블리 프로그래밍을 할 수 있다면 아마도 어셈블리 프로그래밍을 하려는 사람 이 거의 없을 것이다.
하지만 여기에도 어김없이 80/20 법칙 혹은 파레토의 법칙이 성립한다. 명령어의 사용빈도 통계를 내어 보면 전체 명령어의 약 20% 가 프로그램의 80%를 차지한다. 더군다나 일반적인 응용프로그램의 경우는 운영체제에서 사용하는 명령어들은 전혀 사용할 필요가 없고, 단지 사용의 편의를 위해서 후기에 추가된 복합 명령어들은 기존의 단순 명령어들의 조합으로 대체가 가능하기 때문에 실제로 사용하게 되는 명령어의 숫자는 더 줄어들게 된다. 그리고 C/C++로 작성된 보통의 응용 프로그램에 인라인으로 삽입되는 어셈블리 코드라면 사용하게 되는 명령어의 숫자는 더 줄어들게 된다.
FPU 명령어들을 제외하고 실제로 응용 프로그램에서 사용 가능성이 높은 명령어를 알파벳 순으로 나열해보면...
ADC
ADD
AND
CALL
CBW
CDQ
CLD
CMP
CWD
DEC
DIV
IDIV
IMUL
INC
JZ/JE/JA/JAE/JB/JBE/JG/JGE/JL/JLE/JNE
JMP
LEA
LOOP
MOV
MOVSB/MOVSW/MOVSD
MOVSX
MOVZX
MUL
NEG
NOT
OR
POP
PUSH
REP
RET
ROL/ROR
SAL/SAR
SHL/SHR
SHLD
SHRD
STD
STOSB/STOSW/STOSD
SUB
TEST
XOR
이처럼 대략 50개 정도가 된다.
물론 이중에서도 자주 쓰이는 명령어는 또 일부분에 지나지 않는다. 이중에서 가장 많이 사용되는 명령어는 MOV 로 데이터를 복사해서 옮기는 명령어이다. 대략적인 통계를 내어보면 전체 프로그램에서 MOV 명령어가 차지하는 비율이 약 70%에 이른다. 이는 곧 MOV 명령어 하나만 알아도 프로그램의 70%를 작성할 수 있다는 얘기이 기도하다.
5. 명령어의 분류
위에 나열한 명령어들은 명령어의 역할에 따라 몇 가지 종류로 분류할 수 있다.
데이터 이동 명령어
MOV
MOVSX
MOVZX
MOVS/MOVSB/MOVSW/MOVSD
STOSB/STOSW/STOSD
(CLD/STD)
(REP)
비트 이동 명령어
ROL/ROR
SAL/SAR/SHL/SHR
SHLD/SHRD
산술연산 명령어
ADD
ADC
SUB
INC
DEC
MUL
DIV
IMUL
IDIV
NEG
데이터 확장 명령어
CBW
CWD
CDQ
논리연산 명령어
AND
OR
XOR
NOT
CMP
TEST
분기 및 제어 명령어
JMP
JZ/JE/JA/JAE/JB/JBE/JG/JGE/JL/JLE/JNE...
LOOP
CALL
RET
50개 정도의 명령어도 이렇게 종류별로 분류해서 보면 좀 더 쉽게 바라볼수 있다. 눈치빠른 이라면 이름만으로도 벌써 몇개의 명령어들의 역할을 짐작하고 있을 것이다.
6. 명령어의 형식
인텔 어셈블리 코드는 앞에서 알아본 명령어 opcode 와 명령어의 대상이 되는 오퍼랜드 operand 로 구성되고 일반적으로 다음과 같은 세가지 형식을 가진다.
1) opcode
2) opcode operand1
3) opcode operand1, operand2
4) opcode operand1, operand2, operand3
1) 오퍼랜드가 없이 명령어만 있는 경우가 있고,
2), 3), 4) 처럼 오퍼랜드가 하나 에서 세개까지 올 수 있다.
특수한 경우로 명령어 앞에 접두어 prefix 가 올수도 있는데 MOVS 같은 스트링 명령어의 경우는 앞에 접두어 REP 가 붙어서
rep movsd
식으로 쓸 수도 있다.
이 때 REP 의 의미는 REPEAT 로 여러번 반복하라는 뜻이다.
오퍼랜드가 없는 명령어 예
cld
std
오퍼랜드가 하나인 명령어 예
push eax
오퍼랜드가 두개인 명령어 예
mov eax, ebx
오퍼랜드가 세개인 명령어 예
shld edx, eax, 16
명령어에 따라서는 오퍼랜드 갯수가 달라지기도 한다.
imul ebx
imul eax, ebx
imul eax, ebx, 4
6.1 오퍼랜드의 종류
명령어의 대상이 되는 오퍼랜드에는 다음의 세가지 종류가 있다.
레지스터, 메모리, 직접값
예컨데 mov 명령어의 경우 아래와 같이 다양한 형태로 오퍼랜드가 올 수 있다.
mov 레지스터, 레지스터 mov eax, ebx
mov 레지스터, 메모리 mov eax, Value
mov 메모리, 레지스터 mov Value, eax
mov 레지스터, 직접값 mov eax, 100
mov 메모리, 직접값 mov Value, 100
(여기서 Value 미리 선언된 메모리 변수명이다.)
명령어에 따라서는 올 수 있는 오퍼랜드에 제약이 있기도 하고, 오퍼랜드가 코드 상에 명시적으로 적히지 않고 암시적으로 내정된 경우도 있다.