Hello x86(2)

“Hello, x86!” 출력하기

.CODE — 코드 섹션 실제 실행될 명령어(instruction)들이 들어가는 세그먼트다. .STARTUP부터 END까지가 코드 섹션에 해당한다.

코드 섹션 vs 데이터 섹션의 기원 .CODE.DATA의 분리는 x86/MS-DOS 시절부터 존재했던 개념이다. 이것이 현대 언어까지 그대로 이어진다.

섹션내용현대 언어에서
.CODE실행할 명령어C의 .text 섹션
.DATA사용할 데이터C의 .data 섹션

C언어의 코드/데이터 섹션 개념은 여기서 유래했다.

.STARTUP — 프로그램 진입점 디렉티브

프로그램이 실행될 때 처음 시작되는 지점을 MASM에게 알려주는 디렉티브다.

왜 필요한가 — .COM 파일 구조 .MODEL TINY를 사용하면 결과물이 .COM 파일이 되는데, .COM 파일은 첫 번째 명령어가 반드시 오프셋 100h 위치에 있어야 한다는 규칙이 있다.

.COM 파일 메모리 구조:

0000h ~ 00FFh  : PSP (Program Segment Prefix) — MS-DOS가 사용하는 영역
0100h ~        : ← 실제 코드 시작 위치 (여기서부터 실행)

PSP(Program Segment Prefix)는 MS-DOS가 프로그램 실행 전에 앞 256바이트(100h)를 자신의 관리 정보로 채우기 때문에, 코드는 반드시 그 뒤인 100h부터 시작해야 한다.

.STARTUP이 해주는 것 .STARTUP 바로 다음에 오는 명령어(mov ah, 09h)가 정확히 오프셋 100h에 배치되도록 MASM이 자동으로 맞춰준다. 개발자가 직접 오프셋을 계산할 필요가 없다.

END — 소스 파일 종료 디렉티브

MASM에게 “여기까지 어셈블하라”고 알려주는 디렉티브다. 이 모듈(소스 파일)의 마지막을 의미한다.

END 이후에 코드가 있어도 MASM은 무시한다.

END vs 프로그램 종료 END어셈블러에게 파일 끝을 알리는 것이고, 실제 프로그램 실행 종료는 그 위의 int 21h (함수 4Ch)가 담당한다. 둘을 혼동하지 말 것.

mov — 데이터 이동 명령어

오른쪽(src) 값을 왼쪽(dest)에 대입한다.

피연산자가 2개인 이유 어디에 넣을지(dest)를 명시해줘야 한다.

6502는 누산기(A 레지스터) 하나만 있어서 목적지가 항상 A로 고정이었다. x86은 레지스터가 여러 개(AX, BX, CX, DX…)이기 때문에

6502:  LDA #$09     ; 목적지는 항상 A로 고정
x86:   mov ah, 09h  ; 목적지를 직접 지정

AX 레지스터 구조

AX (16비트)
├── AH (상위 8비트)
└── AL (하위 8비트)

mov ah, 09h 는 AX 전체가 아닌 상위 8비트(AH)에만 값을 넣는다.

숫자 리터럴 접미사

접미사진수예시
h16진수 (hex)09h, 4Ch
b2진수 (binary)00001001b
o8진수 (octal)11o
없음10진수9

lea — Load Effective Address

메모리 주소 자체를 레지스터에 로드하는 명령어다.

lea dx, msg   ; dx = msg의 메모리 주소

mov vs lea 차이

mov dx, msg   ; msg 주소에 있는 값(데이터)을 dx에 로드
lea dx, msg   ; msg 주소 자체를 dx에 로드  ← 포인터

C언어로 비유하면:

dx = *msg;   // mov — 역참조
dx = &msg;   // lea — 주소값

DX 레지스터 msg는 16비트 주소값이므로 AH(8비트)가 아닌 DX(16비트) 전체에 로드한다. D 레지스터는 Data Register로, 입출력 연산에 주로 사용된다.

DX (16비트) ← 주소값 전체가 들어감
├── DH (상위 8비트)
└── DL (하위 8비트)

