프로그램의 기계수준 표현(Machine-Level Representation of Programs)

컴퓨터는 기계어 코드를 실행한다. 기계어 코드는 데이터 처리, 메모리 관리, 저장장치 읽기·쓰기, 네트워크 통신 같은 하위 동작들을 인코딩한 연속된 바이트다.

1. 왜 어셈블리어를 알아야 할까

최신 최적화 컴파일러가 만든 코드는 잘 훈련된 어셈블리어 프로그래머가 손으로 작성한 것에 버금가는 효율을 낸다. 그래서 어셈블리어 코드를 읽고 이해할 수 있으면 다음이 가능해진다.

  • 컴파일러의 최적화 성능을 파악할 수 있다
  • 코드에 숨어 있는 비효율성을 분석할 수 있다

2. 기계수준 코드의 특징

기계수준 프로그래밍에는 두 가지 중요한 특징이 있다.

  1. 기계수준 프로그램의 형식과 동작은 인스트럭션 집합 구조(ISA, Instruction Set Architecture) 에 의해 정의된다.
  2. 기계수준 프로그램이 사용하는 주소는 가상 주소이며, 메모리를 아주 큰 바이트 배열처럼 보이게 하는 메모리 모델을 제공한다.

프로세서 내부 구조

구성 요소설명
프로그램 카운터 (PC)실행할 다음 인스트럭션의 메모리 주소를 가리킨다. x86-64에서는 %rip
정수 레지스터 파일64비트 값을 저장하는 16개의 이름 붙은 위치. 주소(C의 포인터)나 정수 데이터를 저장하며, 일부는 프로그램 상태 추적, 일부는 지역변수·인자·리턴 값 같은 임시 값 저장에 쓰인다
조건 코드 레지스터가장 최근에 실행한 산술·논리 인스트럭션의 상태 정보를 저장. if, while의 조건 분기 구현에 사용
벡터 레지스터하나 이상의 정수나 부동소수점 값을 저장

프로그램 메모리

프로그램 메모리에는 실행 기계어 코드, 운영체제용 정보, 프로시저 호출·리턴을 관리하는 런타임 스택, 그리고 사용자가 malloc 등으로 할당한 메모리 블록이 포함된다.

3. 정보 접근하기

인스트럭션은 16개 레지스터의 하위 바이트들에 저장된 다양한 크기의 데이터를 다룬다. 연산 크기에 따라 접근 범위가 달라진다.

연산 크기접근 범위
바이트(8비트)가장 덜 중요한 1바이트
워드(16비트)가장 덜 중요한 2바이트
더블워드(32비트)가장 덜 중요한 4바이트
쿼드워드(64비트)레지스터 전체

오퍼랜드 식별자 (Operand Specifier)

대부분의 인스트럭션은 하나 이상의 오퍼랜드를 가진다. 오퍼랜드는 연산할 소스(source) 값과 결과를 저장할 목적지(destination) 위치를 명시한다.

명령어 접미사와 크기

접미사크기예시 레지스터
b (byte)8비트%al, %bl
w (word)16비트%ax, %bx
l (long)32비트%eax, %ebx
q (quad)64비트%rax, %rbx

4. 자료형 변환 (Type Casting)

어셈블리에는 “타입”이라는 개념이 없고, 크기부호 여부만 다룬다. 그래서 작은 타입을 큰 타입으로 넣거나 그 반대로 바꿀 때 처리 방식이 중요하다.

4-1. 작은 타입 → 큰 타입 (확장)

예: char(8bit)int(32bit). 두 가지 확장 방식이 있다.

방식설명명령어
Zero Extension상위 비트를 0으로 채움 (부호 없는 확장)MOVZX
Sign Extension상위 비트를 부호 비트로 채움 (부호 있는 확장)MOVSX
; 8비트 AL 레지스터 → 32비트 EAX로 확장
movzx eax, al     ; zero-extension: AL을 EAX로 확장 (상위를 0으로)
movsx eax, al     ; sign-extension: AL이 음수면 상위를 1로 채움

C 언어에서는 unsigned char → int는 zero-extension, signed char → int는 sign-extension으로 처리된다.

4-2. 큰 타입 → 작은 타입 (축소)

예: int(32bit)char(8bit). 단순히 상위 비트를 버리므로, 잘못하면 값이 잘릴 수 있다.

mov al, bl       ; 단순 복사 → 상위 비트 무시

4-3. 같은 비트, 다른 해석

