데이터 전송, 주소 지정과 산술 연산

이번엔 어셈블리 언어가 고급 언어와 어떤 근본적인 차이점을 가지는지 소개한다.
우선 데이터 전송 명령어들에 대해 알아보자.

 

 

피연산자 타입

이전에 기본적인 명령어 형식은 다음과 같이 소개했다

[label :] mnemonic [operands][ ; comment]

여기에 명령어의 개수에 따라 mnemonic [destination], [source-1], [source-2] 등으로 하나씩 추가했다.

이 상태에서 더 추가적인 유연성을 주기 위해 다음과 같은 피연산자들을 사용한다. (표 참고)

  • 즉시값(immediate) - 숫자 수식을 사용한다.
    값은 명령어로 인코딩된다.
  • 레지스터 - CPU에 있는 명명된 레지스터를 사용한다.
    레지스터 이름은 숫자로 변환되고 명령어로 인코딩된다.
  • 메모리 - 메모리 위치를 참조한다.
    메모리 주소는 명령어로 인코딩된다.

명령어 피연산자 표기법

 

 

직접 메모리 피연산자

이전 포스팅에서 변수 이름은 데이터 세그먼트 내의 오프셋을 가리킨다고 했다.
직접 메모리 피연산자는 메모리 내의 저장공간에 명명된 참조이다.

어셈블러는 자동으로 명명된 참조를 역참조 시킨다. 그래서 역참조를 의미하는 [ ] 대괄호를 써도 되고 안써도 된다.

.data
var1 BYTE 10h
.code
mov al,var1	; AL = 10h
mov al,[var1]	; AL = 10h

 

 

MOV 명령어

이 명령어는 소스 피연산자로부터 목적지 피연산자로 데이터를 복사한다.
첫 번째 피연산자가 목적지이고 두 번째가 소스이다.

MOV destination, source

이 때, 두 피연산자는 같은 크기어야 하며, 두 피연산자가 모두 메모리 피연산자일 수 없다.
CS, EIP, IP는 목적지 피연산자일 수 없고 즉시값이 세그먼트 레지스터(CS, DS, ES, SS)에 이동될 수 없다.

추가로, MOV 명령어는 한 메모리 위치에서 다른 메모리 위치로 데이터를 직접 이동시킬 수 없다. 대신, 소스 피연산자의 값을 레지스터로 이동시킨 후에 그 값을 메모리 피연산자로 이동시켜야 한다.

data
bVal BYTE 100
bVal2 BYTE ?
wVal WORD 2
dVal DWORD 5
.code
mov ds,45	;immediate move to DS not permitted
mov esi,wVal	;size mismatch
mov eip,dVal	;EIP cannot be the destination
mov 25,bVal	;immediate value cannot be destination
mov bVal2,bVal	;memory-to-memory move not permitted

 

 

MOVZX 명령어 (move with zero-extend)

이 명령어는 소스 피연산자를 목적지 피연산자로 복사하고 값을 16비트 또는 32비트로 제로확장(zero-extend)한다. (확장된 비트들은 0으로 채워진다.) 이 명령어는 부호없는 정수에만 사용된다.

mov bl,10001111b
movzx ax,bl ; zero-extension, AX = 0000000010001111b

 

다음 그림은 소프 피연산자가 16비트 목적지로 어떻게 제로확장되는 지를 보여준다.

 

 

MOVSX 명령어 (move with sign-extend)

이 명령어는 소스 피연산자의 내용을 목적지 피연산자로 복사하고 값을 16비트 또는 32비트로 부호확장한다. (확장된 비트는 부호비트로 채워진다.) 이 명령어는 부호있는 정수에만 사용된다.

mov bl,10001111b
movsx ax,bl ; sign extension. AX = 1111111110001111b

 

이 과정은 아래 그림과 같이 소스의 최상위 비트가 목적지의 상위 8비트의 각각에 복사된다.

 

 

XCHG 명령어 (exchange data)

이 명령어는 두 피연산자의 내용을 서로 교환한다.
이 때, 적어도 하나는 레지스터여야 하고, 즉시값은 사용할 수 없다.
나머지는 MOV 명령어의 규칙과 같다.

