색상 팔레트

색상 팔레트

학습 연습 문제

소개

메모리

프로그램의 메모리는 보통 운영체제(Operating System, OS)가 다음과 같은 일반적인 배치로 매핑해요:

주소 메모리 영역
높음 스택
...
힙
읽기-쓰기 세그먼트
코드/읽기 전용 세그먼트
낮음 예약됨

세그먼트로 나뉜 메모리는 서로 다른 권한을 가진 섹션들로 구성돼요.

지금까지 정의한 함수는 모두 section .text에 있었어요. 이 섹션은 읽기 전용의 실행 가능한 데이터를 담아요. 다른 섹션은 데이터 변수를 선언하는 데 사용되는데, 읽기 전용일 수도 있고 읽기-쓰기가 가능할 수도 있지만 실행할 수는 없어요.

.data 섹션

초기화된 데이터는 section .data에 선언해요.

NASM(The Netwide Assembler, 이 트랙에서 사용하는 어셈블러)에서는 초기화된 변수가 이름, 데이터 크기를 나타내는 지시어, 그리고 쉼표로 구분된 값들의 목록으로 이루어져 있어요. 이 요소들은 서로 공백으로 구분하고, 레이블 뒤에는 선택적으로 :를 붙일 수 있어요.

주요 지시어와 그에 대응하는 데이터 크기는 다음과 같아요:

지시어 크기
db 1바이트
dw 2바이트
dd 4바이트
dq 8바이트

예를 들어, 다음은 space라는 이름의 바이트 변수를 값 10으로 선언해요:

section .data
    space db 10

section .data에 선언한 변수는 가변적이에요. 즉, 읽기-쓰기가 가능해요. 또한 정적 저장 기간을 가지는데, 이는 프로그램이 실행되는 동안 내내 존재한다는 뜻이에요.

.rodata 섹션

section .rodata는 section .data와 비슷해요. 두 섹션 모두 초기화된 데이터를 담고, 같은 방식으로 선언하며 저장 기간도 같아요.

둘의 가장 큰 차이는 section .rodata의 데이터가 불변이며, 즉 읽기 전용이라는 점이에요.

Note

equ로 정의한 상수는 section .rodata에 정의한 상수와 달라요.

equ로 정의한 상수는 메모리 공간을 차지하지 않고, 어셈블러가 그 값을 직접 대입해요. 사실 그 값의 자리 표시자일 뿐이에요.

반면에 section .rodata에 정의한 상수는 실제로 메모리에 저장되고 주소를 가져요.

데이터 접근하기

레이블과 간접 참조

선언한 데이터에는 반드시 이름이 연결되어 있어야 해요. 이 이름을 레이블이라고 해요.

레이블은 메모리에서 데이터의 구체적인 주소를 나타내는 기호예요. x86-64에서 주소는 64비트 값이에요.

NASM에서는 레이블로 데이터에 직접 접근하려고 하면 할당된 메모리가 아니라 그 주소를 얻어요:

section .data
    example dq 27 ; this declares a 8-byte variable initialized with 27

section .text
fn:
    mov rax, example ; this stores the address of the declared variable in rax, not its contents
    ...

메모리 주소의 내용에 접근하려면 그것을 _역참조_해야 해요. 이것을 간접 참조라고 해요.

NASM에서는 []로 해요:

section .data
    example dq -27 ; this declares a 8-byte variable initialized with -27

section .text
fn:
    mov rax, [example] ; this dereferences example and access the value stored in memory (-27)
    ...

그런데 역참조하는 메모리의 크기가 모호해질 수 있는 상황도 있어요. 그럴 때는 이 크기를 지정하는 접두사를 사용해야 해요.

일반적인 x86-64 프로그램에서 가장 중요한 접두사와 그 크기는 다음과 같아요:

접두사 크기
byte 1바이트
word 2바이트
dword 4바이트
qword 8바이트

