어셈블리 언어 기초

이번엔 어셈블리 언어를 기초부터 차근차근 알아가는 챕터이다. 진짜 완전 기초부터 시작하는 느낌이었다.
우선 정수 상수부터 알아보자.

 

정수 상수는 선택사항인 부호, 한 자리 이상의 숫자와 선택사항인 접미 문자로 구성된다.

[ { + | - } ] digits [radix]

여기서 대괄호 안의 구성요소는 선택사항이며 중괄호 안의 구성요소는 포함된 요소 중 하나를 선택하는 것이다. 요소들은 |문자로 구분된다. 이탤릭체로 된 구성요소는 정의와 설명이 알려진 아이템이다.

radix는 진수라고 부르며 16진수는 뒤에 h를, 8진수는 q/o, 10진수는 d, 2진수는 b를 붙인다.
[부호화 실수에 r, 10진수에 t, 2진수에 y를 붙이기도 한다.]

 

이것은 어셈블리 언어의 산술 연산자 우선순위이다. 우리가 알고있던 우선순위와 다르지 않다.

 

 

다음으로 실수 상수이다.

[sign] integer.[integer][exponent]

부호와 지수에 대한 구문 형식은 다음과 같다.

sign             {+, - }
exponent    E [ { +, - } ] integer

예를 들어

  • 2.
  • +3.0
  • -44.2E+05
  • 26.E5

이런 식으로 나타낼 수 있다.

 

 

문자 상수

작은따옴표 ' 또는 큰 따옴표" 로 둘러싸인 한 문자이다. MASM은 문자를 문자에 대한 2진 아스키 코드로 메모리에 저장한다. 각 문자당 1byte크기를 갖는다. 만약 문자들의 열이 따옴표로 둘러싸이면 문자열 상수이다.

 

 

예약어

MASM에서 특별한 목적으로 사용하며 올바른 문맥에서만 사용할 수 있다. 이것들을 식별자로 사용할 수 없다.
명령어는 다음과 같은 유형들이 있다.

  • 명령어 mnemonics - MOV, ADD, MUL 등의 명령어
  • 레지스터 이름
  • directive - MASM에게 프로그램을 어떻게 어셈블하는 지를 알려준다.
  • 속성 - 변수와 피연산자의 크기와 사용정보를 제공한다. BYTE와 WORD 등
  • 연산자 - 수식에서 사용된다.
  • 미리 정의된 기호 - 어셈블할 때에 상수 정수 값을 반환하는 @data와 같은 기호

 

 

식별자

1~247개 사이의 문자를 사용할 수 있고 대소문자를 구분하지 않는다. 하지만 첫 번째 문자는 알파벳 문자나 _, @, ?, $이어야 한다. 그 다음의 문자들에는 숫자가 올 수 있다.

 

 

directive

디렉티브는 어셈블러가 인식하여 그것에 따라서 동작하는, 소스코드에서 포함된 명령이다.
이것은 변수, 매크로, 프로시저를 정의할 수 있고 실행시간에 실행되지 않는다.
주로 메모리 세그먼트에 이름을 부여하고 어셈블러와 관련된 많은 기타 관리 작업을 수행한다.
또한 대소문자를 구분하지 않는다.

이러면 명령어랑 어떤 차이가 있나 싶은데, 다음 예를 보자.

myVar DWORD 26		; DWORD directive
mov   eax,myVar		; MOV instruction

여기에서 DWORD 디렉티브는 어셈블러에게 프로그램에서 더블워드 변수용으로 공간을 예약하라고 지시한다. 한편, MOV 명령어는 실행시간 동안 myVar의 내용을 EAX 레지스터에 복사하는 동작을 수행한다.

모든 인텔 프로세서용 어셈블러가 같은 명령어 집합을 공유할지라도, 각 어셈블러는 서로 완전히 다른 디렉티브 집합을 갖고 있다. 예를들어 NASM과 MASM의 경우 서로 디렉티브의 집합이 다르다.

 

 

명령어

프로그램이 어셈블되었을 때에 실행 가능하게 되는 문장이다. 명령어는 어셈블러에 의해서 기계어 바이트들로 변환되고 런타임에 CPU에 의해서 적재되어 실행된다. 명령어는 네 개의 기본적인 부분을 포함한다.

  • 레이블 (선택사항)
  • 명령어 mnemonic (필수)
  • 피연산자 (명령어에 의존. 보통 필수)
  • 주석 (선택사항)