.data
var1 WORD 1000h
var2 WORD 2000h
.code
xchg ax,bx ; exchange 16-bit regs
xchg ah,al ; exchange 8-bit regs
xchg var1,bx ; exchange mem, reg
xchg eax,ebx ; exchange 32-bit regs
xchg var1,var2 ; error: two memory operands

 

 

직접 오프셋 피연산자

변수의 이름에 변위를 더하여 직접 오프셋 피연산자를 만들 수 있다.
이 연산자를 사용하면 명시적인 레이블을 갖지 않은 메모리 위치에 접근할 수 있다.

data
arrayB BYTE 10h,20h,30h,40h
.code
mov al,arrayB ; AL = 10h
mov al,arrayB+1 ; AL = 20h
mov al,[arrayB+1] ; alternative notation

 

.data
arrayW WORD 1000h,2000h,3000h
arrayD DWORD 1,2,3,4
.code
mov ax,[arrayW+2] ; AX = 2000h
mov ax,[arrayW+4] ; AX = 3000h
mov eax,[arrayD+4] ; EAX = 00000002h

 

 

 

덧셈과 뺄셈

INC, DEC, ADD, SUB, NEG 명령어들로 상태플래그(Carry, Sign, Zero)가 어떻게 영향을 받는지에 대해 알아보자.

 

그 전에 우선 INC와 DEC 명령어를 보자.

이름 그대로 별건 없다. 그냥 각각의 단일 피연산자에서 1을 더하고, 뺀다.

.data
myWord WORD 1000h
myDword DWORD 10000000h
.code
inc myWord ; 1001h
dec myWord ; 1000h
inc myDword ; 10000001h
mov ax,00FFh
inc ax ; AX = 0100h
mov ax,00FFh
inc al ; AX = 0000h

이것은 INC와 DEC의 명령어 예시인데, 다른건 다 이해가도 마지막 줄은 조금 어렵다.

일단 inc al 명령어는 AL 레지스터를 1씩 증가시키는 명령어이다. 그러나 AL 레지스터는 8비트 레지스터이므로, 증가된 값이 8비트로 저장되기 때문에 오버플로우(overflow)가 발생할 경우, 증가된 값이 하위 8비트에만 유지된다.

mov ax, 00FFh에서 AX 레지스터는 16비트이므로 00FFh 값이 저장된다. inc al을 실행하면 AL 레지스터의 값이 1 증가하여 0100h가 된다. 그러나 이 값은 AL 레지스터의 하위 8비트에만 저장되므로, AX 레지스터의 상위 8비트는 변하지 않고 여전히 00h로 유지되기 때문에 AX 레지스터의 값은 0000h가 된다.

더보기

하위 8 비트가 이해가 되지 않는 경우를 대비해서..

AX = 1010 1011 1100 1101

 예를 들어 16비트 레지스터인 AX의 값이 0xABCD를 가정해보자.

여기서 하위 8비트는 CD, 즉 1100 1101을 말한다.
나는 16진수가 4개의 비트를 표현한다는 점을 처음에 잘 몰랐었다..

아무튼 그럼 inc al에서 0100h가 된 경우, 하위 8비트라면 여기서 00h만 저장하니까 0000h가 됐다는 말을 하고있다.

추가 응용

.data
myByte BYTE 0FFh, 0
.code
mov al,myByte ; AL =FFh
mov ah,[myByte+1] ; AH =00h
dec ah ; AH =FFh
inc al ; AL =00h
dec ax ; AX = FEFF

 

이렇게 INC와 DEC가 각각 1씩 더하고 빼는 연산이었다면, ADD와 SUB는 source만큼을 더하고 뺀다.

ADD destination, source
SUB destination, source

응용

.data
var1 DWORD 10000h
var2 DWORD 20000h
.code 		; ---EAX---
mov eax,var1 	; 00010000h
add eax,var2 	; 00030000h
add ax,0FFFFh 	; 0003FFFFh
add eax,1	; 00040000h
sub ax,1	; 0004FFFFh

 

 

