010-014. 비간접 피연산자

즉시 피연산자

즉시 피연산자 — 상세

  1. 즉시값은 어셈블 시점에 바이너리 안에 직접 인코딩된다.
  2. 실행 시 메모리를 별도로 참조하지 않고, 명령어 바이트에서 바로 꺼낸다.
.DATA
num    DB  5       ; 메모리 공간 차지 O
CONST  EQU 9       ; 메모리 공간 차지 X — 어셈블 중 숫자로 치환됨
 
mov bx, 9+3        ; → mov bx, 12   (0Ch) — 어셈블러가 계산
mov cx, CONST+3    ; → mov cx, 12   (0Ch) — EQU가 9로 치환된 뒤 계산
add num, 3         ; num 주소의 값에 3을 더함 — 3이 즉시값
or  bx, 12h        ; → 12h가 바이너리에 그대로
cmp cx, 00110001b  ; → 31h가 바이너리에 그대로
int 21h            ; → 21h가 바이너리에 그대로

바이너리에서 빨간 박스로 하이라이트된 바이트가 즉시 피연산자다.

명령어바이너리
mov bx, 12BB 0C 00
mov cx, 12B9 0C 00
add num, 380 06 14 01 03
or bx, 12h83 CB 12
cmp cx, 31h83 F9 31
int 21hCD 21 00

EQU Directive

EQU — 어셈블 타임 상수

EQU는 메모리를 전혀 할당하지 않는다. DB와의 차이가 핵심.

DBEQU
선언num DB 5CONST EQU 9
메모리 할당✅ 있음❌ 없음
동작주소를 통해 값 참조어셈블 중 숫자로 매크로처럼 복붙
결과메모리 피연산자즉시 피연산자로 인라인됨

CONST+3 은 어셈블러가 9+3 = 12 로 계산해 바이너리에 0C 00 을 박는다. 런타임에는 CONST 라는 이름 자체가 존재하지 않는다.

Programmer's Guide.pdf, Chapter 1 (Page 12) - Symbolic Integer Constants

You can define symbolic integer constants with either of the data assignment directives, EQUor the equal sign (=).

The directives EQU and = have slightly different purposes. Integers defined with = can be redefined, but those defined with EQU cannot. Once a symbolic constant is defined with EQU, redefining it generates an error.

Syntax: symbol EQU expression

원본 링크

6502와 비교

6502의 # 즉시 주소와 동일한 개념. 값이 바이너리에 직접 들어간다.

6502x86-16
표기LDA #$0CMOV AX, 12
바이너리A9 0CB8 0C 00
크기 제한1바이트 고정8bit / 16bit
상수 정의없음 (직접 숫자)EQU 로 이름 붙여 사용 가능

레지스터 피연산자

레지스터 직접 피연산자 (Register Direct Operand)

레지스터 이름 자체가 피연산자. 레지스터를 지칭하는 식별자가 바이너리에 인코딩된다. 메모리 참조 없이 CPU 내부 레지스터에서 바로 읽고 쓴다.

; ✅ 모든 피연산자가 레지스터 직접
add ax, bx      ; AX ← AX + BX
mov ds, ax      ; DS ← AX
cmp ah, bh      ; AH 와 BH 비교

혼합 예시 — 피연산자 종류 구분

명령어 하나에 서로 다른 종류의 피연산자가 섞일 수 있다.

and num, dx             ; num → 메모리 직접 / dx → 레지스터 직접
sub nums[bx+si], ax     ; nums[bx+si] → 메모리 간접(베이스+색인) / ax → 레지스터 직접
shl ax, 1               ; ax → 레지스터 직접 / 1 → 즉시
cmp cx, counter         ; cx → 레지스터 직접 / counter → 메모리 직접

단일 피연산자

단일 피연산자 명령어도 마찬가지로 레지스터를 피연산자로 사용한다.

mul bx      ; bx → 레지스터 직접
pop cx      ; cx → 레지스터 직접
inc ah      ; ah → 레지스터 직접

6502와 비교

6502의 A (누산기) 표기와 동일한 개념.

레지스터 자체를 피연산자로 지칭하고, 레지스터의 식별자가 opcode에 인코딩된다.

6502x86-16
표기A (누산기 고정)ax, bx, ah, si 등 다수
레지스터 수A, X, Y — 3개범용 + 색인 + 세그먼트 다수
8비트 분할불가ah/al 처럼 상위·하위 분리 가능

직접 메모리 피연산자

직접 메모리 피연산자와 색인 연산자

직접 메모리 피연산자 — table[4] 오프셋

table[4]table 심볼의 시작 주소 + 4바이트 위치를 가리킨다. 어셈블 시점에 table의 주소 + 4 가 계산되어 바이너리에 16비트 오프셋으로 인코딩된다.

.DATA
table  DW  10, 20, 30, 40, 50   ; DW = 2바이트씩
 
mov ax, table[0]   ; table + 0  → 10
mov ax, table[2]   ; table + 2  → 20
mov ax, table[4]   ; table + 4  → 30  ← 3번째 원소의 주소
mov ax, table[6]   ; table + 6  → 40

[4]인덱스가 아니라 바이트 오프셋이다. DW(2바이트) 기준으로 3번째 원소에 접근하려면 4를 써야 한다.