[ label : ] mnemonic [operands] [ ; comment]

레이블부터 하나씩 살펴보자.

 

레이블

  • 레이블은 명령어 또는 데이터의 위치를 표시하는 식별자이다. 명령어 바로 앞에 있는 레이블은 그 명령어의 주소를 의미한다. 마찬가지로, 변수 앞에 있는 레이블은 변수의 주소를 의미한다.
  • 데이터 레이블 - 코드에서 변수를 편리하게 참조하는 방법을 제공하는 변수의 위치에 대한 식별자이다.
  • 코드 레이블 - 명령어가 위치한 프로그램 코드 영역에 있는 레이블은 콜론: 으로 끝나야 한다. 코드 레이블은 분기나 루프 명령어의 명령어의 목적지로 사용된다. 예를 들어 L1: 후에 jmp L1의 경우에 사용된다.

 

 

명령어 mnemonics

명령어를 식별하기 위한 짧은 단어이다. 영어로 니모닉은 기억을 돕는 방책이란 뜻이다. 이에 따라 mov, add, sub와 같이 니모닉 명령어는 어떤 연산이 수행될지 힌트를 준다.

 

 

피연산자 operands

어셈블리 언어 명령어는 상수(1), 상수 수식(1+2), 레지스터, 메모리 피연산자 이렇게 4개를 가질 수 있다.
여기서 메모리 피연산자는 변수 이름 또는 변수의 주소를 포함하는 1개 이상의 레지스터로 지정된다.
쉽게 말해서 변수의 이름이 변수의 주소를 의미하며 컴퓨터가 주어진 주소의 메모리 내용을 참조하도록 지시한다.

명령어는 0에서 3개의 피연산자를 가질 수 있는데, 다음의 경우를 보자

mov count, ebx

이렇게 두 개의 피연산자를 갖는 명령어에서 첫 번째 피연산자를 목적지라고 한다.
두 번째 피연산자는 소스source이다. 일반적으로 명령어는 목적지의 내용을 수정한다.
위의 예에서도 mov 명령어에서 데이터는 소스로부터 목적지로 복사된다.

 

그렇다면 3개의 피연산자를 갖는 명령어는 어떨까

imul eax, ebx, 5

이 경우는 ebx에 5를 곱하여 곱을 eax 레지스터에 저장한다.

 

 

주석

주석은 프로그램 작성자가 프로그램 소스코드를 읽는 사람에게 프로그램 설계에 대한 정보를 제공한다. 보통 다음과 같은 내용을 적는다.

  • 프로그램 목적에 대한 기술
  • 프로그램을 작성하고 수정한 사람들의 이름
  • 프로그램 작성일과 수정일
  • 프로그램 구현에 대한 기술적인 사항의 기록

한 줄 짜리 주석은 세미콜론으로 시작하여 뒤에 설명을 적고
여러 줄 주석은 COMMENT 디렉티브와 임의의 사용자 정의 기호로 시작하여 어셈블러는 해당 블록을 무시한다.

COMMENT !
	asdf
        asdf
!

 

 

정수의 덧셈과 뺄셈 예제코드

TITLE Add and Subtract
; TITLE은 해당 줄 전체가 주석임을 나타낸다.

INCLUDE Irvine32.inc
; INCLUDE디렉티브는 필요한 정의와 설정 정보를 어셈블러의 INCLUDE 디렉토리에 있는 Irvine32.inc라는 텍스트 파일에서 복사한다.

.code
; .code 디렉티브는 프로그램의 모든 실행문이 있는 코드 세그먼트의 시작을 나타낸다.
main: PROC
; 프로시저의 시작을 알린다. 프로시저의 이름은 main

mov eax,10000h
add eax,40000h
sub eax,20000h
call DumpRegs
;레지스터들을 보여준다. call 명령어는 CPU 레지스터의 현재 값을 화면에 표시하는 프로시저를 호출한다.

	exit
main ENDP
; exit 문장은 프로그램을 종료시키는 MS-Windows의 함수를 간접적으로 호출한다. 이것은 MASM 키워드가 아니라 irvine32.inc 파일에서 정의된 매크로 명령이다.
; ENDP 디렉티브는 main 프로시저의 끝을 나타낸다.