NEG 명령어 (negate)

이 명령어는 숫자를 2의 보수로 변환하여 숫자의 부호를 바꾼다. 레지스터와 메모리 피연산자를 사용할 수 있다.

.data
valB BYTE -1
valW WORD +32767
.code
mov al,valB	 ; AL = -1
neg al		 ; AL = +1
neg valW	 ; valW = -32767

참고로 2의 보수는 목적지 피연산자의 모든 비트를 반대로 하고 1을 더하는 것으로 구할 수 있다.

 

이렇게 부호 뒤집기, 덧셈, 뺄셈을 배웠으니 우리는 다음의 식을 구할 수 있다.
Rval = -Xval + (Yval – Zval)

Rval DWORD ?
Xval DWORD 26
Yval DWORD 30
Zval DWORD 40
.code
mov eax,Xval
neg eax 	; EAX = -26
mov ebx,Yval
sub ebx,Zval 	; EBX = -10
add eax,ebx
mov Rval,eax 	; -36

 

 

산술 연산에 영향받는 플래그

우리는 산술 연산을 할 때 결과가 음수인지, 양수인지, 아니면 0인지 또는 결과가 목적지 피연산자의 메모리보다 더 큰지 작은지 등의 결과를 확인하기 위해 CPU 상태 플래그 값을 사용한다.
다음은 상태 플래그의 개요이다. 여기서 설정은 0 값에서 1로 설정한다는 말이다.

  • 제로 플래그 - 산술 결과 대상이 0일 때 설정
  • 부호 플래그 - 산술 결과 대상이 음수일 때 설정. 피연산자의 부호비트MSB를 보고 판단.
  • 캐리 플래그 - 부호가 없는 값이 범위를 벗어날 경우 설정
  • 오버플로 플래그 - 부호가 있는 값이 범위를 벗어날 때 설정

여기서 개인적으로 흥미로운 과정이 있었다.

캐리 플래그는 위에 나와 있듯이, 부호가 없는 값이 범위를 벗어날 경우 설정되는데
이 연산을 덧셈과 뺄셈을 따로 나눠서 이해하자.

 

덧셈에서 carry 플래그의 경우를 보자.
두 부호없는 정수를 더할 때에 carry플래그는 목적지 피연산자의 최상위 비트로부터의 캐리를 복사한 것이다.
직관적으로 합이 목적지 피연산자가 저장할 수 있는 크기를 넘을 때에 CF=1 이라고 한다.
이 때 OF는 CF와 MSB의 XOR연산으로 구한다.

mov al,0FFh
add al,1

이 예시에서 1을 더하면 AL의 최상위 비트의 캐리 출력이 carry 플래그로 복사된다.

한편, AX에 있는 00FFh에 1을 더한다면 합은 16비트에 쉽게 들어갈 수 있으며 carry 플래그는 0으로 해제된다.

mov ax,00FFh
add ax,1	;AX = 0100h, CF=0

 

뺄셈에서 carry 플래그의 경우를 보자.
이 경우, CPU가 뺄셈을 2의 보수로 만들고 그 숫자를 더해주는 것으로 생각하자.

  • 소스 피연산자의 부호를 바꾸고 목적지 피연산자에 더한다.
  • MSB의 캐리 출력을 반전시켜서 carry플래그로 복사한다.

이 때 OF는 CF와 MSB의 XOR연산으로 구한다.

mov al,1
sub al,2	; AL=FFh, CF=1

0000 0001에서 2의 보수 1111 1110을 더하면 1111 1111이 된다. 다시 보수를 계산해주면 0000 0001로 비트 7 MSB의 캐리(올림) 출력은 반전되어 carry 플래그에 저장되는 것으로 CF=1이 된다.

 

 


 

 

데이터 연산자와 디렉티브

데이터의 주소와 크기 특성에 대한 정보를 얻기 위해서 아래와 같은 MASM 디렉티브를 알아보자.

  • OFFSET Operator
  • PTR Operator
  • TYPE Operator
  • LENGTHOF Operator
  • SIZEOF Operator
  • LABEL Directive

 