같은 로드를 크기를 명시해서 쓸 수도 있어요:

    mov rax, qword [example] ; same dereference, size stated explicitly

메모리를 역참조할 때는 항상 접두사를 사용하는 것이 좋아요.

메모리에 쓰기

메모리에 쓰는 것도 주소를 역참조하는 같은 방식으로 해요:

section .data
    example1 db 10            ; example1 is a 1-byte memory location initialized with value 10
    example2 dq -456          ; example2 is a 8-byte memory location initialized with value -456
    example3 dd 54            ; example3 is a 4-byte memory location initialized with value 54

section .text
fn:
    mov byte [example1], 20   ; example1 now has value 20
    mov qword [example2], rdx ; example2 now has value equal to the contents in rdx
    mov dword [example3], eax ; example3 now has value equal to the contents in eax

대부분의 명령어에서 내용을 먼저 레지스터에 로드하지 않고도 메모리 피연산자를 사용할 수 있다는 점을 기억해 둬요. 하지만 보통은 원본 피연산자와 목적지 피연산자 양쪽에 모두 사용할 수는 없고, 둘 중 하나에만 쓸 수 있어요:

section .data
    example4 dw 4
    example5 dq -8
    example6 dd 15

section .text
fn:
    add word [example4], 5     ; example4 is now a 2-byte memory location with the value 4 + 5 = 9
    imul rax, qword [example5] ; rax = rax * (-8)
    ; this is not possible -> sub dword [example6], dword [example6]
LEA 명령어

mov로도 변수의 주소를 레지스터에 저장할 수 있지만, 이 목적만을 위한 명령어가 있어요: lea.

이 명령어는 메모리 형식의 피연산자를 사용하지만, 메모리를 읽지는 않아요. 대신 유효 주소 식을 계산해서 그 결과를 목적지 피연산자에 써요:

lea rax, [example] ; this stores the address of 'example' in rax

메모리 주소를 계산해서 레지스터에 저장할 때는 lea를 사용하는 편이 더 자연스러워요.

상대 주소 지정

메모리 위치에 접근할 때 NASM의 기본 동작은 절대 주소, 즉 고정된 메모리 주소를 생성하는 거예요.

보안상의 이유로 실행 파일은 흔히 **PIE(Position Independent Executable)**로 빌드되는데, 이때 메모리 영역은 무작위로 정해진 위치에 놓여요. PIE에서는 링크할 때 변수의 최종 주소를 알 수 없어요. 그래서 코드는 다음에 실행할 명령어를 가리키는 rip라는 특별한 레지스터의 값으로부터의 오프셋으로 주소를 계산해요.

이것을 보통 RIP 상대 주소 지정이라고 해요.

NASM에서는 rel 연산자로 RIP 상대 접근을 요청할 수 있어요:

mov rax, qword [rel variable]

소스 파일 맨 위에 default rel을 두면 상대 주소 지정을 기본값으로 만들 수도 있어요.

이 트랙의 모든 연습 문제는 PIE로 컴파일하고 링크하기 때문에, 상대 주소를 생성하려면 rel을 사용해야 해요.

가시성

어떤 섹션(예: .text, .data, .rodata)에 정의한 레이블(함수와 데이터)이든 같은 소스 파일 안에서는 보여요. global로 선언하면 다른 소스 파일에서도 보여요.

반대로 다른 소스 파일에 정의한 레이블은 extern으로 선언하면 현재 소스 파일에서 볼 수 있어요. 이 경우 어셈블리에는 데이터 크기를 나타내는 표시가 없으니, 크기를 미리 알고 있어야 해요.

default rel

section .data

global number1 ; 'number1' is a variable visible to other source files
number1 db 200

extern number2 ; 'number2' is a variable visible to the current source file, but defined in another

section .text

extern sum ; sum is a function visible to the current source file, but defined in another

fn:
    mov dil, byte [number1]
    mov sil, byte [number2]
    call sum
    ...