부호성(sign)에 따라 같은 비트 패턴도 전혀 다른 값으로 해석된다.

이진수(8bit)signedunsigned
11111111-1255
10000000-128128
mov al, -1         ; al = 0xFF (-1)
movzx eax, al      ; eax = 0x000000FF (255) ← zero-extension
movsx eax, al      ; eax = 0xFFFFFFFF (-1)  ← sign-extension

4-4. 부호성은 언제 신경 써야 하나

  • 작은 자료형을 큰 레지스터에 넣을 때(함수 호출, 시스템 콜 인자 전달 시 정수형 확장 필수)
  • 부호 있는 비교(cmp)와 부호 없는 비교(cmp + ja/jb)에서 분기 명령이 달라짐
  • 곱셈에서 imul(부호 있음)과 mul(부호 없음)을 구분해 사용

정리

개념설명
타입 없음어셈블리는 타입보다 “크기”와 “부호 여부”에 집중
Zero-extensionunsigned 확장. 상위 비트를 0으로 채움 (movzx)
Sign-extensionsigned 확장. 부호 비트를 상위에 복사 (movsx)
축소(Casting down)상위 비트가 잘림 (주의 필요)
부호 해석같은 비트라도 signed/unsigned로 다르게 해석됨
주요 명령어mov, movzx, movsx, cmp, imul, mul

5. C 언어에서의 signed vs unsigned

기본 개념부터 보면, 같은 크기라도 부호 여부에 따라 표현 범위가 다르다.

타입표현 가능한 값
signed (기본)음수 ~ 양수
unsigned0 ~ 양수 (양수 범위가 두 배)

char는 1바이트(256개 값)이므로 signed char는 -128~127, unsigned char는 0~255를 표현한다.

5-1. unsigned를 쓰는 이유

이유설명예시
의미를 명확히음수가 나올 수 없는 값에 적합나이, 인덱스, 배열·파일 크기, 시간
범위 확장같은 크기로 더 큰 수까지 표현색상값 unsigned char red = 255;
비트 연산 안전성부호 비트를 걱정 안 해도 됨비트 마스크, 쉬프트 연산
unsigned int age = 23;            // 음수가 될 수 없는 값
unsigned char red = 255;          // 딱 1바이트로 0~255
unsigned char mask = 0xF0;
value = value & mask;             // 비트 마스킹

5-2. signed를 명시하는 이유

C에서 int는 기본이 signed다(char는 컴파일러마다 다름). 부호 있는 정수임을 강조하거나, 플랫폼 독립적으로 표현 범위를 보장하고 싶을 때 명시한다.

signed char temperature = -20;

5-3. char / signed char / unsigned char

타입범위용도
char컴파일러에 따라 다름문자 표현용
signed char-128 ~ 127음수를 포함할 때
unsigned char0 ~ 255바이트 데이터 처리, 파일 읽기 등

이미지 파일을 바이트 단위로 읽을 때는 unsigned char가 적절하다. 0xFF(255)가 음수로 해석되면 문제가 생기기 때문이다.

5-4. 오버플로 / 언더플로

  • unsigned: overflow/underflow 시 모듈로 연산처럼 순환한다
  • signed: overflow 시 동작이 정의되지 않는다 (UB, Undefined Behavior)
unsigned int x = 0;
x--;              // x = 4294967295 (underflow → 순환)

int y = 2147483647;
y++;              // UB (비정상 동작 가능)

6. 스택 포인터 %rsp의 감소

%rsp는 데이터를 push하거나 스택 프레임을 만들 때 감소하며, 감소량은 저장되는 데이터의 크기를 기준으로 한다(64비트 시스템에서 레지스터 하나는 8바이트).

push %rbp         ; 8바이트 감소 (레지스터 1개 = 8바이트)
sub $16, %rsp     ; 16바이트 공간 확보 → %rsp -= 16

함수 진입 시 흔한 패턴:

push %rbp         ; 이전 프레임 포인터 저장 → %rsp -= 8
mov %rsp, %rbp    ; 현재 스택 포인터를 프레임 포인터로 복사
sub $32, %rsp     ; 로컬 변수 32바이트 공간 확보 → %rsp -= 32

함수가 끝날 때:

leave             ; mov %rbp, %rsp + pop %rbp 와 동일 (프레임 복구)
ret               ; 스택에서 리턴 주소 pop → %rsp += 8

© 2022 JeongHwan Yun.