첫 번째로 OFFSET 연산자이다.

이 연산자는 데이터 레이블의 오프셋을 반환한다. 

오프셋은 데이터 세그먼트의 시작에서 레이블의 바이트 단위 거리를 나타낸다.

.data
bVal BYTE ?
wVal WORD ?
dVal DWORD ?
dVal2 DWORD ?
.code
mov esi,OFFSET bVal 	; ESI = 00404000
mov esi,OFFSET wVal	 ; ESI = 00404001
mov esi,OFFSET dVal	 ; ESI = 00404003
mov esi,OFFSET dVal2	 ; ESI = 00404007

위의 경우, 시작 주소를 00404000에서 했다. 시작에서부터 거리 offset을 나타내기 때문에 처음 시작 주소인 00404000이 반환됐다.

 

OFFSET은 c계열 언어의 포인터와 맞닿는 부분이 있다. 다음 코드로 서로 비교해보자.

// C++ version:
char array[1000];
char * p = array;


; Assembly language:
.data
array BYTE 1000 DUP(?)
.code
mov esi,OFFSET array

 

 

PTR 연산자

레이블(변수)의 기본 유형을 재정의한다. 또한 변수의 일부에 액세스할 수 있는 유연성을 제공한다.
메모리에 저장할 때는 리틀 엔디안 순서를 사용한다.
(거꾸로 저장하는 방식)

.data
myDouble DWORD 12345678h
.code
mov ax,myDouble 		; error – why?
mov ax,WORD PTR myDouble 	; loads 5678h
mov WORD PTR myDouble,4321h 	; saves 4321h

이 예시에서 에러가나는 경우는 DWORD 변수의 하위 16비트를 AX로 이동시키려고 했기 때문이다.
피연산자의 크기가 같지 않기 때문에 에러를 발생시켰지만, WORD PTR 연산자를 이용해서 하위 워드 5678h를 AX로 이동시켰다.

 

위의 리틀 엔디안 방식의 메모리 배치를 보고 왜 1234h는 AX로 이동할 수 없었는지 알 수 있다.

mov ax,WORD PTR myDouble ; AX = 5678h
mov ax,WORD PTR [myDouble+2] ; AX = 1234h

 

추가로, PTR은 더 작은 데이터 유형의 요소들을 결합하여 더 큰 피연산자로 이동시키는 데 사용될 수 있다.

.data
myBytes BYTE 12h,34h,56h,78h
.code
mov ax,WORD PTR [myBytes] ; AX = 3412h
mov ax,WORD PTR [myBytes+2] ; AX = 7856h
mov eax,DWORD PTR myBytes ; EAX = 78563412h

 

예시코드

.data
varB BYTE 65h,31h,02h,05h
varW WORD 6543h,1202h
varD DWORD 12345678h
.code
mov ax,WORD PTR [varB+2] 	; a.0502h
mov bl,BYTE PTR varD 		; b.78h
mov bl,BYTE PTR [varW+2] 	; c.02h
mov ax,WORD PTR [varD+2] 	; d.1234h
mov eax,DWORD PTR varW 		; e.12026543h

 

 

TYPE 연산자

변수의 단일 원소의 크기를 바이트 단위로 반환한다. 

.data
var1 BYTE ?
var2 WORD ?
var3 DWORD ?
var4 QWORD ?
.code
mov eax,TYPE var1 ; 1
mov eax,TYPE var2 ; 2
mov eax,TYPE var3 ; 4
mov eax,TYPE var4 ; 8

 

 

LENGTHOF 연산자

레이블과 같은 줄에 있는 값들로 정의되는 배열에 있는 원소의 개수를 반환한다.

.data 				LENGTHOF
byte1 BYTE 10,20,30 		; 3
array1 WORD 30 DUP(?),0,0 	; 32
array2 WORD 5 DUP(3 DUP(?)) 	; 15
array3 DWORD 1,2,3,4 		; 4
digitStr BYTE "12345678",0 	; 9
.code
mov ecx,LENGTHOF array1 	; 32

 

 