지침

친구 José는 동네 학교의 선생님이에요. 색을 조합해서 다른 색을 만드는 방법을 보여 줄 재미있는 실험 아이디어가 떠올랐어요.

그래서 그 실험들을 함께 도와달라고 부탁했어요.

Note

이 연습 문제에서 색은 RGB 값을 담은 32비트(4바이트) 숫자로 표현돼요.

RGB 값은 Red, Green, Blue 세 개의 채널로 이루어져 있고, 각 채널은 8비트(1바이트)를 차지해요. 네 번째 바이트는 보통 Alpha 채널을 위해 남겨 두지만, 이 연습 문제에서는 값이 비어 있어요(0).

1. 색의 RGB 값 가져오기

각 색의 값은 다른 소스 파일에 정의된 표에 이미 저장되어 있어요. 색은 이 표에서 고유한 주소로 식별돼요.

색의 32비트 값을 반환하는 함수 get_color_value를 정의해요. 이 함수는 색 표에서 해당 색의 유효한 주소를 매개변수로 받아요.

get_color_value(black)
// => 0

힌트 - 32비트는 4바이트와 같아요.

2. 기본 색 추가하기

여러 색을 섞기 위해 José는 먼저 기준이 될 색을 정하고, 그 색과 조합할 보조 색만 바꿀 거예요.

색의 32비트 값을 변수 base_color에 저장해서 나중에 쓸 수 있게 하는 함수 add_base_color를 정의해요. 이 함수는 반환값이 없고, 색 표에서 색의 주소를 매개변수로 받아요.

변수 base_color는 직접 정의하고, 다른 소스 파일에서도 접근할 수 있어야 해요.

기본 색은 한 번에 하나만 있을 수 있어요. 새로운 기본 색을 추가하면 이전 색은 버려져요.

기본적으로 프로그램이 시작될 때 base_color는 _white_의 32비트 값인 0xFFFFFF00으로 초기화되어야 해요.

힌트 - NASM은 0xFFFFFF00처럼 앞에 0x를 붙여 정의한 16진수를 받아들여요.

3. 원색 상수 정의하기

José는 원색을 사용해 많은 조합을 만들 계획이라, 빠르게 접근할 수 있도록 원색을 따로 두고 싶어 해요. 색을 표현하는 데 RGB를 사용하므로, 원색은 다음과 같아요:

  • RED, 값은 0xFF000000.
  • GREEN, 값은 0x00FF0000.
  • BLUE, 값은 0x0000FF00.

이 색들 각각에 대해 상수를 하나씩 정의해요. 이 상수들은 다른 소스 파일에서도 접근할 수 있어야 해요.

4. 색 조합하기

색은 다른 소스 파일에 정의된 combining_function에 따라 조합되어야 해요. 이 함수는 base_color의 32비트 값과 함께 섞을 보조 색의 32비트 값을 매개변수로 받아요. 그리고 조합된 색의 32비트 값을 반환해요.

두 색을 조합하고 그 결과를 메모리에 저장하는 함수 make_color_combination을 정의해요. 이 함수는 반환값이 없고, 다음 순서로 매개변수를 받아요:

  • 조합된 색의 32비트 값을 저장할 주소.
  • 색 표에서, 기본 색과 조합할 보조 색의 주소.
Caution

combining_function이 사용 중인 레지스터의 값을 바꿀 수 있다는 점에 주의해요. 함수를 호출하기 전에 필요한 변수를 반드시 메모리에 저장해 둬요.

GitHub에서 편집 링크가 새 창이나 탭에서 열려요
x86-64 Assembly Exercism

색상 팔레트 문제를 시작해 볼 준비가 됐나요?

Exercism에 가입하고 x86-64 Assembly 트랙을 개념 22개연습 문제 130개, 그리고 실제 사람의 멘토링과 함께 배우고 익혀 보세요. 모두 무료예요.