near 포인터 오프셋(16비트)만 로드하는 것이기 때문이다. 세그먼트까지 포함한 32비트 전체 주소는 far 포인터라고 부른다. x86-16 세그먼트 오프셋 주소 읽는 법 참고

슬라이드에서 “near 포인터를 로드”라고 표현한 이유는, 8086의 세그먼트:오프셋 주소 체계에서

int 21h — MS-DOS 소프트웨어 인터럽트

MS-DOS가 제공하는 서비스를 호출하는 명령어다. 21h는 MS-DOS 서비스 인터럽트 번호이며, AH에 넣은 값이 함수 번호가 된다.

mov ah, 09h   ; 함수 번호 지정 (09h = 문자열 출력)
lea dx, msg   ; 출력할 문자열 주소를 DX에 전달
int 21h       ; MS-DOS 서비스 호출

동작 원리 int 21h 실행 시 CPU는 인터럽트 벡터 테이블(IVT)에서 21h번 항목을 찾아 MS-DOS 커널 함수로 점프한다. (21h 항목이 MS-DOS 커널 함수이다.) MS-DOS는 AH 값을 보고 어떤 함수를 실행할지 결정한다.

AH = 09h → Display String 함수 실행
            DS:DX(데이터 세그먼트 + 데이터 오프셋)가 가리키는 문자열을 '$'까지 출력

DOS API 함수 번호 (AH 값)

AH기능
07hDirect console input without echo
08hConsole input without echo
09hDisplay string ← 지금 사용
0AhBuffered keyboard input
4ChTerminate program (프로그램 종료)

출처: Wikipedia - DOS API

프로그램 종료 — int 21h 함수 4Ch

MS-DOS 서비스 4Ch (Terminate with return code)로 프로그램을 종료한다.

mov ah, 4Ch   ; 함수 번호: Terminate with return code
xor al, al    ; 리턴값 = 0 (정상 종료)
int 21h       ; MS-DOS 종료 서비스 호출

종료 스펙 4Ch 함수의 스펙상 AL에 리턴값(종료 코드) 을 넣어야 한다. C언어의 return 0과 동일한 개념이다.

AH = 4Ch  → 종료 함수
AL = 00h  → 리턴값 (0 = 정상 종료)

xor al, al vs mov al, 0 둘 다 AL을 0으로 만들지만 xor이 더 효율적이다.

명령어기계어 크기
mov al, 02바이트
xor al, al2바이트

크기는 같지만 xor즉시값(immediate)을 메모리에서 읽어올 필요가 없어서 더 빠르다. 이 관용구는 x86-64에서도 여전히 쓰인다.

왜 즉시값 로드가 느리냐면:

  • mov al, 0 → 명령어 안에 0이 인코딩되어 있음 → 명령어 fetch 시 0도 같이 읽어와야 함
  • xor al, al → 피연산자가 레지스터 두 개 → 이미 CPU 안에 있는 값끼리 연산 → 메모리 접근 없음

MS-DOS 의 9번 함수는 어떻게 구현되어 있나?

화면 출력의 3가지 방법

같은 “문자열 출력”이라도 어느 계층에 접근하느냐에 따라 3가지 방법이 있다.

내 프로그램
    ↓  방법 1
MS-DOS API (int 21h, 함수 09h)
    ↓  방법 2
ROM BIOS API (BIOS가 제공하는 함수)
    ↓  방법 3
하드웨어 직접 접근 (비디오 메모리 주소에 직접 쓰기)
방법접근 계층특징
int 21hMS-DOS가장 추상화, 이식성 높음
BIOS 함수 직접 호출ROM BIOSDOS 없이도 동작
직접 접근하드웨어그래픽 카드에 직접 write

MS-DOS → BIOS → 하드웨어 흐름 int 21h 출력 함수도 내부적으로는 BIOS가 제공하는 함수를 호출하고, BIOS는 하드웨어 제조사가 ROM에 구워넣은 코드로 비디오 하드웨어에 접근한다. 즉 방법 1은 방법 2와 3을 내부적으로 거치는 것이다.

MS-DOS의