SIZEOF 연산자

방금 얘기한 LENGTHOF 연산자와 TYPE 연산자를 곱한 값을 반환한다.

.data 				SIZEOF
byte1 BYTE 10,20,30 		; 3
array1 WORD 30 DUP(?),0,0 	; 64
array2 WORD 5 DUP(3 DUP(?)) 	; 30
array3 DWORD 1,2,3,4 		; 16
digitStr BYTE "12345678",0 	; 9
.code
mov ecx,SIZEOF array1 		; 64

 

 

LABEL 디렉티브

기존의 저장 위치에 대체 레이블 이름 및 타입을 할당한다. 따라서 추가적인 저장소가 필요하지 않다.

보통 데이터 세그먼트에서 다음에 선언된 변수에 대해서 다른 이름과 크기 속성을 제공하는 방식으로 사용된다. 

.data
dwList LABEL DWORD
wordList LABEL WORD
intList BYTE 00h,10h,00h,20h
.code
mov eax,dwList 		; 20001000h
mov cx,wordList 	; 1000h
mov dl,intList 		; 00h

이렇게 intList 앞에 레이블을 선언하고 각각의 속성을 부여한다.

레이블 디렉티브를 사용함으로서 PTR 연산자가 굳이 필요하지 않게 됐다.

 

 

 


 

 

 

간접 주소지정

대규모 배열의 요소 주소를 직접 지정하는 것은 상수 오프셋을 통한 효율적인 방법이 아니다.
대신, 레지스터를 포인터로 활용하고, 이 포인터를 조작하여 간접적으로 배열에 접근하는 것이 훨씬 효율적이다.
(간접 주소지정)
피연산자가 간접 주소지정을 사용할 때에 간접 피연산자라고 한다.

 

간접 피연산자

간접 피연산자는 변수(일반적으로 배열 또는 문자열)의 주소를 유지한다. 이것은 (포인터처럼) 재참조될 수 있다.

.data
val1 BYTE 10h,20h,30h
.code
mov esi,OFFSET val1
mov al,[esi] ; dereference ESI (AL = 10h)
inc esi
mov al,[esi] ; AL = 20h
inc esi
mov al,[esi] ; AL = 30h

 

게다가, PTR을 사용하여 메모리 피연산자의 크기 속성을 명확히 할 수 있다.

.data
myCount WORD 0
.code
mov esi,OFFSET myCount
inc [esi] 			; error: ambiguous
inc WORD PTR [esi] 		; ok

 

 

배열

간접 피연산자는 배열을 순서대로 처리하는 데에 아주 이상적이다. 괄호 안의 레지스터는 배열 유형과 일치하는 값만큼 증가한다.

.data
arrayW WORD 1000h,2000h,3000h
.code
mov esi,OFFSET arrayW
mov ax,[esi]
add esi,2 		; or: add esi,TYPE arrayW
add ax,[esi]
add esi,2
add ax,[esi] 		; AX = sum of the array

 

 

Indexed 피연산자

인덱스 피연산자는 레지스터에 상수를 추가하여 유효한 주소를 생성한다.
두 가지 표기법 형식이 있다.

[constant+ reg]         constant[reg]

변수 이름은 어셈블러에 의해서 변수의 오프셋을 나타내는 상수로 변환된다.

 

.data
arrayW WORD 1000h,2000h,3000h
.code
mov esi,0
mov ax,[arrayW + esi] 		; AX = 1000h
mov ax,arrayW[esi] 		; alternate format
add esi,2
add ax,[arrayW + esi]
etc.

 

 

Index Scaling

간접 피연산자 또는 인덱싱된 피연산자를 배열 요소의 오프셋으로 스케일링할 수 있다.
이것은 인덱스에 배열의 TYPE을 곱하여 실행한다.

.data
arrayB BYTE 0,1,2,3,4,5
arrayW WORD 0,1,2,3,4,5
arrayD DWORD 0,1,2,3,4,5
.code
mov esi,4
mov al,arrayB[esi*TYPE arrayB] ; 04
mov bx,arrayW[esi*TYPE arrayW] ; 0004
mov edx,arrayD[esi*TYPE arrayD] ; 00000004

 

 