END main
;END 디렉티브는 어셈블된 프로그램의 마지막 줄을 나타낸다.

 

 

 


 

 

 

프로그램 어셈블, 링크, 실행하기

 

어셈블리 언어로 작성된 소스 프로그램은 컴퓨터에서 곧바로 실행될 수 없다.
이 프로그램은 실행 코드로 변환되어야(어셈블되야) 한다.

어셈블러는 오브젝트 파일이라는 기계어를 포함한 파일을 만든다. 이 파일은 바로 실행되지는 않고, 링커라는 프로그램에 넘겨진 후에 여기에서 실행 가능 파일 executable file을 만든다.

 

어셈블-링크-실행 사이클

위 그림은 어셈블리 언어 프로그램을 편집, 어셈블, 링크, 실행하는 과정을 그린 것이다.

  1. 프로그래머는 텍스트 에디터를 사용하여 소스파일이라고 하는 아스키 텍스트 파일을 만든다.
  2. 어셈블러는 소스 파일을 읽어서 프로그램을 기계어로 변환한 오브젝트 파일을 만든다.
    (선택사항에 따라 listing file을 만든다.)
  3. 링커는 오브젝트 파일을 읽어서 프로그램이 링크 라이브러리에 있는 프로시저에 대한 호출을 포함하는지를 확인하기 위하여 검사한다. 링커는 링크 라이브러리에서 필요한 프로시저를 복사하고 이들을 오브젝트 파일과 합쳐서 실행파일을 만든다.
  4. 운영체제의 로더loader 유틸리티는 실행 파일을 메모리로 읽어들이고 CPU를 프로그램의 시작 주소로 분기하여 프로그램을 실행한다.

 

 

리스트 파일

리스트 파일은 세그먼트 이름, 오프셋 주소, 변환된 기계어 코드(object code), 심볼 테이블(변수, 상수, 프로시저)이 함께 있는 인쇄하기에 적합한 형태의 소스 코드의 복사본을 갖는다.
위에서 배운 과정들을 직접 우리의 프로그램이 어떻게 컴파일되었는지 보여주기 위해 사용한다.

 

 

데이터 정의

MASM은 변수와 수식에 할당될 수 있는 값들의 집합을 나타내는 고유 자료형들을 정의한다.
이렇게 정의하는 것으로 메모리에 변수를 위한 저장공간을 확보한다. 아래는 데이터들의 디렉티브이다.

  • BYTE, SBYTE : 8-bit unsigned integer; 8-bit signed integer 
  • WORD, SWORD : 16-bit unsigned & signed integer 
  • DWORD, SDWORD : 32-bit unsigned & signed integer 
  • QWORD : 64-bit integer
  • TBYTE : 80-bit integer
  • REAL4 : 4-byte IEEE short real 
  • REAL8 : 8-byte IEEE long real 
  • REAL10 : 10-byte IEEE extended real
[name] directive initializer [,initializer] ...

예를 들어 value BYTE 10 이나, count DWORD 12345 의 형태가 있다.

위의 데이터 타입들을 하나씩 정리해보자.

 

 

BYTE와 SBYTE(signed byte) 데이터 정의

이 디렉티브는 하나 이상의 부호없는, 또는 부호있는 값을 위한 저장공간을 할당하며 각 저장공간은 8비트 저장공간에 맞는 크기여야 한다.

value1 BYTE 'A' ; character constant
value2 BYTE 0 ; smallest unsigned byte
value3 BYTE 255 ; largest unsigned byte
value4 SBYTE -128 ; smallest signed byte
value5 SBYTE +127 ; largest signed byte
value6 BYTE ? ; uninitialized byte

여기서 ? 초기값은 변수를 초기화하지 않은 채로 두며, 실행시간에 값이 할당됨을 의미한다.

 

 

byte 배열의 초기값 (여러 개의 초기값)

여러 개의 초기값이 같은 데이터 정의에서 사용된다면 그것의 레이블은 첫 번째 초기값의 오프셋만을 나타낸다.

list BYTE 10,20,30,40

위의 예에서 list가 오프셋 0000에 위치한다면, 10이 오프셋 0000에 있고 20은 0001, 30은 0002에 있다.

 

모든 데이터 정의가 레이블을 필요로 하는 것은 아니다.