오프셋바이트 위치DW 원소
[0]+010
[2]+220
[4]+430
[6]+640

흔한 실수

C/C# 의 table[2]2번 인덱스(3번째) 지만, 어셈블리의 table[2]+2바이트(2번째 원소) 다. 고수준 언어처럼 첨자를 사용하려면 자료형 크기를 직접 곱해서 오프셋을 계산해야 한다.

직접 메모리 피연산자의 동작은 다음과 같다.

  1. 어셈블 과정에서 2바이트 메모리 주소(offset)값이 바이너리에 박힌다.
  2. 실행 도중에는 박힌 offset값과 세그먼트를 이용해 20비트 주소를 계산한다.

PTR

PTR operator Reference, Chapter 3 — Symbols and Operators (p.46–47)

type PTR expression

Forces the expression to be treated as having the specified type.

BYTE PTR nums 표현에서 BYTEtype 에 대응된다. numsexpression 에 대응된다.

슬라이드의 예시를 보면, numsDW로 선언되어 기본 타입이 WORD(2바이트)다. 그런데 mov al, BYTE PTR nums라고 쓰면 “nums 주소에서 WORD가 아니라 BYTE(1바이트)만 읽어서 al에 넣어라”라고 어셈블러에게 알려주게 된다.

어셈블 시점에 메모리 피연산자의 기본 크기를 무시하고, 원하는 크기로 간주할 수 있다.

  • 어셈블 시점에 nums 변수의 크기를 알 수 있다.
  • BYTE PTR nums는 어셈블 시점에 1바이트 크기로 계산된다.

TYPE

TYPE operator Reference, Chapter 3 — Symbols and Operators (p.46–47)

TYPE expression

Returns the type of expression.

Programmer's Guide, Chapter 4 (p.86)

The SIZEOF and TYPE operators, when applied to a type, return the size of an integer of that type.

Data TypeBytes
BYTE, SBYTE1
WORD, SWORD2
DWORD, SDWORD4
FWORD6
QWORD8
TBYTE10

TYPE nums어셈블 시점에 2를 반환한다. numsDW로 선언 되었기 때문이다.

직접 메모리 피연산자 예시

nums DW 1,2,3,4,5 — nums의 시작 주소가 0121h 라고 가정. 바이너리(메모리)에는 리틀 엔디언으로 21 01 로 기록된다.

오프셋 접근 3가지 비교

mov ax, nums[2]              ; 0121h + 2 = 0123h → 23 01 (메모리의 바이너리)
mov ax, nums+2               ; 위와 동일 — 표기만 다름
mov ax, nums[TYPE nums * 2]  ; TYPE nums = 2(DW크기, 어셈블 시점에 계산) → 2*2=4 → 0121h+4 = 0125h → 25 01 (메모리의 바이너리)

C#의 nums[2] 처럼 원소 단위로 접근하려면 TYPE 을 곱해야 한다. TYPE nums 는 어셈블러가 선언(DW)을 보고 2 로 계산해준다.

타이니 모델에서의 세그먼트

타이니 모델(.COM)은 cs = ds = es = ss — 모든 세그먼트 레지스터가 동일한 주소를 가리킨다.

  • 이 때문에 es:num은 “01 20”으로 계산된다.

따라서 ds:num 이든 es:num 이든 실제로는 같은 메모리에 접근한다.

세그먼트를 바꿀 일이 없으므로 오버라이드 문법을 쓸 실질적 이유가 없다.

즉시 피연산자

mov nums, 3 에서 “21 01” 옆 하이라이트한 “03 00”은 즉시 피연산자이다.

주의: 상수 “색인”과 피연산자 종류

왼쪽 표현은 모두 동일하다. 오른쪽도 마찬가지로 모두 동일하다.

주의 — 상수 색인과 피연산자 종류 혼동

100h 라는 같은 숫자가 피연산자 종류에 따라 완전히 다르게 동작한다.

ds의 값은 00 00 이라고 가정한다.

직접 메모리 피연산자즉시 피연산자
코드mov ax, ds:100hmov ax, 100h
동작주소 0100h 의 을 읽어 ax에 저장값 0100h 를 ax에 저장
결과ax = 0xA010 (메모리 내용)ax = 0x0100 (숫자 그 자체)

ds: 가 붙는 순간 무조건 직접 메모리 피연산자 — 100h는 값이 아닌 주소다.

코딩 습관 — [] 사용 원칙

[] 는 본래 간접 메모리 접근 표기다. 피연산자 종류를 명확히 드러내기 위해 아래 습관을 권장한다.

; ✅ 권장
mov ax, 100h          ; 즉시 피연산자  — [] 없음
mov ax, ds:100h       ; 직접 메모리   — [] 없음
mov ax, [bx]          ; 간접 메모리   — [] 있음
 
; ⚠️ 혼동 유발
mov ax, [100h]        ; 즉시처럼 보이지만 사실 즉시 피연산자
mov ax, ds:[100h]     ; 직접 메모리 — [] 가 없는 버전과 동일 동작
  • [] 없음 → 즉시 또는 직접 메모리 피연산자
  • [] 있음 → 간접 메모리 접근 전용으로 남겨두기

동작은 같아도 의도를 코드에서 바로 읽을 수 있도록 구분해서 쓰는 것이 좋다.

0건의 항목