포인터

다른 변수의 오프셋을 포함하는 포인터 변수를 선언할 수 있다.

.data
arrayW WORD 1000h,2000h,3000h
ptrW DWORD arrayW	;ptrW DWORD OFFSET arrayW 도 가능
.code
mov esi,ptrW
mov ax,[esi] 	; AX = 1000h

 

 

 


 

 

 

JMP and LOOP Instructions

기본적으로 CPU는 프로그램을 적재하여 순차적으로 실행한다. 하지만 CPU의 상태 플래그에 따라 프로그램의 위치가 제어된다.

 

JMP 명령어

JMP는 일반적으로 동일한 절차 내에 있는 레이블로 무조건 점프한다.

top:
.
.
jmp top

 

 

LOOP 명령어

루프 명령어는 문장들의 블록을 지정된 횟수만큼 반복한다.
ECX는 자동으로 카운터 역할을 한다. 루프를 반복할 때마다 감소한다.

만약 습관적으로 ECX를 0으로 초기화시켰다면, LOOP명령어는 ECX를 FFFF FFFFh로 감소시켜서 4,294,967,296번의 루프를 실행할 것이다. 만약 CX가 루프 카운터라면 65,536번 반복된다.

어셈블러는 다음 명령의 오프셋과 대상 레이블의 오프셋 사이의 거리를 바이트 단위로 계산한다. 이를 상대 오프셋이라고 한다. 상대 오프셋은 EIP에 추가된다.

LOOP가 어셈블되면
현재 위치 = 0000000E(다음 명령의 offset) - 5(FBh)가 현재 위치에 추가되어 00000009 위치로 점프한다.
00000009 << 0000000E + FB(-5의 2의 보수 표현)

 

 

중첩 루프

루프 내에서 루프를 코딩해야 하는 경우, 외부 루프 카운터의 ECX 값을 저장해야 한다. 다음 예제에서 외부 루프는 100번 실행되고 내부 루프는 20번 실행된다.

.data
count DWORD ?
.code
mov ecx,100 	; set outer loop count
L1:
mov count,ecx 	; save outer loop count
mov ecx,20 	; set inner loop count
L2:

loop L2 	; repeat the inner loop
mov ecx,count 	; restore outer loop count
loop L1 	; repeat the outer loop

 

 

정수 배열 합치기

.data
intarray WORD 100h,200h,300h,400h
.code
mov edi,OFFSET intarray 	; address of intarray
mov ecx,LENGTHOF intarray 	; loop counter
mov ax,0 			; zero the accumulator
L1:
add ax,[edi] 			; add an integer
add edi,TYPE intarray 		; point to next integer
loop L1 			; repeat until ECX = 0

이 과정은 다음의 단계를 밟는다.

  1. 배열의 주소를 인덱스 피연산자로 사용할 레지스터에 지정한다.
  2. 루프 카운터를 배열의 길이로 초기화한다.
  3. 합을 저장할 레지스터를 0으로 놓는다.
  4. 루프의 시작을 표시할 레이블을 만든다.
  5. 루프의 몸체에서 합을 저장하는 레지스터에 배열 원소를 더하기 위해서 간접 주소지정을 한다.
  6. 인덱스 레지스터를 다음 배열 원소를 가리키도록 앞으로 이동한다.
  7. LOOP 명령어를 사용하여 시작 레이블부터 루프를 반복한다.

 

 

문자열 복사하기

.data
source BYTE "This is the source string",0
target BYTE SIZEOF source DUP(0)
.code
mov esi,0 		; index register
mov ecx,SIZEOF source 	; loop counter
L1:
mov al,source[esi] 	; get char from source
mov target[esi],al 	; store it in the target
inc esi 		; move to next character
loop L1 		; repeat for entire string

MOV 명령어는 두 개의 메모리 피연산자를 가질 수 없다. 그래서 각 문자는 소스 문자열에서 AL로 이동되고 그 다음에 AL에서 목적지 문자열로 이동된다.