list BYTE 10,20,30,40
     BYTE 50,60,70,80
     BYTE 11,22,33,44

list로 시작하는 바이트의 배열을 계속 하기 위해서 다음 줄에 추가적인 바이트를 정의할 수 있다.

 

 

문자열 정의

문자열을 정의하기 위해서 작은따옴표 또는 큰 따옴표에 문자열을 넣어야 한다. 일반적으로 문자열의 유형은 (0을 포함하는)널 바이트로 끝난다.

str1 BYTE "Enter your name",0
str2 BYTE 'Error: halting program',0    ; 작은 따옴표든 큰 따옴표든 상관X
str3 BYTE 'A','E','I','O','U'
greeting BYTE "Welcome to the Encryption Demo program "
	BYTE "created by Kip Irvine.",0
    ; 각 줄에 레이블을 사용하지 않고 여러 줄에 나누어 놓을 수 있다.

각 문자는 하나의 바이트 저장 공간을 사용하며, 문자열은 문자들의 배열로 구현된다.

 

16진수 코드 0Dh와 0Ah는 CR/LF 또는 줄끝 문자라고 부른다.
0Dh = carriage return , 0Ah = line feed
이 코드가 출력되면 커서를 다음 줄의 가장 왼쪽으로 이동시킨다. 

줄 연속 문자 \는 두 개의 소스 코드 줄을 하나의 문장으로 합친다.

greeting1 BYTE "hi"

greeting1 \
BYTE "hi"
; 이 두 문장은 서로 동일

 

만약 소스코드 말고 여러 줄의 문자열을 하나의 문자열로 합치고 싶다면 문자열의 끝에 널 문자 0을 넣지 말고 쉼표를 넣는다.

menu BYTE "Checking Account",0dh,0ah,0dh,0ah,
"1. Create a new account",0dh,0ah,
"2. Open an existing account",0dh,0ah,
"3. Credit the account",0dh,0ah,
"4. Debit the account",0dh,0ah,
"5. Exit",0ah,0ah,
"Choice> ",0

 

 

DUP 연산자

이 연산자는 상수 수식만큼 반복하고 여러 개의 데이터를 위한 저장 공간을 할당한다.
이 연산자는 문자열과 배열을 위한 공간을 할당할 때에 특히 유용하고 초기화되거나 초기화되지 않은 데이터 정의에 모두 사용될 수 있다.

var1 BYTE 20 DUP(0) ; 20 bytes, all equal to zero
var2 BYTE 20 DUP(?) ; 20 bytes, uninitialized
var3 BYTE 4 DUP("STACK") ; 20 bytes: "STACKSTACKSTACKSTACK"
var4 BYTE 10,3 DUP(0),20 ; 5 bytes

마지막 부분의 var4 BYTE 10,3 DUP(0),20 ; 5 bytes 는 처음볼 때 조금 복잡해보이는데
10,3 DUP(0),20 에서 10은 초기화된 바이트의 개수를 나타내고 ,3 DUP(0)는 0으로 초기화된 바이트가 3번 반복됨을 나타낸다. 마지막으로 20은 마지막 바이트의 값을 나타낸다.

따라서 var4 변수는 총 5바이트를 차지하고, 처음 세 바이트는 0으로 초기화, 그 다음 두 바이트는 각각 20의 값을 갖는다.

 

 

WORD와 SWORD 데이터 정의

하나 이상의 16비트 정수의 저장 공간을 만든다.

word1 WORD 65535 ; largest unsigned value
word2 SWORD –32768 ; smallest signed value
word3 WORD ? ; uninitialized, unsigned
word4 WORD "AB" ; double characters
myList WORD 1,2,3,4,5 ; array of words
array WORD 5 DUP(?) ; uninitialized array

만약 myList WORD 1,2,3,4 를 정의했다고 치면 값이 2바이트를 차지하기 때문에 오프셋의 주소는 2씩 증가한다.

 

 

DWORD와 SDWORD 데이터 정의

이 디렉티브는 하나 이상의 32비트 정수를 위한 저장 공간을 할당한다.

val1 DWORD 12345678h ; unsigned
val2 SDWORD –2147483648 ; signed
val3 DWORD 20 DUP(?) ; unsigned array
val4 SDWORD –3,–2,–1,0,1 ; signed array

이 경우 4바이트씩 오프셋이 증가한다.

 

 

리틀 엔디언 순서

x86 프로세서들은 리틀 엔디언(하위부터 상위로) 순서를 사용하여 메모리에 데이터를 저장하고 꺼낸다.
최하위 유효 바이트가 데이터를 위해서 할당된 첫 번째 메모리 주소에 저장된다. 나머지는 연속된 메모리 위치에 차례로 저장된다. 예를들어서 더블 워드 12345678h를 생각해보자.

 

 

 

다음은 AddSub 프로그램에 몇 개의 더블 워드 변수를 추가할 수 있는 코드의 예시이다.

TITLE Add and Subtract, Version 2 (AddSub2.asm)
; This program adds and subtracts 32-bit unsigned
; integers and stores the sum in a variable.
INCLUDE Irvine32.inc
.data
val1 DWORD 10000h
val2 DWORD 40000h
val3 DWORD 20000h
finalVal DWORD ?
.code
main PROC
	mov eax,val1 ; start with 10000h
	add eax,val2 ; add 40000h
	sub eax,val3 ; subtract 20000h
	mov finalVal,eax ; store the result (30000h)
	call DumpRegs ; display the registers
exit
main ENDP
END main

뭐 복잡해보이고 많은것 같은데, 그냥 eax 레지스터에 변수를 더하고 빼고 옮기는 작업을 보여주는 것이다.

 

 

 


 

 

 

기호 상수 symbolic constant

식별자(symbol)를 정수 수식 또는 텍스트와 연관시켜서 만든다. 그냥 우리가 c에서 알던 매크로다.

#define MAX_VALUE 100
#define PI 3.141592

어셈블러에서 이러한 기호상수는 기억장소를 보유하지 않고, 어셈블러가 프로그램을 스캔할 때에만 사용되며 실행 시간에는 바뀔 수 없다.
(변수는 저장공간을 사용하고 실행시간에 값이 변화함)

 

 

등호 디렉티브

등호 디렉티브는 기호 이름을 정수 수식과 연관되게 한다. 

name = expression

보통 expression은 32비트 정수 값이다. 여기서 name이 symbolic constant라고 불린다.
매크로와 같이 사용될 때 값으로 바뀐다.

이런 기호 상수를 굳이 사용하는 이유는 프로그램의 유지 보수에 더 쉽기 때문이다. 예를 들어 매크로가 프로그램 전체에 수백번 사용되었을 때, 나중에 매크로 값만 바꾸는 것으로 쉽게 다시 정의할 수 있다.

 

 

배열과 문자열의 크기 계산

배열의 크기를 선언하는 방법은 어셈블러가 배열의 크기를 계산하게 하는 것이다. $ 연산자(현재 위치 카운터)는 현재의 프로그램 문장에 대한 오프셋을 반환한다. 다음 예시를 통해 배열size를 계산할 수 있다.

list BYTE 10,20,30,40
ListSize = ($ - list)

여기서 ListSize는 list 바로 뒤에 와야 한다. 만약 중간에 다른 기억 공간이 들어간다면 현재 위치 카운터와 list의 오프셋 사이의 거리에 영향을 줄 수 있기 때문에 이 경우 ListSize는 너무 큰 값이 만들어진다.

 

이런 방식은 문자열의 길이를 계산하는 경우에도 사용할 수 있다.

myString BYTE "example"
         BYTE "asdf"
myString_len = ($ - myString)

 

항상 $ - 주소만 하면 되는것은 아니고, 바이트가 아닌 워드나 더블워드의 경우 배열의 원소 수를 계산할 때는 전체 배열 크기를 개별 원소의 크기로 나누어야 한다. 예를 들어 WORD의 경우 2 바이트를 차지하기 때문에 주소 범위 ListSize에 2를 나눠준다.

 

 

EQU 디렉티브

이것은 기호를 정수 수식이나 임의의 텍스트와 연관시킨다. 그리고 재정의될 수 없다.

name EQU {expression | symbol | <text>}

EQU는 정수로 계산되지 않는 값을 정의할 때에 유용하다. 예를 들어 다음과 같은 경우가 있다.

PI EQU <3.1416>
pressKey EQU <"Press any key to continue...",0>
.data
prompt BYTE pressKey

PI같이 실수 상수를 정의할 수도 있고, 기호를 문자열과 연관시켜서 그 기호를 초기값으로 사용할 수도 있다.